atst 1.4.3__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 (207) hide show
  1. atst/__init__.py +220 -0
  2. atst/__main__.py +13 -0
  3. atst/_version.py +6 -0
  4. atst/batch.py +218 -0
  5. atst/catalog/__init__.py +14 -0
  6. atst/catalog/capability.py +797 -0
  7. atst/catalog/capability_audit.py +70 -0
  8. atst/catalog/provider_audit.py +52 -0
  9. atst/catalog/provider_bindings.py +305 -0
  10. atst/catalog/provider_contract.py +77 -0
  11. atst/catalog/provider_guard.py +51 -0
  12. atst/charset/__init__.py +42 -0
  13. atst/charset/encoding.py +468 -0
  14. atst/cli/__init__.py +68 -0
  15. atst/cli/_common.py +243 -0
  16. atst/cli/parser.py +414 -0
  17. atst/cli/runtime_commands.py +1027 -0
  18. atst/client/__init__.py +97 -0
  19. atst/client/_mixin.py +1000 -0
  20. atst/client/api.py +645 -0
  21. atst/client/async_.py +484 -0
  22. atst/client/core.py +365 -0
  23. atst/client/factory.py +66 -0
  24. atst/client/sync.py +654 -0
  25. atst/codec/__init__.py +60 -0
  26. atst/codec/framing.py +370 -0
  27. atst/codec/primitive.py +654 -0
  28. atst/config/__init__.py +58 -0
  29. atst/config/loader.py +321 -0
  30. atst/config/schema.py +340 -0
  31. atst/diagnostics.py +178 -0
  32. atst/domain/__init__.py +36 -0
  33. atst/domain/adjust.py +348 -0
  34. atst/domain/calendar.py +479 -0
  35. atst/domain/finance.py +300 -0
  36. atst/domain/integrity.py +106 -0
  37. atst/domain/models.py +256 -0
  38. atst/domain/period.py +83 -0
  39. atst/domain/records.py +383 -0
  40. atst/domain/symbol.py +290 -0
  41. atst/error_envelope.py +247 -0
  42. atst/errors.py +504 -0
  43. atst/feedback/__init__.py +41 -0
  44. atst/feedback/reporter.py +313 -0
  45. atst/feedback/stats.py +226 -0
  46. atst/feedback/telemetry.py +220 -0
  47. atst/integration/__init__.py +22 -0
  48. atst/integration/mcp/__init__.py +40 -0
  49. atst/integration/mcp/__main__.py +15 -0
  50. atst/integration/mcp/_common.py +98 -0
  51. atst/integration/mcp/_server.py +272 -0
  52. atst/integration/mcp/_tools_impl.py +167 -0
  53. atst/integration/mcp/_tools_spec.py +205 -0
  54. atst/integration/runtime_http.py +323 -0
  55. atst/integration/runtime_ws.py +623 -0
  56. atst/integration/runtime_ws_server.py +123 -0
  57. atst/integration/serialization.py +59 -0
  58. atst/integration/wire_fields.py +254 -0
  59. atst/observability/__init__.py +86 -0
  60. atst/observability/metrics.py +603 -0
  61. atst/observability/otel_exporter.py +578 -0
  62. atst/observability/prometheus_exporter.py +229 -0
  63. atst/observability/statsd_exporter.py +451 -0
  64. atst/output/__init__.py +404 -0
  65. atst/profile/__init__.py +101 -0
  66. atst/profile/detect.py +788 -0
  67. atst/profile/presets.py +226 -0
  68. atst/protocol/__init__.py +69 -0
  69. atst/protocol/commands.py +380 -0
  70. atst/protocol/generic.py +403 -0
  71. atst/protocol/handshake.py +130 -0
  72. atst/protocol/parsers/__init__.py +182 -0
  73. atst/protocol/parsers/_std7709_bars.py +488 -0
  74. atst/protocol/parsers/_std7709_common.py +191 -0
  75. atst/protocol/parsers/_std7709_quote.py +400 -0
  76. atst/protocol/parsers/f10.py +111 -0
  77. atst/protocol/parsers/goods.py +372 -0
  78. atst/protocol/parsers/mac.py +367 -0
  79. atst/protocol/parsers/std7709.py +93 -0
  80. atst/protocol/parsers/std7709_extra.py +448 -0
  81. atst/protocol/parsers/std7727.py +348 -0
  82. atst/protocol/prober.py +657 -0
  83. atst/protocol/registry.py +463 -0
  84. atst/providers/__init__.py +776 -0
  85. atst/py.typed +4 -0
  86. atst/query.py +743 -0
  87. atst/reader/__init__.py +50 -0
  88. atst/reader/formats.py +573 -0
  89. atst/reader/profile.py +719 -0
  90. atst/result.py +166 -0
  91. atst/runtime/__init__.py +14 -0
  92. atst/runtime/audit.py +35 -0
  93. atst/runtime/executor.py +992 -0
  94. atst/runtime/freshness.py +83 -0
  95. atst/runtime/identity.py +37 -0
  96. atst/runtime/kernel.py +260 -0
  97. atst/runtime/orchestration.py +197 -0
  98. atst/runtime/provenance.py +54 -0
  99. atst/sink/__init__.py +20 -0
  100. atst/sink/local_day.py +308 -0
  101. atst/stream_contract.py +105 -0
  102. atst/streaming/__init__.py +52 -0
  103. atst/streaming/base.py +672 -0
  104. atst/streaming/engine.py +525 -0
  105. atst/streaming/push.py +313 -0
  106. atst/streaming/state.py +132 -0
  107. atst/streaming/stateful.py +237 -0
  108. atst/tools/__init__.py +50 -0
  109. atst/tools/_console.py +25 -0
  110. atst/tools/_yaml_min.py +494 -0
  111. atst/tools/capture.py +981 -0
  112. atst/tools/check_originality.py +726 -0
  113. atst/tools/codegen.py +537 -0
  114. atst/tools/golden_audit.py +560 -0
  115. atst/tools/golden_expand.py +422 -0
  116. atst/tools/host_audit.py +547 -0
  117. atst/tools/spec_audit.py +552 -0
  118. atst/trade/__init__.py +166 -0
  119. atst/trade/client.py +271 -0
  120. atst/trade/constants.py +111 -0
  121. atst/trade/errors.py +48 -0
  122. atst/trade/frames.py +415 -0
  123. atst/trade/security.py +49 -0
  124. atst/trade/simulator.py +594 -0
  125. atst/transport/__init__.py +56 -0
  126. atst/transport/_validation.py +318 -0
  127. atst/transport/async_.py +1452 -0
  128. atst/transport/base.py +584 -0
  129. atst/transport/hosts.py +791 -0
  130. atst/transport/pool.py +1208 -0
  131. atst/transport/ratelimit.py +336 -0
  132. atst/transport/sniff.py +437 -0
  133. atst/transport/speedtest.py +387 -0
  134. atst/typed_query.py +691 -0
  135. atst/universe/__init__.py +333 -0
  136. atst/universe/_classes.py +290 -0
  137. atst/universe/_scan.py +52 -0
  138. atst/universe/_sources.py +282 -0
  139. atst/web/__init__.py +529 -0
  140. atst/web/_base_core.py +352 -0
  141. atst/web/_base_em.py +169 -0
  142. atst/web/_base_http.py +624 -0
  143. atst/web/_base_retry.py +107 -0
  144. atst/web/_mob_fund.py +149 -0
  145. atst/web/_paginate.py +182 -0
  146. atst/web/_session_astock.py +116 -0
  147. atst/web/_session_baidu.py +340 -0
  148. atst/web/_session_efinance.py +369 -0
  149. atst/web/_session_fund_v2.py +295 -0
  150. atst/web/_session_fundamental.py +330 -0
  151. atst/web/_session_gaps.py +601 -0
  152. atst/web/_session_info.py +665 -0
  153. atst/web/_session_market.py +665 -0
  154. atst/web/_session_news.py +75 -0
  155. atst/web/_session_p1.py +151 -0
  156. atst/web/_session_p2.py +258 -0
  157. atst/web/_session_p3.py +295 -0
  158. atst/web/adapters_fund.py +264 -0
  159. atst/web/astock_toolkit.py +344 -0
  160. atst/web/baidu/__init__.py +9 -0
  161. atst/web/baidu/adapters.py +449 -0
  162. atst/web/base.py +114 -0
  163. atst/web/boards.py +513 -0
  164. atst/web/boc/__init__.py +9 -0
  165. atst/web/boc/adapters.py +66 -0
  166. atst/web/chip.py +293 -0
  167. atst/web/cninfo/__init__.py +2 -0
  168. atst/web/cninfo/adapters.py +332 -0
  169. atst/web/corporate.py +1132 -0
  170. atst/web/eastmoney/__init__.py +9 -0
  171. atst/web/eastmoney/adapters.py +603 -0
  172. atst/web/efinance_deriv.py +423 -0
  173. atst/web/efinance_fund.py +378 -0
  174. atst/web/efinance_options.py +263 -0
  175. atst/web/esg.py +499 -0
  176. atst/web/fin_report.py +412 -0
  177. atst/web/fund_company.py +345 -0
  178. atst/web/fund_manager.py +289 -0
  179. atst/web/fund_rank.py +396 -0
  180. atst/web/fundflow.py +1015 -0
  181. atst/web/global_market.py +244 -0
  182. atst/web/governance.py +455 -0
  183. atst/web/hot_rank.py +136 -0
  184. atst/web/jsl/__init__.py +9 -0
  185. atst/web/jsl/adapters.py +69 -0
  186. atst/web/limits.py +38 -0
  187. atst/web/longhu.py +189 -0
  188. atst/web/market_stats.py +200 -0
  189. atst/web/news.py +416 -0
  190. atst/web/normalize.py +603 -0
  191. atst/web/session.py +182 -0
  192. atst/web/sina/__init__.py +9 -0
  193. atst/web/sina/adapters.py +558 -0
  194. atst/web/sources.py +529 -0
  195. atst/web/tencent/__init__.py +9 -0
  196. atst/web/tencent/adapters.py +734 -0
  197. atst/web/ths/__init__.py +2 -0
  198. atst/web/ths/adapters.py +245 -0
  199. atst/web/ticks.py +260 -0
  200. atst/web/wallstreet/__init__.py +2 -0
  201. atst/web/wallstreet/adapters.py +92 -0
  202. atst/web/wencai.py +159 -0
  203. atst-1.4.3.dist-info/METADATA +671 -0
  204. atst-1.4.3.dist-info/RECORD +207 -0
  205. atst-1.4.3.dist-info/WHEEL +4 -0
  206. atst-1.4.3.dist-info/entry_points.txt +2 -0
  207. atst-1.4.3.dist-info/licenses/LICENSE +47 -0
atst/__init__.py ADDED
@@ -0,0 +1,220 @@
1
+ # Copyright (c) 2026 atst contributors
2
+ # Licensed under the MIT License
3
+
4
+ """atst —— 通达信(TDX)行情数据通用协议库。
5
+
6
+ 设计目标
7
+ --------
8
+ * **协议全覆盖**:7709 标准 / 7727 扩展市场 / MAC 专属 / F10 资料 / 商品语义,
9
+ 未知命令走 L2 启发式 + L3 原始透传,**永不丢包**。
10
+ * **数据全兼容**:市场 × 品种 × 周期 × 口径差异全部参数化为 ``DataProfile``。
11
+ * **实时为一等公民**:PushChannel + 增量合并 + 断线补数 + 背压 + 重连。
12
+ * **Provider-first 运行时**:公开查询先编译为单 Provider / 单 Channel 的
13
+ ``QueryPlan``,跨 Provider fallback 只能由显式策略层触发。
14
+ * **Fail-closed Streaming**:canonical stream 采用显式 ``StreamState``,
15
+ worker 半死、启动失败、stop 超时与终态重启都不能静默生成第二 worker。
16
+ * **零缓存直达数据源**:每次公开查询都编译为唯一 ``QueryPlan`` 并直接请求绑定的
17
+ Provider;不存在结果级缓存、结果级负缓存或请求合并层(web 传输层对失败主机有
18
+ 进程级 TTL 排序,只改变尝试顺序、不省掉任何一次数据请求),provenance 始终反映
19
+ 真实直连。
20
+ * **原创实现**:洁净室流程,协议事实源于自有抓包与本地文件分析。
21
+
22
+ 分层(自底向上)::
23
+
24
+ codec 报文帧 / 变长数值
25
+ charset 字符集自动探测(GBK/GB18030/Big5/UTF-8)
26
+ protocol 命令登记 + 三级解析(L1/L2/L3)
27
+ config 6 源合并 + 严格校验(配置面即执行面契约)
28
+ transport TCP 连接 / 连接池 Slot / 心跳 / 限流 / 主站测速
29
+ client 同步 + 异步客户端(Standard / Extended / MAC / Goods)
30
+ reader 本地 vipdoc 二进制(.day/.lc1/.lc5/.dat/gpcw)
31
+ profile 数据规格探测(帧 / 文件双探测器 + presets)
32
+ sink LocalDaySink:写回 vipdoc .day 二进制
33
+ domain 数据模型 / 复权 / 日历 / 时区
34
+ providers Provider / Channel / Capability 单一事实源
35
+ catalog 能力目录 + 规划期签名校验 + channel→adapter 绑定(无执行)
36
+ query QuerySpec / QueryPlan / QueryFingerprint
37
+ result QueryResult / Provenance
38
+ runtime 零缓存 provider-first 执行内核(UnifiedRuntime,唯一内核)
39
+ streaming 流式订阅 + 显式生命周期状态机
40
+ web HTTP Web 行情源(新浪/腾讯/东财/集思录/港股/中行)
41
+ output DataFrame / Parquet / DuckDB 输出层
42
+ observability Prometheus 风格指标 / 埋点(零硬依赖)
43
+ feedback 错误 / 用量上报 + 使用统计
44
+ universe 标的清单(按类别取全部标的:代码表 / 新浪节点 / tdx 段表三级降级)
45
+ tools 协议账本审计 / 代码生成 / 原创性检查
46
+ trade 交易协议模拟器(实验性:纯内存模拟,不接入内核)
47
+ cli 命令行面(32 子命令,只翻译不执行)
48
+ integration HTTP / WS / MCP 服务面(只翻译不执行)
49
+
50
+ Quick start(离线,读取本地通达信数据)::
51
+
52
+ from atst.reader import DayBarReader
53
+ bars = DayBarReader().read(r"D:/tdx/vipdoc/sh/lday/sh600519.day")
54
+
55
+ Quick start(在线,TDX 协议)::
56
+
57
+ from atst import Client
58
+ with Client(default_provider="tdx") as c:
59
+ bars = c.bars("sh600519", period="day", count=30)
60
+
61
+ Quick start(HTTP Web 源,无需 TDX 主站)::
62
+
63
+ from atst.web import get_quotes
64
+ quotes = get_quotes(["sh600519", "sz000001"], source="tencent")
65
+ """
66
+
67
+ from __future__ import annotations
68
+
69
+ from typing import TYPE_CHECKING, Any
70
+
71
+ from ._version import __version__
72
+
73
+ __all__ = [
74
+ "__version__",
75
+ # v13 canonical business client surface(唯一推荐入口)。
76
+ "Client",
77
+ "AsyncClient",
78
+ "DayBarReader",
79
+ "MinBarReader",
80
+ "BlockReader",
81
+ "FinanceReader",
82
+ "DataProfile",
83
+ "CurrentnessMode",
84
+ "QuerySpec",
85
+ "QueryFingerprint",
86
+ "QueryPlan",
87
+ "QueryPlanner",
88
+ "ProvenanceKind",
89
+ "Provenance",
90
+ "ResultMeta",
91
+ "QueryResult",
92
+ "ProviderRegistry",
93
+ "PROVIDERS",
94
+ "StreamState",
95
+ "StatefulQuoteStream",
96
+ "AsyncStatefulQuoteStream",
97
+ "BatchItem",
98
+ "BatchResult",
99
+ "StreamSpec",
100
+ "UnifiedRuntime",
101
+ "FallbackPolicy",
102
+ "ProviderOrchestrator",
103
+ "ErrorEnvelope",
104
+ "to_error_envelope",
105
+ "records_from_response",
106
+ "FinancialRecord",
107
+ "FundRecord",
108
+ "BondRecord",
109
+ "NewsRecord",
110
+ "ResearchRecord",
111
+ "OptionRecord",
112
+ "MarketDataRecord",
113
+ "SearchRecord",
114
+ "MacroRecord",
115
+ "configure",
116
+ "get_config",
117
+ "load_config",
118
+ "observability",
119
+ "streaming",
120
+ ]
121
+
122
+ from . import codec, errors, protocol # noqa: E402,F401
123
+ from .errors import TdxError # noqa: E402,F401
124
+
125
+
126
+ def configure(**kwargs: Any) -> Any:
127
+ """以关键字参数覆盖进程级配置,并立即写回 :func:`get_config` 单例。
128
+
129
+ 键是配置段名,值是字段字典,例如 ``configure(core={"timeout": 8})``。
130
+ 返回合并后的 :class:`~atst.config.schema.Config`;后续 ``Client()``
131
+ 与 ``WebQuoteClient()`` 都读取到该值,显式构造参数仍然优先。
132
+ """
133
+ from .config import load_config
134
+
135
+ return load_config(overrides=kwargs, set_global=True)
136
+
137
+
138
+ def get_config() -> Any:
139
+ """获取当前生效的全局配置对象。"""
140
+ from .config import get_config as _get
141
+
142
+ return _get()
143
+
144
+
145
+ if TYPE_CHECKING: # pragma: no cover
146
+ from .config import load_config
147
+ from .providers import PROVIDERS, ProviderRegistry
148
+ from .query import CurrentnessMode, QueryFingerprint, QueryPlan, QueryPlanner, QuerySpec
149
+ from .reader import BlockReader, DataProfile, DayBarReader, FinanceReader, MinBarReader
150
+ from .result import Provenance, ProvenanceKind, QueryResult, ResultMeta
151
+ from .runtime.kernel import UnifiedRuntime
152
+ from .runtime.orchestration import FallbackPolicy, ProviderOrchestrator
153
+ from .stream_contract import StreamSpec
154
+ from .streaming.state import StreamState
155
+ from .streaming.stateful import AsyncStatefulQuoteStream, StatefulQuoteStream
156
+
157
+
158
+ _LAZY: dict[str, tuple[str, str]] = {
159
+ "Client": ("atst.client.api", "Client"),
160
+ "AsyncClient": ("atst.client.api", "AsyncClient"),
161
+ "DayBarReader": ("atst.reader", "DayBarReader"),
162
+ "MinBarReader": ("atst.reader", "MinBarReader"),
163
+ "BlockReader": ("atst.reader", "BlockReader"),
164
+ "FinanceReader": ("atst.reader", "FinanceReader"),
165
+ "DataProfile": ("atst.reader", "DataProfile"),
166
+ "CurrentnessMode": ("atst.query", "CurrentnessMode"),
167
+ "QuerySpec": ("atst.query", "QuerySpec"),
168
+ "QueryFingerprint": ("atst.query", "QueryFingerprint"),
169
+ "QueryPlan": ("atst.query", "QueryPlan"),
170
+ "QueryPlanner": ("atst.query", "QueryPlanner"),
171
+ "ProvenanceKind": ("atst.result", "ProvenanceKind"),
172
+ "Provenance": ("atst.result", "Provenance"),
173
+ "ResultMeta": ("atst.result", "ResultMeta"),
174
+ "QueryResult": ("atst.result", "QueryResult"),
175
+ "ProviderRegistry": ("atst.providers", "ProviderRegistry"),
176
+ "PROVIDERS": ("atst.providers", "PROVIDERS"),
177
+ "StreamState": ("atst.streaming.state", "StreamState"),
178
+ "StatefulQuoteStream": ("atst.streaming.stateful", "StatefulQuoteStream"),
179
+ "AsyncStatefulQuoteStream": (
180
+ "atst.streaming.stateful",
181
+ "AsyncStatefulQuoteStream",
182
+ ),
183
+ "BatchItem": ("atst.batch", "BatchItem"),
184
+ "BatchResult": ("atst.batch", "BatchResult"),
185
+ "StreamSpec": ("atst.stream_contract", "StreamSpec"),
186
+ "UnifiedRuntime": ("atst.runtime.kernel", "UnifiedRuntime"),
187
+ "FallbackPolicy": ("atst.runtime.orchestration", "FallbackPolicy"),
188
+ "ProviderOrchestrator": ("atst.runtime.orchestration", "ProviderOrchestrator"),
189
+ "ErrorEnvelope": ("atst.error_envelope", "ErrorEnvelope"),
190
+ "to_error_envelope": ("atst.error_envelope", "to_error_envelope"),
191
+ "records_from_response": ("atst.typed_query", "records_from_response"),
192
+ "FinancialRecord": ("atst.domain.records", "FinancialRecord"),
193
+ "FundRecord": ("atst.domain.records", "FundRecord"),
194
+ "BondRecord": ("atst.domain.records", "BondRecord"),
195
+ "NewsRecord": ("atst.domain.records", "NewsRecord"),
196
+ "ResearchRecord": ("atst.domain.records", "ResearchRecord"),
197
+ "OptionRecord": ("atst.domain.records", "OptionRecord"),
198
+ "MarketDataRecord": ("atst.domain.records", "MarketDataRecord"),
199
+ "SearchRecord": ("atst.domain.records", "SearchRecord"),
200
+ "MacroRecord": ("atst.domain.records", "MacroRecord"),
201
+ "load_config": ("atst.config", "load_config"),
202
+ "observability": ("atst.observability", ""),
203
+ "streaming": ("atst.streaming", ""),
204
+ }
205
+
206
+
207
+ def __getattr__(name: str) -> Any:
208
+ if name in _LAZY:
209
+ mod_path, attr = _LAZY[name]
210
+ import importlib
211
+
212
+ mod = importlib.import_module(mod_path)
213
+ value = mod if not attr else getattr(mod, attr)
214
+ globals()[name] = value
215
+ return value
216
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
217
+
218
+
219
+ def __dir__() -> list[str]:
220
+ return sorted(set(globals()) | set(_LAZY))
atst/__main__.py ADDED
@@ -0,0 +1,13 @@
1
+ # Copyright (c) 2026 atst contributors
2
+ # Licensed under the MIT License
3
+
4
+ """``python -m atst`` 入口(与 ``atst`` 控制台脚本等价)。"""
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ if __name__ == "__main__": # pragma: no cover
11
+ from .cli import main
12
+
13
+ sys.exit(main())
atst/_version.py ADDED
@@ -0,0 +1,6 @@
1
+ # Copyright (c) 2026 atst contributors
2
+ # Licensed under the MIT License
3
+
4
+ """Single source of truth for the atst package version."""
5
+
6
+ __version__ = "1.4.3"
atst/batch.py ADDED
@@ -0,0 +1,218 @@
1
+ # Copyright (c) 2026 atst contributors
2
+ # Licensed under the MIT License
3
+
4
+ """Canonical batch audit contracts.
5
+
6
+ The batch path is one loop in :meth:`atst.runtime.kernel.UnifiedRuntime.quotes_batch`:
7
+ it expands the requested symbols into independent single-Provider requests and
8
+ records each outcome here. There is no separate batch request envelope, no
9
+ coalescing layer and no negative cache — one symbol, one direct Provider call.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import copy
15
+ from collections.abc import Mapping, Sequence
16
+ from dataclasses import dataclass, field
17
+ from types import MappingProxyType
18
+ from typing import Any, Generic, TypeVar
19
+
20
+ from .error_envelope import ErrorEnvelope
21
+
22
+ __all__ = ["BatchItem", "BatchResult"]
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class BatchItem(Generic[T]):
29
+ status: str
30
+ value: T | None = None
31
+ error: Exception | None = None
32
+
33
+ def __post_init__(self) -> None:
34
+ if self.status not in {"ok", "failed", "missing", "not_attempted"}:
35
+ raise ValueError(f"invalid batch status: {self.status!r}")
36
+ if self.status == "ok" and self.error is not None:
37
+ raise ValueError("successful batch item cannot carry error")
38
+ if self.status == "ok" and self.value is None:
39
+ raise ValueError("successful batch item requires value")
40
+ if self.status != "ok" and self.value is not None:
41
+ raise ValueError("non-success batch item cannot carry value")
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class BatchResult(Generic[T]):
46
+ """Auditable outcome of one batch request.
47
+
48
+ ``items`` carries either a ``{symbol: BatchItem}`` mapping (what
49
+ :meth:`build` and :meth:`atst.runtime.kernel.UnifiedRuntime.quotes_batch`
50
+ produce) or a plain value sequence. ``errors`` maps a requested symbol to
51
+ the safe :class:`~atst.error_envelope.ErrorEnvelope` that explains why it
52
+ is absent, and ``partial`` follows from — and must agree with — whether
53
+ ``errors`` is non-empty.
54
+ """
55
+
56
+ items: tuple[T, ...] | Mapping[str, BatchItem[Any]] = ()
57
+ errors: Mapping[str, ErrorEnvelope] = field(default_factory=dict)
58
+ requested: tuple[str, ...] = ()
59
+ partial: bool = False
60
+ meta: Any = field(default_factory=dict)
61
+
62
+ def __post_init__(self) -> None:
63
+ items = self.items
64
+ if isinstance(items, Mapping):
65
+ object.__setattr__(self, "items", MappingProxyType(copy.deepcopy(dict(items))))
66
+ elif not isinstance(items, tuple):
67
+ object.__setattr__(self, "items", tuple(items))
68
+
69
+ requested = self.requested
70
+ if not isinstance(requested, tuple):
71
+ object.__setattr__(self, "requested", tuple(requested))
72
+
73
+ errors = self.errors
74
+ if not isinstance(errors, Mapping):
75
+ raise TypeError("BatchResult.errors 必须是 Mapping[str, ErrorEnvelope]")
76
+ if bool(errors) != bool(self.partial):
77
+ raise ValueError(
78
+ "BatchResult.partial 必须与 errors 是否为空严格一致:"
79
+ f"partial={self.partial!r} errors={len(errors)}"
80
+ )
81
+ if requested:
82
+ unexpected = sorted(set(errors) - set(requested))
83
+ if unexpected:
84
+ raise ValueError(f"errors 包含未请求的 symbol: {unexpected}")
85
+ # 防御性拷贝:调用方后续修改传入的映射/上下文不得影响已产出的审计结果。
86
+ object.__setattr__(
87
+ self,
88
+ "errors",
89
+ MappingProxyType({key: copy.deepcopy(value) for key, value in errors.items()}),
90
+ )
91
+ object.__setattr__(self, "meta", _safe_deepcopy(self.meta))
92
+
93
+ @classmethod
94
+ def build(
95
+ cls,
96
+ items: Mapping[str, BatchItem[T]],
97
+ *,
98
+ errors: Mapping[str, ErrorEnvelope] | None = None,
99
+ requested: Sequence[str] = (),
100
+ partial: bool | None = None,
101
+ meta: Any = None,
102
+ ) -> BatchResult[T]:
103
+ error_map = dict(errors or {})
104
+ return cls(
105
+ items=dict(items),
106
+ errors=error_map,
107
+ requested=tuple(requested),
108
+ partial=bool(error_map) if partial is None else bool(partial),
109
+ meta={} if meta is None else meta,
110
+ )
111
+
112
+ def __deepcopy__(self, memo: dict[int, Any]) -> BatchResult[T]:
113
+ items = self.items
114
+ clone = self.__class__(
115
+ # 保留 ``items`` 的形态:映射视图按符号建键,值序列保持元组。
116
+ items=dict(items) if isinstance(items, Mapping) else tuple(items),
117
+ errors=dict(self.errors),
118
+ requested=self.requested,
119
+ partial=self.partial,
120
+ meta=_safe_deepcopy(self.meta, memo),
121
+ )
122
+ memo[id(self)] = clone
123
+ return clone
124
+
125
+ def _status_map(self) -> dict[str, str]:
126
+ if isinstance(self.items, Mapping):
127
+ return {str(key): item.status for key, item in self.items.items()}
128
+ statuses: dict[str, str] = {}
129
+ for symbol in self.requested:
130
+ if symbol not in statuses:
131
+ statuses[symbol] = self.status_for(symbol)
132
+ return statuses
133
+
134
+ def status_for(self, symbol: str) -> str:
135
+ """Return the auditable status of one requested symbol."""
136
+
137
+ envelope = self.errors.get(symbol)
138
+ if envelope is not None:
139
+ recorded = envelope.context.get("batch_status")
140
+ if isinstance(recorded, str) and recorded:
141
+ return recorded
142
+ return "failed"
143
+ items = self.items
144
+ if isinstance(items, Mapping):
145
+ item = items.get(symbol)
146
+ if item is not None:
147
+ return item.status
148
+ if symbol in set(self.requested):
149
+ return "ok"
150
+ raise KeyError(symbol)
151
+
152
+ @property
153
+ def status_counts(self) -> Mapping[str, int]:
154
+ counts = {"ok": 0, "failed": 0, "missing": 0, "not_attempted": 0}
155
+ for status in self._status_map().values():
156
+ if status in counts:
157
+ counts[status] += 1
158
+ else: # pragma: no cover - defensive: never silently drop an audit row
159
+ counts[status] = counts.get(status, 0) + 1
160
+ return MappingProxyType(counts)
161
+
162
+ @property
163
+ def success(self) -> bool:
164
+ if self.errors:
165
+ return False
166
+ items = self.items
167
+ if isinstance(items, Mapping):
168
+ return all(item.status == "ok" for item in items.values())
169
+ return True
170
+
171
+ @property
172
+ def failed(self) -> tuple[str, ...]:
173
+ return tuple(key for key, status in self._status_map().items() if status == "failed")
174
+
175
+ @property
176
+ def missing(self) -> tuple[str, ...]:
177
+ return tuple(key for key, status in self._status_map().items() if status == "missing")
178
+
179
+ @property
180
+ def not_attempted(self) -> tuple[str, ...]:
181
+ return tuple(key for key, status in self._status_map().items() if status == "not_attempted")
182
+
183
+ def to_dict(self) -> dict[str, Any]:
184
+ items = self.items
185
+ if isinstance(items, Mapping):
186
+ values: list[Any] = [
187
+ _serialize_item(item.value) for item in items.values() if item.status == "ok"
188
+ ]
189
+ else:
190
+ values = [_serialize_item(value) for value in items]
191
+ return {
192
+ "items": values,
193
+ "errors": {key: value.to_dict() for key, value in self.errors.items()},
194
+ "requested": list(self.requested),
195
+ "partial": self.partial,
196
+ "meta": _serialize_item(self.meta),
197
+ }
198
+
199
+
200
+ def _safe_deepcopy(value: Any, memo: dict[int, Any] | None = None) -> Any:
201
+ """Deep-copy a diagnostic payload, degrading to the original on failure."""
202
+
203
+ try:
204
+ return copy.deepcopy(value, memo) if memo is not None else copy.deepcopy(value)
205
+ except Exception:
206
+ return value
207
+
208
+
209
+ def _serialize_item(value: Any) -> Any:
210
+ to_dict = getattr(value, "to_dict", None)
211
+ if callable(to_dict):
212
+ try:
213
+ return to_dict()
214
+ except Exception: # pragma: no cover - defensive: never fail serialization
215
+ return value
216
+ if isinstance(value, Mapping):
217
+ return dict(value)
218
+ return value
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2026 atst contributors
2
+ # Licensed under the MIT License
3
+
4
+ """Static capability and Provider contract facts.
5
+
6
+ This package holds the *declarations* the execution kernel consults but never
7
+ mutates: the capability catalog and its call validation, the Provider
8
+ ``channel -> adapter`` binding tables, and the Provider-isolation contract,
9
+ guards and consistency audits. It performs no provider selection and has no
10
+ fallback behaviour; runtime execution lives in :mod:`atst.runtime`.
11
+
12
+ Submodules are imported by their full path (``atst.catalog.capability``) so
13
+ the dependency direction toward :mod:`atst.runtime` stays one-way.
14
+ """