Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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%.

Expand Down Expand Up @@ -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/
```
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 `###`
Expand Down
6 changes: 3 additions & 3 deletions docs/MIGRATING_0_4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions docs/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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.
5 changes: 4 additions & 1 deletion examples/redis_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
3 changes: 2 additions & 1 deletion examples/session_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
3 changes: 2 additions & 1 deletion examples/session_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion fastapi_cachex/state/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
17 changes: 13 additions & 4 deletions fastapi_cachex/state/manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""State manager for OAuth and session state handling."""
"""State manager for one-time OAuth state tokens."""

import hashlib
import hmac
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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``.
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions i18n/zh-TW/docs/MIGRATING_0_4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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
# 修改前
Expand All @@ -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
# 修改前
Expand Down
4 changes: 2 additions & 2 deletions i18n/zh-TW/docs/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ StateManagerProxy.set(StateManager(key_prefix="csrf:", default_ttl=300))
```
CacheXError
└── StateError
├── InvalidStateError # 不存在或已被消耗
├── InvalidStateError # 不存在、已被消耗或綁定不符
├── StateExpiredError # 已過期
└── StateDataError # 內容格式錯誤
```
Expand All @@ -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 等級記錄一次。
3 changes: 2 additions & 1 deletion scripts/changelog_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading