fastapi-cachex 0.2.6__tar.gz → 0.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/PKG-INFO +1 -1
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/redis.py +6 -1
- fastapi_cachex-0.2.8/fastapi_cachex/proxy.py +100 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/models.py +0 -41
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/token_serializers.py +44 -4
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/state/manager.py +29 -22
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/pyproject.toml +1 -1
- fastapi_cachex-0.2.6/fastapi_cachex/proxy.py +0 -43
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/README.md +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/__init__.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/__init__.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/base.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/config.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/memcached.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/memory.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/cache.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/dependencies.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/exceptions.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/py.typed +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/routes.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/__init__.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/config.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/dependencies.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/exceptions.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/manager.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/middleware.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/security.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/state/__init__.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/state/exceptions.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/state/models.py +0 -0
- {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-cachex
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: A caching library for FastAPI with support for Cache-Control, ETag, and multiple backends.
|
|
5
5
|
Keywords: fastapi,cache,etag,cache-control,redis,memcached,in-memory
|
|
6
6
|
Author: allen0099
|
|
@@ -128,11 +128,16 @@ class AsyncRedisCacheBackend(BaseCacheBackend):
|
|
|
128
128
|
return serialized.decode() if isinstance(serialized, bytes) else serialized
|
|
129
129
|
|
|
130
130
|
def _deserialize(self, value: str | None) -> ETagContent | None:
|
|
131
|
-
"""Deserialize JSON string to ETagContent.
|
|
131
|
+
"""Deserialize JSON string to ETagContent.
|
|
132
|
+
|
|
133
|
+
Converts string content back to bytes to maintain consistency with
|
|
134
|
+
other backends and standard Response.body type (bytes).
|
|
135
|
+
"""
|
|
132
136
|
if value is None:
|
|
133
137
|
return None
|
|
134
138
|
try:
|
|
135
139
|
data = json.loads(value)
|
|
140
|
+
logger.debug("Content type in JSON: %s", type(data["content"]))
|
|
136
141
|
return ETagContent(
|
|
137
142
|
etag=data["etag"],
|
|
138
143
|
content=data["content"].encode()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Backend proxy for managing cache backend instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from typing import Generic
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
from .backends import BaseCacheBackend
|
|
11
|
+
from .exceptions import BackendNotFoundError
|
|
12
|
+
from .session.manager import SessionManager
|
|
13
|
+
|
|
14
|
+
ProxyInstance = TypeVar("ProxyInstance")
|
|
15
|
+
|
|
16
|
+
logger = getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProxyMeta(type):
|
|
20
|
+
"""Metaclass for BackendProxy to prevent instantiation."""
|
|
21
|
+
|
|
22
|
+
def __call__(cls) -> None:
|
|
23
|
+
"""Prevent instantiation of BackendProxy."""
|
|
24
|
+
msg = "Proxy class cannot be instantiated. Use static methods instead."
|
|
25
|
+
raise TypeError(msg)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProxyBase(Generic[ProxyInstance], metaclass=ProxyMeta):
|
|
29
|
+
"""Abstract base class for proxy classes."""
|
|
30
|
+
|
|
31
|
+
_instance: ProxyInstance | None = None
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get(cls) -> ProxyInstance:
|
|
35
|
+
"""Get the current instance of the proxy.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The current instance
|
|
39
|
+
"""
|
|
40
|
+
if cls._instance is None:
|
|
41
|
+
msg = "Instance is not set. Please set the instance first."
|
|
42
|
+
raise BackendNotFoundError(msg)
|
|
43
|
+
return cls._instance
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def set(cls, instance: ProxyInstance | None) -> None:
|
|
47
|
+
"""Set the instance for the proxy.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
instance: The instance to set, or None to clear
|
|
51
|
+
"""
|
|
52
|
+
logger.info(
|
|
53
|
+
"Setting instance to: <%s>",
|
|
54
|
+
instance.__class__.__name__ if instance else "None",
|
|
55
|
+
)
|
|
56
|
+
cls._instance = instance
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BackendProxy(ProxyBase[BaseCacheBackend]):
|
|
60
|
+
"""FastAPI CacheX Proxy for backend management."""
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def get_backend() -> BaseCacheBackend:
|
|
64
|
+
"""Get the current backend instance.
|
|
65
|
+
|
|
66
|
+
.. deprecated:: 0.3.0
|
|
67
|
+
Use :meth:`get` instead. Will be removed in version 0.4.0.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The current backend instance
|
|
71
|
+
"""
|
|
72
|
+
warnings.warn(
|
|
73
|
+
"get_backend() is deprecated, use get() instead. "
|
|
74
|
+
"Will be removed in version 0.4.0.",
|
|
75
|
+
DeprecationWarning,
|
|
76
|
+
stacklevel=2,
|
|
77
|
+
)
|
|
78
|
+
return BackendProxy.get()
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def set_backend(backend: BaseCacheBackend | None) -> None:
|
|
82
|
+
"""Set the backend instance.
|
|
83
|
+
|
|
84
|
+
.. deprecated:: 0.3.0
|
|
85
|
+
Use :meth:`set` instead. Will be removed in version 0.4.0.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
backend: The backend instance to set, or None to clear
|
|
89
|
+
"""
|
|
90
|
+
warnings.warn(
|
|
91
|
+
"set_backend() is deprecated, use set() instead. "
|
|
92
|
+
"Will be removed in version 0.4.0.",
|
|
93
|
+
DeprecationWarning,
|
|
94
|
+
stacklevel=2,
|
|
95
|
+
)
|
|
96
|
+
BackendProxy.set(backend)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SessionManagerProxy(ProxyBase[SessionManager]):
|
|
100
|
+
"""FastAPI CacheX Proxy for session manager management."""
|
|
@@ -13,9 +13,6 @@ from pydantic import Field
|
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
|
-
# Token format constant
|
|
17
|
-
TOKEN_PARTS_COUNT = 3
|
|
18
|
-
|
|
19
16
|
|
|
20
17
|
class SessionStatus(str, Enum):
|
|
21
18
|
"""Session status enumeration."""
|
|
@@ -145,41 +142,3 @@ class SessionToken(BaseModel):
|
|
|
145
142
|
session_id: str
|
|
146
143
|
signature: str
|
|
147
144
|
issued_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
148
|
-
|
|
149
|
-
def to_string(self) -> str:
|
|
150
|
-
"""Convert token to string format.
|
|
151
|
-
|
|
152
|
-
Format: {session_id}.{signature}.{timestamp}
|
|
153
|
-
"""
|
|
154
|
-
timestamp = int(self.issued_at.timestamp())
|
|
155
|
-
|
|
156
|
-
logger.debug("SessionToken to_string called; id=%s", self.session_id)
|
|
157
|
-
return f"{self.session_id}.{self.signature}.{timestamp}"
|
|
158
|
-
|
|
159
|
-
@classmethod
|
|
160
|
-
def from_string(cls, token_str: str) -> "SessionToken":
|
|
161
|
-
"""Parse token from string format.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
token_str: Token string in format {session_id}.{signature}.{timestamp}
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
SessionToken instance
|
|
168
|
-
|
|
169
|
-
Raises:
|
|
170
|
-
ValueError: If token format is invalid
|
|
171
|
-
"""
|
|
172
|
-
parts = token_str.split(".")
|
|
173
|
-
if len(parts) != TOKEN_PARTS_COUNT:
|
|
174
|
-
msg = "Invalid token format"
|
|
175
|
-
raise ValueError(msg)
|
|
176
|
-
|
|
177
|
-
session_id, signature, timestamp = parts
|
|
178
|
-
try:
|
|
179
|
-
issued_at = datetime.fromtimestamp(int(timestamp), tz=timezone.utc)
|
|
180
|
-
except (ValueError, OSError) as e:
|
|
181
|
-
msg = f"Invalid timestamp in token: {e}"
|
|
182
|
-
raise ValueError(msg) from e
|
|
183
|
-
|
|
184
|
-
logger.debug("SessionToken parsed from string; id=%s", session_id)
|
|
185
|
-
return cls(session_id=session_id, signature=signature, issued_at=issued_at)
|
|
@@ -25,6 +25,9 @@ if TYPE_CHECKING: # Import for typing only to avoid circular import concerns
|
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
|
+
# Token format constant - 3 parts: session_id, signature, timestamp
|
|
29
|
+
TOKEN_PARTS_COUNT = 3
|
|
30
|
+
|
|
28
31
|
|
|
29
32
|
class TokenSerializer(Protocol):
|
|
30
33
|
"""Protocol for token serialization strategies."""
|
|
@@ -42,12 +45,49 @@ class SimpleTokenSerializer:
|
|
|
42
45
|
"""Serializer for the default simple token format."""
|
|
43
46
|
|
|
44
47
|
def to_string(self, token: SessionToken) -> str:
|
|
45
|
-
"""
|
|
46
|
-
|
|
48
|
+
"""Convert token to string format.
|
|
49
|
+
|
|
50
|
+
Format: {session_id}.{signature}.{timestamp}
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
token: SessionToken instance to serialize
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Token string in format {session_id}.{signature}.{timestamp}
|
|
57
|
+
"""
|
|
58
|
+
timestamp = int(token.issued_at.timestamp())
|
|
59
|
+
|
|
60
|
+
logger.debug("SimpleTokenSerializer to_string called; id=%s", token.session_id)
|
|
61
|
+
return f"{token.session_id}.{token.signature}.{timestamp}"
|
|
47
62
|
|
|
48
63
|
def from_string(self, token_str: str) -> SessionToken:
|
|
49
|
-
"""Parse
|
|
50
|
-
|
|
64
|
+
"""Parse token from string format.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
token_str: Token string in format {session_id}.{signature}.{timestamp}
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
SessionToken instance
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If token format is invalid
|
|
74
|
+
"""
|
|
75
|
+
parts = token_str.split(".")
|
|
76
|
+
if len(parts) != TOKEN_PARTS_COUNT:
|
|
77
|
+
msg = "Invalid token format"
|
|
78
|
+
raise ValueError(msg)
|
|
79
|
+
|
|
80
|
+
session_id, signature, timestamp = parts
|
|
81
|
+
try:
|
|
82
|
+
issued_at = datetime.fromtimestamp(int(timestamp), tz=timezone.utc)
|
|
83
|
+
except (ValueError, OSError) as e:
|
|
84
|
+
msg = f"Invalid timestamp in token: {e}"
|
|
85
|
+
raise ValueError(msg) from e
|
|
86
|
+
|
|
87
|
+
logger.debug("SimpleTokenSerializer parsed from string; id=%s", session_id)
|
|
88
|
+
return SessionToken(
|
|
89
|
+
session_id=session_id, signature=signature, issued_at=issued_at
|
|
90
|
+
)
|
|
51
91
|
|
|
52
92
|
|
|
53
93
|
class JWTTokenSerializer:
|
|
@@ -108,7 +108,9 @@ class StateManager:
|
|
|
108
108
|
|
|
109
109
|
# Extract content from ETagContent
|
|
110
110
|
json_content = cached_etag_content.content
|
|
111
|
-
if
|
|
111
|
+
if isinstance(json_content, bytes):
|
|
112
|
+
json_content = json_content.decode("utf-8")
|
|
113
|
+
elif not isinstance(json_content, str):
|
|
112
114
|
msg = "Unexpected state data format"
|
|
113
115
|
logger.error(
|
|
114
116
|
"Unexpected content type in state; state=%s type=%s",
|
|
@@ -164,7 +166,16 @@ class StateManager:
|
|
|
164
166
|
|
|
165
167
|
# Extract content from ETagContent
|
|
166
168
|
json_content = cached_etag_content.content
|
|
167
|
-
if
|
|
169
|
+
if isinstance(json_content, bytes):
|
|
170
|
+
try:
|
|
171
|
+
json_content = json_content.decode("utf-8")
|
|
172
|
+
except UnicodeDecodeError:
|
|
173
|
+
logger.exception(
|
|
174
|
+
"Failed to decode bytes content in state; state=%s",
|
|
175
|
+
state,
|
|
176
|
+
)
|
|
177
|
+
return False
|
|
178
|
+
elif not isinstance(json_content, str):
|
|
168
179
|
logger.error(
|
|
169
180
|
"Unexpected content type in state; state=%s type=%s",
|
|
170
181
|
state,
|
|
@@ -174,19 +185,11 @@ class StateManager:
|
|
|
174
185
|
|
|
175
186
|
try:
|
|
176
187
|
state_dict: dict[str, Any] = json.loads(json_content)
|
|
177
|
-
|
|
178
|
-
logger.exception(
|
|
179
|
-
"Failed to parse state data during validation; state=%s",
|
|
180
|
-
state,
|
|
181
|
-
)
|
|
182
|
-
return False
|
|
183
|
-
|
|
184
|
-
# Validate and create StateData model
|
|
185
|
-
try:
|
|
188
|
+
# Validate and create StateData model
|
|
186
189
|
state_data = StateData(**state_dict)
|
|
187
|
-
except ValueError:
|
|
190
|
+
except (json.JSONDecodeError, ValueError):
|
|
188
191
|
logger.exception(
|
|
189
|
-
"Failed to
|
|
192
|
+
"Failed to parse or validate state data; state=%s",
|
|
190
193
|
state,
|
|
191
194
|
)
|
|
192
195
|
return False
|
|
@@ -216,7 +219,16 @@ class StateManager:
|
|
|
216
219
|
|
|
217
220
|
# Extract content from ETagContent
|
|
218
221
|
json_content = cached_etag_content.content
|
|
219
|
-
if
|
|
222
|
+
if isinstance(json_content, bytes):
|
|
223
|
+
try:
|
|
224
|
+
json_content = json_content.decode("utf-8")
|
|
225
|
+
except UnicodeDecodeError:
|
|
226
|
+
logger.exception(
|
|
227
|
+
"Failed to decode bytes content in state; state=%s",
|
|
228
|
+
state,
|
|
229
|
+
)
|
|
230
|
+
return None
|
|
231
|
+
elif not isinstance(json_content, str):
|
|
220
232
|
logger.error(
|
|
221
233
|
"Unexpected content type in state; state=%s type=%s",
|
|
222
234
|
state,
|
|
@@ -226,15 +238,10 @@ class StateManager:
|
|
|
226
238
|
|
|
227
239
|
try:
|
|
228
240
|
state_dict: dict[str, Any] = json.loads(json_content)
|
|
229
|
-
|
|
230
|
-
logger.exception("Failed to parse state data; state=%s", state)
|
|
231
|
-
return None
|
|
232
|
-
|
|
233
|
-
# Validate and create StateData model
|
|
234
|
-
try:
|
|
241
|
+
# Validate and create StateData model
|
|
235
242
|
state_data = StateData(**state_dict)
|
|
236
|
-
except ValueError:
|
|
237
|
-
logger.exception("Failed to
|
|
243
|
+
except (json.JSONDecodeError, ValueError):
|
|
244
|
+
logger.exception("Failed to parse or validate state data; state=%s", state)
|
|
238
245
|
return None
|
|
239
246
|
|
|
240
247
|
# Check expiry
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""Backend proxy for managing cache backend instances."""
|
|
2
|
-
|
|
3
|
-
from logging import getLogger
|
|
4
|
-
|
|
5
|
-
from .backends import BaseCacheBackend
|
|
6
|
-
from .exceptions import BackendNotFoundError
|
|
7
|
-
|
|
8
|
-
_default_backend: BaseCacheBackend | None = None
|
|
9
|
-
logger = getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class BackendProxy:
|
|
13
|
-
"""FastAPI CacheX Proxy for backend management."""
|
|
14
|
-
|
|
15
|
-
@staticmethod
|
|
16
|
-
def get_backend() -> BaseCacheBackend:
|
|
17
|
-
"""Get the current cache backend instance.
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
The current cache backend
|
|
21
|
-
|
|
22
|
-
Raises:
|
|
23
|
-
BackendNotFoundError: If no backend has been set
|
|
24
|
-
"""
|
|
25
|
-
if _default_backend is None:
|
|
26
|
-
msg = "Backend is not set. Please set the backend first."
|
|
27
|
-
raise BackendNotFoundError(msg)
|
|
28
|
-
|
|
29
|
-
return _default_backend
|
|
30
|
-
|
|
31
|
-
@staticmethod
|
|
32
|
-
def set_backend(backend: BaseCacheBackend | None) -> None:
|
|
33
|
-
"""Set the backend for caching.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
backend: The backend to use for caching, or None to clear the current backend
|
|
37
|
-
"""
|
|
38
|
-
global _default_backend
|
|
39
|
-
logger.info(
|
|
40
|
-
"Setting backend to: <%s>",
|
|
41
|
-
backend.__class__.__name__ if backend else "None",
|
|
42
|
-
)
|
|
43
|
-
_default_backend = backend
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|