Metadata-Version: 2.5
Name: openapi-crud-gen
Version: 0.1.0
Summary: Generate pytest CRUD test skeletons from OpenAPI specs
Project-URL: Homepage, https://github.com/zzzzzzzcc1/openapi-crud-gen
Project-URL: Repository, https://github.com/zzzzzzzcc1/openapi-crud-gen
Project-URL: Issues, https://github.com/zzzzzzzcc1/openapi-crud-gen/issues
Author-email: Zzzzc <3468307320@qq.com>
License-Expression: MIT
License-File: LICENSE
Keywords: codegen,contract-testing,crud,openapi,pytest
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: click>=8.0
Requires-Dist: jinja2>=3.0
Requires-Dist: openapi-spec-validator>=0.7.0
Requires-Dist: pytest>=7.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.28
Requires-Dist: rich>=13.0
Provides-Extra: dev
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Description-Content-Type: text/markdown

# openapi-crud-gen

从 OpenAPI 3.x 描述文件生成 pytest + requests 的 CRUD 测试骨架。

`openapi-crud-gen` 是一个**纯静态代码生成 CLI**：它读取 OpenAPI 3.0 / 3.1 文档（YAML 或 JSON），
识别 RESTful 资源及其 CRUD 操作，为每个资源生成一个可直接被 pytest 收集的测试模块。
生成过程不发起任何 HTTP 请求，业务数据与断言细节以 `TODO` 形式留给使用者补齐。

适用于契约测试、测试左移、CRUD 自动化与 CI 质量门禁的起点搭建。

## 快速上手（60 秒）

想跟着一步步走（真实输出、为什么必须填 TODO、怎么接进 CI），看 [docs/TUTORIAL.md](docs/TUTORIAL.md)。

```bash
pip install -e .                                                    # 1. 安装（pytest/requests 已在核心依赖里）
openapi-crud-gen examples/openapi.yaml --output-dir tests/generated # 2. 生成骨架
python examples/mock_api.py                                         # 3. 另开一个终端：起个假后端
pytest tests/generated                                              # 4. 看到 5 passed
```

第 3 步只是让你立刻看到"生成的东西真的能跑"。真实使用时跳过它，把
`--base-url` 指向你自己的服务，并把生成文件里的 `TODO`（路径参数、请求体、断言）补齐。

生成结束后 CLI 会直接打印这三步提示，不用记：

```
Generated 2 file(s) in tests/generated (0 skipped, 0 endpoint(s) ignored)

Next steps:
  1. Fill in the TODOs in the generated files (path parameters, request payload, assertions).
  2. Start your API at http://localhost:8000, or regenerate with a different --base-url.
  3. Run: pytest tests/generated
```

## 常见问题

**跑生成出来的测试报 `ConnectionError: [WinError 10061] 由于目标计算机积极拒绝`**
生成的是会真实发请求的骨架，`BASE_URL` 上必须真的有人监听。启动你的后端，或先
`python examples/mock_api.py`；也可以换地址重新生成：`--base-url http://127.0.0.1:9000`。

**`requests` 报 `ModuleNotFoundError`**
`requests` 已经随本工具一起装好，出现这个错说明跑 pytest 的解释器**不是装工具的那个**。
在跑测试的环境里执行 `pip install openapi-crud-gen`（源码目录用 `pip install .`）。

**为什么路径参数是 `id = None`**
骨架不知道你的真实 ID。写成 `None` 是为了避免运行时 `NameError`，跑之前替换成真实值。

**状态码断言失败：文档写 `[201]`，接口却返回 200**
断言取自文档声明的响应码。接口实现和文档不一致时，改文档或改断言，别把断言删成
`assert response.status_code`。

**文档里声明了 404/400，为什么断言里没有**
只自动断言 2xx/3xx，避免断言恒真失去意义。需要时在 `# TODO: 补充响应体断言` 处自己加。

**只想看会生成什么、不写文件**
加 `--dry-run`。

**重新生成时提示 `skipped ... (exists)`**
默认不覆盖已有文件，确认要覆盖就加 `--overwrite`；手写的断言不会被悄悄冲掉。

## 安装

需要 Python 3.10+。

```bash
pip install openapi-crud-gen     # 工具 + pytest + requests，一条命令把环境装好
```

`pytest` 与 `requests` 是核心依赖（生成的测试要用它们跑），装完就能直接
`pytest tests/generated`，不会出现 `ModuleNotFoundError: No module named 'requests'`。

