Metadata-Version: 2.4
Name: sfs-v2
Version: 0.0.1.dev1
Summary: Python-first SDK for SqlFileSystem v2
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# SqlFileSystem v2 Python SDK

> **状态：活跃维护。** Python 是当前唯一开发、测试和发布的 SDK，将先在多个真实项目
> 中验证稳定性，再作为未来 Kotlin/Node.js SDK 的行为基准。

SDK 通过私有 ctypes 层调用 `sfs-ffi` ABI v2，同时提供同步 `Sfs` 和 asyncio
`AsyncSfs`。低层 C 结构、raw handle、伪流式接口及尚未完成验收的后端不会进入稳定
公共 API。

## 安装

PyPI 分发名为 **`sfs-v2`**（代码仍 `import sfs`）。与 PyPI 已有的其他项目 `sfs` 不同，勿同时安装二者。每个平台的 wheel 仅内嵌该平台的原生库，由 pip 自动选择：

```bash
pip install sfs-v2==0.0.1.dev1
```

首次发布仅提供 Windows x86_64 与 Linux x86_64；不要将只包含某平台原生库的 wheel 标记为 `py3-none-any`。Linux wheel 的 manylinux/glibc 最低要求以实际发布文件名为准。

源码开发时先构建 FFI，并让 `SFS_NATIVE_PATH` 指向产物：

```bash
cargo build --release -p sfs-ffi

# Linux/macOS
export SFS_NATIVE_PATH=../../target/release/libsfs_ffi.so

# Windows PowerShell
$env:SFS_NATIVE_PATH="..\..\target\release\sfs_ffi.dll"
```

加载时 SDK 会执行 ABI 版本和全部 C 结构尺寸握手；Python 包与动态库不匹配时会在
open 前明确报错，而不是继续错误解释内存。

## 同步 API

```python
from sfs import Sfs, WriteItem

with Sfs.open_sqlite("/path/to/db") as store:
    store.makedirs("/pages/2026")
    store.write_text("/pages/2026/a.html", "<html>...</html>")
    print(store.read_text("/pages/2026/a.html"))

    store.write_many([
        WriteItem("/pages/2026/b.html", b"..."),
        WriteItem("/pages/2026/c.html", b"..."),
    ])
```

`Sfs` 会为每次操作取得生命周期 lease；`close()` 会阻止新操作并等待所有在途调用
结束，随后只释放一次原生 handle。close 后调用任何方法都会抛 `SfsClosedError`。

`OpenOptions(readonly=True)` 会以只读方式挂载已有字典用于解码，不训练或改写字典。
所需字典缺失/损坏时读取必须明确报错，不会把压缩 payload 当成文件内容返回。

## asyncio API

```python
from sfs import AsyncOptions, AsyncSfs

store = await AsyncSfs.open_sqlite(
    "/path/to/db",
    async_options=AsyncOptions(
        queue_capacity=256,
        max_queued_bytes=256 * 1024 * 1024,
        write_batch_size=100,
        flush_interval=0.05,
    ),
)

async with store:
    await store.write("/result.bin", payload)
```

`AsyncSfs` 使用一个专用单线程 executor、有界队列、字节水位和单 writer 自动聚合并发
写请求。以下是稳定契约：

- `await write/write_many` 正常返回，表示底层同步批量写已经成功提交；
- 仅进入 asyncio Queue 不会完成 Future；
- 同批失败会传播给该批全部等待者；
- `aclose()` 停止接收新任务、排空队列、等待原生调用，再关闭 handle；
- 调用方取消等待、远程断线或提交 ACK 丢失时底层写可能仍已完成；这些情况的
  outcome unknown，不表示“未写入”。只有 `await` 正常返回才是 commit 成功 ACK。

## 分页与扫描

普通列表默认最多返回 1000 条；大目录优先使用惰性 iterator：

```python
for info in store.iter_files("/pages", page_size=1000):
    print(info.full_path)
```

扫描默认遇错中止，避免静默漏文件：

```python
from sfs import ScanFilter, ScanOptions

with store.scan(ScanOptions(filter=ScanFilter(extensions=("html",)))) as scan:
    for item in scan:
        process(item.info, item.data)
```

asyncio 版本返回可显式关闭的 `AsyncSfsScan`，可读取最终统计和 Collect 错误：

```python
scan = store.scan(ScanOptions(filter=ScanFilter(extensions=("html",))))
async with scan:
    async for item in scan:
        await process(item)
stats = await scan.stats()
errors = await scan.take_errors()
```

SDK 不提供 `scan.to_list()`，防止把大规模扫描结果全部装入内存。扫描事件携带完整
内容，因此单文件硬上限为 4 MiB；更大文件应改走分块读取。同步 `SfsScan` 与异步
`AsyncSfsScan` 都应显式使用 context manager；store close 也会等待仍活动的异步扫描
句柄关闭后再终止 executor。

## 错误

所有错误继承 `SfsError`，并携带稳定字段：

```python
try:
    store.read("/missing")
except SfsError as error:
    print(error.code, error.operation, error.path, error.database_code, error.retryable)
```

