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.
Files changed (43) hide show
  1. {papycli-0.8.0 → papycli-0.9.0}/CLAUDE.md +30 -12
  2. {papycli-0.8.0 → papycli-0.9.0}/PKG-INFO +13 -7
  3. {papycli-0.8.0 → papycli-0.9.0}/README.ja.md +12 -6
  4. {papycli-0.8.0 → papycli-0.9.0}/README.md +12 -6
  5. {papycli-0.8.0 → papycli-0.9.0}/design_doc.md +44 -4
  6. {papycli-0.8.0 → papycli-0.9.0}/examples/request_filter/src/papycli_debug_filter/__init__.py +1 -1
  7. {papycli-0.8.0 → papycli-0.9.0}/examples/response_filter/README.md +1 -1
  8. {papycli-0.8.0 → papycli-0.9.0}/examples/response_filter/src/papycli_debug_response_filter/__init__.py +1 -1
  9. {papycli-0.8.0 → papycli-0.9.0}/pyproject.toml +1 -1
  10. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/api_call.py +12 -4
  11. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/completion.py +20 -7
  12. papycli-0.8.0/src/papycli/request_filter.py → papycli-0.9.0/src/papycli/filters.py +39 -12
  13. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/main.py +6 -1
  14. {papycli-0.8.0 → papycli-0.9.0}/tests/test_api_call.py +29 -16
  15. {papycli-0.8.0 → papycli-0.9.0}/tests/test_completion.py +76 -8
  16. papycli-0.8.0/tests/test_request_filter.py → papycli-0.9.0/tests/test_filters.py +256 -36
  17. {papycli-0.8.0 → papycli-0.9.0}/.github/workflows/release.yml +0 -0
  18. {papycli-0.8.0 → papycli-0.9.0}/.gitignore +0 -0
  19. {papycli-0.8.0 → papycli-0.9.0}/.python-version +0 -0
  20. {papycli-0.8.0 → papycli-0.9.0}/LICENSE +0 -0
  21. {papycli-0.8.0 → papycli-0.9.0}/examples/petstore/docker-compose.yml +0 -0
  22. {papycli-0.8.0 → papycli-0.9.0}/examples/petstore/petstore-oas3.json +0 -0
  23. {papycli-0.8.0 → papycli-0.9.0}/examples/request_filter/README.md +0 -0
  24. {papycli-0.8.0 → papycli-0.9.0}/examples/request_filter/pyproject.toml +0 -0
  25. {papycli-0.8.0 → papycli-0.9.0}/examples/response_filter/pyproject.toml +0 -0
  26. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/__init__.py +0 -0
  27. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/checker.py +0 -0
  28. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/config.py +0 -0
  29. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/i18n.py +0 -0
  30. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/init_cmd.py +0 -0
  31. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/response_checker.py +0 -0
  32. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/spec_loader.py +0 -0
  33. {papycli-0.8.0 → papycli-0.9.0}/src/papycli/summary.py +0 -0
  34. {papycli-0.8.0 → papycli-0.9.0}/tests/conftest.py +0 -0
  35. {papycli-0.8.0 → papycli-0.9.0}/tests/test_checker.py +0 -0
  36. {papycli-0.8.0 → papycli-0.9.0}/tests/test_config.py +0 -0
  37. {papycli-0.8.0 → papycli-0.9.0}/tests/test_i18n.py +0 -0
  38. {papycli-0.8.0 → papycli-0.9.0}/tests/test_init_cmd.py +0 -0
  39. {papycli-0.8.0 → papycli-0.9.0}/tests/test_main.py +0 -0
  40. {papycli-0.8.0 → papycli-0.9.0}/tests/test_response_checker.py +0 -0
  41. {papycli-0.8.0 → papycli-0.9.0}/tests/test_spec_loader.py +0 -0
  42. {papycli-0.8.0 → papycli-0.9.0}/tests/test_summary.py +0 -0
  43. {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
- │ ├── request_filter.py # リクエストフィルタープラグイン機構
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
- │ ├── test_request_filter.py
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
- **`request_filter.py`** — リクエスト・レスポンスフィルタープラグイン機構
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
- ## Git リポジトリ運用
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
- | `feat:` | 新機能の追加 |
233
- | `fix:` | バグ修正 |
234
- | `refactor:` | 機能変更を伴わないリファクタリング |
235
- | `chore:` | ビルド・依存関係・設定等のメンテナンス |
236
- | `docs:` | ドキュメントのみの変更 |
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
- - PR でレビュー指摘を受けた場合、必要であればコードを修正し、PR に修正した旨のコメントを追加する
257
+ - ソースコード修正時、異なるタイプの修正 (たとえば機能追加とリファクタリング) は極力コミットを分ける。
258
+ - PR でレビュー指摘を受けた場合、必要であればコードを修正し、修正コミットをプッシュする
259
+ - プッシュ後、修正内容を簡潔にまとめてPRに返信する
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papycli
3
- Version: 0.8.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 photoUrls
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.request_filter import RequestContext
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.request_filter import ResponseContext
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 photoUrls
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.request_filter import RequestContext
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.request_filter import ResponseContext
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 photoUrls
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.request_filter import RequestContext
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.request_filter import ResponseContext
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
- │ ├── request_filter.py # リクエストフィルタープラグイン機構
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
- │ ├── test_request_filter.py
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
- - `request_filter.py`
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
- - テスト: `test_request_filter.py`
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
  ### ブランチ・コミット戦略
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
  import json
14
14
  import sys
15
15
 
16
- from papycli.request_filter import RequestContext
16
+ from papycli.filters import RequestContext
17
17
 
18
18
 
19
19
  def _to_json(value: object) -> str:
@@ -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
 
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
  import json
14
14
  import sys
15
15
 
16
- from papycli.request_filter import ResponseContext
16
+ from papycli.filters import ResponseContext
17
17
 
18
18
 
19
19
  def _to_json(value: object) -> str:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "papycli"
3
- version = "0.8.0"
3
+ version = "0.9.0"
4
4
  description = "A CLI tool to call REST APIs defined in OpenAPI 3.0 specs"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -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.request_filter import RequestContext
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
- from papycli.request_filter import (
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
- return [
112
- p["name"]
113
- for p in op.get(key, [])
114
- if p["name"].startswith(incomplete) and (used is None or p["name"] not in used)
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"] == param_name and "enum" in p:
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