Metadata-Version: 2.4
Name: oraclekit
Version: 0.2.0
Summary: 零客户端依赖的 Oracle 数据库连接工具包（基于 python-oracledb Thin 模式，支持 Oracle 12.1+）
Author-email: your-name <you@example.com>
Maintainer-email: your-name <you@example.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/your-name/oraclekit
Project-URL: Source, https://github.com/your-name/oraclekit
Project-URL: Issues, https://github.com/your-name/oraclekit/issues
Keywords: oracle,oracledb,database,thin-mode,no-instant-client,connection-pool,sql
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
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: Topic :: Database
Classifier: Topic :: Database :: Front-Ends
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: oracledb>=1.4.0
Provides-Extra: pandas
Requires-Dist: pandas>=1.3; extra == "pandas"
Provides-Extra: odpnet
Requires-Dist: pythonnet>=3.0; extra == "odpnet"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=4.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Dynamic: license-file

# oraclekit

> 零客户端依赖的 Oracle 数据库连接工具包 —— 基于 **python-oracledb Thin 模式**，
> 不需要安装 Oracle Instant Client、不配 `TNS_ADMIN`、不改 `PATH`、`NLS_LANG`。

```bash
pip install oraclekit
```

---

## 为什么它不需要 Oracle 客户端

python-oracledb 有两种运行模式：

| 模式 | 是否需要 Instant Client | 支持的数据库版本 | 说明 |
| --- | --- | --- | --- |
| **Thin**（oraclekit 默认） | ❌ 不需要，纯 Python 实现 | **12.1 及以上**（19c / 21c / 23ai 均可） | 直接走 Oracle Net 协议；普通业务 CRUD、批量写入、LOB 读写全都够用 |
| Thick（**连 11g 必选**） | ✅ 需要本机 Oracle Client（≥ 19.1） | 11.2.0.4 及以上 | 连接 11g 的唯一官方途径；也用于外部认证、SEPS 钱包等高级特性 |

oraclekit 默认走 Thin。也就是说：**`pip install oraclekit` 之后就能连库，不需要额外下载 100MB 的 Instant Client、不需要配环境变量。**

