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.
- atst/__init__.py +220 -0
- atst/__main__.py +13 -0
- atst/_version.py +6 -0
- atst/batch.py +218 -0
- atst/catalog/__init__.py +14 -0
- atst/catalog/capability.py +797 -0
- atst/catalog/capability_audit.py +70 -0
- atst/catalog/provider_audit.py +52 -0
- atst/catalog/provider_bindings.py +305 -0
- atst/catalog/provider_contract.py +77 -0
- atst/catalog/provider_guard.py +51 -0
- atst/charset/__init__.py +42 -0
- atst/charset/encoding.py +468 -0
- atst/cli/__init__.py +68 -0
- atst/cli/_common.py +243 -0
- atst/cli/parser.py +414 -0
- atst/cli/runtime_commands.py +1027 -0
- atst/client/__init__.py +97 -0
- atst/client/_mixin.py +1000 -0
- atst/client/api.py +645 -0
- atst/client/async_.py +484 -0
- atst/client/core.py +365 -0
- atst/client/factory.py +66 -0
- atst/client/sync.py +654 -0
- atst/codec/__init__.py +60 -0
- atst/codec/framing.py +370 -0
- atst/codec/primitive.py +654 -0
- atst/config/__init__.py +58 -0
- atst/config/loader.py +321 -0
- atst/config/schema.py +340 -0
- atst/diagnostics.py +178 -0
- atst/domain/__init__.py +36 -0
- atst/domain/adjust.py +348 -0
- atst/domain/calendar.py +479 -0
- atst/domain/finance.py +300 -0
- atst/domain/integrity.py +106 -0
- atst/domain/models.py +256 -0
- atst/domain/period.py +83 -0
- atst/domain/records.py +383 -0
- atst/domain/symbol.py +290 -0
- atst/error_envelope.py +247 -0
- atst/errors.py +504 -0
- atst/feedback/__init__.py +41 -0
- atst/feedback/reporter.py +313 -0
- atst/feedback/stats.py +226 -0
- atst/feedback/telemetry.py +220 -0
- atst/integration/__init__.py +22 -0
- atst/integration/mcp/__init__.py +40 -0
- atst/integration/mcp/__main__.py +15 -0
- atst/integration/mcp/_common.py +98 -0
- atst/integration/mcp/_server.py +272 -0
- atst/integration/mcp/_tools_impl.py +167 -0
- atst/integration/mcp/_tools_spec.py +205 -0
- atst/integration/runtime_http.py +323 -0
- atst/integration/runtime_ws.py +623 -0
- atst/integration/runtime_ws_server.py +123 -0
- atst/integration/serialization.py +59 -0
- atst/integration/wire_fields.py +254 -0
- atst/observability/__init__.py +86 -0
- atst/observability/metrics.py +603 -0
- atst/observability/otel_exporter.py +578 -0
- atst/observability/prometheus_exporter.py +229 -0
- atst/observability/statsd_exporter.py +451 -0
- atst/output/__init__.py +404 -0
- atst/profile/__init__.py +101 -0
- atst/profile/detect.py +788 -0
- atst/profile/presets.py +226 -0
- atst/protocol/__init__.py +69 -0
- atst/protocol/commands.py +380 -0
- atst/protocol/generic.py +403 -0
- atst/protocol/handshake.py +130 -0
- atst/protocol/parsers/__init__.py +182 -0
- atst/protocol/parsers/_std7709_bars.py +488 -0
- atst/protocol/parsers/_std7709_common.py +191 -0
- atst/protocol/parsers/_std7709_quote.py +400 -0
- atst/protocol/parsers/f10.py +111 -0
- atst/protocol/parsers/goods.py +372 -0
- atst/protocol/parsers/mac.py +367 -0
- atst/protocol/parsers/std7709.py +93 -0
- atst/protocol/parsers/std7709_extra.py +448 -0
- atst/protocol/parsers/std7727.py +348 -0
- atst/protocol/prober.py +657 -0
- atst/protocol/registry.py +463 -0
- atst/providers/__init__.py +776 -0
- atst/py.typed +4 -0
- atst/query.py +743 -0
- atst/reader/__init__.py +50 -0
- atst/reader/formats.py +573 -0
- atst/reader/profile.py +719 -0
- atst/result.py +166 -0
- atst/runtime/__init__.py +14 -0
- atst/runtime/audit.py +35 -0
- atst/runtime/executor.py +992 -0
- atst/runtime/freshness.py +83 -0
- atst/runtime/identity.py +37 -0
- atst/runtime/kernel.py +260 -0
- atst/runtime/orchestration.py +197 -0
- atst/runtime/provenance.py +54 -0
- atst/sink/__init__.py +20 -0
- atst/sink/local_day.py +308 -0
- atst/stream_contract.py +105 -0
- atst/streaming/__init__.py +52 -0
- atst/streaming/base.py +672 -0
- atst/streaming/engine.py +525 -0
- atst/streaming/push.py +313 -0
- atst/streaming/state.py +132 -0
- atst/streaming/stateful.py +237 -0
- atst/tools/__init__.py +50 -0
- atst/tools/_console.py +25 -0
- atst/tools/_yaml_min.py +494 -0
- atst/tools/capture.py +981 -0
- atst/tools/check_originality.py +726 -0
- atst/tools/codegen.py +537 -0
- atst/tools/golden_audit.py +560 -0
- atst/tools/golden_expand.py +422 -0
- atst/tools/host_audit.py +547 -0
- atst/tools/spec_audit.py +552 -0
- atst/trade/__init__.py +166 -0
- atst/trade/client.py +271 -0
- atst/trade/constants.py +111 -0
- atst/trade/errors.py +48 -0
- atst/trade/frames.py +415 -0
- atst/trade/security.py +49 -0
- atst/trade/simulator.py +594 -0
- atst/transport/__init__.py +56 -0
- atst/transport/_validation.py +318 -0
- atst/transport/async_.py +1452 -0
- atst/transport/base.py +584 -0
- atst/transport/hosts.py +791 -0
- atst/transport/pool.py +1208 -0
- atst/transport/ratelimit.py +336 -0
- atst/transport/sniff.py +437 -0
- atst/transport/speedtest.py +387 -0
- atst/typed_query.py +691 -0
- atst/universe/__init__.py +333 -0
- atst/universe/_classes.py +290 -0
- atst/universe/_scan.py +52 -0
- atst/universe/_sources.py +282 -0
- atst/web/__init__.py +529 -0
- atst/web/_base_core.py +352 -0
- atst/web/_base_em.py +169 -0
- atst/web/_base_http.py +624 -0
- atst/web/_base_retry.py +107 -0
- atst/web/_mob_fund.py +149 -0
- atst/web/_paginate.py +182 -0
- atst/web/_session_astock.py +116 -0
- atst/web/_session_baidu.py +340 -0
- atst/web/_session_efinance.py +369 -0
- atst/web/_session_fund_v2.py +295 -0
- atst/web/_session_fundamental.py +330 -0
- atst/web/_session_gaps.py +601 -0
- atst/web/_session_info.py +665 -0
- atst/web/_session_market.py +665 -0
- atst/web/_session_news.py +75 -0
- atst/web/_session_p1.py +151 -0
- atst/web/_session_p2.py +258 -0
- atst/web/_session_p3.py +295 -0
- atst/web/adapters_fund.py +264 -0
- atst/web/astock_toolkit.py +344 -0
- atst/web/baidu/__init__.py +9 -0
- atst/web/baidu/adapters.py +449 -0
- atst/web/base.py +114 -0
- atst/web/boards.py +513 -0
- atst/web/boc/__init__.py +9 -0
- atst/web/boc/adapters.py +66 -0
- atst/web/chip.py +293 -0
- atst/web/cninfo/__init__.py +2 -0
- atst/web/cninfo/adapters.py +332 -0
- atst/web/corporate.py +1132 -0
- atst/web/eastmoney/__init__.py +9 -0
- atst/web/eastmoney/adapters.py +603 -0
- atst/web/efinance_deriv.py +423 -0
- atst/web/efinance_fund.py +378 -0
- atst/web/efinance_options.py +263 -0
- atst/web/esg.py +499 -0
- atst/web/fin_report.py +412 -0
- atst/web/fund_company.py +345 -0
- atst/web/fund_manager.py +289 -0
- atst/web/fund_rank.py +396 -0
- atst/web/fundflow.py +1015 -0
- atst/web/global_market.py +244 -0
- atst/web/governance.py +455 -0
- atst/web/hot_rank.py +136 -0
- atst/web/jsl/__init__.py +9 -0
- atst/web/jsl/adapters.py +69 -0
- atst/web/limits.py +38 -0
- atst/web/longhu.py +189 -0
- atst/web/market_stats.py +200 -0
- atst/web/news.py +416 -0
- atst/web/normalize.py +603 -0
- atst/web/session.py +182 -0
- atst/web/sina/__init__.py +9 -0
- atst/web/sina/adapters.py +558 -0
- atst/web/sources.py +529 -0
- atst/web/tencent/__init__.py +9 -0
- atst/web/tencent/adapters.py +734 -0
- atst/web/ths/__init__.py +2 -0
- atst/web/ths/adapters.py +245 -0
- atst/web/ticks.py +260 -0
- atst/web/wallstreet/__init__.py +2 -0
- atst/web/wallstreet/adapters.py +92 -0
- atst/web/wencai.py +159 -0
- atst-1.4.3.dist-info/METADATA +671 -0
- atst-1.4.3.dist-info/RECORD +207 -0
- atst-1.4.3.dist-info/WHEEL +4 -0
- atst-1.4.3.dist-info/entry_points.txt +2 -0
- 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
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
|
atst/catalog/__init__.py
ADDED
|
@@ -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
|
+
"""
|