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.
Files changed (32) hide show
  1. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/PKG-INFO +1 -1
  2. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/redis.py +6 -1
  3. fastapi_cachex-0.2.8/fastapi_cachex/proxy.py +100 -0
  4. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/models.py +0 -41
  5. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/token_serializers.py +44 -4
  6. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/state/manager.py +29 -22
  7. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/pyproject.toml +1 -1
  8. fastapi_cachex-0.2.6/fastapi_cachex/proxy.py +0 -43
  9. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/README.md +0 -0
  10. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/__init__.py +0 -0
  11. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/__init__.py +0 -0
  12. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/base.py +0 -0
  13. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/config.py +0 -0
  14. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/memcached.py +0 -0
  15. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/backends/memory.py +0 -0
  16. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/cache.py +0 -0
  17. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/dependencies.py +0 -0
  18. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/directives.py +0 -0
  19. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/exceptions.py +0 -0
  20. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/py.typed +0 -0
  21. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/routes.py +0 -0
  22. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/__init__.py +0 -0
  23. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/config.py +0 -0
  24. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/dependencies.py +0 -0
  25. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/exceptions.py +0 -0
  26. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/manager.py +0 -0
  27. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/middleware.py +0 -0
  28. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/session/security.py +0 -0
  29. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/state/__init__.py +0 -0
  30. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/state/exceptions.py +0 -0
  31. {fastapi_cachex-0.2.6 → fastapi_cachex-0.2.8}/fastapi_cachex/state/models.py +0 -0
  32. {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.6
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
- """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.8"
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"
@@ -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