From d43a1ea2e4eb36c43aad9d91b415b170cee4f144 Mon Sep 17 00:00:00 2001 From: allen0099 Date: Fri, 2 Oct 2026 12:39:47 +0000 Subject: [PATCH 1/2] feat: deprecate session and OAuth state, to be removed in 0.5.0 (#420) Importing fastapi_cachex.session or fastapi_cachex.state, or reading one of their names from the package, emits a FutureWarning that points at the importing line. A plain `import fastapi_cachex` no longer imports either package: the top-level names load lazily and leave __all__, and @cache inlines the X-Session-Token header name instead of importing it. The migration guide, the session/state/JWT pages (English and zh-TW), the README, the examples index and SECURITY.md say where to move. --- README.md | 10 +- SECURITY.md | 3 + changelog.d/420.deprecated.md | 8 ++ docs/JWT_CLAIMS.md | 3 + docs/MIGRATING_0_4.md | 30 ++++++ docs/SESSION.md | 3 + docs/STATE.md | 3 + examples/README.md | 4 + fastapi_cachex/__init__.py | 130 ++++++++++++++---------- fastapi_cachex/_deprecation.py | 68 +++++++++++++ fastapi_cachex/cache.py | 5 +- fastapi_cachex/session/__init__.py | 11 +- fastapi_cachex/state/__init__.py | 11 +- i18n/zh-TW/docs/JWT_CLAIMS.md | 3 + i18n/zh-TW/docs/MIGRATING_0_4.md | 30 ++++++ i18n/zh-TW/docs/SESSION.md | 3 + i18n/zh-TW/docs/STATE.md | 3 + i18n/zh-TW/docs/index.md | 7 +- pyproject.toml | 5 + tests/test_session_state_deprecation.py | 75 ++++++++++++++ zensical.toml | 2 +- zensical.zh-TW.toml | 2 +- 22 files changed, 352 insertions(+), 67 deletions(-) create mode 100644 changelog.d/420.deprecated.md create mode 100644 fastapi_cachex/_deprecation.py create mode 100644 tests/test_session_state_deprecation.py diff --git a/README.md b/README.md index 3ce1747..6ed1a1c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [English](https://fastapi-cachex.readthedocs.io/en/latest/) | [繁體中文](https://fastapi-cachex.readthedocs.io/zh-tw/latest/) -A high-performance caching extension for FastAPI: a server-side response cache with `Cache-Control` and `ETag` support, application-level caching, and optional session management. +A high-performance caching extension for FastAPI: a server-side response cache with `Cache-Control` and `ETag` support, and application-level caching. **Documentation:** — guides and the full API reference. @@ -26,9 +26,9 @@ A high-performance caching extension for FastAPI: a server-side response cache w your own code, with compute-on-miss `get_or_set()` and atomic store-if-absent `add()`. - **Backends** — in-memory, Redis and Memcached, with atomic counters, one-shot values and locks. -- **Sessions (optional)** — HMAC-signed or JWT session tokens over headers, - bearer tokens or cookies, with sliding expiration and IP/User-Agent binding. -- **OAuth state** — one-time state tokens for CSRF protection in OAuth/OIDC flows. +- **Sessions and OAuth state (deprecated)**: signed session tokens and one-time + OAuth state tokens. Both are deprecated in 0.4.0 and removed in 0.5.0; see + [where to move](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/#session-state-deprecated). ## Installation @@ -105,7 +105,7 @@ async def report(cache: AppCache): - [Application cache](https://fastapi-cachex.readthedocs.io/en/latest/APP_CACHE/) — `CacheManager` - [Backends](https://fastapi-cachex.readthedocs.io/en/latest/BACKENDS/) — choosing and configuring a backend, atomic primitives - [Distributed lock](https://fastapi-cachex.readthedocs.io/en/latest/LOCK/) — `CacheLock` for multi-process mutual exclusion -- [Session management](https://fastapi-cachex.readthedocs.io/en/latest/SESSION/), [OAuth state](https://fastapi-cachex.readthedocs.io/en/latest/STATE/) (one-shot OAuth/CSRF state tokens) and [JWT claims](https://fastapi-cachex.readthedocs.io/en/latest/JWT_CLAIMS/) +- Deprecated, removed in 0.5.0: [Session management](https://fastapi-cachex.readthedocs.io/en/latest/SESSION/), [OAuth state](https://fastapi-cachex.readthedocs.io/en/latest/STATE/) (one-shot OAuth/CSRF state tokens) and [JWT claims](https://fastapi-cachex.readthedocs.io/en/latest/JWT_CLAIMS/) - [Cache flow](https://fastapi-cachex.readthedocs.io/en/latest/CACHE_FLOW/) — what happens inside a cached request - [Runnable examples](https://github.com/allen0099/FastAPI-CacheX/tree/master/examples) — one complete app per feature, each covered by the test suite - [API reference](https://fastapi-cachex.readthedocs.io/en/latest/api/http-caching/) diff --git a/SECURITY.md b/SECURITY.md index 0c252fe..4c0fbdf 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,6 +13,9 @@ still there. A fix ships as a new 0.3.x patch release; earlier releases are not patched. +`fastapi_cachex.session` and `fastapi_cachex.state` are deprecated in 0.4.0. +They get security fixes during 0.4.x and none after 0.5.0 removes them. + ## Reporting a vulnerability Please do **not** open a public issue, pull request or discussion for a diff --git a/changelog.d/420.deprecated.md b/changelog.d/420.deprecated.md new file mode 100644 index 0000000..85aa7cd --- /dev/null +++ b/changelog.d/420.deprecated.md @@ -0,0 +1,8 @@ +**Sessions and OAuth state are deprecated and will be removed in 0.5.0.** +FastAPI-CacheX is narrowing to HTTP and application caching. Importing +`fastapi_cachex.session` or `fastapi_cachex.state`, or reading one of their +names from the `fastapi_cachex` package, emits a `FutureWarning`; a plain +`import fastapi_cachex` does not. The names are no longer in +`fastapi_cachex.__all__`. Both packages get security fixes only until 0.5.0; +"Sessions and OAuth state are deprecated" in the 0.4.0 migration guide says +where to move. diff --git a/docs/JWT_CLAIMS.md b/docs/JWT_CLAIMS.md index e6a7f23..1d2d565 100644 --- a/docs/JWT_CLAIMS.md +++ b/docs/JWT_CLAIMS.md @@ -1,5 +1,8 @@ # JWT Claims: Implementation Notes and Extension Guide +> [!WARNING] +> **Deprecated.** `fastapi_cachex.session` is deprecated in 0.4.0 and removed in 0.5.0 ([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)). Importing it emits a `FutureWarning`. [Migrating to 0.4.0](MIGRATING_0_4.md#session-state-deprecated) says where to move. + ## Overview FastAPI-CacheX's JWT token serializer implements a minimal set of JWT claims to carry session tokens securely. This document explains: diff --git a/docs/MIGRATING_0_4.md b/docs/MIGRATING_0_4.md index 86b0da9..7eece17 100644 --- a/docs/MIGRATING_0_4.md +++ b/docs/MIGRATING_0_4.md @@ -26,6 +26,7 @@ Every warning below names the setting to change and links to its issue. `FutureW | Change | Issue | Warned in 0.3.9 | Section | |--------|-------|-----------------|---------| +| Sessions and OAuth state deprecated, removed in 0.5.0 | [#420](https://github.com/allen0099/FastAPI-CacheX/issues/420) | No (0.4.0 warns: `FutureWarning`) | [Sessions and OAuth state are deprecated](#session-state-deprecated) | | Session cookie defaults to `__Host-session` with `Secure` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `FutureWarning` | [Session cookie](#session-cookie) | | Contradictory `__Host-` / `__Secure-` cookie settings are rejected | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `UserWarning` (`FutureWarning` when `cookie_name` is left at its default, under the middleware only) | [Session cookie](#session-cookie) | | Explicit `login()` / `logout()`, read-only `Session.user` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | No | [Login and logout](#login-logout) | @@ -47,8 +48,37 @@ Every warning below names the setting to change and links to its issue. `FutureW | HTTP cache key format | [#271](https://github.com/allen0099/FastAPI-CacheX/issues/271), [#270](https://github.com/allen0099/FastAPI-CacheX/issues/270), [#269](https://github.com/allen0099/FastAPI-CacheX/issues/269), [#266](https://github.com/allen0099/FastAPI-CacheX/issues/266), [#265](https://github.com/allen0099/FastAPI-CacheX/issues/265), [#72](https://github.com/allen0099/FastAPI-CacheX/issues/72) | No | [Cache keys](#cache-keys) | | Conditional session writes | [#128](https://github.com/allen0099/FastAPI-CacheX/issues/128) | No | [Session writes](#session-writes) | +## Sessions and OAuth state are deprecated {#session-state-deprecated} + +`fastapi_cachex.session` and `fastapi_cachex.state` are deprecated in 0.4.0 and removed in 0.5.0 ([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420), [#421](https://github.com/allen0099/FastAPI-CacheX/issues/421)). FastAPI-CacheX is narrowing to HTTP and application caching. Session handling and OAuth state are security-sensitive, and libraries built for them maintain them better. 0.3.9 did not announce this, so both packages keep working throughout 0.4.x and get security fixes only. + +Importing either package, or reading one of their names from `fastapi_cachex` (such as `fastapi_cachex.SessionConfig`), emits a `FutureWarning` that points at the importing line. `import fastapi_cachex` on its own does not warn, and neither do `@cache`, `CacheManager`, `CacheLock` or the backends. The session and state names are no longer in `fastapi_cachex.__all__`, so `from fastapi_cachex import *` stops providing them. Until you migrate, import them by name. + +Where to move: + +| You use | Move to | +|---------|---------| +| `FastAPICacheXSessionMiddleware` with cookie sessions | Starlette's `SessionMiddleware` keeps small session data in a signed cookie. If sessions must live on the server (large data, server-side revocation), use a server-side session library, for example `starsessions`. | +| Session tokens in a header or `Authorization: Bearer` for an API | The access tokens of your authentication stack, for example OAuth 2 bearer tokens verified with a JWT library. | +| `StateManager` for the OAuth/OIDC `state` | Your OAuth client library. Authlib's Starlette integration, for example, creates and checks `state` and `nonce` for you. | +| `CacheManager`, `CacheLock`, `@cache` | Nothing to do: they are not deprecated. | + +Sessions and states already in the backend need no cleanup; they expire on their own TTL. + +To silence the warning while you migrate, filter it before the first import: + +```python +import warnings + +warnings.filterwarnings( + "ignore", message="fastapi_cachex.session is deprecated", category=FutureWarning +) +``` + ## Sessions {#sessions} +These changes still apply if you keep using sessions during 0.4.x. + ### Session cookie defaults {#session-cookie} 0.4.0 changes two `SessionConfig` defaults ([#256](https://github.com/allen0099/FastAPI-CacheX/issues/256)): `cookie_name` becomes `"__Host-session"` (from `"session"`) and `cookie_https_only` becomes `True` (from `False`). Browsers accept a `__Host-` cookie only when it is `Secure`, has `Path=/` and no `Domain`, and never from a subdomain, which removes the usual way to plant a session cookie. Two consequences: diff --git a/docs/SESSION.md b/docs/SESSION.md index 1c73a5b..54729d6 100644 --- a/docs/SESSION.md +++ b/docs/SESSION.md @@ -1,5 +1,8 @@ # Session Management Extension +> [!WARNING] +> **Deprecated.** `fastapi_cachex.session` is deprecated in 0.4.0 and removed in 0.5.0 ([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)). Importing it emits a `FutureWarning`. [Migrating to 0.4.0](MIGRATING_0_4.md#session-state-deprecated) says where to move. + FastAPI-CacheX Session Management provides complete user session handling, including signed tokens, sliding expiration, and optional IP/User-Agent binding. Session contents always live in the cache backend; the client only holds a single signed token. diff --git a/docs/STATE.md b/docs/STATE.md index bccf1fa..f1f3fe5 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -1,5 +1,8 @@ # State Management Extension +> [!WARNING] +> **Deprecated.** `fastapi_cachex.state` is deprecated in 0.4.0 and removed in 0.5.0 ([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)). Importing it emits a `FutureWarning`. [Migrating to 0.4.0](MIGRATING_0_4.md#session-state-deprecated) says where to move. + `fastapi_cachex.state` provides **one-time state tokens** for OAuth / OIDC authorization flows. Before starting the authorization, generate a random state and store it in the cache backend. When the callback comes back, **consume** it. A consumed state cannot be used a diff --git a/examples/README.md b/examples/README.md index e01e929..44a2de4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,6 +4,10 @@ Each file here is a complete FastAPI app that shows one feature of FastAPI-CacheX. They use only the public API and the in-memory backend (except `redis_backend.py` and `session_redis.py`), so they run without any server. +The `session_*.py` and `oauth_state.py` examples use `fastapi_cachex.session` +and `fastapi_cachex.state`, which are deprecated in 0.4.0 and removed in 0.5.0 +([where to move](https://fastapi-cachex.readthedocs.io/en/latest/MIGRATING_0_4/#session-state-deprecated)). + | Example | What it shows | Needs | |---------|---------------|-------| | [`http_cache.py`](http_cache.py) | `@cache` with a TTL, ETag and `304 Not Modified`, `no_cache` and `private` routes, `clear_path()` after an update, the monitoring routes behind an admin check | — | diff --git a/fastapi_cachex/__init__.py b/fastapi_cachex/__init__.py index 17ddee2..f793606 100644 --- a/fastapi_cachex/__init__.py +++ b/fastapi_cachex/__init__.py @@ -1,8 +1,10 @@ """FastAPI-CacheX: A powerful and flexible caching extension for FastAPI.""" import logging +from importlib import import_module from importlib.metadata import PackageNotFoundError from importlib.metadata import version +from typing import TYPE_CHECKING from .cache import build_cache_key as build_cache_key from .cache import cache as cache @@ -23,34 +25,39 @@ from .manager_proxy import CacheManagerProxy as CacheManagerProxy from .proxy import BackendProxy as BackendProxy from .routes import add_routes as add_routes -from .session import FastAPICacheXSessionMiddleware as FastAPICacheXSessionMiddleware -from .session import Session as Session -from .session import SessionConfig as SessionConfig -from .session import SessionManager as SessionManager -from .session import SessionManagerProxy as SessionManagerProxy -from .session import SessionUser as SessionUser -from .session import get_optional_session as get_optional_session -from .session import get_session as get_session -from .session import get_session_manager as get_session_manager -from .session import require_session as require_session -from .session import require_user_session as require_user_session -from .session.exceptions import SessionError as SessionError -from .session.exceptions import SessionExpiredError as SessionExpiredError -from .session.exceptions import SessionInvalidError as SessionInvalidError -from .session.exceptions import SessionNotFoundError as SessionNotFoundError -from .session.exceptions import SessionSecurityError as SessionSecurityError -from .session.exceptions import SessionTokenError as SessionTokenError -from .state import InvalidStateError as InvalidStateError -from .state import StateData as StateData -from .state import StateDataError as StateDataError -from .state import StateError as StateError -from .state import StateExpiredError as StateExpiredError -from .state import StateManager as StateManager -from .state import StateManagerDep as StateManagerDep -from .state import StateManagerProxy as StateManagerProxy -from .state import get_state_manager as get_state_manager from .types import CacheKeyBuilder as CacheKeyBuilder +if TYPE_CHECKING: + # Type checkers see the real types; at runtime `__getattr__` loads them. + from .session import ( + FastAPICacheXSessionMiddleware as FastAPICacheXSessionMiddleware, + ) + from .session import Session as Session + from .session import SessionConfig as SessionConfig + from .session import SessionManager as SessionManager + from .session import SessionManagerProxy as SessionManagerProxy + from .session import SessionUser as SessionUser + from .session import get_optional_session as get_optional_session + from .session import get_session as get_session + from .session import get_session_manager as get_session_manager + from .session import require_session as require_session + from .session import require_user_session as require_user_session + from .session.exceptions import SessionError as SessionError + from .session.exceptions import SessionExpiredError as SessionExpiredError + from .session.exceptions import SessionInvalidError as SessionInvalidError + from .session.exceptions import SessionNotFoundError as SessionNotFoundError + from .session.exceptions import SessionSecurityError as SessionSecurityError + from .session.exceptions import SessionTokenError as SessionTokenError + from .state import InvalidStateError as InvalidStateError + from .state import StateData as StateData + from .state import StateDataError as StateDataError + from .state import StateError as StateError + from .state import StateExpiredError as StateExpiredError + from .state import StateManager as StateManager + from .state import StateManagerDep as StateManagerDep + from .state import StateManagerProxy as StateManagerProxy + from .state import get_state_manager as get_state_manager + def _read_version() -> str: """Return the installed distribution's version. @@ -72,6 +79,51 @@ def _read_version() -> str: logging.NullHandler() ) # Attach a NullHandler to avoid "No handler found" warnings in user applications. +# Session and OAuth state names, deprecated in 0.4.0 and removed in 0.5.0 +# (#420). They load on first use, so `import fastapi_cachex` does not import +# either package; importing one emits its FutureWarning. They are left out of +# `__all__`, so `from fastapi_cachex import *` does not load them either. +_DEPRECATED_NAMES = { + "FastAPICacheXSessionMiddleware": "fastapi_cachex.session", + "InvalidStateError": "fastapi_cachex.state", + "Session": "fastapi_cachex.session", + "SessionConfig": "fastapi_cachex.session", + "SessionError": "fastapi_cachex.session.exceptions", + "SessionExpiredError": "fastapi_cachex.session.exceptions", + "SessionInvalidError": "fastapi_cachex.session.exceptions", + "SessionManager": "fastapi_cachex.session", + "SessionManagerProxy": "fastapi_cachex.session", + "SessionNotFoundError": "fastapi_cachex.session.exceptions", + "SessionSecurityError": "fastapi_cachex.session.exceptions", + "SessionTokenError": "fastapi_cachex.session.exceptions", + "SessionUser": "fastapi_cachex.session", + "StateData": "fastapi_cachex.state", + "StateDataError": "fastapi_cachex.state", + "StateError": "fastapi_cachex.state", + "StateExpiredError": "fastapi_cachex.state", + "StateManager": "fastapi_cachex.state", + "StateManagerDep": "fastapi_cachex.state", + "StateManagerProxy": "fastapi_cachex.state", + "get_optional_session": "fastapi_cachex.session", + "get_session": "fastapi_cachex.session", + "get_session_manager": "fastapi_cachex.session", + "get_state_manager": "fastapi_cachex.state", + "require_session": "fastapi_cachex.session", + "require_user_session": "fastapi_cachex.session", +} + + +def __getattr__(name: str) -> object: + """Resolve a deprecated session or state name on first use.""" + module_name = _DEPRECATED_NAMES.get(name) + if module_name is None: + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) + value = getattr(import_module(module_name), name) + globals()[name] = value + return value + + __all__ = [ "AppCache", "BackendNotFoundError", @@ -83,29 +135,9 @@ def _read_version() -> str: "CacheManager", "CacheManagerProxy", "CacheXError", - "FastAPICacheXSessionMiddleware", - "InvalidStateError", "LockTimeoutError", "ProxyNotSetError", "RequestNotFoundError", - "Session", - "SessionConfig", - "SessionError", - "SessionExpiredError", - "SessionInvalidError", - "SessionManager", - "SessionManagerProxy", - "SessionNotFoundError", - "SessionSecurityError", - "SessionTokenError", - "SessionUser", - "StateData", - "StateDataError", - "StateError", - "StateExpiredError", - "StateManager", - "StateManagerDep", - "StateManagerProxy", "__version__", "add_routes", "build_cache_key", @@ -113,11 +145,5 @@ def _read_version() -> str: "default_key_builder", "get_app_cache", "get_cache_backend", - "get_optional_session", - "get_session", - "get_session_manager", - "get_state_manager", "invalidate", - "require_session", - "require_user_session", ] diff --git a/fastapi_cachex/_deprecation.py b/fastapi_cachex/_deprecation.py new file mode 100644 index 0000000..1245ead --- /dev/null +++ b/fastapi_cachex/_deprecation.py @@ -0,0 +1,68 @@ +"""Deprecation of the session and OAuth state subsystems (#420). + +Both packages warn once, when they are first imported, and are removed in +0.5.0 (#421). +""" + +import inspect +import warnings +from types import FrameType + +_MIGRATION_URL = ( + "https://fastapi-cachex.readthedocs.io/en/latest/" + "MIGRATING_0_4/#session-state-deprecated" +) + +SESSION_DEPRECATION = ( + "fastapi_cachex.session is deprecated and will be removed in " + "fastapi-cachex 0.5.0. Use Starlette's SessionMiddleware for signed-cookie " + f"sessions, or a dedicated session library for server-side sessions; see {_MIGRATION_URL}" +) + +STATE_DEPRECATION = ( + "fastapi_cachex.state is deprecated and will be removed in " + "fastapi-cachex 0.5.0. Use the state handling of your OAuth client library " + f"(Authlib, for example); see {_MIGRATION_URL}" +) + + +def _is_import_machinery(frame: FrameType) -> bool: + """Whether ``warnings.warn()`` skips ``frame`` when it counts stacklevel.""" + filename = frame.f_code.co_filename + return "importlib" in filename and "_bootstrap" in filename + + +def _importer_stacklevel() -> int: + """Return the ``stacklevel`` of the first frame outside this package. + + Frames of fastapi_cachex and of the import machinery are skipped, so the + warning names the application's ``import`` line, or the line that read a + deprecated name from the ``fastapi_cachex`` package. + """ + frame = inspect.currentframe() + if frame is None or frame.f_back is None: # no frame support + return 2 + # Level 1 is warn_deprecated(); start at its caller. + level, frame = 2, frame.f_back.f_back + while frame is not None: + module = frame.f_globals.get("__name__", "") + if not module.startswith(("fastapi_cachex.", "importlib.")) and module not in { + "fastapi_cachex", + "importlib", + }: + break + # warnings.warn() does not count the frozen import machinery's frames + # towards stacklevel, so neither may this. + if not _is_import_machinery(frame): + level += 1 + frame = frame.f_back + return level + + +def warn_deprecated(message: str) -> None: + """Emit the ``FutureWarning`` for a deprecated subsystem. + + ``FutureWarning`` rather than ``DeprecationWarning``: it is shown by + default, and the removal affects the application, not only its tests. + """ + warnings.warn(message, FutureWarning, stacklevel=_importer_stacklevel()) diff --git a/fastapi_cachex/cache.py b/fastapi_cachex/cache.py index c96017b..36356c3 100644 --- a/fastapi_cachex/cache.py +++ b/fastapi_cachex/cache.py @@ -45,7 +45,6 @@ from .headers import add_vary from .proxy import BackendProxy from .proxy import get_backend_or_fallback -from .session.config import DEFAULT_SESSION_HEADER_NAME from .types import CACHE_KEY_SEPARATOR from .types import CacheEntry from .types import CacheKeyBuilder @@ -195,7 +194,9 @@ def _validate_vary(vary: Sequence[str] | None) -> list[str]: "authorization", "proxy-authorization", "cookie", - DEFAULT_SESSION_HEADER_NAME.lower(), + # The session token header (`SessionConfig.header_name`'s default). + # Inlined, so @cache does not import the deprecated session package. + "x-session-token", } ) _HASHED_VARY_MARKER = "sha256:" diff --git a/fastapi_cachex/session/__init__.py b/fastapi_cachex/session/__init__.py index 7d7f76e..ef4efc5 100644 --- a/fastapi_cachex/session/__init__.py +++ b/fastapi_cachex/session/__init__.py @@ -1,4 +1,11 @@ -"""Session management extension for FastAPI-CacheX.""" +"""Session management extension for FastAPI-CacheX. + +Deprecated in 0.4.0 and removed in 0.5.0 (#420): importing this package +emits a ``FutureWarning``. +""" + +from fastapi_cachex._deprecation import SESSION_DEPRECATION +from fastapi_cachex._deprecation import warn_deprecated from .config import SessionConfig from .dependencies import get_optional_session @@ -17,6 +24,8 @@ from .models import SessionUser from .proxy import SessionManagerProxy +warn_deprecated(SESSION_DEPRECATION) + __all__ = [ "FastAPICacheXSessionMiddleware", "Session", diff --git a/fastapi_cachex/state/__init__.py b/fastapi_cachex/state/__init__.py index 560b07b..c5410cd 100644 --- a/fastapi_cachex/state/__init__.py +++ b/fastapi_cachex/state/__init__.py @@ -1,4 +1,11 @@ -"""State management extension for FastAPI-CacheX.""" +"""State management extension for FastAPI-CacheX. + +Deprecated in 0.4.0 and removed in 0.5.0 (#420): importing this package +emits a ``FutureWarning``. +""" + +from fastapi_cachex._deprecation import STATE_DEPRECATION +from fastapi_cachex._deprecation import warn_deprecated from .dependencies import StateManagerDep as StateManagerDep from .dependencies import get_state_manager as get_state_manager @@ -9,3 +16,5 @@ from .manager import StateManager as StateManager from .models import StateData as StateData from .proxy import StateManagerProxy as StateManagerProxy + +warn_deprecated(STATE_DEPRECATION) diff --git a/i18n/zh-TW/docs/JWT_CLAIMS.md b/i18n/zh-TW/docs/JWT_CLAIMS.md index b5ec0da..18311c5 100644 --- a/i18n/zh-TW/docs/JWT_CLAIMS.md +++ b/i18n/zh-TW/docs/JWT_CLAIMS.md @@ -1,5 +1,8 @@ # JWT claims:實作說明與擴充指南 {#jwt-claims-implementation-notes-and-extension-guide} +> [!WARNING] +> **已棄用。** `fastapi_cachex.session` 在 0.4.0 已棄用,並將在 0.5.0 移除([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420))。匯入時會發出 `FutureWarning`。遷移方向請見[遷移至 0.4.0](MIGRATING_0_4.md#session-state-deprecated)。 + ## 概觀 {#overview} FastAPI-CacheX 的 JWT 權杖序列化器只實作了最小的一組 JWT claim,用來安全地承載 Session 權杖。本文件說明: diff --git a/i18n/zh-TW/docs/MIGRATING_0_4.md b/i18n/zh-TW/docs/MIGRATING_0_4.md index b799f4b..6204e4a 100644 --- a/i18n/zh-TW/docs/MIGRATING_0_4.md +++ b/i18n/zh-TW/docs/MIGRATING_0_4.md @@ -26,6 +26,7 @@ filterwarnings = [ | 變更 | Issue | 0.3.9 是否警告 | 章節 | |------|-------|----------------|------| +| Session 與 OAuth state 已棄用,0.5.0 移除 | [#420](https://github.com/allen0099/FastAPI-CacheX/issues/420) | 否(0.4.0 會發出 `FutureWarning`) | [Session 與 OAuth state 已棄用](#session-state-deprecated) | | Session Cookie 預設為 `__Host-session` 並帶 `Secure` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `FutureWarning` | [Session Cookie](#session-cookie) | | 拒絕互相矛盾的 `__Host-`/`__Secure-` Cookie 設定 | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `UserWarning`(`cookie_name` 維持預設值時為 `FutureWarning`,僅限使用中介軟體時) | [Session Cookie](#session-cookie) | | 明確的 `login()`/`logout()`、唯讀的 `Session.user` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | 否 | [登入與登出](#login-logout) | @@ -47,8 +48,37 @@ filterwarnings = [ | HTTP 快取鍵格式 | [#271](https://github.com/allen0099/FastAPI-CacheX/issues/271)、[#270](https://github.com/allen0099/FastAPI-CacheX/issues/270)、[#269](https://github.com/allen0099/FastAPI-CacheX/issues/269)、[#266](https://github.com/allen0099/FastAPI-CacheX/issues/266)、[#265](https://github.com/allen0099/FastAPI-CacheX/issues/265)、[#72](https://github.com/allen0099/FastAPI-CacheX/issues/72) | 否 | [快取鍵](#cache-keys) | | 有條件的 Session 寫入 | [#128](https://github.com/allen0099/FastAPI-CacheX/issues/128) | 否 | [Session 寫入](#session-writes) | +## Session 與 OAuth state 已棄用 {#session-state-deprecated} + +`fastapi_cachex.session` 與 `fastapi_cachex.state` 在 0.4.0 已棄用,並將在 0.5.0 移除([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)、[#421](https://github.com/allen0099/FastAPI-CacheX/issues/421))。FastAPI-CacheX 的範圍將收斂到 HTTP 快取與應用層快取。Session 處理與 OAuth state 都牽涉安全性,由專門的函式庫維護會更好。由於 0.3.9 沒有預告這項變更,兩個套件在整個 0.4.x 期間都能繼續使用,但只會收到安全性修正。 + +匯入其中任一個套件,或從 `fastapi_cachex` 讀取它們的名稱(例如 `fastapi_cachex.SessionConfig`),都會發出 `FutureWarning`,並指向匯入的那一行。單純 `import fastapi_cachex` 不會發出警告,`@cache`、`CacheManager`、`CacheLock` 與各後端也不會。Session 與 state 的名稱已不在 `fastapi_cachex.__all__` 中,因此 `from fastapi_cachex import *` 不再提供它們。遷移完成前,請以名稱個別匯入。 + +遷移方向: + +| 目前使用 | 改用 | +|----------|------| +| 搭配 Cookie Session 的 `FastAPICacheXSessionMiddleware` | Starlette 的 `SessionMiddleware` 會把少量的 Session 資料存放在簽署過的 Cookie 中。若 Session 必須存放在伺服器端(資料量大,或需要在伺服器端撤銷),請改用伺服器端 Session 函式庫,例如 `starsessions`。 | +| API 以標頭或 `Authorization: Bearer` 傳遞 Session 權杖 | 改用驗證機制本身的存取權杖,例如以 JWT 函式庫驗證的 OAuth 2 Bearer 權杖。 | +| 以 `StateManager` 處理 OAuth/OIDC 的 `state` | 改用 OAuth 用戶端函式庫。例如 Authlib 的 Starlette 整合會替你產生並檢查 `state` 與 `nonce`。 | +| `CacheManager`、`CacheLock`、`@cache` | 不需處理,這些並未棄用。 | + +後端中已存在的 Session 與 state 不需要清理,會依各自的 TTL 自行過期。 + +遷移期間若要隱藏警告,請在第一次匯入之前加上篩選: + +```python +import warnings + +warnings.filterwarnings( + "ignore", message="fastapi_cachex.session is deprecated", category=FutureWarning +) +``` + ## Session {#sessions} +若在 0.4.x 期間繼續使用 Session,下列變更仍然適用。 + ### Session Cookie 預設值 {#session-cookie} 0.4.0 會改變兩個 `SessionConfig` 預設值([#256](https://github.com/allen0099/FastAPI-CacheX/issues/256)):`cookie_name` 從 `"session"` 改為 `"__Host-session"`,`cookie_https_only` 從 `False` 改為 `True`。瀏覽器只接受帶 `Secure`、`Path=/` 且沒有 `Domain` 的 `__Host-` Cookie,也不接受子網域設定的這種 Cookie,因此移除了植入 Session Cookie 最常見的途徑。這帶來兩個後果: diff --git a/i18n/zh-TW/docs/SESSION.md b/i18n/zh-TW/docs/SESSION.md index f98c039..e919ab8 100644 --- a/i18n/zh-TW/docs/SESSION.md +++ b/i18n/zh-TW/docs/SESSION.md @@ -1,5 +1,8 @@ # Session 管理擴充 {#session-management-extension} +> [!WARNING] +> **已棄用。** `fastapi_cachex.session` 在 0.4.0 已棄用,並將在 0.5.0 移除([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420))。匯入時會發出 `FutureWarning`。遷移方向請見[遷移至 0.4.0](MIGRATING_0_4.md#session-state-deprecated)。 + FastAPI-CacheX 的 Session 管理提供完整的使用者 Session 處理,包括簽署過的權杖、滑動過期,以及可選的 IP / User-Agent 綁定。Session 內容一律存放在快取後端;用戶端只持有一個簽署過的權杖。 **`FastAPICacheXSessionMiddleware` 如何傳遞權杖:** diff --git a/i18n/zh-TW/docs/STATE.md b/i18n/zh-TW/docs/STATE.md index 3bb6edc..ba0b750 100644 --- a/i18n/zh-TW/docs/STATE.md +++ b/i18n/zh-TW/docs/STATE.md @@ -1,5 +1,8 @@ # State 管理擴充 {#state-management-extension} +> [!WARNING] +> **已棄用。** `fastapi_cachex.state` 在 0.4.0 已棄用,並將在 0.5.0 移除([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420))。匯入時會發出 `FutureWarning`。遷移方向請見[遷移至 0.4.0](MIGRATING_0_4.md#session-state-deprecated)。 + `fastapi_cachex.state` 為 OAuth / OIDC 授權流程提供**一次性 state 權杖**。開始授權之前,先產生一個隨機 state 並存入快取後端;回呼(callback)回來時,再將它**消耗**掉。已消耗的 state 無法再使用第二次。 只有當 state **綁定到發起流程的瀏覽器**時,它才能保護流程免於 CSRF 攻擊(RFC 6749 §10.12)。光是儲存並不夠:攻擊者可以在自己的瀏覽器中發起流程,再把受害者導向帶有攻擊者 state 與 code 的回呼,使受害者登入攻擊者的帳號。請在建立 state 時傳入 `binding`(一個同時設為 Cookie 的隨機 nonce),並在消耗時傳入相同的值,如下方快速開始所示。 diff --git a/i18n/zh-TW/docs/index.md b/i18n/zh-TW/docs/index.md index f4feb18..e9ba183 100644 --- a/i18n/zh-TW/docs/index.md +++ b/i18n/zh-TW/docs/index.md @@ -14,7 +14,7 @@ [English](https://fastapi-cachex.readthedocs.io/en/latest/) | [繁體中文](https://fastapi-cachex.readthedocs.io/zh-tw/latest/) -FastAPI-CacheX 是 FastAPI 的高效能快取擴充套件:提供支援 `Cache-Control` 與 `ETag` 的伺服器端回應快取、應用層快取,以及可選的 Session 管理。 +FastAPI-CacheX 是 FastAPI 的高效能快取擴充套件:提供支援 `Cache-Control` 與 `ETag` 的伺服器端回應快取,以及應用層快取。 **文件:** (尚未翻譯的頁面與完整 API 參考請見[英文文件](https://fastapi-cachex.readthedocs.io/en/latest/)) @@ -23,8 +23,7 @@ FastAPI-CacheX 是 FastAPI 的高效能快取擴充套件:提供支援 `Cache- - **HTTP 快取**:GET 路由專用的 `@cache` 裝飾器,支援 `Cache-Control`、`ETag` / `If-None-Match`(304)與單一路由的快取失效。 - **應用層快取**:`CacheManager` 可在自己的程式碼中快取任意 JSON 值,提供未命中時才計算的 `get_or_set()` 與原子性的「不存在才寫入」`add()`。 - **後端**:記憶體、Redis 與 Memcached,支援原子操作的計數器、一次性取值與鎖。 -- **Session(可選)**:以 HMAC 簽章或 JWT 發行的 Session 權杖,可經由標頭、Bearer 權杖或 Cookie 傳遞,支援滑動過期與 IP / User-Agent 綁定。 -- **OAuth state**:OAuth / OIDC 流程中用於防範 CSRF 的一次性 state 權杖。 +- **Session 與 OAuth state(已棄用)**:簽署過的 Session 權杖與一次性的 OAuth state 權杖。兩者在 0.4.0 已棄用,並將在 0.5.0 移除,遷移方向請見[這裡](MIGRATING_0_4.md#session-state-deprecated)。 ## 安裝 {#installation} @@ -94,7 +93,7 @@ async def report(cache: AppCache): - [應用層快取](APP_CACHE.md):`CacheManager` - [後端](BACKENDS.md):選擇與設定後端、原子操作的基本功能 - [分散式鎖](LOCK.md):以 `CacheLock` 在多個行程之間互斥 -- [Session 管理](SESSION.md)、[OAuth state](STATE.md)(一次性的 OAuth / CSRF state 權杖)與 [JWT claims](JWT_CLAIMS.md) +- 已棄用、0.5.0 移除:[Session 管理](SESSION.md)、[OAuth state](STATE.md)(一次性的 OAuth / CSRF state 權杖)與 [JWT claims](JWT_CLAIMS.md) - [快取流程](CACHE_FLOW.md):快取請求內部的處理流程 - [可執行範例](https://github.com/allen0099/FastAPI-CacheX/tree/master/examples)(英文):每個功能一個完整的應用程式,皆由測試套件涵蓋 - [API 參考](https://fastapi-cachex.readthedocs.io/en/latest/api/http-caching/)(英文) diff --git a/pyproject.toml b/pyproject.toml index 1e66825..f5a6e4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,11 @@ filterwarnings = [ # TestClient through an alias that newer anyio deprecates. Later starlette # releases no longer use it; nothing in this package or its tests does. "ignore:The anyio.abc.BlockingPortal alias is deprecated:DeprecationWarning", + # Session and OAuth state are deprecated (#420) and warn once, on first + # import; their own tests keep running until 0.5.0 removes them. + # tests/test_session_state_deprecation.py checks the warnings themselves. + "ignore:fastapi_cachex.session is deprecated:FutureWarning", + "ignore:fastapi_cachex.state is deprecated:FutureWarning", ] [tool.ruff.lint] diff --git a/tests/test_session_state_deprecation.py b/tests/test_session_state_deprecation.py new file mode 100644 index 0000000..e82223b --- /dev/null +++ b/tests/test_session_state_deprecation.py @@ -0,0 +1,75 @@ +"""Session and OAuth state are deprecated in 0.4.0 (#420). + +Each package warns once per process, on first import, so these tests run the +import in a fresh interpreter. +""" + +import subprocess +import sys +from pathlib import Path + +import pytest + +import fastapi_cachex + + +def _run(script: str, tmp_path: Path) -> str: + """Run ``script`` as a file in a new interpreter; return its stderr.""" + path = tmp_path / "app.py" + path.write_text(script) + result = subprocess.run( # noqa: S603 - fixed interpreter, script written by the test + [sys.executable, "-W", "always::FutureWarning", str(path)], + capture_output=True, + text=True, + check=True, + ) + return result.stderr + + +def test_plain_import_does_not_warn(tmp_path: Path) -> None: + stderr = _run( + "import fastapi_cachex\n" + "from fastapi_cachex import cache, CacheManager, CacheLock\n", + tmp_path, + ) + assert "FutureWarning" not in stderr + + +@pytest.mark.parametrize( + ("script", "package"), + [ + ("from fastapi_cachex.session import SessionConfig\n", "session"), + ("from fastapi_cachex.session.config import SessionConfig\n", "session"), + ("from fastapi_cachex import SessionConfig\n", "session"), + ("import fastapi_cachex\nfastapi_cachex.get_session\n", "session"), + ("from fastapi_cachex.state import StateManager\n", "state"), + ("from fastapi_cachex import StateManager\n", "state"), + ], +) +def test_import_warns_at_the_importing_line( + script: str, package: str, tmp_path: Path +) -> None: + stderr = _run(script, tmp_path) + line = script.count("\n") + assert ( + f"app.py:{line}: FutureWarning: fastapi_cachex.{package} is deprecated " + "and will be removed in fastapi-cachex 0.5.0" + ) in stderr + assert "MIGRATING_0_4/#session-state-deprecated" in stderr + + +def test_state_does_not_import_session(tmp_path: Path) -> None: + stderr = _run("from fastapi_cachex.state import StateManager\n", tmp_path) + assert "fastapi_cachex.session is deprecated" not in stderr + + +@pytest.mark.parametrize("name", sorted(fastapi_cachex._DEPRECATED_NAMES)) +def test_deprecated_name_resolves_but_is_not_exported(name: str) -> None: + module = __import__(fastapi_cachex._DEPRECATED_NAMES[name], fromlist=[name]) + assert getattr(fastapi_cachex, name) is getattr(module, name) + assert name not in fastapi_cachex.__all__ + + +def test_unknown_name_raises_attribute_error() -> None: + with pytest.raises(AttributeError, match="has no attribute 'nope'"): + fastapi_cachex.nope # noqa: B018 diff --git a/zensical.toml b/zensical.toml index f66a759..2adc224 100644 --- a/zensical.toml +++ b/zensical.toml @@ -6,7 +6,7 @@ [project] site_name = "FastAPI-CacheX" -site_description = "HTTP caching, application caching and session management for FastAPI." +site_description = "HTTP caching and application caching for FastAPI." # Zensical cannot read READTHEDOCS_CANONICAL_URL, so the canonical URL is fixed # to the default version Read the Docs serves. site_url = "https://fastapi-cachex.readthedocs.io/en/latest/" diff --git a/zensical.zh-TW.toml b/zensical.zh-TW.toml index 8f3dfd5..2239cbd 100644 --- a/zensical.zh-TW.toml +++ b/zensical.zh-TW.toml @@ -10,7 +10,7 @@ [project] site_name = "FastAPI-CacheX" -site_description = "FastAPI 的 HTTP 快取、應用層快取與 Session 管理。" +site_description = "FastAPI 的 HTTP 快取與應用層快取。" site_url = "https://fastapi-cachex.readthedocs.io/zh-tw/latest/" repo_url = "https://github.com/allen0099/FastAPI-CacheX" repo_name = "allen0099/FastAPI-CacheX" From 5e5c66951537b1fd5611b94e1ebce4e22ff5a026 Mon Sep 17 00:00:00 2001 From: allen0099 Date: Fri, 2 Oct 2026 12:48:48 +0000 Subject: [PATCH 2/2] feat: drop the 0.4.0 session notices and warn on import only Address review feedback on #420: - importing fastapi_cachex no longer imports the session package - deprecated top-level names resolve lazily and warn at the caller - the #131 and #75 notices are dropped; #377 is removed in 0.5.0 - docs, MIGRATING_0_4 and the zh-TW translation follow --- docs/MIGRATING_0_4.md | 44 ++----- docs/SESSION.md | 28 ++--- docs/api/session.md | 3 + docs/api/state.md | 3 + fastapi_cachex/session/__init__.py | 6 +- fastapi_cachex/session/config.py | 20 +-- fastapi_cachex/session/dependencies.py | 39 ------ fastapi_cachex/session/middleware.py | 36 ------ fastapi_cachex/state/__init__.py | 6 +- i18n/zh-TW/docs/MIGRATING_0_4.md | 43 ++----- i18n/zh-TW/docs/SESSION.md | 10 +- ...tices.py => test_dropped_0_4_0_changes.py} | 118 +++++------------- tests/test_session_state_deprecation.py | 38 +++++- zensical.toml | 6 +- zensical.zh-TW.toml | 2 +- 15 files changed, 125 insertions(+), 277 deletions(-) rename tests/session/{test_0_4_0_notices.py => test_dropped_0_4_0_changes.py} (58%) diff --git a/docs/MIGRATING_0_4.md b/docs/MIGRATING_0_4.md index 7eece17..dae2f97 100644 --- a/docs/MIGRATING_0_4.md +++ b/docs/MIGRATING_0_4.md @@ -31,8 +31,8 @@ Every warning below names the setting to change and links to its issue. `FutureW | Contradictory `__Host-` / `__Secure-` cookie settings are rejected | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `UserWarning` (`FutureWarning` when `cookie_name` is left at its default, under the middleware only) | [Session cookie](#session-cookie) | | Explicit `login()` / `logout()`, read-only `Session.user` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | No | [Login and logout](#login-logout) | | `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) | -| `token_source_priority` names every token source; a list without `"cookie"` disables the cookie | [#75](https://github.com/allen0099/FastAPI-CacheX/issues/75) | `FutureWarning` | [Token sources](#token-source-priority) | +| Dropped: `get_session_manager` resolving through `SessionManagerProxy` | [#131](https://github.com/allen0099/FastAPI-CacheX/issues/131) | `FutureWarning` (0.4.0 no longer warns) | [get_session_manager](#get-session-manager) | +| Dropped: `token_source_priority` naming every token source | [#75](https://github.com/allen0099/FastAPI-CacheX/issues/75) | `FutureWarning` (0.4.0 no longer warns) | [Token sources](#token-source-priority) | | `add_routes()` requires `dependencies`, no content preview by default | [#298](https://github.com/allen0099/FastAPI-CacheX/issues/298) | `UserWarning` (only when `dependencies` is left out) | [Monitoring routes](#add-routes) | | 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) | @@ -40,7 +40,7 @@ Every warning below names the setting to change and links to its issue. `FutureW | `BackendProxy.get_backend()` / `set_backend()` removed | [#70](https://github.com/allen0099/FastAPI-CacheX/issues/70) | `DeprecationWarning` | [BackendProxy](#backend-proxy) | | `CacheError` removed | [#130](https://github.com/allen0099/FastAPI-CacheX/issues/130) | `DeprecationWarning` | [CacheError](#cache-error) | | Redis `clear_pattern()` prefix-stripped retry removed | [#125](https://github.com/allen0099/FastAPI-CacheX/issues/125) | `DeprecationWarning` | [Redis clear_pattern](#redis-clear-pattern) | -| `SessionConfig.use_bearer_token` removed | [#377](https://github.com/allen0099/FastAPI-CacheX/issues/377) | `DeprecationWarning` | [Token sources](#token-source-priority) | +| `SessionConfig.use_bearer_token` stays deprecated, removed in 0.5.0 | [#377](https://github.com/allen0099/FastAPI-CacheX/issues/377) | `DeprecationWarning` | [Token sources](#token-source-priority) | | `UserSessionDep` requires a user | [#127](https://github.com/allen0099/FastAPI-CacheX/issues/127) | No | [UserSessionDep](#user-session-dep) | | `memcache` extra removed | [#202](https://github.com/allen0099/FastAPI-CacheX/issues/202) | No | [memcache extra](#memcache-extra) | | `BaseCacheBackend.delete()` returns `bool` | [#71](https://github.com/allen0099/FastAPI-CacheX/issues/71) | No | [delete() return value](#backend-delete) | @@ -60,7 +60,7 @@ Where to move: |---------|---------| | `FastAPICacheXSessionMiddleware` with cookie sessions | Starlette's `SessionMiddleware` keeps small session data in a signed cookie. If sessions must live on the server (large data, server-side revocation), use a server-side session library, for example `starsessions`. | | Session tokens in a header or `Authorization: Bearer` for an API | The access tokens of your authentication stack, for example OAuth 2 bearer tokens verified with a JWT library. | -| `StateManager` for the OAuth/OIDC `state` | Your OAuth client library. Authlib's Starlette integration, for example, creates and checks `state` and `nonce` for you. | +| `StateManager` for the OAuth/OIDC `state` | Your OAuth client library. Authlib's Starlette integration, for example, creates and checks `state` and `nonce` for you; it keeps them in `request.session`, so it needs Starlette's `SessionMiddleware`. | | `CacheManager`, `CacheLock`, `@cache` | Nothing to do: they are not deprecated. | Sessions and states already in the backend need no cleanup; they expire on their own TTL. @@ -73,6 +73,9 @@ import warnings warnings.filterwarnings( "ignore", message="fastapi_cachex.session is deprecated", category=FutureWarning ) +warnings.filterwarnings( + "ignore", message="fastapi_cachex.state is deprecated", category=FutureWarning +) ``` ## Sessions {#sessions} @@ -146,23 +149,7 @@ await login(request, SessionUser(user_id=user_id)) ### get_session_manager {#get-session-manager} -`get_session_manager` (and `SessionManagerDep`, `ClientIPDep` and `rotate_session_id()`, which use it) currently returns the manager the middleware stored on `app.state`. 0.4.0 resolves it through `SessionManagerProxy` only, like `BackendProxy` and `CacheManagerProxy` ([#131](https://github.com/allen0099/FastAPI-CacheX/issues/131)). In 0.3.9 it emits a `FutureWarning`, once per app, when the proxy holds no manager or a different one. - -Before: - -```python -session_manager = SessionManager(backend, config) -app.add_middleware(FastAPICacheXSessionMiddleware, session_manager=session_manager) -``` - -After: - -```python -session_manager = SessionManager(backend, config) -SessionManagerProxy.set(session_manager) -# The middleware picks the manager up from the proxy. -app.add_middleware(FastAPICacheXSessionMiddleware) -``` +0.3.9 announced that 0.4.0 would resolve `get_session_manager` (and `SessionManagerDep`, `ClientIPDep` and `rotate_session_id()`, which use it) through `SessionManagerProxy` only ([#131](https://github.com/allen0099/FastAPI-CacheX/issues/131)), and emitted a `FutureWarning`. Since sessions are deprecated ([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)), 0.4.0 does not make this change. It keeps returning the manager the middleware stored on `app.state` and no longer warns. If you already call `SessionManagerProxy.set()`, keeping it is harmless. ### UserSessionDep {#user-session-dep} @@ -226,19 +213,10 @@ SessionConfig( ### Token sources {#token-source-priority} -In 0.4.0, `SessionConfig.token_source_priority` names every token source ([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75)). `FastAPICacheXSessionMiddleware` resolves the token by walking the list, `"cookie"` may appear anywhere in it, and the default becomes `["header", "bearer", "cookie"]`, which is the order used today. A list without `"cookie"` means no cookie: the middleware neither reads nor sets it, and a session created for a request without a token sends its token in the `header_name` response header. That is how an API-only app turns the cookie off. - -In 0.3.9, `"cookie"` is accepted as the last entry (where the cookie is read today, so it changes nothing yet), and `FastAPICacheXSessionMiddleware` emits a `FutureWarning` when the list was set explicitly without it. The default list does not warn. - -```python -# Before: the cookie is read after these, although the list does not say so -SessionConfig(secret_key=SECRET, token_source_priority=["bearer", "header"]) - -# After, keeping the cookie (works on 0.3.9 and 0.4.0) -SessionConfig(secret_key=SECRET, token_source_priority=["bearer", "header", "cookie"]) -``` +0.3.9 announced two changes here. Since sessions are deprecated ([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)), 0.4.0 makes neither of them: -`use_bearer_token` is removed ([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)); the list alone decides whether bearer tokens are read. 0.3.9 emits a `DeprecationWarning` when it is passed, whatever its value: +- **`token_source_priority` naming every token source ([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75)) is dropped.** The middleware still reads the session cookie after the header sources whether or not the list names it, and `"cookie"` is still accepted only as the last entry. 0.4.0 no longer emits the `FutureWarning` for a list without `"cookie"`. A list that already ends in `"cookie"` keeps working. +- **`use_bearer_token` is not removed in 0.4.0 ([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)).** It stays deprecated, still emits a `DeprecationWarning` when it is passed, and is removed in 0.5.0 together with `fastapi_cachex.session`. The list alone decides whether bearer tokens are read: ```python # Before diff --git a/docs/SESSION.md b/docs/SESSION.md index 54729d6..72a2609 100644 --- a/docs/SESSION.md +++ b/docs/SESSION.md @@ -67,9 +67,7 @@ keeps the token it gets back and sends it on later requests. ``` -The example also registers the manager on `SessionManagerProxy`, where -`get_session_manager` looks for it from 0.4.0 (see -[Migrating to 0.4.0](MIGRATING_0_4.md#get-session-manager)). With the manager +The example also registers the manager on `SessionManagerProxy`. With the manager there, the middleware can pick it up instead of taking it as an argument. When `config` is omitted, the middleware uses `session_manager.config`: @@ -299,19 +297,13 @@ the expiry reaches that cap no further renewed tokens are issued. and only falls back to the cookie when none of them yields a token. The response side follows the token's source (header in, header out; cookie in, `Set-Cookie` out). -Until 0.4.0 the cookie is read whether or not the list names it. `"cookie"` is accepted only as the -last entry, which is where it is read anyway, so listing it changes nothing yet; any other position -raises a `ValidationError`. +The cookie is read whether or not the list names it. `"cookie"` is accepted only as the last +entry, which is where it is read anyway, so listing it changes nothing; any other position raises a +`ValidationError`. (0.3.9 announced that 0.4.0 would make the list name every token source; that +change was dropped when sessions were deprecated. See +[Migrating to 0.4.0](MIGRATING_0_4.md#token-source-priority).) -In 0.4.0 the list names every token source, and its default becomes `["header", "bearer", -"cookie"]`, the order used today. A list without `"cookie"` then means no cookie at all: the -middleware neither reads nor sets it, and a session created for a request without a token sends -its token in the `header_name` response header ([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75)). -So `FastAPICacheXSessionMiddleware` emits a `FutureWarning` when the list was set explicitly -without `"cookie"`. Add `"cookie"` as the last entry to keep the cookie; the default list does not -warn. See [Migrating to 0.4.0](MIGRATING_0_4.md#token-source-priority). - -`use_bearer_token` is deprecated and removed in 0.4.0 ([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)): +`use_bearer_token` is deprecated and removed in 0.5.0 with this package ([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)): passing it emits a `DeprecationWarning`. Instead of `use_bearer_token=False`, leave `"bearer"` out of the list (`token_source_priority=["header", "cookie"]`); `use_bearer_token=True` is the default and can simply be dropped. @@ -645,11 +637,7 @@ from fastapi_cachex.session.dependencies import ( `get_session_manager` returns the manager the middleware stored on `app.state` when it handled its first request; it responds with `500` if no session middleware has run yet. Using it avoids -importing the manager into your route modules. From 0.4.0 it resolves the manager through -`SessionManagerProxy` instead, so register it there with `SessionManagerProxy.set(manager)`: -until then, `get_session_manager` (and `SessionManagerDep`, `ClientIPDep` and -`rotate_session_id()`, which use it) emits a `FutureWarning` once per app when the proxy holds no -manager or a different one. See [Migrating to 0.4.0](MIGRATING_0_4.md#get-session-manager). +importing the manager into your route modules. ```python from fastapi_cachex.session import SessionUser diff --git a/docs/api/session.md b/docs/api/session.md index 54a1825..7a7dd9e 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -1,5 +1,8 @@ # Session +> [!WARNING] +> **Deprecated.** `fastapi_cachex.session` is deprecated in 0.4.0 and removed in 0.5.0 ([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)). See [Migrating to 0.4.0](../MIGRATING_0_4.md#session-state-deprecated). + See the [session management guide](../SESSION.md) for how the pieces fit together. ::: fastapi_cachex.session.middleware.FastAPICacheXSessionMiddleware diff --git a/docs/api/state.md b/docs/api/state.md index 842c961..82aa805 100644 --- a/docs/api/state.md +++ b/docs/api/state.md @@ -1,5 +1,8 @@ # State +> [!WARNING] +> **Deprecated.** `fastapi_cachex.state` is deprecated in 0.4.0 and removed in 0.5.0 ([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)). See [Migrating to 0.4.0](../MIGRATING_0_4.md#session-state-deprecated). + See the [state management guide](../STATE.md) for the OAuth flow these support. ::: fastapi_cachex.state.manager.StateManager diff --git a/fastapi_cachex/session/__init__.py b/fastapi_cachex/session/__init__.py index ef4efc5..e7fbc5f 100644 --- a/fastapi_cachex/session/__init__.py +++ b/fastapi_cachex/session/__init__.py @@ -4,8 +4,8 @@ emits a ``FutureWarning``. """ -from fastapi_cachex._deprecation import SESSION_DEPRECATION -from fastapi_cachex._deprecation import warn_deprecated +from fastapi_cachex._deprecation import SESSION_DEPRECATION as _SESSION_DEPRECATION +from fastapi_cachex._deprecation import warn_deprecated as _warn_deprecated from .config import SessionConfig from .dependencies import get_optional_session @@ -24,7 +24,7 @@ from .models import SessionUser from .proxy import SessionManagerProxy -warn_deprecated(SESSION_DEPRECATION) +_warn_deprecated(_SESSION_DEPRECATION) __all__ = [ "FastAPICacheXSessionMiddleware", diff --git a/fastapi_cachex/session/config.py b/fastapi_cachex/session/config.py index 1900908..a0399bb 100644 --- a/fastapi_cachex/session/config.py +++ b/fastapi_cachex/session/config.py @@ -108,13 +108,13 @@ class SessionConfig(BaseModel): use_bearer_token: bool = Field( default=True, description="Whether to accept Authorization Bearer tokens (deprecated: " - 'leave "bearer" out of token_source_priority instead; removed in 0.4.0)', + 'leave "bearer" out of token_source_priority instead)', ) token_source_priority: list[Literal["header", "bearer", "cookie"]] = Field( default=["header", "bearer"], description="Priority order for token sources. The session cookie is " - 'read after the header sources; "cookie" may only be the last entry ' - "until 0.4.0, which drops the cookie from a list without it", + "read after the header sources, whether or not the list names it; " + '"cookie" may only be the last entry', ) # JWT settings (used when token_format == 'jwt') @@ -302,24 +302,23 @@ def _check_cookie_prefix(self) -> "SessionConfig": def _check_cookie_is_last( cls, value: list[Literal["header", "bearer", "cookie"]] ) -> list[Literal["header", "bearer", "cookie"]]: - """Only accept ``"cookie"`` where the cookie is read today: last. + """Only accept ``"cookie"`` where the cookie is read: last. ``FastAPICacheXSessionMiddleware`` reads the cookie after the header - sources, so listing it last changes nothing before 0.4.0, when the - list decides the order and a list without it disables the cookie (#75). + sources whether or not the list names it, so listing it anywhere else + would promise an order the middleware does not follow. """ if "cookie" in value and value.index("cookie") != len(value) - 1: msg = ( '"cookie" must be the last entry of token_source_priority: the ' - "session cookie is read after the header sources until 0.4.0 " - "(https://github.com/allen0099/FastAPI-CacheX/issues/75)" + "session cookie is read after the header sources" ) raise ValueError(msg) return value @model_validator(mode="after") def _warn_use_bearer_token(self) -> "SessionConfig": - """Deprecate ``use_bearer_token``, which 0.4.0 removes (#377). + """Deprecate ``use_bearer_token``, removed with the session package in 0.5.0. ``token_source_priority`` already decides whether bearer tokens are read, so passing the flag at all warns, whatever its value. @@ -327,7 +326,8 @@ def _warn_use_bearer_token(self) -> "SessionConfig": if "use_bearer_token" in self.model_fields_set: warnings.warn( "SessionConfig(use_bearer_token=...) is deprecated and will be " - "removed in version 0.4.0. token_source_priority decides the " + "removed in version 0.5.0 with fastapi_cachex.session. " + "token_source_priority decides the " "token sources: drop use_bearer_token=True (the default), and " 'replace use_bearer_token=False by leaving "bearer" out of ' 'token_source_priority, keeping "cookie" last, e.g. ' diff --git a/fastapi_cachex/session/dependencies.py b/fastapi_cachex/session/dependencies.py index 741e812..9722e01 100644 --- a/fastapi_cachex/session/dependencies.py +++ b/fastapi_cachex/session/dependencies.py @@ -1,6 +1,5 @@ """FastAPI dependency injection utilities for session management.""" -import warnings from collections.abc import Iterable from typing import TYPE_CHECKING from typing import Annotated @@ -12,24 +11,18 @@ from fastapi.security import HTTPAuthorizationCredentials from fastapi.security import HTTPBearer -from fastapi_cachex.exceptions import ProxyNotSetError - from .exceptions import SessionError from .middleware import _SESSION_READ_KEY from .middleware import _log_in from .middleware import _RequestSession from .middleware import get_client_ip from .models import Session -from .proxy import SessionManagerProxy if TYPE_CHECKING: from .manager import SessionManager from .models import SessionUser -# ``app.state`` flag: get_session_manager() already warned about #131 there. -_PROXY_WARNED = "__fastapi_cachex_session_manager_proxy_warned" - # HTTPBearer security scheme for OpenAPI UI _http_bearer = HTTPBearer( scheme_name="SessionBearer", @@ -159,12 +152,6 @@ async def login( Raises: HTTPException: 500 if no session middleware has registered a SessionManager yet - - Warns: - FutureWarning: Once per app, if the manager the middleware registered - is not the one set with ``SessionManagerProxy.set()`` (or none is - set there). 0.4.0 resolves this dependency through - ``SessionManagerProxy`` only. """ state = request.app.state manager: SessionManager | None = getattr( @@ -178,35 +165,9 @@ async def login( "FastAPICacheXSessionMiddleware is added to the app." ), ) - proxy_manager = _proxy_manager() - if not getattr(state, _PROXY_WARNED, False) and manager is not proxy_manager: - setattr(state, _PROXY_WARNED, True) - registered = ( - "no SessionManager is set in SessionManagerProxy" - if proxy_manager is None - else "a different SessionManager is set in SessionManagerProxy" - ) - warnings.warn( - "get_session_manager() returned the SessionManager the session " - f"middleware registered, but {registered}. Version 0.4.0 resolves " - "get_session_manager() (and SessionManagerDep, ClientIPDep and " - "rotate_session_id(), which use it) through SessionManagerProxy " - "only. Call SessionManagerProxy.set(session_manager) at startup " - "(https://github.com/allen0099/FastAPI-CacheX/issues/131).", - FutureWarning, - stacklevel=2, - ) return manager -def _proxy_manager() -> "SessionManager | None": - """Return the manager set in ``SessionManagerProxy``, or None.""" - try: - return SessionManagerProxy.get() - except ProxyNotSetError: - return None - - async def rotate_session_id(request: Request) -> bool: """Give the request's session a new ID, as a defence against session fixation. diff --git a/fastapi_cachex/session/middleware.py b/fastapi_cachex/session/middleware.py index 050088d..664e203 100644 --- a/fastapi_cachex/session/middleware.py +++ b/fastapi_cachex/session/middleware.py @@ -1,7 +1,6 @@ """Session middleware for FastAPI.""" import logging -import warnings from typing import TYPE_CHECKING from typing import Any @@ -165,36 +164,6 @@ def _stash_session_manager(app: Any, manager: SessionManager) -> None: setattr(app.state, "__fastapi_cachex_session_manager", manager) -def _warn_if_priority_without_cookie(config: SessionConfig) -> None: - """Warn that 0.4.0 stops using the cookie for a list without ``"cookie"``. - - In 0.4.0 ``token_source_priority`` lists every token source, so a list - set without ``"cookie"`` means the middleware neither reads nor sets the - session cookie (#75). The default list becomes ``["header", "bearer", - "cookie"]``, which is today's order, so it does not warn. - - Args: - config: The configuration the middleware uses - """ - if ( - "token_source_priority" not in config.model_fields_set - or "cookie" in config.token_source_priority - ): - return - warnings.warn( - f"SessionConfig(token_source_priority={config.token_source_priority!r}) " - 'does not list "cookie". In version 0.4.0 the list names every token ' - "source, so FastAPICacheXSessionMiddleware will neither read nor set the " - "session cookie, and a session created for a request without a token " - 'sends its token in the header_name response header. Add "cookie" as the ' - "last entry to keep the cookie; leaving it out opts into 0.4.0's " - "cookie-less behaviour, which only takes effect then " - "(https://github.com/allen0099/FastAPI-CacheX/issues/75).", - FutureWarning, - stacklevel=3, - ) - - class _RequestSession(StarletteSession): """``request.session`` that remembers an explicit ``clear()``. @@ -251,15 +220,10 @@ def __init__( Raises: ProxyNotSetError: If ``session_manager`` is omitted and ``SessionManagerProxy`` holds none. - - Warns: - FutureWarning: If the configuration sets ``token_source_priority`` - without ``"cookie"``, which disables the cookie in 0.4.0. """ self.app = app self.session_manager = session_manager or SessionManagerProxy.get() self.config = config or self.session_manager.config - _warn_if_priority_without_cookie(self.config) security_flags = f"httponly; samesite={self.config.cookie_same_site}" if self.config.cookie_https_only: diff --git a/fastapi_cachex/state/__init__.py b/fastapi_cachex/state/__init__.py index c5410cd..44cc25c 100644 --- a/fastapi_cachex/state/__init__.py +++ b/fastapi_cachex/state/__init__.py @@ -4,8 +4,8 @@ emits a ``FutureWarning``. """ -from fastapi_cachex._deprecation import STATE_DEPRECATION -from fastapi_cachex._deprecation import warn_deprecated +from fastapi_cachex._deprecation import STATE_DEPRECATION as _STATE_DEPRECATION +from fastapi_cachex._deprecation import warn_deprecated as _warn_deprecated from .dependencies import StateManagerDep as StateManagerDep from .dependencies import get_state_manager as get_state_manager @@ -17,4 +17,4 @@ from .models import StateData as StateData from .proxy import StateManagerProxy as StateManagerProxy -warn_deprecated(STATE_DEPRECATION) +_warn_deprecated(_STATE_DEPRECATION) diff --git a/i18n/zh-TW/docs/MIGRATING_0_4.md b/i18n/zh-TW/docs/MIGRATING_0_4.md index 6204e4a..2589329 100644 --- a/i18n/zh-TW/docs/MIGRATING_0_4.md +++ b/i18n/zh-TW/docs/MIGRATING_0_4.md @@ -31,8 +31,8 @@ filterwarnings = [ | 拒絕互相矛盾的 `__Host-`/`__Secure-` Cookie 設定 | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `UserWarning`(`cookie_name` 維持預設值時為 `FutureWarning`,僅限使用中介軟體時) | [Session Cookie](#session-cookie) | | 明確的 `login()`/`logout()`、唯讀的 `Session.user` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | 否 | [登入與登出](#login-logout) | | `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) | -| `token_source_priority` 列出所有權杖來源;沒有 `"cookie"` 的清單會停用 Cookie | [#75](https://github.com/allen0099/FastAPI-CacheX/issues/75) | `FutureWarning` | [權杖來源](#token-source-priority) | +| 已取消:`get_session_manager` 透過 `SessionManagerProxy` 取得 | [#131](https://github.com/allen0099/FastAPI-CacheX/issues/131) | `FutureWarning`(0.4.0 不再警告) | [get_session_manager](#get-session-manager) | +| 已取消:`token_source_priority` 列出所有權杖來源 | [#75](https://github.com/allen0099/FastAPI-CacheX/issues/75) | `FutureWarning`(0.4.0 不再警告) | [權杖來源](#token-source-priority) | | `add_routes()` 必須傳入 `dependencies`,預設不含內容預覽 | [#298](https://github.com/allen0099/FastAPI-CacheX/issues/298) | `UserWarning`(僅在省略 `dependencies` 時) | [監控路由](#add-routes) | | 移除 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) | @@ -40,7 +40,7 @@ filterwarnings = [ | 移除 `BackendProxy.get_backend()`/`set_backend()` | [#70](https://github.com/allen0099/FastAPI-CacheX/issues/70) | `DeprecationWarning` | [BackendProxy](#backend-proxy) | | 移除 `CacheError` | [#130](https://github.com/allen0099/FastAPI-CacheX/issues/130) | `DeprecationWarning` | [CacheError](#cache-error) | | 移除 Redis `clear_pattern()` 去除前綴後的重試 | [#125](https://github.com/allen0099/FastAPI-CacheX/issues/125) | `DeprecationWarning` | [Redis clear_pattern](#redis-clear-pattern) | -| 移除 `SessionConfig.use_bearer_token` | [#377](https://github.com/allen0099/FastAPI-CacheX/issues/377) | `DeprecationWarning` | [權杖來源](#token-source-priority) | +| `SessionConfig.use_bearer_token` 維持已棄用,0.5.0 移除 | [#377](https://github.com/allen0099/FastAPI-CacheX/issues/377) | `DeprecationWarning` | [權杖來源](#token-source-priority) | | `UserSessionDep` 需要使用者 | [#127](https://github.com/allen0099/FastAPI-CacheX/issues/127) | 否 | [UserSessionDep](#user-session-dep) | | 移除 `memcache` extra | [#202](https://github.com/allen0099/FastAPI-CacheX/issues/202) | 否 | [memcache extra](#memcache-extra) | | `BaseCacheBackend.delete()` 回傳 `bool` | [#71](https://github.com/allen0099/FastAPI-CacheX/issues/71) | 否 | [delete() 的回傳值](#backend-delete) | @@ -60,7 +60,7 @@ filterwarnings = [ |----------|------| | 搭配 Cookie Session 的 `FastAPICacheXSessionMiddleware` | Starlette 的 `SessionMiddleware` 會把少量的 Session 資料存放在簽署過的 Cookie 中。若 Session 必須存放在伺服器端(資料量大,或需要在伺服器端撤銷),請改用伺服器端 Session 函式庫,例如 `starsessions`。 | | API 以標頭或 `Authorization: Bearer` 傳遞 Session 權杖 | 改用驗證機制本身的存取權杖,例如以 JWT 函式庫驗證的 OAuth 2 Bearer 權杖。 | -| 以 `StateManager` 處理 OAuth/OIDC 的 `state` | 改用 OAuth 用戶端函式庫。例如 Authlib 的 Starlette 整合會替你產生並檢查 `state` 與 `nonce`。 | +| 以 `StateManager` 處理 OAuth/OIDC 的 `state` | 改用 OAuth 用戶端函式庫。例如 Authlib 的 Starlette 整合會替你產生並檢查 `state` 與 `nonce`;它把這些值存放在 `request.session`,因此需要 Starlette 的 `SessionMiddleware`。 | | `CacheManager`、`CacheLock`、`@cache` | 不需處理,這些並未棄用。 | 後端中已存在的 Session 與 state 不需要清理,會依各自的 TTL 自行過期。 @@ -73,6 +73,9 @@ import warnings warnings.filterwarnings( "ignore", message="fastapi_cachex.session is deprecated", category=FutureWarning ) +warnings.filterwarnings( + "ignore", message="fastapi_cachex.state is deprecated", category=FutureWarning +) ``` ## Session {#sessions} @@ -146,22 +149,7 @@ await login(request, SessionUser(user_id=user_id)) ### get_session_manager {#get-session-manager} -`get_session_manager`(以及使用它的 `SessionManagerDep`、`ClientIPDep` 與 `rotate_session_id()`)目前回傳中介軟體存放在 `app.state` 的管理器。0.4.0 改為只透過 `SessionManagerProxy` 取得,與 `BackendProxy` 和 `CacheManagerProxy` 一致([#131](https://github.com/allen0099/FastAPI-CacheX/issues/131))。在 0.3.9 中,當 proxy 沒有管理器或持有不同的管理器時,每個應用程式會發出一次 `FutureWarning`。 - -修改前: - -```python -session_manager = SessionManager(backend, config) -app.add_middleware(FastAPICacheXSessionMiddleware, session_manager=session_manager) -``` - -修改後: - -```python -session_manager = SessionManager(backend, config) -SessionManagerProxy.set(session_manager) -app.add_middleware(FastAPICacheXSessionMiddleware) # 從 proxy 取得管理器 -``` +0.3.9 曾預告 0.4.0 會改為只透過 `SessionManagerProxy` 取得 `get_session_manager`(以及使用它的 `SessionManagerDep`、`ClientIPDep` 與 `rotate_session_id()`)([#131](https://github.com/allen0099/FastAPI-CacheX/issues/131)),並發出 `FutureWarning`。由於 Session 已棄用([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)),0.4.0 不會做這項變更:它仍回傳中介軟體存放在 `app.state` 的管理器,也不再發出警告。若你已經呼叫 `SessionManagerProxy.set()`,保留它也沒有影響。 ### UserSessionDep {#user-session-dep} @@ -225,19 +213,10 @@ SessionConfig( ### 權杖來源 {#token-source-priority} -0.4.0 起,`SessionConfig.token_source_priority` 列出所有權杖來源([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75))。`FastAPICacheXSessionMiddleware` 會依照清單順序尋找權杖,`"cookie"` 可以放在任何位置,預設值改為 `["header", "bearer", "cookie"]`,也就是目前使用的順序。沒有 `"cookie"` 的清單代表不使用 Cookie:中介軟體既不讀取也不設定它,而為沒有權杖的請求建立的 Session 會在 `header_name` 回應標頭中送出權杖。純 API 的應用就是用這種方式關閉 Cookie。 - -0.3.9 接受把 `"cookie"` 放在最後一項(也就是目前讀取 Cookie 的位置,因此暫時不會改變任何行為);明確設定的清單沒有它時,`FastAPICacheXSessionMiddleware` 會發出 `FutureWarning`。預設清單不會發出警告。 - -```python -# 之前:雖然清單沒寫,Cookie 仍會在這兩者之後被讀取 -SessionConfig(secret_key=SECRET, token_source_priority=["bearer", "header"]) - -# 之後,保留 Cookie(在 0.3.9 與 0.4.0 皆可運作) -SessionConfig(secret_key=SECRET, token_source_priority=["bearer", "header", "cookie"]) -``` +0.3.9 在這裡預告了兩項變更。由於 Session 已棄用([#420](https://github.com/allen0099/FastAPI-CacheX/issues/420)),0.4.0 兩項都不會做: -`use_bearer_token` 已移除([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)),是否讀取 Bearer 權杖只由清單決定。0.3.9 在傳入它時,不論值為何,都會發出 `DeprecationWarning`: +- **取消 `token_source_priority` 列出所有權杖來源([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75))。** 不論清單是否列出 `"cookie"`,中介軟體仍會在標頭來源之後讀取 Session Cookie,且 `"cookie"` 仍只能放在最後一項。0.4.0 不再對沒有 `"cookie"` 的清單發出 `FutureWarning`。已經以 `"cookie"` 結尾的清單可以照常使用。 +- **0.4.0 不會移除 `use_bearer_token`([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377))。** 它維持已棄用,傳入時仍會發出 `DeprecationWarning`,並在 0.5.0 隨 `fastapi_cachex.session` 一起移除。是否讀取 Bearer 權杖只由清單決定: ```python # 之前 diff --git a/i18n/zh-TW/docs/SESSION.md b/i18n/zh-TW/docs/SESSION.md index e919ab8..23c7722 100644 --- a/i18n/zh-TW/docs/SESSION.md +++ b/i18n/zh-TW/docs/SESSION.md @@ -58,7 +58,7 @@ uv add "fastapi-cachex[jwt]" ``` -範例也把管理器註冊到 `SessionManagerProxy`,0.4.0 起 `get_session_manager` 只會從那裡取得它(見[遷移至 0.4.0](MIGRATING_0_4.md#get-session-manager))。管理器註冊在那裡之後,中介軟體也可以從 proxy 取得,而不必以參數傳入。省略 `config` 時,中介軟體會使用 `session_manager.config`: +範例也把管理器註冊到 `SessionManagerProxy`。管理器註冊在那裡之後,中介軟體可以從 proxy 取得,而不必以參數傳入。省略 `config` 時,中介軟體會使用 `session_manager.config`: ```python app.add_middleware(FastAPICacheXSessionMiddleware) # 從 proxy 取得 @@ -185,11 +185,9 @@ Session 會在 `session_ttl` 秒後過期。啟用 `sliding_expiration` 時, `FastAPICacheXSessionMiddleware` 先依照 `token_source_priority` 的順序讀取標頭來源,只有它們都沒有產生權杖時才退回使用 Cookie。回應端依權杖的來源決定(標頭進、標頭出;Cookie 進、`Set-Cookie` 出)。 -在 0.4.0 以前,不論清單是否列出 Cookie,都會讀取 Cookie。`"cookie"` 只能放在清單的最後一項,也就是它現在本來就被讀取的位置,因此列出它目前不會改變任何行為;放在其他位置會引發 `ValidationError`。 +不論清單是否列出 Cookie,都會讀取 Cookie。`"cookie"` 只能放在清單的最後一項,也就是它本來就被讀取的位置,因此列出它不會改變任何行為;放在其他位置會引發 `ValidationError`。(0.3.9 曾預告 0.4.0 會讓清單列出所有權杖來源;Session 棄用後,這項變更已取消。請參閱[遷移至 0.4.0](MIGRATING_0_4.md#token-source-priority)。) -0.4.0 起,這個清單列出所有權杖來源,預設值改為 `["header", "bearer", "cookie"]`,也就是目前使用的順序。沒有 `"cookie"` 的清單代表完全不使用 Cookie:中介軟體既不讀取也不設定它,而為沒有權杖的請求建立的 Session 會在 `header_name` 回應標頭中送出權杖([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75))。因此,明確設定了不含 `"cookie"` 的清單時,`FastAPICacheXSessionMiddleware` 會發出 `FutureWarning`。要保留 Cookie,請把 `"cookie"` 加在最後一項;預設清單不會發出警告。見[遷移至 0.4.0](MIGRATING_0_4.md#token-source-priority)。 - -`use_bearer_token` 已棄用,並於 0.4.0 移除([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)):傳入它會發出 `DeprecationWarning`。請以不在清單中列出 `"bearer"`(`token_source_priority=["header", "cookie"]`)取代 `use_bearer_token=False`;`use_bearer_token=True` 是預設值,直接拿掉即可。 +`use_bearer_token` 已棄用,並將在 0.5.0 隨本套件一起移除([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)):傳入它會發出 `DeprecationWarning`。請以不在清單中列出 `"bearer"`(`token_source_priority=["header", "cookie"]`)取代 `use_bearer_token=False`;`use_bearer_token=True` 是預設值,直接拿掉即可。 **標頭/Bearer 用戶端**應將權杖存放在 `localStorage` 或 `sessionStorage`,並以 `Authorization: Bearer ` 或 `X-Session-Token: ` 送出。**Cookie 用戶端**(瀏覽器)不需要自行處理權杖,但要留意 CSRF:瀏覽器會自動附上 Cookie,因此請將 `cookie_same_site` 與你自己的 CSRF 防護搭配使用。 @@ -415,7 +413,7 @@ from fastapi_cachex.session.dependencies import ( ) ``` -`get_session_manager` 回傳中介軟體在處理第一個請求時存放在 `app.state` 上的管理器;若尚未有任何 Session 中介軟體執行過,它會回應 `500`。使用它可以避免在路由模組中匯入管理器。0.4.0 起它改為透過 `SessionManagerProxy` 取得管理器,因此請以 `SessionManagerProxy.set(manager)` 註冊:在那之前,當 proxy 沒有管理器或持有不同的管理器時,`get_session_manager`(以及使用它的 `SessionManagerDep`、`ClientIPDep` 與 `rotate_session_id()`)每個應用程式會發出一次 `FutureWarning`。請參閱[遷移至 0.4.0](MIGRATING_0_4.md#get-session-manager)。 +`get_session_manager` 回傳中介軟體在處理第一個請求時存放在 `app.state` 上的管理器;若尚未有任何 Session 中介軟體執行過,它會回應 `500`。使用它可以避免在路由模組中匯入管理器。 ```python from fastapi_cachex.session import SessionUser diff --git a/tests/session/test_0_4_0_notices.py b/tests/session/test_dropped_0_4_0_changes.py similarity index 58% rename from tests/session/test_0_4_0_notices.py rename to tests/session/test_dropped_0_4_0_changes.py index 2150026..a4f59a8 100644 --- a/tests/session/test_0_4_0_notices.py +++ b/tests/session/test_dropped_0_4_0_changes.py @@ -1,8 +1,11 @@ -"""Advance notices for the session changes 0.4.0 makes (#352). +"""Session changes 0.3.9 announced for 0.4.0 and dropped with #420. -- #131: ``get_session_manager`` resolves through ``SessionManagerProxy`` only. -- #75: a ``token_source_priority`` without ``"cookie"`` disables the cookie. -- #377: ``SessionConfig.use_bearer_token`` is removed. +Sessions are deprecated and leave in 0.5.0, so 0.4.0 keeps 0.3.9's behaviour +and no longer warns about these changes: + +- #131: ``get_session_manager`` keeps returning the middleware's manager. +- #75: the cookie is read whether or not ``token_source_priority`` lists it. +- #377: ``SessionConfig.use_bearer_token`` stays deprecated, now until 0.5.0. """ import warnings @@ -32,7 +35,7 @@ def _middleware(config: SessionConfig) -> FastAPICacheXSessionMiddleware: return FastAPICacheXSessionMiddleware(FastAPI(), session_manager=manager) -# --- #131: get_session_manager through SessionManagerProxy --------------------- +# --- #131: get_session_manager keeps the middleware's manager ----------------- def _manager_app(manager: SessionManager, config: SessionConfig) -> FastAPI: @@ -56,72 +59,24 @@ async def rotate(request: Request) -> dict[str, bool]: return app -@pytest.mark.parametrize("path", ["/manager", "/ip"]) -def test_get_session_manager_warns_without_the_proxy( - manager: SessionManager, config: SessionConfig, path: str -) -> None: - client = TestClient(_manager_app(manager, config)) - - with pytest.warns(FutureWarning, match=r"SessionManagerProxy\.set\(") as record: - first = client.get(path) - # Once per app: later requests stay quiet. - second = client.get(path) - - assert first.status_code == second.status_code == 200 - assert "issues/131" in str(record[0].message) - - -def test_rotate_session_id_warns_without_the_proxy( - manager: SessionManager, config: SessionConfig -) -> None: - client = TestClient(_manager_app(manager, config)) - - with pytest.warns(FutureWarning, match="rotate_session_id"): - response = client.post("/rotate") - - assert response.json() == {"rotated": False} - - -def test_get_session_manager_warns_when_the_proxy_holds_another_manager( - manager: SessionManager, config: SessionConfig -) -> None: - SessionManagerProxy.set(SessionManager(MemoryBackend(), config)) - client = TestClient(_manager_app(manager, config)) - - with pytest.warns( - FutureWarning, match="a different SessionManager is set in SessionManagerProxy" - ): - response = client.get("/manager") - - # 0.3.x still answers with the middleware's manager. - assert response.json() == {"is_same": True} - - -def test_get_session_manager_warns_when_the_proxy_is_empty( - manager: SessionManager, config: SessionConfig +@pytest.mark.parametrize("proxy", ["empty", "other"]) +def test_get_session_manager_returns_the_middleware_manager_silently( + manager: SessionManager, config: SessionConfig, proxy: str ) -> None: - client = TestClient(_manager_app(manager, config)) - - with pytest.warns( - FutureWarning, match="no SessionManager is set in SessionManagerProxy" - ): - response = client.get("/manager") - - assert response.json() == {"is_same": True} - - -def test_get_session_manager_is_silent_when_the_proxy_agrees( - manager: SessionManager, config: SessionConfig -) -> None: - SessionManagerProxy.set(manager) + """Whatever the proxy holds, 0.4.0 answers with the middleware's manager.""" + if proxy == "other": + SessionManagerProxy.set(SessionManager(MemoryBackend(), config)) client = TestClient(_manager_app(manager, config)) with warnings.catch_warnings(): warnings.simplefilter("error") response = client.get("/manager") - client.post("/rotate") + ip = client.get("/ip") + rotated = client.post("/rotate") assert response.json() == {"is_same": True} + assert ip.status_code == 200 + assert rotated.json() == {"rotated": False} def test_middleware_from_the_proxy_is_silent(config: SessionConfig) -> None: @@ -142,35 +97,22 @@ async def read_manager(mgr: SessionManagerDep) -> dict[str, bool]: assert response.json() == {"is_same": True} -# --- #75: token_source_priority names every token source ----------------------- +# --- #75: the cookie is read whether or not the list names it ------------------ _COOKIE = {"cookie_name": "session", "cookie_https_only": False} -@pytest.mark.parametrize( - "priority", [["header", "bearer"], ["bearer", "header"], ["header"], []] -) -def test_middleware_warns_for_an_explicit_list_without_cookie( - priority: list[str], -) -> None: - config = SessionConfig(secret_key=SECRET, token_source_priority=priority, **_COOKIE) - - with pytest.warns(FutureWarning, match='does not list "cookie"') as record: - _middleware(config) - - assert "issues/75" in str(record[0].message) - - @pytest.mark.parametrize( "settings", [ - {}, # the default list becomes ["header", "bearer", "cookie"]: same order + {}, + {"token_source_priority": ["header", "bearer"]}, + {"token_source_priority": ["header"]}, + {"token_source_priority": []}, {"token_source_priority": ["header", "bearer", "cookie"]}, - {"token_source_priority": ["bearer", "cookie"]}, - {"token_source_priority": ["cookie"]}, ], ) -def test_middleware_is_silent_for_the_default_or_a_list_with_cookie( +def test_middleware_is_silent_with_or_without_cookie( settings: dict[str, object], ) -> None: config = SessionConfig(secret_key=SECRET, **settings, **_COOKIE) @@ -188,14 +130,12 @@ def test_cookie_is_accepted_only_as_the_last_source(priority: list[str]) -> None SessionConfig(secret_key=SECRET, token_source_priority=priority) -async def test_the_cookie_is_still_read_before_0_4_0_with_or_without_the_entry() -> ( - None -): - """Listing "cookie" last is today's order, and leaving it out changes nothing yet.""" +async def test_the_cookie_is_read_with_or_without_the_entry() -> None: + """Listing "cookie" last is the order it is read in; leaving it out changes nothing.""" backend = MemoryBackend() for priority in (["header", "cookie"], ["header"]): with warnings.catch_warnings(): - warnings.simplefilter("ignore", FutureWarning) + warnings.simplefilter("error") config = SessionConfig( secret_key=SECRET, token_source_priority=priority, **_COOKIE ) @@ -220,7 +160,7 @@ async def read(session: OptionalSession) -> dict[str, object]: assert client.get("/").json() == {"user": "u"}, priority -# --- #377: use_bearer_token is removed ------------------------------------------ +# --- #377: use_bearer_token is deprecated until 0.5.0 ---------------------------- @pytest.mark.parametrize("value", [True, False]) @@ -229,7 +169,7 @@ def test_passing_use_bearer_token_warns(value: bool) -> None: SessionConfig(secret_key=SECRET, use_bearer_token=value) message = str(record[0].message) - assert "0.4.0" in message + assert "removed in version 0.5.0" in message assert "issues/377" in message diff --git a/tests/test_session_state_deprecation.py b/tests/test_session_state_deprecation.py index e82223b..9ee249c 100644 --- a/tests/test_session_state_deprecation.py +++ b/tests/test_session_state_deprecation.py @@ -4,6 +4,8 @@ import in a fresh interpreter. """ +import ast +import pkgutil import subprocess import sys from pathlib import Path @@ -26,10 +28,25 @@ def _run(script: str, tmp_path: Path) -> str: return result.stderr -def test_plain_import_does_not_warn(tmp_path: Path) -> None: +def _core_modules() -> list[str]: + """Every fastapi_cachex module outside the session and state packages.""" + return sorted( + info.name + for info in pkgutil.walk_packages( + fastapi_cachex.__path__, prefix="fastapi_cachex." + ) + if not info.name.startswith(("fastapi_cachex.session", "fastapi_cachex.state")) + ) + + +def test_core_modules_do_not_warn(tmp_path: Path) -> None: + """Nothing outside session and state imports them, the backends included.""" + modules = _core_modules() + assert "fastapi_cachex.backends.redis" in modules stderr = _run( "import fastapi_cachex\n" - "from fastapi_cachex import cache, CacheManager, CacheLock\n", + "from fastapi_cachex import cache, CacheManager, CacheLock\n" + + "".join(f"import {name}\n" for name in modules), tmp_path, ) assert "FutureWarning" not in stderr @@ -73,3 +90,20 @@ def test_deprecated_name_resolves_but_is_not_exported(name: str) -> None: def test_unknown_name_raises_attribute_error() -> None: with pytest.raises(AttributeError, match="has no attribute 'nope'"): fastapi_cachex.nope # noqa: B018 + + +def test_type_checking_imports_match_the_lazy_names() -> None: + """The names type checkers see are the names ``__getattr__`` resolves.""" + tree = ast.parse(Path(fastapi_cachex.__file__).read_text()) + block = next( + node + for node in tree.body + if isinstance(node, ast.If) and ast.unparse(node.test) == "TYPE_CHECKING" + ) + imported = { + alias.name: f"fastapi_cachex.{node.module}" + for node in block.body + if isinstance(node, ast.ImportFrom) + for alias in node.names + } + assert imported == fastapi_cachex._DEPRECATED_NAMES diff --git a/zensical.toml b/zensical.toml index 2adc224..57a319d 100644 --- a/zensical.toml +++ b/zensical.toml @@ -25,7 +25,7 @@ nav = [ { "Backends" = "BACKENDS.md" }, { "Distributed lock" = "LOCK.md" }, ] }, - { "Sessions and sign-in" = [ + { "Sessions and sign-in (deprecated)" = [ { "Session management" = "SESSION.md" }, { "OAuth state" = "STATE.md" }, { "JWT claims" = "JWT_CLAIMS.md" }, @@ -38,8 +38,8 @@ nav = [ { "CacheManager" = "api/cache-manager.md" }, { "Backends" = "api/backends.md" }, { "CacheLock" = "api/lock.md" }, - { "Session" = "api/session.md" }, - { "State" = "api/state.md" }, + { "Session (deprecated)" = "api/session.md" }, + { "State (deprecated)" = "api/state.md" }, { "Types and exceptions" = "api/types.md" }, ] }, { "Development" = [ diff --git a/zensical.zh-TW.toml b/zensical.zh-TW.toml index 2239cbd..a69d9da 100644 --- a/zensical.zh-TW.toml +++ b/zensical.zh-TW.toml @@ -29,7 +29,7 @@ nav = [ { "後端" = "BACKENDS.md" }, { "分散式鎖" = "LOCK.md" }, ] }, - { "Session 與登入" = [ + { "Session 與登入(已棄用)" = [ { "Session 管理" = "SESSION.md" }, { "OAuth state" = "STATE.md" }, { "JWT claims" = "JWT_CLAIMS.md" },