From 26699af9cfca6c34830c0454b5993d49bba65fe0 Mon Sep 17 00:00:00 2001 From: allen0099 Date: Tue, 29 Sep 2026 08:36:53 +0000 Subject: [PATCH] docs(state): correct state, migration, development and example docs against master --- docs/DEVELOPMENT.md | 21 ++++++++++++--------- docs/MIGRATING_0_4.md | 6 +++--- docs/STATE.md | 8 +++++--- examples/redis_backend.py | 5 ++++- examples/session_jwt.py | 3 ++- examples/session_redis.py | 3 ++- fastapi_cachex/state/exceptions.py | 2 +- fastapi_cachex/state/manager.py | 17 +++++++++++++---- i18n/zh-TW/docs/MIGRATING_0_4.md | 6 +++--- i18n/zh-TW/docs/STATE.md | 4 ++-- scripts/changelog_release.py | 3 ++- 11 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d9f71c7..6e92cce 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -58,8 +58,8 @@ so do not point these at anything you care about. When a port is set but nothing is listening, the suites skip and say so. A run with nothing opted in still clears the coverage gate (`fail_under = 90`) -at about 92.2%, but only because the rest of the suite carries it — `redis.py` -alone drops to roughly 27%. The margin is thin, so the first place an untested +at about 94.5%, but only because the rest of the suite carries it — `redis.py` +alone drops to roughly 42%. The margin is thin, so the first place an untested line shows up as a failure is a local opted-out run, not CI, which sets both variables against its own service containers and sees 99.95%. @@ -119,8 +119,8 @@ The way to find those is to break the thing on purpose and see if the suite notices: ```bash -# neuter one mechanism, then run the whole suite -git stash -- fastapi_cachex/ # or edit the function to return early +# neuter one mechanism (edit a function in fastapi_cachex/ to return early), +# then run the whole suite and restore the source uv run pytest -q -p no:randomly git checkout fastapi_cachex/ ``` @@ -332,14 +332,16 @@ The workflow runs in this order: [Changelog fragments](#changelog-fragments)), renames it to `## [X.Y.Z] - YYYY-MM-DD`, opens a fresh empty `## [Unreleased]` above it, rewrites the compare links at the bottom, and writes the release body: - each entry's bold summary and issue links, and a link to the full entries - on the documentation site. + the notice, if any, each entry's bold summary and issue links, and a link + to the full entries on the documentation site. The workflow then appends an + installation snippet and, when there is a previous tag, a compare link. 4. **The build.** `uv build`. The bumped files, the release notes and `dist/` are uploaded as one artifact, which the next two jobs download instead of building anything again. 5. **The permanent part**, kept together at the end: commit the version bump, the promoted changelog and the removal of the merged fragments, push it to master, tag, push the tag by refspec, - create the GitHub release from the promoted section, publish to PyPI. + create the GitHub release from the release notes written in step 3, + publish to PyPI. Steps 1–4 are the `build` job, which installs every dev dependency and so gets read access to the repository and nothing else: no git credentials, no PyPI @@ -399,8 +401,9 @@ opens with a bold summary, and the release body is just those summaries: becomes ``- Add `CacheManager.add()` for store-if-absent writes. ([#65](...))`` under the same `### Added` heading. The issue links are carried over from -anywhere in the entry, and the body ends with a link to the version's section -on the documentation site's changelog page. Write the summary for someone +anywhere in the entry, and the script ends the body with a link to the +version's section on the documentation site's changelog page (the workflow +then appends the installation snippet and the compare link). Write the summary for someone deciding whether this release matters to them: what changed, in the imperative or as a plain statement, not how. An entry without one fails the run and is named in the error, and so does a line in the section that is neither a `###` diff --git a/docs/MIGRATING_0_4.md b/docs/MIGRATING_0_4.md index ec496d2..d412568 100644 --- a/docs/MIGRATING_0_4.md +++ b/docs/MIGRATING_0_4.md @@ -32,7 +32,7 @@ Every warning below names the setting to change and links to its issue. `FutureW | `get_or_set()` locks by default | [#280](https://github.com/allen0099/FastAPI-CacheX/issues/280) | `FutureWarning` | [get_or_set lock](#get-or-set-lock) | | `get_session_manager` resolves through `SessionManagerProxy` | [#131](https://github.com/allen0099/FastAPI-CacheX/issues/131) | `FutureWarning` | [get_session_manager](#get-session-manager) | | `add_routes()` requires `dependencies`, no content preview by default | [#298](https://github.com/allen0099/FastAPI-CacheX/issues/298) | `UserWarning` | [Monitoring routes](#add-routes) | -| Redis `encoding` option removed | [#126](https://github.com/allen0099/FastAPI-CacheX/issues/126) | `DeprecationWarning` | [Redis encoding](#redis-encoding) | +| Redis `encoding` option removed | [#126](https://github.com/allen0099/FastAPI-CacheX/issues/126) | `DeprecationWarning` (`RuntimeWarning` for a value other than UTF-8) | [Redis encoding](#redis-encoding) | | JWT HMAC secrets shorter than the hash output are rejected | [#129](https://github.com/allen0099/FastAPI-CacheX/issues/129) | `UserWarning` | [JWT secret length](#jwt-secret) | | `SessionMiddleware` removed | [#69](https://github.com/allen0099/FastAPI-CacheX/issues/69) | `DeprecationWarning` | [SessionMiddleware](#session-middleware) | | `BackendProxy.get_backend()` / `set_backend()` removed | [#70](https://github.com/allen0099/FastAPI-CacheX/issues/70) | `DeprecationWarning` | [BackendProxy](#backend-proxy) | @@ -258,7 +258,7 @@ entry.headers["link"] ### Monitoring routes {#add-routes} -`add_routes()` requires `dependencies` in 0.4.0, and `include_content_preview` defaults to `False` ([#298](https://github.com/allen0099/FastAPI-CacheX/issues/298)). 0.3.x emits a `UserWarning` when `dependencies` is left out. +`add_routes()` requires `dependencies` in 0.4.0, and `include_content_preview` defaults to `False` ([#298](https://github.com/allen0099/FastAPI-CacheX/issues/298)). 0.3.9 emits a `UserWarning` when `dependencies` is left out. ```python # Before @@ -274,7 +274,7 @@ add_routes(app, dependencies=[], include_content_preview=True) ### Redis encoding {#redis-encoding} -The Redis client reads raw bytes in 0.4.0, and the `encoding` option is removed from `AsyncRedisCacheBackend` and `RedisConfig` ([#126](https://github.com/allen0099/FastAPI-CacheX/issues/126)). Entries were always written as UTF-8, so leaving it out changes nothing. In 0.3.9, passing `encoding` at all emits a `DeprecationWarning` (and a value other than UTF-8 keeps its `RuntimeWarning`). +The Redis client reads raw bytes in 0.4.0, and the `encoding` option is removed from `AsyncRedisCacheBackend` and `RedisConfig` ([#126](https://github.com/allen0099/FastAPI-CacheX/issues/126)). Entries were always written as UTF-8, so leaving it out changes nothing. In 0.3.9, a UTF-8 `encoding` passed to `AsyncRedisCacheBackend` emits a `DeprecationWarning`, and any other value emits only its `RuntimeWarning`, which also announces the removal. A `RedisConfig` that sets `encoding` emits the `DeprecationWarning` when it is passed to `load_from_config()`, plus the `RuntimeWarning` for a value other than UTF-8. ```python # Before diff --git a/docs/STATE.md b/docs/STATE.md index e2bab91..bccf1fa 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -144,7 +144,7 @@ if no backend has been set, the request fails with `BackendNotFoundError`. ``` CacheXError └── StateError - ├── InvalidStateError # missing or already consumed + ├── InvalidStateError # missing, already consumed, or binding mismatch ├── StateExpiredError # expired └── StateDataError # malformed content ``` @@ -166,5 +166,7 @@ CacheXError cache backend. - **Logs never contain the state itself.** Log lines from `fastapi_cachex.state.manager` identify a state by `state_ref`, the first 12 hex characters of its SHA-256, which you can - compute from a known state to match it. An unknown or expired state is logged at INFO, - since it is routine client input; malformed stored data is logged once at WARNING. + compute from a known state to match it. An unknown, expired or differently bound state + rejected by `consume_state()` is logged at INFO, since it is routine client input + (`validate_state()` and `get_state_metadata()` log a missing or expired state at DEBUG); + malformed stored data is logged once at WARNING. diff --git a/examples/redis_backend.py b/examples/redis_backend.py index ce9b7d8..5bf03ab 100644 --- a/examples/redis_backend.py +++ b/examples/redis_backend.py @@ -27,7 +27,10 @@ @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncIterator[None]: - """Connect to Redis on startup and close the connection pool on shutdown.""" + """Register a Redis backend on startup and close its connection pool on shutdown. + + The client connects lazily, on the first command. + """ backend = AsyncRedisCacheBackend( host=os.environ.get("REDIS_HOST", "127.0.0.1"), port=int(os.environ.get("REDIS_PORT", "6379")), diff --git a/examples/session_jwt.py b/examples/session_jwt.py index 69c123c..5d9f700 100644 --- a/examples/session_jwt.py +++ b/examples/session_jwt.py @@ -52,7 +52,8 @@ cookie_https_only=False, ) session_manager = SessionManager(backend, config) -# ClientIPDep finds the manager through the proxy (only through it from 0.4.0). +# From 0.4.0 ClientIPDep finds the manager only through the proxy; 0.3.9 reads +# the middleware's and warns (FutureWarning) when the proxy holds another one. SessionManagerProxy.set(session_manager) diff --git a/examples/session_redis.py b/examples/session_redis.py index d8844f3..40cee85 100644 --- a/examples/session_redis.py +++ b/examples/session_redis.py @@ -61,7 +61,8 @@ cookie_https_only=True, ) session_manager = SessionManager(backend, config) -SessionManagerProxy.set(session_manager) # ClientIPDep resolves it through the proxy +# From 0.4.0 ClientIPDep resolves the manager only through the proxy. +SessionManagerProxy.set(session_manager) @asynccontextmanager diff --git a/fastapi_cachex/state/exceptions.py b/fastapi_cachex/state/exceptions.py index f42da65..6a73fb4 100644 --- a/fastapi_cachex/state/exceptions.py +++ b/fastapi_cachex/state/exceptions.py @@ -8,7 +8,7 @@ class StateError(CacheXError): class InvalidStateError(StateError): - """Raised when a state is invalid or not found.""" + """Raised when a state is invalid or not found, or its binding does not match.""" class StateExpiredError(StateError): diff --git a/fastapi_cachex/state/manager.py b/fastapi_cachex/state/manager.py index ebc360c..6b4b40a 100644 --- a/fastapi_cachex/state/manager.py +++ b/fastapi_cachex/state/manager.py @@ -1,4 +1,4 @@ -"""State manager for OAuth and session state handling.""" +"""State manager for one-time OAuth state tokens.""" import hashlib import hmac @@ -64,7 +64,7 @@ def _log_decode_failure(state: str) -> None: class StateManager: - """Manages OAuth state and session state lifecycle and storage.""" + """Manages the lifecycle and storage of one-time OAuth state tokens.""" def __init__( self, @@ -82,7 +82,10 @@ def __init__( Raises: BackendNotFoundError: If ``backend`` is None and no backend has been set with ``BackendProxy.set()``. - ValueError: If ``default_ttl`` is zero or negative. + TypeError: If ``default_ttl`` is not an ``int`` (a float or bool + is rejected). + ValueError: If ``default_ttl`` is zero, negative or above + ``MAX_TTL``. """ validate_ttl(default_ttl) self.backend = backend if backend is not None else BackendProxy.get() @@ -166,7 +169,10 @@ async def create_state( The generated state string Raises: - ValueError: If ``ttl`` is zero or negative, or ``binding`` is empty. + TypeError: If ``ttl`` is not an ``int`` (a float or bool is + rejected). + ValueError: If ``ttl`` is zero, negative or above ``MAX_TTL``, or + ``binding`` is empty. Backend errors (for example a Redis connection error) propagate unchanged; they are not wrapped in ``StateDataError``. @@ -222,6 +228,9 @@ async def consume_state( does not match StateExpiredError: If state has expired StateDataError: If state data format is invalid + + Backend errors propagate unchanged; on Memcached, a value replaced by + other writers 16 times in a row raises ``CacheXError``. """ # Take the state out of the backend atomically: of several concurrent # callers presenting the same state exactly one gets the entry, so a diff --git a/i18n/zh-TW/docs/MIGRATING_0_4.md b/i18n/zh-TW/docs/MIGRATING_0_4.md index 1120b84..abc5476 100644 --- a/i18n/zh-TW/docs/MIGRATING_0_4.md +++ b/i18n/zh-TW/docs/MIGRATING_0_4.md @@ -32,7 +32,7 @@ filterwarnings = [ | `get_or_set()` 預設使用鎖 | [#280](https://github.com/allen0099/FastAPI-CacheX/issues/280) | `FutureWarning` | [get_or_set 的鎖](#get-or-set-lock) | | `get_session_manager` 透過 `SessionManagerProxy` 取得 | [#131](https://github.com/allen0099/FastAPI-CacheX/issues/131) | `FutureWarning` | [get_session_manager](#get-session-manager) | | `add_routes()` 必須傳入 `dependencies`,預設不含內容預覽 | [#298](https://github.com/allen0099/FastAPI-CacheX/issues/298) | `UserWarning` | [監控路由](#add-routes) | -| 移除 Redis 的 `encoding` 選項 | [#126](https://github.com/allen0099/FastAPI-CacheX/issues/126) | `DeprecationWarning` | [Redis encoding](#redis-encoding) | +| 移除 Redis 的 `encoding` 選項 | [#126](https://github.com/allen0099/FastAPI-CacheX/issues/126) | `DeprecationWarning`(UTF-8 以外的值為 `RuntimeWarning`) | [Redis encoding](#redis-encoding) | | 拒絕短於雜湊輸出的 JWT HMAC 密鑰 | [#129](https://github.com/allen0099/FastAPI-CacheX/issues/129) | `UserWarning` | [JWT 密鑰長度](#jwt-secret) | | 移除 `SessionMiddleware` | [#69](https://github.com/allen0099/FastAPI-CacheX/issues/69) | `DeprecationWarning` | [SessionMiddleware](#session-middleware) | | 移除 `BackendProxy.get_backend()`/`set_backend()` | [#70](https://github.com/allen0099/FastAPI-CacheX/issues/70) | `DeprecationWarning` | [BackendProxy](#backend-proxy) | @@ -257,7 +257,7 @@ entry.headers["link"] ### 監控路由 {#add-routes} -0.4.0 起 `add_routes()` 必須傳入 `dependencies`,`include_content_preview` 預設為 `False`([#298](https://github.com/allen0099/FastAPI-CacheX/issues/298))。0.3.x 在省略 `dependencies` 時會發出 `UserWarning`。 +0.4.0 起 `add_routes()` 必須傳入 `dependencies`,`include_content_preview` 預設為 `False`([#298](https://github.com/allen0099/FastAPI-CacheX/issues/298))。0.3.9 在省略 `dependencies` 時會發出 `UserWarning`。 ```python # 修改前 @@ -273,7 +273,7 @@ add_routes(app, dependencies=[], include_content_preview=True) ### Redis encoding {#redis-encoding} -0.4.0 的 Redis 用戶端會直接讀取原始位元組,並從 `AsyncRedisCacheBackend` 與 `RedisConfig` 移除 `encoding` 選項([#126](https://github.com/allen0099/FastAPI-CacheX/issues/126))。項目一律以 UTF-8 寫入,因此省略它不會改變任何行為。在 0.3.9 中,只要傳入 `encoding` 就會發出 `DeprecationWarning`(UTF-8 以外的值仍會另外發出 `RuntimeWarning`)。 +0.4.0 的 Redis 用戶端會直接讀取原始位元組,並從 `AsyncRedisCacheBackend` 與 `RedisConfig` 移除 `encoding` 選項([#126](https://github.com/allen0099/FastAPI-CacheX/issues/126))。項目一律以 UTF-8 寫入,因此省略它不會改變任何行為。在 0.3.9 中,傳給 `AsyncRedisCacheBackend` 的 UTF-8 `encoding` 會發出 `DeprecationWarning`,其他值則只會發出原有的 `RuntimeWarning`,其訊息同樣預告此參數將被移除。設定了 `encoding` 的 `RedisConfig` 在傳入 `load_from_config()` 時會發出 `DeprecationWarning`,若值不是 UTF-8 還會另外發出 `RuntimeWarning`。 ```python # 修改前 diff --git a/i18n/zh-TW/docs/STATE.md b/i18n/zh-TW/docs/STATE.md index 2969a9e..3bb6edc 100644 --- a/i18n/zh-TW/docs/STATE.md +++ b/i18n/zh-TW/docs/STATE.md @@ -105,7 +105,7 @@ StateManagerProxy.set(StateManager(key_prefix="csrf:", default_ttl=300)) ``` CacheXError └── StateError - ├── InvalidStateError # 不存在或已被消耗 + ├── InvalidStateError # 不存在、已被消耗或綁定不符 ├── StateExpiredError # 已過期 └── StateDataError # 內容格式錯誤 ``` @@ -115,4 +115,4 @@ CacheXError - **後端必須在多個行程之間共用。** 多 worker 部署請使用 Redis 或 Memcached。使用 `MemoryBackend` 時,state 只存在於建立它的行程中,因此落到其他 worker 的授權回呼會失敗。 - **一次性保證來自後端的原子操作。** `get_and_delete()` 在 Redis 上是 `GETDEL`(需要 Redis 伺服器 6.2 或更新版本);在 Memcached 上是 `gets` 後接 `cas(..., exptime=-1)`(若中間有其他寫入者替換了值則會重試;連續 16 次都被替換時會拋出 `CacheXError`,而不是當成 state 不存在);在記憶體後端上則是在鎖內 `pop`。只實作抽象方法的自訂後端會退回使用 `BaseCacheBackend` 的非原子性版本,因此並行的重送可能兩邊都成功。這種情況請覆寫 `get_and_delete()`。 - 不要在 state 中存放敏感資料。`metadata` 會以明文 JSON 存放在快取後端中。 -- **日誌中絕不會出現 state 本身。** 來自 `fastapi_cachex.state.manager` 的日誌以 `state_ref` 識別 state,也就是其 SHA-256 的前 12 個十六進位字元;你可以從已知的 state 計算出它來比對。未知或已過期的 state 以 INFO 等級記錄,因為那是常見的用戶端輸入;格式錯誤的儲存資料則以 WARNING 等級記錄一次。 +- **日誌中絕不會出現 state 本身。** 來自 `fastapi_cachex.state.manager` 的日誌以 `state_ref` 識別 state,也就是其 SHA-256 的前 12 個十六進位字元;你可以從已知的 state 計算出它來比對。被 `consume_state()` 拒絕的未知、已過期或綁定不符的 state 以 INFO 等級記錄,因為那是常見的用戶端輸入(`validate_state()` 與 `get_state_metadata()` 對不存在或已過期的 state 則以 DEBUG 等級記錄);格式錯誤的儲存資料則以 WARNING 等級記錄一次。 diff --git a/scripts/changelog_release.py b/scripts/changelog_release.py index 509abd0..640cd49 100644 --- a/scripts/changelog_release.py +++ b/scripts/changelog_release.py @@ -13,7 +13,8 @@ and the release body is those summaries with their issue links, grouped under the same `###` headings, followed by a link to the full entries on the -documentation site. +documentation site. A notice written above the section's first `###` heading or +entry is copied verbatim to the top of the body. Three rules drive the implementation: