fastapi-cachex 0.2.5__tar.gz → 0.2.7__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.5 → fastapi_cachex-0.2.7}/PKG-INFO +1 -1
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/redis.py +6 -1
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/dependencies.py +5 -1
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/models.py +0 -41
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/token_serializers.py +44 -4
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/state/manager.py +29 -22
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/pyproject.toml +1 -1
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/README.md +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/__init__.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/__init__.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/base.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/config.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/memcached.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/memory.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/cache.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/dependencies.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/directives.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/exceptions.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/proxy.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/py.typed +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/routes.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/__init__.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/config.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/exceptions.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/manager.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/middleware.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/session/security.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/state/__init__.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/state/exceptions.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/fastapi_cachex/state/models.py +0 -0
- {fastapi_cachex-0.2.5 → fastapi_cachex-0.2.7}/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.7
|
|
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()
|
|
@@ -17,7 +17,11 @@ if TYPE_CHECKING:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
# HTTPBearer security scheme for OpenAPI UI
|
|
20
|
-
_http_bearer = HTTPBearer(
|
|
20
|
+
_http_bearer = HTTPBearer(
|
|
21
|
+
scheme_name="SessionBearer",
|
|
22
|
+
description="Session authentication using Bearer token",
|
|
23
|
+
auto_error=False,
|
|
24
|
+
)
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
def get_optional_session(
|
|
@@ -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
|
|
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
|