常用子类包括 `SfsNotFoundError`、`SfsDatabaseLockedError`、
`SfsCorruptDataError`、`SfsReadonlyError` 和 `SfsResourceExhaustedError`。

## 维护与实验能力

空间回收不与常规读写平级：

```python
report = store.maintenance.shrink()
if not report.succeeded:
    print(report.vacuum_failed_volumes)
```

SDK 优先使用 optional `sfs_shrink_v2` 获取并释放 `vacuum_failed_volumes`；早期 ABI v2
缺少新符号时会回退 legacy 24-byte 报告，失败卷只能显示为空。需要完整维护诊断时应
使用当前 native。

PostgreSQL、MySQL 和远程 gRPC 入口尚未完成 Python 集成矩阵，只能显式从
`sfs.experimental` 导入，不属于当前兼容承诺。当前稳定支持范围仍是 SQLite；
Kotlin/Node.js SDK 继续封存。

远程连接可匿名，也可传 opaque bearer token：

```python
import os
from sfs.experimental import connect_remote

store = connect_remote(
    "http://127.0.0.1:50051",
    token=os.environ.get("SFS_TOKEN"),
)
```

调用方传不含 scheme 的 opaque token；Python 不改写、不记录它，gRPC 适配器自动构造
`authorization: Bearer <token>` header。远程句柄直接复用现有 `Sfs.scan()`；底层 ScanV2
会传输 COLLECT 错误并要求唯一 final stats，无需新增 remote scan API。远程 scan 的最低
协议要求是服务端同样实现当前 ScanV2；连接旧服务端收到 gRPC `Unimplemented` 时会映射
为 `SfsUnsupportedError`，不会保真假成功地回退到缺少 COLLECT errors/final stats 的
legacy scan。当前没有 Python 公开 stream/per-call-deadline API，也没有用于自签/私有 CA 的 Python 参数；
实验性 `connect_remote("https://your-host:51337", token=...)` 的 HTTPS endpoint 可直通
底层 gRPC 客户端，使用操作系统信任根验证证书链和主机名，不禁用 TLS 验证。此能力
尚不属于稳定远程 SDK API，私有/自签 CA 目前只能通过 Rust
`GrpcClient::connect_with_auth_and_ca(endpoint, token, Some(ca_pem))` 显式配置。异步爬虫可用
`async with await sfs.experimental.connect_remote_async(endpoint, token) as store:`，其连接/读写在
专用单线程 executor 中执行，成功 `await store.write(...)` 才代表服务端写入已提交。
Rust 客户端为普通 unary RPC 设置默认 120 秒截止时间；爬虫进程可用环境变量
`SFS_REMOTE_REQUEST_TIMEOUT_SECS=1..600` 调整。超时的写入结果**可能已经提交**，
不能自动盲重试；流式传输尚无客户端完整/空闲截止时间。
**SFS→PostgreSQL 是独立 TLS 链路**：每个 SFS 进程的 PG DSN 指定 `sslmode=require`，驱动才强制 TLS、验证 CA 与服务器主机名；私有 CA 在 SFS 主机上用 `SFS_PG_TLS_CA_FILE=/absolute/path/pg-ca.pem` 指定（否则使用操作系统信任根），不要在爬虫进程设置 PG DSN 或分发数据库凭据。未指定 `sslmode` 或使用 `prefer`（SFS 的 prefer 不尝试 TLS）只有明确的数字回环 IP/Unix socket 才保留明文，远端/`localhost` 主机名将拒绝；可信隧道若确需明文须显式 `sslmode=disable`；`sslmode=verify-full` 和 `sslrootcert` 不是当前 tokio-postgres 可解析的 DSN 参数，SFS 的 `require` 明确执行更严格的完整证书/主机名验证；各业务/字典连接与异步连接池会固定打开时的 CA 信任配置，轮换 CA 后重启服务。直连 PG 的实验性 Python FFI 路径也遵守同一配置，但这不是推荐的多爬虫架构。
服务端内置 TLS 同时保护 HTTPS REST 与 gRPC（HTTP-only 模式也启用 HTTPS）；
**非 loopback 监听必须同时配置有效 bearer token 和 TLS PEM 证书/私钥**，仅配置 token
不能使用明文监听。不得用 Python 示例经非 loopback 明文 HTTP 传 token。证书密钥文件
路径可通过 CLI `--tls-cert` / `--tls-key`（或环境变量 `SFS_TLS_CERT` /
`SFS_TLS_KEY`）成对传递；token 建议从安全秘密管理器注入 `SFS_AUTH_TOKEN`，勿写在
日志/源码/命令行。

容量边界必须区分：远程普通 `read`/`write` 受 300 MiB unary message ceiling 约束；底层 C 真流
（SQLite 本地与 gRPC 远程）单 chunk 最大 4 MiB、单文件最大 8 GiB。C 写流只有显式
flush/finish 成功才拿到 durable commit ACK；未 flush 直接 close 会 abort。由于 Python
尚未公开 stream wrapper，8 GiB 不是当前 Python 普通 `read`/`write` 的承诺。

## 测试

```bash
python -m pytest test_open_options.py test_sfs.py test_async_sfs.py -v
```
