fastapi-cachex 0.2.6__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.
Files changed (31) hide show
  1. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/PKG-INFO +1 -1
  2. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/redis.py +6 -1
  3. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/models.py +0 -41
  4. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/token_serializers.py +44 -4
  5. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/state/manager.py +29 -22
  6. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/pyproject.toml +1 -1
  7. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/README.md +0 -0
  8. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/__init__.py +0 -0
  9. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/__init__.py +0 -0
  10. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/base.py +0 -0
  11. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/config.py +0 -0
  12. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/memcached.py +0 -0
  13. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/backends/memory.py +0 -0
  14. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/cache.py +0 -0
  15. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/dependencies.py +0 -0
  16. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/directives.py +0 -0
  17. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/exceptions.py +0 -0
  18. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/proxy.py +0 -0
  19. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/py.typed +0 -0
  20. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/routes.py +0 -0
  21. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/__init__.py +0 -0
  22. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/config.py +0 -0
  23. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/dependencies.py +0 -0
  24. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/exceptions.py +0 -0
  25. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/manager.py +0 -0
  26. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/middleware.py +0 -0
  27. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/session/security.py +0 -0
  28. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/state/__init__.py +0 -0
  29. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/state/exceptions.py +0 -0
  30. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.7}/fastapi_cachex/state/models.py +0 -0
  31. {fastapi_cachex-0.2.6 → 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.6
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()
@@ -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
- """Serialize using the built-in simple format."""
46
- return token.to_string()
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 the built-in simple token format."""
50
- return SessionToken.from_string(token_str)
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 not isinstance(json_content, str):
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 not isinstance(json_content, str):
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
- except json.JSONDecodeError:
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 create StateData model during validation; state=%s",
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 not isinstance(json_content, str):
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
- except json.JSONDecodeError:
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 create StateData model; state=%s", state)
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-cachex"
3
- version = "0.2.6"
3
+ version = "0.2.7"
4
4
  description = "A caching library for FastAPI with support for Cache-Control, ETag, and multiple backends."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
File without changes