papycli 0.8.0__tar.gz → 0.9.0__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.
- {papycli-0.8.0 → papycli-0.9.0}/CLAUDE.md +30 -12
- {papycli-0.8.0 → papycli-0.9.0}/PKG-INFO +13 -7
- {papycli-0.8.0 → papycli-0.9.0}/README.ja.md +12 -6
- {papycli-0.8.0 → papycli-0.9.0}/README.md +12 -6
- {papycli-0.8.0 → papycli-0.9.0}/design_doc.md +44 -4
- {papycli-0.8.0 → papycli-0.9.0}/examples/request_filter/src/papycli_debug_filter/__init__.py +1 -1
- {papycli-0.8.0 → papycli-0.9.0}/examples/response_filter/README.md +1 -1
- {papycli-0.8.0 → papycli-0.9.0}/examples/response_filter/src/papycli_debug_response_filter/__init__.py +1 -1
- {papycli-0.8.0 → papycli-0.9.0}/pyproject.toml +1 -1
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/api_call.py +12 -4
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/completion.py +20 -7
- papycli-0.8.0/src/papycli/request_filter.py → papycli-0.9.0/src/papycli/filters.py +39 -12
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/main.py +6 -1
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_api_call.py +29 -16
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_completion.py +76 -8
- papycli-0.8.0/tests/test_request_filter.py → papycli-0.9.0/tests/test_filters.py +256 -36
- {papycli-0.8.0 → papycli-0.9.0}/.github/workflows/release.yml +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/.gitignore +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/.python-version +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/LICENSE +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/examples/petstore/docker-compose.yml +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/examples/petstore/petstore-oas3.json +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/examples/request_filter/README.md +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/examples/request_filter/pyproject.toml +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/examples/response_filter/pyproject.toml +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/__init__.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/checker.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/config.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/i18n.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/init_cmd.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/response_checker.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/spec_loader.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/src/papycli/summary.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/conftest.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_checker.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_config.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_i18n.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_init_cmd.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_main.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_response_checker.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_spec_loader.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/tests/test_summary.py +0 -0
- {papycli-0.8.0 → papycli-0.9.0}/uv.lock +0 -0
|
@@ -43,7 +43,8 @@ papycli/
|
|
|
43
43
|
│ ├── completion.py # シェル補完スクリプト生成
|
|
44
44
|
│ ├── config.py # 設定ファイルの読み書き
|
|
45
45
|
│ ├── i18n.py # 日英ヘルプテキストの切り替えユーティリティ
|
|
46
|
-
│ ├──
|
|
46
|
+
│ ├── filters.py # リクエスト・レスポンスフィルタープラグイン機構
|
|
47
|
+
│ ├── response_checker.py # --response-check のレスポンス検証
|
|
47
48
|
│ ├── spec_loader.py # OpenAPI spec の読み込み・$ref 解決
|
|
48
49
|
│ └── summary.py # summary コマンド・CSV 出力
|
|
49
50
|
├── tests/
|
|
@@ -54,7 +55,8 @@ papycli/
|
|
|
54
55
|
│ ├── test_i18n.py
|
|
55
56
|
│ ├── test_init_cmd.py
|
|
56
57
|
│ ├── test_main.py
|
|
57
|
-
│ ├──
|
|
58
|
+
│ ├── test_filters.py
|
|
59
|
+
│ ├── test_response_checker.py
|
|
58
60
|
│ ├── test_spec_loader.py
|
|
59
61
|
│ └── test_summary.py
|
|
60
62
|
├── examples/
|
|
@@ -92,9 +94,12 @@ bash / zsh 向けの補完スクリプトを生成する。補完の候補はメ
|
|
|
92
94
|
**`config.py`** — 設定管理
|
|
93
95
|
`papycli.conf` の読み書きと、`PAPYCLI_CONF_DIR` 環境変数の解決を行う。ログファイルパスの取得・設定・削除も担当する。
|
|
94
96
|
|
|
95
|
-
**`
|
|
97
|
+
**`filters.py`** — リクエスト・レスポンスフィルタープラグイン機構
|
|
96
98
|
エントリポイントグループ `papycli.request_filters` に登録されたフィルター関数をプラグイン名の昇順で呼び出し、リクエスト送信前に URL・クエリパラメータ・ボディ・ヘッダーを変換できるようにする。同様に `papycli.response_filters` グループのフィルター関数を呼び出し、レスポンス受信後にステータスコード・理由フレーズ(reason)・ボディ・ヘッダーを参照・変更できるようにする。`RequestContext` / `ResponseContext` データクラスと `load_filters()` / `apply_filters()` / `load_response_filters()` / `apply_response_filters()` 関数を提供する。
|
|
97
99
|
|
|
100
|
+
**`response_checker.py`** — レスポンス検証
|
|
101
|
+
`--response-check` オプション用のレスポンス検証ロジック。実際のHTTPステータスコードと OpenAPI spec に定義されたレスポンスコードを照合し、不一致の場合に警告する。また JSON レスポンスボディをスキーマに照合し、型・enum・必須フィールド・additionalProperties 等の違反を検出して警告メッセージのリストを返す。
|
|
102
|
+
|
|
98
103
|
**`summary.py`** — サマリー表示
|
|
99
104
|
登録済み API のエンドポイント一覧を整形して出力する。`--summary-csv` では CSV 形式で出力する。
|
|
100
105
|
|
|
@@ -169,6 +174,7 @@ papycli config list
|
|
|
169
174
|
papycli config log [PATH] [--unset]
|
|
170
175
|
papycli config completion-script <bash|zsh>
|
|
171
176
|
papycli spec [resource]
|
|
177
|
+
papycli spec --full [resource]
|
|
172
178
|
papycli summary [resource] [--csv]
|
|
173
179
|
papycli --version
|
|
174
180
|
papycli --help / -h
|
|
@@ -187,6 +193,7 @@ papycli --help / -h
|
|
|
187
193
|
- `-H <header: value>` — カスタム HTTP ヘッダー
|
|
188
194
|
- `--check` — 送信前にパラメータを検証する(警告を stderr に出力、リクエストは送信)
|
|
189
195
|
- `--check-strict` — 送信前にパラメータを検証する(警告を stderr に出力、問題があればリクエスト中止・exit 1)
|
|
196
|
+
- `--response-check` — レスポンスのステータスコードとボディを OpenAPI spec に照合する(違反は stderr に出力、exit code には影響しない)
|
|
190
197
|
|
|
191
198
|
### パステンプレートのマッチング
|
|
192
199
|
|
|
@@ -220,22 +227,33 @@ papycli --help / -h
|
|
|
220
227
|
|
|
221
228
|
---
|
|
222
229
|
|
|
223
|
-
##
|
|
230
|
+
## コード調査・編集のワークフロー
|
|
231
|
+
|
|
232
|
+
- シンボル操作はファイルを直接読む前にできるだけSerenaを使うこと
|
|
233
|
+
- ファイル内容の検索には可能なら `grep` ではなく `rg`(ripgrep)を使うこと
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Git リポジトリ運用 (GitHub)
|
|
224
238
|
|
|
225
239
|
### issue 管理
|
|
226
240
|
|
|
227
241
|
- ソースコードやドキュメントの追加・修正は原則として issue を作成し、それに対応する形で行う
|
|
228
|
-
- issue
|
|
242
|
+
- issue には適切なラベルをつける。使用するラベルは以下のいずれか。
|
|
229
243
|
|
|
230
|
-
|
|
|
244
|
+
| ラベル | 用途 |
|
|
231
245
|
|---------------|------|
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
234
|
-
| `refactor
|
|
235
|
-
| `
|
|
236
|
-
| `
|
|
246
|
+
| `feature` | 新機能の追加 |
|
|
247
|
+
| `bug` | バグ修正 |
|
|
248
|
+
| `refactor` | 機能変更を伴わないリファクタリング |
|
|
249
|
+
| `documentation` | ドキュメントのみの変更 |
|
|
250
|
+
| `chore` | 上記以外 (ビルド・依存関係・設定等のメンテナンス) |
|
|
237
251
|
|
|
238
252
|
### ブランチ・コミット・PR
|
|
239
253
|
|
|
254
|
+
- コミットメッセージは Conventional Commits に従う
|
|
255
|
+
- コミットに互換性を損なう破壊的変更が含まれる場合、"BREAKING CHANGE:" フッターを**必ず**追加する
|
|
240
256
|
- ソースコードの修正は適切な粒度でコミットし、プルリクエスト (PR) を提出する
|
|
241
|
-
-
|
|
257
|
+
- ソースコード修正時、異なるタイプの修正 (たとえば機能追加とリファクタリング) は極力コミットを分ける。
|
|
258
|
+
- PR でレビュー指摘を受けた場合、必要であればコードを修正し、修正コミットをプッシュする
|
|
259
|
+
- プッシュ後、修正内容を簡潔にまとめてPRに返信する
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: papycli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A CLI tool to call REST APIs defined in OpenAPI 3.0 specs
|
|
5
5
|
Project-URL: Homepage, https://github.com/tmonj1/papycli
|
|
6
6
|
Project-URL: Repository, https://github.com/tmonj1/papycli
|
|
@@ -36,6 +36,7 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
- Register and switch between multiple APIs
|
|
37
37
|
- Inspect API specs with `papycli spec`
|
|
38
38
|
- Validate request parameters before sending with `--check` / `--check-strict`
|
|
39
|
+
- Validate response body and status code against the OpenAPI spec with `--response-check`
|
|
39
40
|
- Automatically coerces `-p` values to the correct JSON type (integer, number, boolean) based on the API spec
|
|
40
41
|
- Log requests and responses to a file with `papycli config log`
|
|
41
42
|
- Extend request processing with request filter plugins (`papycli.request_filters` entry point)
|
|
@@ -147,7 +148,7 @@ $ papycli get <TAB>
|
|
|
147
148
|
/pet/findByStatus /pet/{petId} /store/inventory ...
|
|
148
149
|
|
|
149
150
|
$ papycli get /pet/findByStatus <TAB>
|
|
150
|
-
-q -p -H -d --summary --verbose --check --check-strict
|
|
151
|
+
-q -p -H -d --summary --verbose --check --check-strict --response-check
|
|
151
152
|
|
|
152
153
|
$ papycli get /pet/findByStatus -q <TAB>
|
|
153
154
|
status
|
|
@@ -156,7 +157,7 @@ $ papycli get /pet/findByStatus -q status <TAB>
|
|
|
156
157
|
available pending sold
|
|
157
158
|
|
|
158
159
|
$ papycli post /pet -p <TAB>
|
|
159
|
-
name status
|
|
160
|
+
name* photoUrls* status
|
|
160
161
|
|
|
161
162
|
$ papycli post /pet -p status <TAB>
|
|
162
163
|
available pending sold
|
|
@@ -260,6 +261,8 @@ Options:
|
|
|
260
261
|
--summary Show endpoint info without sending a request
|
|
261
262
|
--check Validate params before sending (warn on stderr, request is still sent)
|
|
262
263
|
--check-strict Validate params before sending (warn on stderr, abort with exit 1 on failure)
|
|
264
|
+
--response-check Validate response status code and body against the OpenAPI spec
|
|
265
|
+
(warn on stderr; violations do not affect exit code)
|
|
263
266
|
--verbose / -v Show HTTP status line
|
|
264
267
|
--version Show version
|
|
265
268
|
--help / -h Show help
|
|
@@ -281,7 +284,7 @@ A filter is a callable that receives a `RequestContext` and returns a modified `
|
|
|
281
284
|
|
|
282
285
|
```python
|
|
283
286
|
# my_plugin.py
|
|
284
|
-
from papycli.
|
|
287
|
+
from papycli.filters import RequestContext
|
|
285
288
|
|
|
286
289
|
def request_filter(ctx: RequestContext) -> RequestContext:
|
|
287
290
|
ctx.headers["X-Request-ID"] = "my-id"
|
|
@@ -313,18 +316,20 @@ Install the package and filters are applied automatically on every request, sort
|
|
|
313
316
|
|
|
314
317
|
You can inspect and transform incoming responses by writing a response filter plugin.
|
|
315
318
|
|
|
316
|
-
A filter is a callable that receives a `ResponseContext` and returns a modified `ResponseContext
|
|
319
|
+
A filter is a callable that receives a `ResponseContext` and returns a modified `ResponseContext`, or `None` to suppress the response output and stop the filter chain:
|
|
317
320
|
|
|
318
321
|
```python
|
|
319
322
|
# my_plugin.py
|
|
320
|
-
from papycli.
|
|
323
|
+
from papycli.filters import ResponseContext
|
|
321
324
|
|
|
322
|
-
def response_filter(ctx: ResponseContext) -> ResponseContext:
|
|
325
|
+
def response_filter(ctx: ResponseContext) -> ResponseContext | None:
|
|
323
326
|
if isinstance(ctx.body, dict):
|
|
324
327
|
ctx.body["_status"] = ctx.status_code
|
|
325
328
|
return ctx
|
|
326
329
|
```
|
|
327
330
|
|
|
331
|
+
Returning `None` suppresses the response output entirely and prevents any subsequent filters from running — useful for silencing responses that match certain criteria.
|
|
332
|
+
|
|
328
333
|
Register it in your package's `pyproject.toml`:
|
|
329
334
|
|
|
330
335
|
```toml
|
|
@@ -344,6 +349,7 @@ Install the package and the filters are applied automatically after every respon
|
|
|
344
349
|
| `reason` | `str` | HTTP response reason phrase (e.g. `"OK"`, `"Not Found"`). |
|
|
345
350
|
| `headers` | `dict[str, str]` | Response headers. |
|
|
346
351
|
| `body` | `dict \| list \| str \| int \| float \| bool \| None` | Parsed response body. Modify this field to replace the response body. |
|
|
352
|
+
| `request_body` | `dict \| list \| str \| int \| float \| bool \| None` | Request body sent to the server (read-only). `None` for requests without a body. |
|
|
347
353
|
|
|
348
354
|
---
|
|
349
355
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- 複数 API の登録・切り替え
|
|
10
10
|
- `papycli spec` による API スペックの確認
|
|
11
11
|
- `--check` / `--check-strict` によるリクエスト前のパラメータ検証
|
|
12
|
+
- `--response-check` による OpenAPI spec に基づくレスポンスのステータスコード・ボディ検証
|
|
12
13
|
- API 仕様に基づいて `-p` の値を適切な JSON 型(integer / number / boolean)に自動変換
|
|
13
14
|
- `papycli config log` によるリクエスト/レスポンスのファイルログ
|
|
14
15
|
- リクエストフィルタープラグイン(`papycli.request_filters` エントリポイント)によるリクエスト処理の拡張
|
|
@@ -120,7 +121,7 @@ $ papycli get <TAB>
|
|
|
120
121
|
/pet/findByStatus /pet/{petId} /store/inventory ...
|
|
121
122
|
|
|
122
123
|
$ papycli get /pet/findByStatus <TAB>
|
|
123
|
-
-q -p -H -d --summary --verbose --check --check-strict
|
|
124
|
+
-q -p -H -d --summary --verbose --check --check-strict --response-check
|
|
124
125
|
|
|
125
126
|
$ papycli get /pet/findByStatus -q <TAB>
|
|
126
127
|
status
|
|
@@ -129,7 +130,7 @@ $ papycli get /pet/findByStatus -q status <TAB>
|
|
|
129
130
|
available pending sold
|
|
130
131
|
|
|
131
132
|
$ papycli post /pet -p <TAB>
|
|
132
|
-
name status
|
|
133
|
+
name* photoUrls* status
|
|
133
134
|
|
|
134
135
|
$ papycli post /pet -p status <TAB>
|
|
135
136
|
available pending sold
|
|
@@ -232,6 +233,8 @@ papycli <method> <resource> [options]
|
|
|
232
233
|
--summary リクエストを送らずにエンドポイント情報を表示する
|
|
233
234
|
--check 送信前にパラメータを検証する(警告を stderr に出力、リクエストは送信)
|
|
234
235
|
--check-strict 送信前にパラメータを検証する(警告を stderr に出力、問題があればリクエスト中止・exit 1)
|
|
236
|
+
--response-check レスポンスのステータスコードとボディを OpenAPI spec に照合する
|
|
237
|
+
(違反は stderr に出力、exit code には影響しない)
|
|
235
238
|
--verbose / -v HTTP ステータス行を表示する
|
|
236
239
|
--version バージョンを表示する
|
|
237
240
|
--help / -h 使い方を表示する
|
|
@@ -253,7 +256,7 @@ papycli <method> <resource> [options]
|
|
|
253
256
|
|
|
254
257
|
```python
|
|
255
258
|
# my_plugin.py
|
|
256
|
-
from papycli.
|
|
259
|
+
from papycli.filters import RequestContext
|
|
257
260
|
|
|
258
261
|
def request_filter(ctx: RequestContext) -> RequestContext:
|
|
259
262
|
ctx.headers["X-Request-ID"] = "my-id"
|
|
@@ -285,18 +288,20 @@ my-filter = "my_plugin:request_filter"
|
|
|
285
288
|
|
|
286
289
|
レスポンスフィルタープラグインを作成することで、受信後のレスポンスを参照・変換できます。
|
|
287
290
|
|
|
288
|
-
フィルターは `ResponseContext` を受け取り、変更した `ResponseContext` を返す callable
|
|
291
|
+
フィルターは `ResponseContext` を受け取り、変更した `ResponseContext` を返す callable です。`None` を返すとレスポンスの出力を抑制し、後続のフィルター実行を中止します:
|
|
289
292
|
|
|
290
293
|
```python
|
|
291
294
|
# my_plugin.py
|
|
292
|
-
from papycli.
|
|
295
|
+
from papycli.filters import ResponseContext
|
|
293
296
|
|
|
294
|
-
def response_filter(ctx: ResponseContext) -> ResponseContext:
|
|
297
|
+
def response_filter(ctx: ResponseContext) -> ResponseContext | None:
|
|
295
298
|
if isinstance(ctx.body, dict):
|
|
296
299
|
ctx.body["_status"] = ctx.status_code
|
|
297
300
|
return ctx
|
|
298
301
|
```
|
|
299
302
|
|
|
303
|
+
`None` を返すと、そのレスポンスの出力が完全に抑制され、後続フィルターへの処理も中断されます。特定の条件に合致するレスポンスを無音化したい場合に便利です。
|
|
304
|
+
|
|
300
305
|
パッケージの `pyproject.toml` にエントリポイントを登録します:
|
|
301
306
|
|
|
302
307
|
```toml
|
|
@@ -316,6 +321,7 @@ my-filter = "my_plugin:response_filter"
|
|
|
316
321
|
| `reason` | `str` | HTTP レスポンスの理由フレーズ(例:`"OK"`、`"Not Found"`)。 |
|
|
317
322
|
| `headers` | `dict[str, str]` | レスポンスヘッダー。 |
|
|
318
323
|
| `body` | `dict \| list \| str \| int \| float \| bool \| None` | パース済みレスポンスボディ。このフィールドを変更するとレスポンスボディを差し替えられる。 |
|
|
324
|
+
| `request_body` | `dict \| list \| str \| int \| float \| bool \| None` | サーバーへ送信したリクエストボディ(参照専用)。ボディなしのリクエストは `None`。 |
|
|
319
325
|
|
|
320
326
|
---
|
|
321
327
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- Register and switch between multiple APIs
|
|
10
10
|
- Inspect API specs with `papycli spec`
|
|
11
11
|
- Validate request parameters before sending with `--check` / `--check-strict`
|
|
12
|
+
- Validate response body and status code against the OpenAPI spec with `--response-check`
|
|
12
13
|
- Automatically coerces `-p` values to the correct JSON type (integer, number, boolean) based on the API spec
|
|
13
14
|
- Log requests and responses to a file with `papycli config log`
|
|
14
15
|
- Extend request processing with request filter plugins (`papycli.request_filters` entry point)
|
|
@@ -120,7 +121,7 @@ $ papycli get <TAB>
|
|
|
120
121
|
/pet/findByStatus /pet/{petId} /store/inventory ...
|
|
121
122
|
|
|
122
123
|
$ papycli get /pet/findByStatus <TAB>
|
|
123
|
-
-q -p -H -d --summary --verbose --check --check-strict
|
|
124
|
+
-q -p -H -d --summary --verbose --check --check-strict --response-check
|
|
124
125
|
|
|
125
126
|
$ papycli get /pet/findByStatus -q <TAB>
|
|
126
127
|
status
|
|
@@ -129,7 +130,7 @@ $ papycli get /pet/findByStatus -q status <TAB>
|
|
|
129
130
|
available pending sold
|
|
130
131
|
|
|
131
132
|
$ papycli post /pet -p <TAB>
|
|
132
|
-
name status
|
|
133
|
+
name* photoUrls* status
|
|
133
134
|
|
|
134
135
|
$ papycli post /pet -p status <TAB>
|
|
135
136
|
available pending sold
|
|
@@ -233,6 +234,8 @@ Options:
|
|
|
233
234
|
--summary Show endpoint info without sending a request
|
|
234
235
|
--check Validate params before sending (warn on stderr, request is still sent)
|
|
235
236
|
--check-strict Validate params before sending (warn on stderr, abort with exit 1 on failure)
|
|
237
|
+
--response-check Validate response status code and body against the OpenAPI spec
|
|
238
|
+
(warn on stderr; violations do not affect exit code)
|
|
236
239
|
--verbose / -v Show HTTP status line
|
|
237
240
|
--version Show version
|
|
238
241
|
--help / -h Show help
|
|
@@ -254,7 +257,7 @@ A filter is a callable that receives a `RequestContext` and returns a modified `
|
|
|
254
257
|
|
|
255
258
|
```python
|
|
256
259
|
# my_plugin.py
|
|
257
|
-
from papycli.
|
|
260
|
+
from papycli.filters import RequestContext
|
|
258
261
|
|
|
259
262
|
def request_filter(ctx: RequestContext) -> RequestContext:
|
|
260
263
|
ctx.headers["X-Request-ID"] = "my-id"
|
|
@@ -286,18 +289,20 @@ Install the package and filters are applied automatically on every request, sort
|
|
|
286
289
|
|
|
287
290
|
You can inspect and transform incoming responses by writing a response filter plugin.
|
|
288
291
|
|
|
289
|
-
A filter is a callable that receives a `ResponseContext` and returns a modified `ResponseContext
|
|
292
|
+
A filter is a callable that receives a `ResponseContext` and returns a modified `ResponseContext`, or `None` to suppress the response output and stop the filter chain:
|
|
290
293
|
|
|
291
294
|
```python
|
|
292
295
|
# my_plugin.py
|
|
293
|
-
from papycli.
|
|
296
|
+
from papycli.filters import ResponseContext
|
|
294
297
|
|
|
295
|
-
def response_filter(ctx: ResponseContext) -> ResponseContext:
|
|
298
|
+
def response_filter(ctx: ResponseContext) -> ResponseContext | None:
|
|
296
299
|
if isinstance(ctx.body, dict):
|
|
297
300
|
ctx.body["_status"] = ctx.status_code
|
|
298
301
|
return ctx
|
|
299
302
|
```
|
|
300
303
|
|
|
304
|
+
Returning `None` suppresses the response output entirely and prevents any subsequent filters from running — useful for silencing responses that match certain criteria.
|
|
305
|
+
|
|
301
306
|
Register it in your package's `pyproject.toml`:
|
|
302
307
|
|
|
303
308
|
```toml
|
|
@@ -317,6 +322,7 @@ Install the package and the filters are applied automatically after every respon
|
|
|
317
322
|
| `reason` | `str` | HTTP response reason phrase (e.g. `"OK"`, `"Not Found"`). |
|
|
318
323
|
| `headers` | `dict[str, str]` | Response headers. |
|
|
319
324
|
| `body` | `dict \| list \| str \| int \| float \| bool \| None` | Parsed response body. Modify this field to replace the response body. |
|
|
325
|
+
| `request_body` | `dict \| list \| str \| int \| float \| bool \| None` | Request body sent to the server (read-only). `None` for requests without a body. |
|
|
320
326
|
|
|
321
327
|
---
|
|
322
328
|
|
|
@@ -40,7 +40,7 @@ papycli/
|
|
|
40
40
|
│ ├── checker.py # --check / --check-strict のパラメータ検証
|
|
41
41
|
│ ├── summary.py # summary コマンドの出力
|
|
42
42
|
│ ├── completion.py # bash / zsh 補完スクリプト生成
|
|
43
|
-
│ ├──
|
|
43
|
+
│ ├── filters.py # リクエスト・レスポンスフィルタープラグイン機構
|
|
44
44
|
│ └── i18n.py # 日英ヘルプテキストの切り替えユーティリティ
|
|
45
45
|
├── tests/
|
|
46
46
|
│ ├── test_api_call.py
|
|
@@ -50,7 +50,7 @@ papycli/
|
|
|
50
50
|
│ ├── test_i18n.py
|
|
51
51
|
│ ├── test_init_cmd.py
|
|
52
52
|
│ ├── test_main.py
|
|
53
|
-
│ ├──
|
|
53
|
+
│ ├── test_filters.py
|
|
54
54
|
│ ├── test_spec_loader.py
|
|
55
55
|
│ └── test_summary.py
|
|
56
56
|
├── examples/
|
|
@@ -241,13 +241,13 @@ papycli/
|
|
|
241
241
|
**目的**: サードパーティプラグインがリクエスト送信前に URL・クエリ・ボディ・ヘッダーを変換できる拡張ポイントを提供する。
|
|
242
242
|
|
|
243
243
|
**実装内容**:
|
|
244
|
-
- `
|
|
244
|
+
- `filters.py`
|
|
245
245
|
- `RequestContext` データクラス(`method`, `url`, `query_params`, `body`, `headers`)
|
|
246
246
|
- `load_filters()`: `papycli.request_filters` エントリポイントグループからフィルターをロードし、callable 検証後にプラグイン名の昇順で返す
|
|
247
247
|
- `apply_filters()`: フィルターを順番に適用。各フィルター呼び出し前にスナップショットを作成し(body は deepcopy、他はシャローコピー)、例外・戻り値不正の場合は警告して前の ctx を維持する
|
|
248
248
|
- `api_call.py` の `call_api()` でフィルターを適用するよう更新
|
|
249
249
|
- フィルター適用後の `method` は使用しない(API 定義マッチング時に確定した元の値を使う)
|
|
250
|
-
- テスト: `
|
|
250
|
+
- テスト: `test_filters.py`
|
|
251
251
|
- フィルターの読み込み・適用・例外処理・戻り値型検証など
|
|
252
252
|
|
|
253
253
|
**プラグイン登録例** (`pyproject.toml`):
|
|
@@ -292,6 +292,46 @@ my-filter = "my_plugin:request_filter"
|
|
|
292
292
|
|
|
293
293
|
---
|
|
294
294
|
|
|
295
|
+
### Milestone 8 — `spec --full` とレスポンスフィルタープラグイン機構
|
|
296
|
+
|
|
297
|
+
**目的**: `papycli spec --full` で生の OpenAPI spec を表示できるようにする。また response filter プラグインによりレスポンスの参照・変換を可能にする。
|
|
298
|
+
|
|
299
|
+
**実装内容**:
|
|
300
|
+
- `main.py` に `papycli spec --full [resource]` サブコマンドを追加
|
|
301
|
+
- 内部変換後の API 定義ではなく、元の OpenAPI spec を JSON で出力する
|
|
302
|
+
- `resource` 指定時は該当パスのみ絞り込んで表示する
|
|
303
|
+
- `filters.py` にレスポンスフィルター機構を追加
|
|
304
|
+
- `ResponseContext` データクラス(`status_code`, `reason`, `body`, `headers`)
|
|
305
|
+
- `load_response_filters()`: `papycli.response_filters` エントリポイントグループからフィルターをロード
|
|
306
|
+
- `apply_response_filters()`: フィルターを順番に適用。例外・戻り値不正の場合は警告して前の ctx を維持する
|
|
307
|
+
- `api_call.py` でレスポンス受信後にフィルターを適用するよう更新
|
|
308
|
+
|
|
309
|
+
**完了条件**: `papycli spec --full` で生の OpenAPI spec が出力される。`papycli.response_filters` エントリポイントに登録したフィルターがレスポンス受信後に自動適用される。テストがパスする。
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### Milestone 9 — `--response-check` レスポンス検証
|
|
314
|
+
|
|
315
|
+
**目的**: `--response-check` オプションを追加し、実際のレスポンスを OpenAPI spec に照合して違反を警告できるようにする。
|
|
316
|
+
|
|
317
|
+
**実装内容**:
|
|
318
|
+
- `response_checker.py`
|
|
319
|
+
- `check_response()`: レスポンスのステータスコードとボディを spec に照合し、違反メッセージのリストを返す
|
|
320
|
+
- ステータスコード照合: exact match (`"200"`) → 範囲指定 (`"2XX"`, `"2xx"`) → `"default"` の順で探索。どれにも一致しない場合に警告する
|
|
321
|
+
- ボディスキーマ照合: `application/json` および `+json` サフィックスのメディアタイプに対して実施。型・enum・必須フィールド・additionalProperties・配列 items を再帰的に検証する
|
|
322
|
+
- スキーマ存在確認を先に行い、スキーマが定義されていない場合はボディパースをスキップする(204 等での誤警告防止)
|
|
323
|
+
- YAML 由来の整数キー(`200:` → `200`)を文字列に正規化してから照合する
|
|
324
|
+
- `_check_value()`: 値とスキーマを再帰的に照合するヘルパー
|
|
325
|
+
- union type(`type: ["object", "null"]`)・type 省略スキーマ(`properties`/`items` から推論)をサポート
|
|
326
|
+
- 不正なスキーマ形状(`properties` が非 dict 等)に対して型ガードを設けクラッシュを防止する
|
|
327
|
+
- `main.py` に `--response-check` フラグを追加
|
|
328
|
+
- `api_call.py` の `call_api()` で response filter 適用前に `check_response()` を呼び出す
|
|
329
|
+
- テスト: `test_response_checker.py`、`test_main.py`
|
|
330
|
+
|
|
331
|
+
**完了条件**: `papycli get /pet/1 --response-check` で spec と一致しないレスポンスが返された場合に stderr へ警告が出力される。ステータスコード不一致・ボディスキーマ違反ともに検出される。テストがパスする。
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
295
335
|
## 開発上の方針
|
|
296
336
|
|
|
297
337
|
### ブランチ・コミット戦略
|
|
@@ -71,7 +71,7 @@ The plugin is registered via the `papycli.response_filters` entry point in `pypr
|
|
|
71
71
|
debug = "papycli_debug_response_filter:response_filter"
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
papycli discovers all installed plugins in this group, sorts them by name, and calls each one after receiving the response. The filter receives a `ResponseContext` and must return a (possibly modified) `ResponseContext
|
|
74
|
+
papycli discovers all installed plugins in this group, sorts them by name, and calls each one after receiving the response. The filter receives a `ResponseContext` and must return a (possibly modified) `ResponseContext`, or `None` to suppress the response output and stop the filter chain.
|
|
75
75
|
|
|
76
76
|
## Writing your own filter
|
|
77
77
|
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from papycli.
|
|
13
|
+
from papycli.filters import RequestContext
|
|
14
14
|
|
|
15
15
|
import requests
|
|
16
16
|
|
|
@@ -313,9 +313,13 @@ def call_api(
|
|
|
313
313
|
logfile: str | None = None,
|
|
314
314
|
raw_spec: dict[str, Any] | None = None,
|
|
315
315
|
do_response_check: bool = False,
|
|
316
|
-
) -> requests.Response:
|
|
317
|
-
"""API を呼び出し、レスポンスを返す。
|
|
318
|
-
|
|
316
|
+
) -> requests.Response | None:
|
|
317
|
+
"""API を呼び出し、レスポンスを返す。
|
|
318
|
+
|
|
319
|
+
レスポンスフィルターが ``None`` を返してチェーンを中断した場合は ``None`` を返す。
|
|
320
|
+
呼び出し元はこの戻り値をレスポンスの出力を抑制するシグナルとして扱う。
|
|
321
|
+
"""
|
|
322
|
+
from papycli.filters import (
|
|
319
323
|
RequestContext,
|
|
320
324
|
ResponseContext,
|
|
321
325
|
apply_filters,
|
|
@@ -429,8 +433,12 @@ def call_api(
|
|
|
429
433
|
reason=resp.reason or "",
|
|
430
434
|
headers=dict(resp.headers),
|
|
431
435
|
body=resp_body,
|
|
436
|
+
request_body=ctx.body,
|
|
432
437
|
)
|
|
433
438
|
resp_ctx = apply_response_filters(resp_ctx, response_filters)
|
|
439
|
+
if resp_ctx is None:
|
|
440
|
+
# フィルターが None を返してチェーンを中断した。出力を抑制するため None を返す。
|
|
441
|
+
return None
|
|
434
442
|
|
|
435
443
|
# フィルターがフィールドを変更した場合、resp に反映する。
|
|
436
444
|
# ボディ: 値の等価比較で変更を検出し、_content・encoding・Content-Type を更新する。
|
|
@@ -87,8 +87,9 @@ def _used_param_names(words: list[str], flag: str) -> set[str]:
|
|
|
87
87
|
if words[i] == flag and i + 1 < len(words):
|
|
88
88
|
name = words[i + 1]
|
|
89
89
|
# NAME が別のオプションフラグでも空文字でもない場合のみ使用済みとして扱う
|
|
90
|
+
# 補完で選択された場合に末尾に * が付いていることがあるため取り除いてから登録する
|
|
90
91
|
if name and not name.startswith("-"):
|
|
91
|
-
used.add(name)
|
|
92
|
+
used.add(name.removesuffix("*"))
|
|
92
93
|
# flag と NAME までは確実に読み飛ばすが、VALUE は仮定しない
|
|
93
94
|
i += 2
|
|
94
95
|
else:
|
|
@@ -108,11 +109,21 @@ def _complete_param_names(
|
|
|
108
109
|
if op is None:
|
|
109
110
|
return []
|
|
110
111
|
key = "query_parameters" if kind == "query" else "post_parameters"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
]
|
|
112
|
+
# incomplete の末尾 * を取り除いてからパラメータ名と比較する
|
|
113
|
+
incomplete_stripped = incomplete.removesuffix("*")
|
|
114
|
+
required: list[str] = []
|
|
115
|
+
optional: list[str] = []
|
|
116
|
+
for p in op.get(key, []):
|
|
117
|
+
name = p["name"]
|
|
118
|
+
if not name.startswith(incomplete_stripped):
|
|
119
|
+
continue
|
|
120
|
+
if used is not None and name in used:
|
|
121
|
+
continue
|
|
122
|
+
if p.get("required", False):
|
|
123
|
+
required.append(name + "*")
|
|
124
|
+
else:
|
|
125
|
+
optional.append(name)
|
|
126
|
+
return required + optional
|
|
116
127
|
|
|
117
128
|
|
|
118
129
|
def _complete_enum_values(
|
|
@@ -127,8 +138,10 @@ def _complete_enum_values(
|
|
|
127
138
|
if op is None:
|
|
128
139
|
return []
|
|
129
140
|
key = "query_parameters" if kind == "query" else "post_parameters"
|
|
141
|
+
# 補完で選択された場合、param_name の末尾に * が付いていることがあるため取り除く
|
|
142
|
+
normalized_name = param_name.removesuffix("*")
|
|
130
143
|
for p in op.get(key, []):
|
|
131
|
-
if p["name"] ==
|
|
144
|
+
if p["name"] == normalized_name and "enum" in p:
|
|
132
145
|
return [str(v) for v in p["enum"] if str(v).startswith(incomplete)]
|
|
133
146
|
return []
|
|
134
147
|
|