> ⚠️ 关于"Oracle 14 以下"这类老库：
> Oracle 官方从未发布过 14 这个版本（12c 之后直接跳到 18c/19c），所以"14 以下"通常指的是
> **11g (11.2)** 或 **12.1 / 12.2**。**12.1 / 12.2 用默认的 Thin 模式就能连**；
> **11g 必须切到 Thick 模式**，详见 [连接 11g（11.2）](#连接-11g112必须走-thick-模式)。
> Thin 模式连 11g 时驱动会直接拒绝并报 `DPY-3010`。

---

## 快速开始

```python
from oraclekit import OracleClient, OracleConfig

cfg = OracleConfig(
    host="10.0.0.5",
    port=1521,
    service_name="ORCLPDB1",   # 也可以写 sid="ORCL"
    user="app",
    password="secret",
)

with OracleClient(cfg) as db:
    for row in db.query("SELECT empno, ename, sal FROM emp WHERE deptno = :1", [10]):
        print(row["empno"], row["ename"], row["sal"])

    db.execute("UPDATE emp SET sal = sal * 1.1 WHERE deptno = :1", [10])
```

返回值一律是**字典**（列名默认小写），参数一律走**绑定变量**，天然防 SQL 注入，中文字符集默认 UTF-8。

命令行也能用：

```bash
oraclekit --config oracle.json ping
oraclekit --config oracle.json query "SELECT * FROM emp WHERE deptno=:1" --param 10
oraclekit --config oracle.json exec "UPDATE emp SET sal=9999 WHERE empno=:1" --param 7369
```

---

## 安装

```bash
pip install oraclekit            # 只装 python-oracledb 一个依赖
pip install oraclekit[pandas]    # 额外支持 to_dataframe()
```

要求 Python ≥ 3.8。

> **版本选择建议**：oraclekit 依赖声明写着 `oracledb>=1.4.0`，pip 默认装最新版。
> python-oracledb 4.0 之后的官方测试矩阵只覆盖 19c ~ 26，如果目标库是
> **12.1 / 12.2 / 18c** 这类老版本，建议把驱动固定在 3.x：
> ```bash
> pip install "oracledb<4"
> ```

---

## 配置

三种来源，优先级：**代码参数 > JSON 配置文件 > 环境变量 / `.env`**

### 1. 代码构造

```python
OracleConfig(
    host="10.0.0.5", port=1521, service_name="ORCLPDB1",
    user="app", password="secret",
    max_pool=10, arraysize=1000, lower_case_columns=True,
)
```

### 2. JSON 文件（推荐，记得加进 `.gitignore`）

```json
{
  "host": "10.0.0.5",
  "port": 1521,
  "service_name": "ORCLPDB1",
  "user": "app",
  "password": "secret",
  "max_pool": 10
}
```

```python
cfg = OracleConfig.from_json("oracle.json")
```

### 3. 环境变量 / `.env`

| 环境变量 | 含义 |
| --- | --- |
| `ORACLE_HOST` | 主机 |
| `ORACLE_PORT` | 端口，默认 1521 |
| `ORACLE_SERVICE_NAME` | 服务名（与 `ORACLE_SID` 二选一） |
| `ORACLE_SID` | 实例 SID |
| `ORACLE_DSN` | 完整 Easy Connect 串，优先级最高 |
| `ORACLE_USER` / `ORACLE_PASSWORD` | 账号密码 |
| `ORACLE_SCHEMA` | 登录后切换的 schema |
| `ORACLE_MIN_POOL` / `ORACLE_MAX_POOL` | 连接池大小，默认 1 / 5 |
| `ORACLE_ARRAYSIZE` | 游标预取行数，默认 500 |
| `ORACLE_CALL_TIMEOUT_MS` | 单条 SQL 超时（毫秒），0 表示不限 |
| `ORACLE_TCP_CONNECT_TIMEOUT_S` | TCP 建连超时（秒），默认 15 |
| `ORACLE_INIT_SQLS` | 每个新会话执行的 SQL，多条用分号分隔 |
| `ORACLE_THICK` / `ORACLE_THICK_LIB_DIR` | 是否启用 Thick 模式及其 Instant Client 目录 |

```python
cfg = OracleConfig.from_env()                  # 读环境变量
cfg = OracleConfig.from_env(dotenv=".env")     # 顺带读 .env（无需 python-dotenv）
```

### 常用参数

| 字段 | 默认值 | 说明 |
| --- | --- | --- |
| `service_name` / `sid` / `dsn` | — | 定位数据库，三选一，优先级 `dsn > sid > service_name` |
| `min_pool` / `max_pool` | 1 / 5 | 连接池容量 |
| `arraysize` | 500 | 游标每次网络往返预取行数，大查询可调到 2000 |
| `read_lobs` | `True` | CLOB/BLOB 自动读成 `str`/`bytes` |
| `lower_case_columns` | `True` | 结果字典 key 是否转小写 |
| `autocommit` | `False` | 是否自动提交 |
| `retry_count` / `retry_delay_s` | 2 / 1.0 | 断线后重建连接池重试的次数与间隔 |
| `schema` | `None` | 登录后 `ALTER SESSION SET CURRENT_SCHEMA` |
| `role` | `None` | `sysdba` / `sysoper` |
| `thick` / `thick_lib_dir` | `False` / `None` | Thick 模式兜底 |

---

## API 速查

| 方法 | 作用 |
| --- | --- |
| `query(sql, params=None, *, limit=None)` | 查询，返回 `list[dict]` |
| `query_one(sql, params=None)` | 取第一行，无结果返回 `None` |
| `query_value(sql, params=None, default=None)` | 取第一行第一列的值 |
| `iter(sql, params=None, *, arraysize=None)` | 流式读取，逐行产出 dict |
| `execute(sql, params=None, *, commit=True)` | 写操作，返回影响行数 |
| `execute_many(sql, rows, *, batch_size=1000)` | 批量写入，跳过并记录失败行 |
| `insert_rows(table, rows, *, batch_size=1000)` | 用字典列表批量插入，自动生成绑定 SQL |
| `execute_script(script)` | 执行以 `;` 分隔的脚本 |
| `callproc(name, parameters)` | 调用存储过程 |
| `transaction()` | 事务上下文：正常退出 commit，异常 rollback |
| `connection()` | 借一条原生连接，需要直接用驱动 API 时用 |
| `ping()` | 连通性探测，返回耗时（毫秒） |
| `server_version()` / `is_thin()` | 服务端版本 / 是否 Thin 模式 |
| `to_dataframe(sql, params=None)` | 转成 pandas DataFrame（需 `[pandas]` extra） |
| `close()` | 关闭连接池 |

### 事务

```python
with db.transaction() as conn:
    conn.cursor().execute("DELETE FROM stage WHERE batch_id = :1", [1])
    conn.cursor().execute("INSERT INTO log (batch_id) VALUES (:1)", [1])
# 正常退出自动 commit；抛异常自动 rollback 并向上抛出
```

### 批量写入

```python
rows = [{"empno": 9001, "ename": "TOM"}, {"empno": 9002, "ename": "JERRY"}]
total = db.insert_rows("emp", rows, batch_size=1000)
```

### 流式导出大表（百万行不爆内存）

```python
count = 0
for row in db.iter("SELECT * FROM big_table", arraysize=2000):
    count += 1
```

### CLOB / BLOB

Thin 模式下 LOB 默认是"定位符"，连接一关就读不到——这是最容易踩的坑。
oraclekit 已经替你处理：`read_lobs=True`（默认）时会把 CLOB 直接读成 `str`、BLOB 读成 `bytes`。

### 连接 11g（11.2）：必须走 Thick 模式

Thin 模式官方下限是 12.1，直接连 11g 会**被驱动明确拒绝**：

```
DPY-3010: connections to this database server version are not supported
          by python-oracledb in thin mode
```

**能连，但要满足条件。** 需要同时满足客户端侧和驱动侧的版本要求：

| 组件 | 要求 |
| --- | --- |
| Oracle 数据库 | 11.2.0.4 及以上（11.2.0.1/0.3 会撞 ORA-28040，见下） |
| 本机 Oracle Client | 19c 或更高（python-oracledb ≥ 4.0 的硬性要求；Oracle Client 19 本身支持连 11.2+） |
| python-oracledb | 任意版本。若不想换客户端，可降到 3.x 以复用本机旧的 11.2 客户端 |

因此有两条路，**二选一**：

**路线 A：装 Instant Client 19c（推荐，推荐给长期运维）**

1. 从 Oracle 官网下载免费的 *Instant Client for Windows x64* 的 **Basic** 包
   （不要选 Basic Lite，它的字符集不全，GBK 老库容易出问题）；
2. 解压到任意目录，例如 `C:\oracle\instantclient_19_26`；
3. 安装对应的 Visual Studio Redistributable（19c 需要 2015–2019 运行库）；
4. 代码里启用 Thick 模式并指向该目录。

**路线 B：不动客户端，把驱动降到 3.x**

```bash
pip install "oracledb<4"
```

3.x 及更早版本的驱动支持 Oracle Client 11.2 起，可以直接复用机器上已有的老客户端，
不必额外下载。缺点是拿不到新驱动的功能与安全修复。

> 本机实测：Windows + Python 3.13，机器上装着 Oracle Client **11.2.0.4**。
> 装 `oracledb==3.4.2` 后 `init_oracle_client()` 直接成功，
> `oracledb.clientversion()` 返回 `(11, 2, 0, 4, 0)`、`is_thin_mode()` 为 `False`；
> 而在完全相同的环境下用 `oracledb 26` 则会报 `DPI-1050`。
> 如果你的机器上本来就有 11.2 时代的客户端，路线 B 是最省事的一条。

配置写法（两条路线通用）：

```python
cfg = OracleConfig(
    host="10.0.0.9", port=1521,
    sid="ORCL",                          # 11g 老库优先用 SID
    user="legacy", password="secret",
    thick=True,
    thick_lib_dir=r"C:\oracle\instantclient_19_26",   # 路线 B 时留 None，跟随 PATH
)
with OracleClient(cfg) as db:
    print(db.server_version())
```

完整示例见 [`examples/connect_11g.py`](examples/connect_11g.py)，它会先做环境体检
（Python 位数、PATH 里的客户端版本、驱动版本），再给出你该选哪条路的判断。

**11g 专属的三个坑**

| 错误 | 说明 |
| --- | --- |
| `DPY-3010` | 忘了开 Thick 模式，`thick=True` 后消失 |
| `DPI-1050` | 本机客户端版本低于 19.1（典型场景：机器上装的是 11.2 的旧客户端），换 Instant Client 19c 或按路线 B 降驱动 |
| `ORA-28040` | 11g 服务端没打补丁导致的认证协议不匹配；在**数据库服务器**的 `sqlnet.ora` 里设 `SQLNET.ALLOWED_LOGON_VERSION_SERVER=11` 并重启监听，或升级到 11.2.0.4 |

此外，客户端位数必须与 Python 一致（同为 64 位），否则会报 `DPI-1047`。

---

## 可靠性设计

* **断线自愈**：捕获 `ORA-03113 / ORA-03114 / ORA-01012 / DPY-4011` 等"连接已失效"错误后，
  丢弃整个连接池并重建，然后重试。仅在操作**尚未产出任何数据**时重试，避免重复结果。
* **连接池 + 会话回调**：新建会话时自动设置 `CURRENT_SCHEMA`、超时、应用标识与初始化 SQL。
* **结构化异常**：全部继承 `OracleKitError`，携带 `sql` / `params` / `code`，日志里一眼能看出是哪条 SQL 出问题。
* **脱敏日志**：`cfg.redacted()` 输出的配置会自动把密码替换成 `***`。

---

## 排错

排错前先做自检，`examples/connection_check.py` 会依次打印 DSN、运行模式、服务端版本、
会话用户、字符集，并在失败时给出对应错误码的排查建议：

```bash
python examples/connection_check.py --config oracle.json
```

| 现象 | 原因与处理 |
| --- | --- |
| `DPY-6005: cannot connect` / `ORA-12514` | 服务名不对。用 `SELECT value FROM v$parameter WHERE name='service_names'` 确认；老库用 SID 时请填 `sid=` |
| `ORA-01017: invalid username/password` | 注意 12c 之后区分 CDB / PDB，PDB 要连服务名而不是 SID |
| `ORA-12170: connect timeout` | 防火墙或监听器端口不通，先 `tnsping` / `Test-NetConnection host -Port 1521` |
| 中文乱码 | Thin 模式默认 UTF-8，检查源端字符集是否 `AL32UTF8`；非 UTF-8 老库可设 `encoding="ZHS16GBK"` |
| `ORA-24454: client host name could not be resolved` | Thin 模式会反查主机名，本机 hostname 无法解析时改 `/etc/hosts` 或 `C:\Windows\System32\drivers\etc\hosts` |
| `DPY-4011: connection closed` | 连接被回收或网络中断，oraclekit 已自动重建，不影响业务；频繁出现请检查 `pool_idle_timeout_s` 和防火墙空闲断开策略 |

---

## 发布到 PyPI

本项目采用标准 `pyproject.toml` + `src` 布局，两种发布方式任选。

### 方式一：本地命令行发布（最快）

```bash
# 1. 安装构建与上传工具
pip install --upgrade build twine

# 2. 构建源码包 + wheel
python -m build

# 3. 校验 metadata（README 渲染、许可证字段等）
twine check dist/*

# 4. 先传到测试站验证（可选，推荐）
twine upload -r testpypi dist/*

# 5. 正式发布
twine upload dist/*
```

账号 Token 在 [pypi.org/manage/account/token](https://pypi.org/manage/account/token/) 生成，
用户名填 `__token__`，密码填 token（以 `pypi-` 开头）。

> 发布前记得：修改 `pyproject.toml` 里的 `authors` / `maintainers` / `[project.urls]`，
> 并把 `__version__` 改成新版本号。版本号一旦用过就不能重复上传。

### 方式二：GitHub Actions 自动发布（推荐长期维护）

仓库里配了 `.github/workflows/publish.yml`：推送 `v*` 标签即触发构建并发布（使用 PyPI 官方 Trusted Publishing，不需要 token）。

```bash
git tag -a v0.1.0 -m "release 0.1.0"
git push origin v0.1.0
```

首次使用需要在 PyPI 项目页的 *Publishing* 里登记 GitHub 仓库、工作流文件名和环境。

---

## 本地开发

```bash
pip install -e .[dev]
pytest            # 全部测试用假驱动跑，不需要真实数据库
ruff check src tests
```

---

## License

[MIT](LICENSE)
