tyzarr 0.1.1__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.
@@ -0,0 +1,14 @@
1
+ .idea
2
+ *.log
3
+ *.env
4
+ !*.env.example
5
+ test
6
+ agents
7
+ *.pyc
8
+ *__pycache
9
+ .superpowers
10
+
11
+ # build artifacts
12
+ dist/
13
+ build/
14
+ *.egg-info/
tyzarr-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,452 @@
1
+ Metadata-Version: 2.5
2
+ Name: tyzarr
3
+ Version: 0.1.1
4
+ Summary: 把 xarray.Dataset 存成不可变 zarr v3 快照,支持本地目录与 MinIO,用带 CAS 的 metadata.json 做原子发布
5
+ Author: Rex
6
+ Keywords: dataset,minio,s3,snapshot,timeseries,xarray,zarr
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Database
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: aiobotocore>=2.0
18
+ Requires-Dist: botocore>=1.31
19
+ Requires-Dist: certifi>=2024.2.2
20
+ Requires-Dist: dask[array]>=2024.1.0
21
+ Requires-Dist: minio>=7.2
22
+ Requires-Dist: numpy>=1.26
23
+ Requires-Dist: pandas>=2.2
24
+ Requires-Dist: s3fs>=2024.2.0
25
+ Requires-Dist: urllib3>=2.0
26
+ Requires-Dist: xarray>=2024.3.0
27
+ Requires-Dist: zarr>=3.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # tyzarr 使用说明(v1)
31
+
32
+ tyzarr 把 `xarray.Dataset` 存成 zarr v3 快照,放在本地目录或 MinIO 上,并用一个 `metadata.json`
33
+ 记录"哪一天的数据在哪个快照里"。
34
+
35
+ - 每次 `write()` 生成 **新的、不可变的**快照文件,不在原文件上修改;
36
+ - 写完快照后再发布 `metadata.json`(带 etag 的 CAS)。读者看到的要么是旧版本,要么是新版本,不会读到写了一半的数据;
37
+ - 被替换下来的快照留作历史版本(每个日期最多保留 3 个),超出且不再被引用的快照会自动删除。
38
+
39
+ > v1 不保证向后兼容:以后的版本可能改动 `metadata.json` 格式,届时需要重写数据。
40
+
41
+ ## 安装
42
+
43
+ ```bash
44
+ pip install tyzarr
45
+ # 或
46
+ uv add tyzarr
47
+ ```
48
+
49
+ 需要 Python 3.12 及以上。
50
+
51
+ ---
52
+
53
+ ## 1. 两种数据集类型
54
+
55
+ | | 时序数据集 | 截面(快照)数据集 |
56
+ |------------------|----------------------------------------------------|------------------------------------|
57
+ | 创建方式 | `write(data, time_axis="Date")` | `write(data, time_axis=None)` |
58
+ | 存储单位 | 按**日期**登记,每个日期指向一个快照 | 整个数据集只有一个当前快照 |
59
+ | 再次写入 | 追加新日期;已有日期必须传 `replace=True` 才能覆盖 | 每次写入都**整体替换**旧数据 |
60
+ | 读取 | `read(dstr=...)` 按日期读 | `read()`,不能传 `dstr` |
61
+ | 合并 `compact()` | 支持 | 不支持(只有一个快照,没必要) |
62
+ | 典型数据 | 日频行情、因子、分钟线宽表 | 合约信息表、行业映射等"最新状态"表 |
63
+
64
+ `time_axis` 在第一次写入时确定,之后每次写入都必须传同一个值,否则报错。
65
+
66
+ 时序数据集的时间轴按 **自然日**登记:每个值会被规整成 `YYYY-MM-DD`,所以 **一天只能有一个时间轴取值**(日内时间请放到另一个维度,比如
67
+ `Timestamp` / `Slot`)。时间轴坐标可以是 `"2026-10-08"` 这样的字符串。
68
+
69
+ 截面数据集里也可以有名为 `Date` 的维度,此时它只是普通维度:用 `indexers` 筛选,不用 `dstr`。
70
+
71
+ ---
72
+
73
+ ## 2. 使用限制(v1)
74
+
75
+ ### 2.1 同一个数据集不能并发写
76
+
77
+ 同一个数据集同一时刻只能有 **一个**写入方,包括 `write()`、`compact()`、`rename()` 三种操作。v1 只承诺串行写入的正确性。
78
+
79
+ - 请在调度层保证串行:例如每个数据集只由一个定时任务写,`compact()` 安排在写入窗口之外执行;
80
+ - 写入发布失败后,已经写出且未被引用的快照不会自动清理,可能留在 `data/` 目录里成为孤儿文件。
81
+
82
+ **不同的数据集**之间互不影响,可以并行写。
83
+
84
+ ### 2.2 读写并发的注意点
85
+
86
+ 读和写可以同时进行,读者总是看到一个完整的版本。但有一个例外:
87
+
88
+ - `read(load=False)` 返回的是延迟(dask)数据,只有真正计算时才去读文件;
89
+ - 如果在你计算之前,另一方执行了 `compact()` 或多次覆盖写,把你引用的旧快照删除了,计算会报文件不存在;
90
+ - 需要长期持有结果时,请用 `load=True`,或者尽快 `.load()`。
91
+
92
+ ### 2.3 不支持辅助坐标
93
+
94
+ 只支持 **维度坐标**(坐标名 = 维度名,一维、无重复、无缺失)。像下面这种挂在维度上的非维度坐标(辅助坐标) **v1 不支持**:
95
+
96
+ ```python
97
+ data = data.assign_coords(ExchangeID=("InstrumentID", ["SHFE", "DCE", "CZCE"])) # 不要这样写
98
+ ```
99
+
100
+ v1 不会登记、也不会校验辅助坐标;多日读取后它会变成带时间轴的变量,`compact()` 后的结果也不保证一致。需要这类属性时,请:
101
+
102
+ - 单独存一个截面数据集(如 `instrument_info`),读出后自己 join;或者
103
+ - 把它存成一个普通指标(data variable)。
104
+
105
+ 辅助坐标的正式支持在后续版本设计中。
106
+
107
+ ### 2.4 Schema 规则
108
+
109
+ 第一次写入会登记 schema,之后每次写入都要符合它:
110
+
111
+ | 项目 | 规则 |
112
+ |--------------|---------------------------------------------------------------------------------------------------------|
113
+ | 维度集合 | 每次写入的维度集合必须和登记的**完全一致**,不能增减维度 |
114
+ | 坐标类型 | 每个维度坐标的类型固定(字符串坐标统一登记为 `string`;数值坐标必须同一 dtype) |
115
+ | 坐标值 | 一维、无重复标签、无缺失值;同一个变量里不能出现重复维度(如协方差矩阵请改成 `Industry` × `Industry2`) |
116
+ | 指标 | 可以**新增**指标,也可以某次只写一部分指标;但已登记指标的维度和 dtype 不能变 |
117
+ | 指标 dtype | 严格比对,`int64` 和 `float64` 不同,会报错;建议数值指标统一用 `float64` |
118
+ | 默认值 | `metric_options` 里设置的默认值一旦登记,不能修改 |
119
+ | 名称 | 维度名和指标名不能重名 |
120
+ | 坐标标签集合 | 可以每天不同(例如每天的合约列表不同),多日读取时取并集,缺的位置为 NaN |
121
+
122
+ ### 2.5 其他限制
123
+
124
+ - **历史日期默认不可改**:写入已存在的日期会报错,必须显式 `replace=True`;
125
+ - **整日替换**:`replace=True` 替换的是该日期的 **整个切片**,不是逐个单元格合并。只想改一个指标时,也要把这一天的完整数据重新写一遍;
126
+ - **历史版本只保留 3 个**:每个日期(或截面数据集)最多保留 3 个旧版本,可用 `history()` 查看,更早的会被清理;
127
+ - **按日期列表读取**:`dstr` 是具体的日期列表,不是区间;读区间请先 `list_dates()` 再筛选(见 4.2);
128
+ - 读取时不存在的日期只会打一条 logging 警告并跳过;`indexers` 里写了不存在的标签会报 `KeyError`;
129
+ - 时间轴不能重命名,不能放进 `indexers`;
130
+ - 不适用于高频小批量写入(每次写入都会生成新快照并发布元数据),推荐"日频写入 + 定期 compact"。
131
+
132
+ ---
133
+
134
+ ## 3. 初始化
135
+
136
+ ```python
137
+ from tyzarr.backend import LocalStorage, MinIOStorage
138
+ from tyzarr.dataset import Dataset
139
+
140
+ # 本地目录
141
+ backend = LocalStorage("D:/zarr_root")
142
+
143
+ # 或 MinIO:url 只写主机和端口,协议由 secure 决定;bucket 只写桶名
144
+ # backend = MinIOStorage(key, secret, "minio.example.com:9080", "my-bucket", secure=False)
145
+
146
+ store = Dataset(
147
+ "daily_quote", # 数据集名,存储在 zarr/daily_quote/ 下
148
+ backend=backend,
149
+ granularity="batch", # 快照粒度,见第 6 节
150
+ metric_options=None, # 指标默认值,例如 {"Volume": 0.0}
151
+ )
152
+ ```
153
+
154
+ 存储布局:
155
+
156
+ ```
157
+ zarr/<数据集名>/
158
+ ├── metadata.json # 日期 -> 快照路径、schema、重命名
159
+ └── data/
160
+ ├── 20261012093000_<uuid>.zarr
161
+ └── ...
162
+ ```
163
+
164
+ ---
165
+
166
+ ## 4. 时序数据集
167
+
168
+ 下面的例子都用这个构造函数生成数据:
169
+
170
+ ```python
171
+ import numpy as np
172
+ import pandas as pd
173
+ import xarray as xr
174
+
175
+
176
+ def make_daily(dates, instruments=("A", "B", "C")):
177
+ """生成 Date × InstrumentID 的日频数据"""
178
+ shape = (len(dates), len(instruments))
179
+ return xr.Dataset(
180
+ data_vars={
181
+ "Close": (("Date", "InstrumentID"), np.random.rand(*shape)),
182
+ "Volume": (("Date", "InstrumentID"), np.random.rand(*shape) * 1000),
183
+ },
184
+ coords={
185
+ "Date": pd.to_datetime(dates).values,
186
+ "InstrumentID": list(instruments),
187
+ },
188
+ )
189
+ ```
190
+
191
+ ### 4.1 写入
192
+
193
+ ```python
194
+ store = Dataset("daily_quote", backend=backend)
195
+
196
+ # 首次写入:登记 time_axis 和 schema
197
+ store.write(make_daily(["2026-10-08", "2026-10-09"]), time_axis="Date")
198
+
199
+ # 追加新日期
200
+ store.write(make_daily(["2026-10-10"]), time_axis="Date")
201
+
202
+ # 已有日期再写会报错:ValueError: 禁止修改历史日期 ...
203
+ # 需要覆盖时显式 replace=True(整日替换,旧版本进入 history)
204
+ store.write(make_daily(["2026-10-10"]), time_axis="Date", replace=True)
205
+ ```
206
+
207
+ `write()` 常用参数:
208
+
209
+ | 参数 | 默认 | 说明 |
210
+ |------------------|----------------|------------------------------------------------------------------------|
211
+ | `time_axis` | 必填 | 时间轴维度名;截面数据集传 `None` |
212
+ | `replace` | `False` | 是否允许覆盖已存在的日期 |
213
+ | `granularity` | 跟随 `Dataset` | 本次写入的快照粒度,见第 6 节 |
214
+ | `chunks` | 自动 | 非时间维度的 chunk 大小,例如 `{"InstrumentID": 1000}`;时间维固定为 1 |
215
+ | `compress_level` | `3` | zstd 压缩级别 1–22,`None` 不压缩 |
216
+ | `shard_days` | `256` | 每个分片最多包含的天数(单个分片未压缩不超过 128 MiB);`None` 不分片 |
217
+ | `write_workers` | `8` | 一次写多个快照时的并行线程数 |
218
+
219
+ ### 4.2 查询后读取
220
+
221
+ 先查有哪些日期,再按需读取:
222
+
223
+ ```python
224
+ dates = store.list_dates()
225
+ # ['2026-10-08', '2026-10-09', '2026-10-10']
226
+
227
+ # 读一个日期区间:在日期列表上筛选,再传给 read
228
+ want = [d for d in dates if "2026-10-09" <= d <= "2026-10-10"]
229
+ ds = store.read(want, load=True)
230
+
231
+ # 读单日
232
+ ds = store.read("2026-10-10", load=True)
233
+
234
+ # 不传 dstr:读全部日期
235
+ ds = store.read(load=True)
236
+ ```
237
+
238
+ 按指标、维度标签筛选(会下推到存储层,只读需要的块):
239
+
240
+ ```python
241
+ ds = store.read(
242
+ want,
243
+ metrics=["Close"], # 字符串或列表
244
+ indexers={"InstrumentID": ["A", "C"]}, # 标签列表 / 元组 / slice;不能包含时间轴
245
+ fill_value={"Close": 0.0}, # 读出后把 NaN 填成默认值
246
+ load=True,
247
+ )
248
+ ```
249
+
250
+ `read()` 参数:
251
+
252
+ | 参数 | 说明 |
253
+ |----------------|-------------------------------------------------------------------------|
254
+ | `dstr` | `str` / `list[str]` / `None`。具体日期(不是区间),`None` 表示全部日期 |
255
+ | `metrics` | 只读这些指标;不存在的指标会被忽略 |
256
+ | `indexers` | `{维度: 标签列表或 slice}`,语义同 `Dataset.sel` |
257
+ | `fill_value` | `{指标: 填充值}`,覆盖 `metric_options` 中的默认值 |
258
+ | `load` | `True` 立即读入内存;`False` 返回 dask 延迟数据 |
259
+ | `open_workers` | 并行打开快照的线程数,默认 8 |
260
+
261
+ 数据集不存在或没有匹配的日期时返回 `None`。
262
+
263
+ #### 延迟读取(大范围数据)
264
+
265
+ `load=False` 时返回 dask 数据,可以先构建计算再一次性执行。用完需要 `close()` 释放底层文件句柄:
266
+
267
+ ```python
268
+ ds = store.read(dates, metrics=["Close", "Volume"]) # 不读数据,只建图
269
+ try:
270
+ amount = (ds["Close"] * ds["Volume"]).sum("InstrumentID")
271
+ result = amount.compute()
272
+ finally:
273
+ ds.close()
274
+ ```
275
+
276
+ ### 4.3 查看历史版本
277
+
278
+ ```python
279
+ h = store.history("2026-10-10", metrics="Close", load=True)
280
+ h["current"] # 当前版本
281
+ h["history"] # 旧版本列表,最新的在前,最多 3 个
282
+ ```
283
+
284
+ ---
285
+
286
+ ## 5. 截面(快照)数据集
287
+
288
+ `time_axis=None`。每次写入都是一份完整的新数据,整体替换上一份;不需要 `replace=True`。
289
+
290
+ ```python
291
+ info = Dataset("instrument_info", backend=backend)
292
+
293
+ data = xr.Dataset(
294
+ data_vars={
295
+ "Multiplier": ("InstrumentID", [10.0, 5.0, 20.0]),
296
+ "TickSize": ("InstrumentID", [1.0, 0.5, 0.2]),
297
+ },
298
+ coords={"InstrumentID": ["A", "B", "C"]},
299
+ )
300
+ info.write(data, time_axis=None)
301
+
302
+ # 读取:不能传 dstr
303
+ ds = info.read(load=True)
304
+ ds = info.read(metrics="Multiplier", indexers={"InstrumentID": ["A"]}, load=True)
305
+
306
+ # 再写一次即整体替换;上一份进入历史
307
+ info.write(data.sel(InstrumentID=["A", "B"]), time_axis=None)
308
+ info.history()["history"] # 被替换下来的旧版本
309
+ ```
310
+
311
+ 截面数据集里的 `Date` 若是维度,就是普通维度,用 `indexers={"Date": [...]}` 筛选。
312
+
313
+ ---
314
+
315
+ ## 6. 快照粒度与 compact(写后压缩)
316
+
317
+ ### 6.1 快照粒度 `granularity`
318
+
319
+ 决定一次 `write()` 产生几个快照文件:
320
+
321
+ | 取值 | 含义 |
322
+ |----------------------------|---------------------------------|
323
+ | `"batch"`(默认)/ `"all"` | 本次写入的全部日期放进一个快照 |
324
+ | `"day"` | 每天一个快照 |
325
+ | `"month"` / `"year"` | 按自然月 / 年分组,每组一个快照 |
326
+ | 正整数 `n` | 从 1970-01-01 起每 `n` 天一组 |
327
+
328
+ 可以在 `Dataset(..., granularity=...)` 上设默认值,也可以在 `write()` / `compact()` 上单独指定。
329
+
330
+ ### 6.2 为什么要 compact
331
+
332
+ 每天写一次会每天产生一个快照。快照越多,大范围读取要打开的文件越多,速度越慢(MinIO 上尤其明显)。`compact()`
333
+ 把同一个分组(例如同一个月)里的多个小快照合并成一个大快照:
334
+
335
+ - 数据内容不变;坐标取各快照的 **并集**,缺失位置补 NaN;
336
+ - 某些 `int` 指标因补 NaN 会变成 `float`,结果会在 `upcast` 字段中报告;
337
+ - 合并后旧的小快照不再被引用,会被删除(`keep_history=True` 时旧快照保留在历史中);
338
+ - 只支持时序数据集。
339
+
340
+ 推荐用法: **每天写入 + 每月(或每周)执行一次 `compact(granularity="month")`**。在 5000 标的 × 40 时点 × 5
341
+ 年的测试中,月快照比日快照的全量读取快很多。
342
+
343
+ ### 6.3 用法
344
+
345
+ ```python
346
+ # 先 dry_run 看计划,不写任何数据
347
+ report = store.compact(granularity="month", dry_run=True)
348
+ print(report["planned"])
349
+
350
+ # 合并全部日期,按月
351
+ report = store.compact(granularity="month")
352
+
353
+ # 只合并指定日期(同样是日期列表,不是区间)
354
+ dates = [d for d in store.list_dates() if d.startswith("2026-10")]
355
+ report = store.compact(dates, granularity="month")
356
+ ```
357
+
358
+ 返回的报告:
359
+
360
+ ```python
361
+ {
362
+ "granularity": "month",
363
+ "dates": 3, # 参与的日期数
364
+ "buckets": 1, # 分组数
365
+ "dry_run": False,
366
+ "planned": [...], # 需要合并的分组
367
+ "skipped": [...], # 已经是单个快照,跳过
368
+ "rewritten": [ # 实际合并的分组
369
+ {"bucket": "2026-10", "dates": 3, "stores": 3, "path": "...zarr"}
370
+ ],
371
+ "removed": [...], # 被清理的旧快照
372
+ }
373
+ ```
374
+
375
+ `compact()` 其他参数 `chunks` / `compress_level` / `shard_days` / `open_workers` 与 `write()` 同义。
376
+
377
+ **注意**:`compact()` 也是写操作,不能和同一数据集的 `write()` 同时执行(见 2.1)。
378
+
379
+ ---
380
+
381
+ ## 7. 重命名
382
+
383
+ 指标和非时间维度都可以重命名,只改元数据,不重写数据:
384
+
385
+ ```python
386
+ store.rename({"Close": "ClosePrice"}) # 当前名 -> 新名
387
+ store.read("2026-10-10", metrics="ClosePrice", load=True)
388
+ store.list_renames() # {存储名: 当前名}
389
+ ```
390
+
391
+ - 读取时参数和结果都使用当前名称;
392
+ - 写入时当前名称和存储名称都接受,但同一份数据里不能同时出现新旧两个名字;
393
+ - 时间轴不能重命名。
394
+
395
+ ---
396
+
397
+ ## 8. 异常
398
+
399
+ | 异常 | 场景 |
400
+ |---------------------------|---------------------------------------------------------------------|
401
+ | `MetaError` | schema 不符(维度、类型、默认值)、`time_axis` 不一致、重命名冲突等 |
402
+ | `DatasetError` | 参数错误(日期、指标、indexers、chunks 等格式不对) |
403
+ | `MetaConflictError` | 发布元数据时发现被别人改过(并发写)——本次写入失败,可重试 |
404
+ | `MetaPublishUnknownError` | 网络等原因无法确认是否发布成功;异常中带 `publish_id`,需人工确认 |
405
+ | `ValueError` | 写入已存在日期但没有 `replace=True` 等 |
406
+
407
+ 均在 `tyzarr.error` 中定义(都是 `ValueError` 的子类)。
408
+
409
+ ---
410
+
411
+ ## 9. 完整示例
412
+
413
+ ```python
414
+ import numpy as np
415
+ import pandas as pd
416
+ import xarray as xr
417
+
418
+ from tyzarr.backend import LocalStorage
419
+ from tyzarr.dataset import Dataset
420
+
421
+ backend = LocalStorage("D:/zarr_root")
422
+ # 使用独立名称,避免前面章节的重命名影响本例
423
+ store = Dataset("daily_quote_demo", backend=backend, granularity="day")
424
+
425
+
426
+ def make_day(day, instruments=("A", "B", "C")):
427
+ n = len(instruments)
428
+ return xr.Dataset(
429
+ data_vars={
430
+ "Close": (("Date", "InstrumentID"), np.random.rand(1, n)),
431
+ "Volume": (("Date", "InstrumentID"), np.random.rand(1, n) * 1000),
432
+ },
433
+ coords={"Date": pd.to_datetime([day]).values, "InstrumentID": list(instruments)},
434
+ )
435
+
436
+
437
+ # 1. 每天写一次(同一时间只有这一个写入方)
438
+ # 示例允许重复执行:replace=True 会整日替换已有日期的数据
439
+ for day in ["2026-10-08", "2026-10-09", "2026-10-12"]:
440
+ store.write(make_day(day), time_axis="Date", replace=True)
441
+
442
+ # 某天合约列表变化也可以,读出时取并集
443
+ store.write(make_day("2026-10-13", ("A", "B", "D")), time_axis="Date", replace=True)
444
+
445
+ # 2. 月底合并
446
+ print(store.compact(granularity="month")["rewritten"])
447
+
448
+ # 3. 查询日期后读取
449
+ dates = [d for d in store.list_dates() if d >= "2026-10-09"]
450
+ ds = store.read(dates, metrics=["Close"], indexers={"InstrumentID": ["A", "D"]}, load=True)
451
+ print(ds["Close"].to_pandas()) # 2026-10-09/12 的 D 为 NaN
452
+ ```
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "tyzarr"
3
+ dynamic = ["version"]
4
+ description = "把 xarray.Dataset 存成不可变 zarr v3 快照,支持本地目录与 MinIO,用带 CAS 的 metadata.json 做原子发布"
5
+ readme = "src/tyzarr/README.md"
6
+ requires-python = ">=3.11"
7
+ authors = [{ name = "Rex" }]
8
+ keywords = ["zarr", "xarray", "minio", "s3", "timeseries", "dataset", "snapshot"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "License :: Other/Proprietary License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Topic :: Database",
18
+ "Topic :: Scientific/Engineering",
19
+ ]
20
+ dependencies = [
21
+ "aiobotocore>=2.0",
22
+ "botocore>=1.31",
23
+ "certifi>=2024.2.2",
24
+ "dask[array]>=2024.1.0",
25
+ "minio>=7.2",
26
+ "numpy>=1.26",
27
+ "pandas>=2.2",
28
+ "s3fs>=2024.2.0",
29
+ "urllib3>=2.0",
30
+ "xarray>=2024.3.0",
31
+ "zarr>=3.0",
32
+ ]
33
+
34
+ [build-system]
35
+ requires = ["hatchling"]
36
+ build-backend = "hatchling.build"
37
+
38
+ # 版本号单一来源:src/tyzarr/__init__.py 的 __version__
39
+ [tool.hatch.version]
40
+ path = "src/tyzarr/__init__.py"
41
+
42
+ [tool.hatch.build]
43
+ # 仓库内部的基准测试记录,不随包发布
44
+ exclude = ["**/PERFORMANCE.md"]
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/tyzarr"]
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ include = ["src/tyzarr", "pyproject.toml"]