Metadata-Version: 2.4
Name: csvlog
Version: 0.1.0
Summary: A global, concurrency-safe key/value logger that writes CSV or JSON.
Project-URL: Homepage, https://github.com/zhangyuh15/csvlog
Project-URL: Repository, https://github.com/zhangyuh15/csvlog
Project-URL: Issues, https://github.com/zhangyuh15/csvlog/issues
Author-email: AeroH <zhang.yuh@outlook.com>
License: MIT License
        
        Copyright (c) 2026 zyh
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: csv,experiment,json,logging,metrics,singleton
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: numpy>=1.20; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# csvlog

一个 Python 的全局单例键值对记录器：任意位置直接调用，最终落盘为 CSV 或 JSON。
适合训练/实验代码里按「实验步骤 (index) → 指标 (key/value)」的形式记录数据，
导出后可直接用 pandas 或 Excel 分析。

> 使用 AI/Agent 集成此包时请先读 [AGENTS.md](AGENTS.md)。
>
> 这是 Go 包 `git.risenlighten.com/lasvsim/csvlogger` 的 Python 移植版本。
> PyPI 上的包名是 `csvlog`（`csvlogger` 与已存在的 `csv-logger` 判重冲突）。

## 安装

```bash
pip install csvlog
```

零运行时依赖，Python 3.9+。

## 特点

- **全局单例**：任意位置直接 `csvlog.log(...)`，无需传递实例。
- **动态维护 key**：不需要预声明列。哪怕后半段才出现的新 key，前面几行也会自动填空。
- **同一 index 多次写入**：`log("epoch-1", loss=0.2)` 之后可以再 `log("epoch-1", lr=1e-3)`，合并为一行。
- **自动落盘**：进程退出时自动 `close()`，不用担心忘记。
- **可关闭**：`init(False, ...)` 之后所有调用都是 no-op，无需在业务代码里到处写 `if`。
- **空数据不落盘**：`init` 了但没记录任何数据，什么都不会写。
- **线程安全**：内部有锁，多线程直接并发调用即可。
- **多进程安全**：fork / forkserver / spawn 下各进程写各自的文件，不会互相覆盖，
  被 `terminate()` 打断也不丢数据。
- **原子落盘**：先写临时文件再 rename，绝不留下半截或 0 字节的文件。
- **numpy 友好**：`np.float64` / `np.ndarray` 自动转成原生类型（numpy 为可选依赖）。
- **完整类型标注**：随包分发 `py.typed`，mypy/pyright 可直接检查。

## 快速开始

```python
import csvlog

csvlog.init(True, "runs/exp1.csv")

csvlog.log("epoch-1", loss=0.23, acc=0.95)
csvlog.log("epoch-1", lr=0.001)                        # 同 index 可分多次
csvlog.log_map("epoch-2", {"loss": 0.19, "acc": 0.96})

# 不需要手动 close()，进程退出时自动落盘
```

输出 `runs/exp1.csv`：

```
index,acc,loss,lr
epoch-1,0.95,0.23,0.001
epoch-2,0.96,0.19,
```

- 列按 key 的字典序排列；首列固定是 index。
- 行按 index 首次出现的顺序排列。
- 缺失单元格默认为空字符串。

## 集成方式

### 1. 最常见：入口初始化，其他模块直接调用

```python
# train.py（程序入口）
import csvlog

def main():
    csvlog.init(True, "runs/exp.csv")
    run()   # 内部随意调用 csvlog.log(...)

if __name__ == "__main__":
    main()
```

```python
# trainer/loop.py（任意深度的模块）
import csvlog

def step(epoch, loss, acc):
    csvlog.log(epoch, loss=loss, acc=acc)
```

不需要把 logger 实例往下传，也不需要在每个模块里重新 `init`。

### 2. 通过环境变量决定是否启用

开发/调试打开，生产关闭 —— 业务代码不需要写任何 `if` 判断：

```python
import os
import csvlog

path = os.environ.get("CSV_LOG_PATH", "")   # 例如 "runs/exp.csv"；空则关闭
csvlog.init(bool(path), path)
```

`init(False, ...)` 之后，所有 `log` / `log_map` / `append` / `close` 都是 no-op，
也不会创建文件。

### 3. 用 with 显式控制落盘时机

不想依赖 atexit 时：

```python
with csvlog.init(True, "runs/exp.csv"):
    train()
# 退出 with 即落盘
```

### 4. 多线程

内部有锁，多线程直接并发调用是安全的，不需要你自己加锁：

```python
import threading
import csvlog

def worker(w):
    for i in range(100):
        csvlog.log(f"worker-{w}-step-{i}", value=compute(w, i))

threads = [threading.Thread(target=worker, args=(w,)) for w in range(8)]
for t in threads: t.start()
for t in threads: t.join()
```

### 5. 多进程

各进程写各自的文件，**不合并**。子进程的路径自动加上 `.p<pid>-<随机码>` 后缀，
主进程的路径保持你写的原样。fork / forkserver / spawn 三种启动方式都已覆盖。

推荐写法 —— 在 worker 函数开头无条件 `init` 一次。`init` 是幂等的，所以
fork 下它是 no-op，spawn/forkserver 下则是必需的（这些方式不继承全局状态）：

```python
import multiprocessing as mp
import csvlog

def worker(i):
    csvlog.init(True, "runs/exp.csv")   # fork 下 no-op，spawn/forkserver 下必需
    csvlog.log(f"task-{i}", value=i)

if __name__ == "__main__":
    csvlog.init(True, "runs/exp.csv")
    with mp.Pool(4) as pool:
        pool.map(worker, range(100))
```

产出：

```
runs/exp.csv                  <- 主进程（路径保持你写的原样）
runs/exp.p12346-a1b2c3.csv    <- 每个 worker 进程各一份
runs/exp.p12347-d4e5f6.csv
```

一个进程一个文件 —— pool 复用 worker 时，同一进程跑的多个任务会写进同一个文件。

事后用 pandas 合并：

```python
import glob
import pandas as pd

df = pd.concat(pd.read_csv(f) for f in glob.glob("runs/exp*.csv"))
```

> 默认启动方式随平台和 Python 版本而变（Linux 上历来是 fork，macOS 上
> 3.8+ 是 spawn，Windows 只有 spawn）。上面那种写法不依赖具体是哪种，
> 所以不必自己判断。

`with Pool(...)` 退出时走的是 `terminate()`（给 worker 发 SIGTERM），
本包会拦住这个信号先落盘再退出，所以这种情况下数据也不会丢。

想知道当前进程写到哪个文件，用 `csvlog.output_path()`。

> 注意：`init` 建议只在程序入口（以及每个 worker 的开头）调用。
> `close()` 通常不用手动调 —— 交给 atexit。

## API

```python
def init(
    enabled: bool,
    path: str | os.PathLike,
    *,
    index_column: str = "index",      # 首列表头
    missing_cell: str = "",           # 缺失单元格填充
    auto_close: bool = True,          # 进程退出自动落盘
    process_suffix: bool = True,      # 子进程写独立文件
) -> _InitHandle                      # 可选地当 context manager 用

def log(index, /, **kvs) -> None      # csvlog.log("e1", loss=0.23, acc=0.95)
def log_map(index, values: Mapping[str, Any]) -> None
def append(index, key: str, value) -> None   # 逐个追加元素到 list
def close() -> str | None             # 返回实际写出的路径，没写则 None
def output_path() -> str | None       # 当前进程会写到哪里
def reset() -> None                   # 仅测试用
```

`log` 的 `index` 是位置专属参数，所以 `log("row", index=5)` 不会冲突 ——
这时 `index` 只是一个普通列名。

key 不是合法 Python 标识符时（例如 `grad/norm`）用 `log_map`：

```python
csvlog.log_map("epoch-1", {"grad/norm": 1.42, "loss (train)": 0.2})
```

## JSON 自动输出

当值里包含 list / tuple / `np.ndarray`，或使用了 `append`，`close()` 会自动把
输出格式切换为 JSON（文件后缀从 `.csv` 替换为 `.json`）。

```python
# 方式一：直接传入序列
csvlog.log("step-1", loss=0.23, trajectory=[1.0, 2.0, 3.0])

# 方式二：逐个追加
csvlog.append("step-1", "trajectory", 1.0)
csvlog.append("step-1", "trajectory", 2.0)
csvlog.append("step-1", "trajectory", 3.0)
```

输出：

```json
[
  {
    "index": "step-1",
    "loss": 0.23,
    "trajectory": [1.0, 2.0, 3.0]
  }
]
```

> 注意：如果对同一个 key 先 `log` 了标量值，再 `append`，会抛 `TypeError`。
> `append` 的 key 只能用于 list 数据。

## 语义细节

| 场景 | 行为 |
|---|---|
| `init(False, ...)` | 后续所有调用均 no-op，不创建文件 |
| init 后未记录任何数据 | 不产生文件 |
| 同一 `(index, key)` 多次写入 | 后写覆盖前写，同时 `logging.warning` |
| 未 init 就 log | no-op 并 warning 一次（不抛异常） |
| 重复 init | 只有第一次生效，后续 warning |
| close 后再 log | no-op |
| 非 str 的 key | 抛 `TypeError` |
| 值含 list/tuple/ndarray | 输出 JSON（后缀自动改 `.json`） |
| `append` 到已有标量 key | 抛 `TypeError` |
| `path` 父目录不存在 | 落盘时自动创建 |
| float 值 | 用 `repr`，最短往返表示（同 Go 的 `'g', -1`） |
| `bool` 值 | 写 `True`/`False`，不会退化成 `1`/`0` |
| `str` 值 | 视为标量，不会触发 JSON 输出 |
| 不可 JSON 序列化的对象 | 降级为 `str(obj)`，不会导致整个文件写不出 |
| 落盘被 SIGTERM/SIGINT 打断 | 先写完再退出；绝不留下半截或 0 字节的文件 |

## 日志输出

warning 通过标准 `logging` 发出，logger 名为 `csvlog`。想静音：

```python
import logging
logging.getLogger("csvlog").setLevel(logging.ERROR)
```

## 不支持的用法

- **追加写入已有文件**：`close()` 总是覆盖 `path`。
- **流式写入**：所有数据先在内存中缓冲，`close()` 时一次写出
  （这是动态列的必然代价）。需要中途快照就分多次用不同 path。
- **跨进程合并成一个文件**：按设计各进程写各自的文件，事后用 pandas 合并。
- **通用日志**：没有 `debug`/`info`/`warning` 级别，这不是 `logging` 的替代品。

## 开发

```bash
uv venv --python 3.9
uv pip install -e ".[dev]"
uv run pytest
uv run mypy
uv run ruff check
```

## License

MIT