**从源码装（仓库尚未发布到 PyPI 时）**

```bash
pip install .            # 在项目根目录执行
pip install -e .         # 要改代码时用可编辑安装
pip install ".[dev]"     # 额外装上 pytest-cov，用于跑覆盖率门槛
uv pip install -e .      # 用 uv 也一样
```

> `pip install openapi-crud-gen` 要等包上传 PyPI 后才有效；发布准备已完成（PEP 639 元数据、
> `twine check` 双 PASSED、wheel 内含模板），发布命令见下面的"发布到 PyPI"。

## 使用

```bash
openapi-crud-gen [OPTIONS] OPENAPI_FILE
```

| 选项 | 默认值 | 说明 |
| --- | --- | --- |
| `--output-dir PATH` | `tests/generated` | 生成文件输出目录，不存在则创建 |
| `--base-url TEXT` | `http://localhost:8000` | 写入生成文件 `BASE_URL` 常量的值 |
| `--resource-filter TEXT` | 无 | 逗号分隔资源名，仅生成匹配资源；未匹配项只告警 |
| `--overwrite` | 关闭 | 覆盖已存在文件；关闭时跳过并提示 |
| `--dry-run` | 关闭 | 只打印将要生成的文件，不写盘 |
| `--version` | — | 打印版本号并退出 |

退出码：`0` 成功（含“无可生成资源”）；`1` 文件无法解析或不是 OpenAPI 3.x；
`2` 参数错误（如文件不存在）。OpenAPI schema 校验失败只输出警告，不影响生成。

### 示例输入

`examples/openapi.yaml`：

```yaml
openapi: 3.0.3
info:
  title: User API
  version: 1.0.0
servers:
  - url: http://localhost:8000
paths:
  /users:
    get:
      summary: 获取用户列表
      responses:
        '200':
          description: OK
    post:
      summary: 创建用户
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        '201':
          description: Created
  /users/{id}:
    get:
      summary: 获取用户详情
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
    put:
      summary: 更新用户
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
    delete:
      summary: 删除用户
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: No Content
```

### 运行

```bash
openapi-crud-gen examples/openapi.yaml --output-dir tests/generated --base-url http://localhost:8000
```

```
Loaded examples/openapi.yaml (OpenAPI 3.0.3)
                        Recognized CRUD resources
+-----------------------------------------------------------------------+
| Resource | Base path | Actions                                        |
|----------+-----------+------------------------------------------------|
| users    | /users    | create, read_list, read_detail, update, delete |
+-----------------------------------------------------------------------+
Generated 2 file(s) in tests/generated (0 skipped, 0 endpoint(s) ignored)
```

### 生成结果

```
tests/generated/
├── __init__.py            # 输出目录缺少时自动生成
└── test_users_crud.py     # 每个资源一个文件
```

`tests/generated/test_users_crud.py`：

```python
"""CRUD test skeleton for the users resource.

Generated by openapi-crud-gen from an OpenAPI document. Fill in every TODO
before running these tests against a live service.
"""
import pytest
import requests

BASE_URL = "http://localhost:8000"


def test_users_create():
    # TODO: 准备请求数据
    response = requests.post(f"{BASE_URL}/users", json={})
    assert response.status_code in [201]
    # TODO: 补充响应体断言


def test_users_read_list():
    response = requests.get(f"{BASE_URL}/users")
    assert response.status_code in [200]
    # TODO: 补充响应体断言


def test_users_read_detail():
    id = None  # TODO: 替换路径参数
    response = requests.get(f"{BASE_URL}/users/{id}")
    assert response.status_code in [200]
    # TODO: 补充响应体断言


def test_users_update():
    id = None  # TODO: 替换路径参数
    # TODO: 准备请求数据
    response = requests.put(f"{BASE_URL}/users/{id}", json={})
    assert response.status_code in [200]
    # TODO: 补充响应体断言


def test_users_delete():
    id = None  # TODO: 替换路径参数
    response = requests.delete(f"{BASE_URL}/users/{id}")
    assert response.status_code in [204]
    # TODO: 补充响应体断言
```

生成的文件可以立即被 pytest 收集：

```bash
pytest --collect-only tests/generated
```

## CRUD 识别规则

资源名取路径的第一段：`/users/{id}` → `users`。路径形态只有两种参与识别：

| 形态 | 判定 | 例 |
| --- | --- | --- |
| Collection | 仅一个非参数段 | `/users` |
| Detail | 一个非参数段 + 恰好一个路径参数段 | `/users/{id}` |

| 动作 key | 方法 + 形态 | 默认状态码（文档未声明 2xx/3xx 时） |
| --- | --- | --- |
| `create` | `POST` + Collection | `201` |
| `read_list` | `GET` + Collection | `200` |
| `read_detail` | `GET` + Detail | `200` |
| `update` | `PUT` 或 `PATCH` + Detail | `200` |
| `delete` | `DELETE` + Detail | `204` |

补充约定：

- 状态码取自文档中该操作声明的 `200–399` 码，升序去重；4xx/5xx 不写入断言（避免断言恒真）。
- `PUT` 与 `PATCH` 同时存在时 `PUT` 优先，另一个计入 `ignored`。
- `POST`/`PUT`/`PATCH`（或文档声明了 `requestBody` 的操作）生成 `json={}` 占位。
- 非 CRUD 形态（`/users/{id}/posts`、`/users/me`、`/{tenant}/users`、根路径）以及
  `HEAD`/`OPTIONS`/`TRACE` 不生成用例，只在摘要中计入 `ignored`。
- 资源名规范化为合法标识符：`user-profiles` → `user_profiles`，`2fa` → `r_2fa`，`class` → `class_`。

## Python API

```python
from openapi_crud_gen.crud import identify_crud
from openapi_crud_gen.generator import generate_tests
from openapi_crud_gen.parser import extract_endpoints, load_openapi, validate_spec

spec = load_openapi("openapi.yaml")
warnings = validate_spec(spec)                  # 只返回消息，不抛异常
resources = identify_crud(extract_endpoints(spec))
files = generate_tests(resources, "tests/generated", "http://localhost:8000")
```

## 限制与已知问题

- 只生成骨架：不执行真实请求，不生成认证、数据准备、数据清理逻辑。
- 只识别一级扁平资源，不支持嵌套/子资源与自定义动作（如 `POST /users/{id}/activate`）。
- 生成的用例相互独立，`create` 返回的 id 不会传给 `read_detail`/`update`/`delete`。
- `$ref` 参数与 `components` 复用不会展开，请求体只生成 `{}` 占位。
- `--base-url` 只取命令行值，不读取文档 `servers`（保持与默认值行为一致）。
- 不支持 Swagger 2.0；`servers` 变量模板不解析。
- 资源名规范化可能把两个不同路径段折叠为同名资源（`/user-profiles` 与 `/user.profiles`）。

## 开发指南

```bash
pip install -e ".[dev]"
pytest                                                # 跑测试
pytest --cov=openapi_crud_gen --cov-fail-under=80     # 加上覆盖率门槛（CI 用这条）
```

- 结构：`parser.py`（加载与展平）→ `crud.py`（识别资源与动作）→
  `generator.py` + `templates/test_crud.py.j2`（渲染）→ `cli.py`（命令行与摘要）。
- 测试：`tests/fixtures/` 提供 YAML 与 JSON 示例文档；CLI 测试使用 `click.testing.CliRunner`；
  测试不发起任何网络请求。
- 覆盖率门槛 80%（分支覆盖）由显式命令与 CI 强制；普通 `pytest` 不注入 `--cov`，
  这样按快速上手直接 `pytest tests/generated` 不会被覆盖率门槛误伤。
- CI：`.github/workflows/test.yml` 在 push / PR 上运行 Python 3.10、3.11、3.12 矩阵，
  包含全量测试与一次 CLI 冒烟（生成 + `pytest --collect-only`）。

### 发布到 PyPI（可选）

```bash
python -m pip install build twine
python -m build                  # 产物在 dist/：wheel + sdist
python -m twine check dist/*     # 两个都要 PASSED
python -m twine upload dist/*    # 需要 PyPI API token
pip install openapi-crud-gen          # 发布后从 PyPI 复验一次
```

发布前清单：

- [ ] `pyproject.toml`、`src/openapi_crud_gen/__init__.py`、`CHANGELOG.md` 三处版本号一致
- [ ] `pytest --cov=openapi_crud_gen --cov-fail-under=80` 通过
- [ ] `twine check dist/*` 全 PASSED
- [ ] 补 `[project.urls]`（仓库 / Issues 地址）与作者信息，目前刻意留空以免写错地址
- [ ] 打 tag：`git tag v0.1.0 && git push origin v0.1.0`

## License

MIT，见 [LICENSE](LICENSE)。
