hishel 1.0.0.dev2__py3-none-any.whl → 1.0.0.dev3__py3-none-any.whl

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.
hishel/asgi.py ADDED
@@ -0,0 +1,400 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+ from email.utils import formatdate
6
+ from typing import AsyncIterator
7
+
8
+ from hishel import AsyncBaseStorage, CacheOptions, Headers, Request, Response
9
+ from hishel._async_cache import AsyncCacheProxy
10
+
11
+ # Configure logger for this module
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class _ASGIScope(t.TypedDict, total=False):
16
+ """ASGI HTTP scope type."""
17
+
18
+ type: str
19
+ asgi: dict[str, str]
20
+ http_version: str
21
+ method: str
22
+ scheme: str
23
+ path: str
24
+ query_string: bytes
25
+ root_path: str
26
+ headers: list[tuple[bytes, bytes]]
27
+ server: tuple[str, int | None] | None
28
+ client: tuple[str, int] | None
29
+ state: dict[str, t.Any]
30
+ extensions: dict[str, t.Any]
31
+
32
+
33
+ _Scope = _ASGIScope
34
+ _Receive = t.Callable[[], t.Awaitable[dict[str, t.Any]]]
35
+ _Send = t.Callable[[dict[str, t.Any]], t.Awaitable[None]]
36
+ _ASGIApp = t.Callable[[_Scope, _Receive, _Send], t.Awaitable[None]]
37
+
38
+
39
+ class ASGICacheMiddleware:
40
+ """
41
+ ASGI middleware that provides HTTP caching capabilities.
42
+
43
+ This middleware intercepts HTTP requests and responses, caching them
44
+ according to HTTP caching specifications (RFC 9111) or custom rules.
45
+
46
+ The middleware uses async iterators for request and response bodies,
47
+ ensuring memory-efficient streaming without loading entire payloads
48
+ into memory. This is particularly important for large file uploads
49
+ or downloads.
50
+
51
+ This implementation is thread-safe by creating a new cache proxy for
52
+ each request with closures that capture the request context.
53
+
54
+ Args:
55
+ app: The ASGI application to wrap.
56
+ storage: The storage backend to use for caching. Defaults to AsyncSqliteStorage.
57
+ cache_options: Configuration options for caching behavior.
58
+ ignore_specification: If True, bypasses HTTP caching rules and caches all responses.
59
+
60
+ Example:
61
+ ```python
62
+ from hishel.asgi import ASGICacheMiddleware
63
+ from hishel import AsyncSqliteStorage, CacheOptions
64
+
65
+ # Wrap your ASGI app
66
+ app = ASGICacheMiddleware(
67
+ app=my_asgi_app,
68
+ storage=AsyncSqliteStorage(),
69
+ cache_options=CacheOptions(),
70
+ )
71
+ ```
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ app: _ASGIApp,
77
+ storage: AsyncBaseStorage | None = None,
78
+ cache_options: CacheOptions | None = None,
79
+ ignore_specification: bool = False,
80
+ ) -> None:
81
+ self.app = app
82
+ self.storage = storage
83
+ self._cache_options = cache_options
84
+ self._ignore_specification = ignore_specification
85
+
86
+ logger.info(
87
+ "Initialized ASGICacheMiddleware with storage=%s, ignore_specification=%s",
88
+ type(storage).__name__ if storage else "None",
89
+ ignore_specification,
90
+ )
91
+
92
+ async def __call__(self, scope: _Scope, receive: _Receive, send: _Send) -> None:
93
+ """
94
+ Handle an ASGI request.
95
+
96
+ Args:
97
+ scope: The ASGI scope dictionary.
98
+ receive: The ASGI receive callable.
99
+ send: The ASGI send callable.
100
+ """
101
+ # Only handle HTTP requests
102
+ if scope["type"] != "http":
103
+ logger.debug("Skipping non-HTTP request: type=%s", scope["type"])
104
+ await self.app(scope, receive, send)
105
+ return
106
+
107
+ method = scope.get("method", "UNKNOWN")
108
+ path = scope.get("path", "/")
109
+ query_string = scope.get("query_string", b"").decode("latin1")
110
+ full_path = f"{path}?{query_string}" if query_string else path
111
+
112
+ logger.debug("Incoming HTTP request: method=%s path=%s", method, full_path)
113
+
114
+ try:
115
+ # Create a closure that captures scope and receive for this specific request
116
+ # This makes the code thread-safe by avoiding shared instance state
117
+ async def send_request_to_app(request: Request) -> Response:
118
+ """
119
+ Send a request to the wrapped ASGI application and return the response.
120
+ This closure captures 'scope' and 'receive' from the outer function scope.
121
+ """
122
+ logger.debug("Sending request to wrapped application: url=%s", request.url)
123
+
124
+ # Create a buffered receive callable that replays the request body from the stream
125
+ body_iterator = request.aiter_stream()
126
+ body_exhausted = False
127
+ bytes_received = 0
128
+
129
+ async def inner_receive() -> dict[str, t.Any]:
130
+ nonlocal body_exhausted, bytes_received
131
+ if body_exhausted:
132
+ return {"type": "http.disconnect"}
133
+
134
+ try:
135
+ chunk = await body_iterator.__anext__()
136
+ bytes_received += len(chunk)
137
+ logger.debug("Received request body chunk: size=%d bytes", len(chunk))
138
+ return {
139
+ "type": "http.request",
140
+ "body": chunk,
141
+ "more_body": True,
142
+ }
143
+ except StopAsyncIteration:
144
+ body_exhausted = True
145
+ logger.debug(
146
+ "Request body fully consumed: total_bytes=%d",
147
+ bytes_received,
148
+ )
149
+ return {
150
+ "type": "http.request",
151
+ "body": b"",
152
+ "more_body": False,
153
+ }
154
+
155
+ # Collect response from the app
156
+ response_started = False
157
+ status_code = 200
158
+ response_headers: list[tuple[bytes, bytes]] = []
159
+ response_body_chunks: list[bytes] = []
160
+ bytes_sent = 0
161
+
162
+ async def inner_send(message: dict[str, t.Any]) -> None:
163
+ nonlocal response_started, status_code, response_headers, bytes_sent
164
+ if message["type"] == "http.response.start":
165
+ response_started = True
166
+ status_code = message["status"]
167
+ response_headers = message.get("headers", [])
168
+ logger.debug("Application response started: status=%d", status_code)
169
+ elif message["type"] == "http.response.body":
170
+ body_chunk = message.get("body", b"")
171
+ if body_chunk:
172
+ response_body_chunks.append(body_chunk)
173
+ bytes_sent += len(body_chunk)
174
+ logger.debug(
175
+ "Received response body chunk: size=%d bytes",
176
+ len(body_chunk),
177
+ )
178
+
179
+ try:
180
+ # Call the wrapped application with captured scope
181
+ await self.app(scope, inner_receive, inner_send)
182
+ logger.info(
183
+ "Application response complete: status=%d total_bytes=%d chunks=%d",
184
+ status_code,
185
+ bytes_sent,
186
+ len(response_body_chunks),
187
+ )
188
+ except Exception as e:
189
+ logger.error(
190
+ "Error calling wrapped application: url=%s error=%s",
191
+ request.url,
192
+ str(e),
193
+ exc_info=True,
194
+ )
195
+ raise
196
+
197
+ # Convert to internal Response
198
+ headers_dict = {key.decode("latin1"): value.decode("latin1") for key, value in response_headers}
199
+
200
+ # Add Date header if not present
201
+ if not any(key.lower() == "date" for key in headers_dict.keys()):
202
+ date_header = formatdate(timeval=None, localtime=False, usegmt=True)
203
+ headers_dict["Date"] = date_header
204
+ logger.debug("Added Date header to response: %s", date_header)
205
+
206
+ async def response_stream() -> AsyncIterator[bytes]:
207
+ for chunk in response_body_chunks:
208
+ yield chunk
209
+
210
+ return Response(
211
+ status_code=status_code,
212
+ headers=Headers(headers_dict),
213
+ stream=response_stream(),
214
+ metadata={},
215
+ )
216
+
217
+ # Create a new cache proxy for this request with the closure
218
+ # This ensures complete isolation between concurrent requests
219
+ cache_proxy = AsyncCacheProxy(
220
+ request_sender=send_request_to_app,
221
+ storage=self.storage,
222
+ cache_options=self._cache_options,
223
+ ignore_specification=self._ignore_specification,
224
+ )
225
+
226
+ # Convert ASGI request to internal Request (using async iterator, not reading into memory)
227
+ request = self._asgi_to_internal_request(scope, receive)
228
+ logger.debug("Converted ASGI request to internal format: url=%s", request.url)
229
+
230
+ # Handle request through cache proxy
231
+ logger.debug("Handling request through cache proxy")
232
+ response = await cache_proxy.handle_request(request)
233
+
234
+ logger.info(
235
+ "Request processed: method=%s path=%s status=%d",
236
+ method,
237
+ full_path,
238
+ response.status_code,
239
+ )
240
+
241
+ # Send the cached or fresh response
242
+ await self._send_internal_response(response, send)
243
+ logger.debug("Response sent successfully")
244
+
245
+ except Exception as e:
246
+ logger.error(
247
+ "Error processing request: method=%s path=%s error=%s",
248
+ method,
249
+ full_path,
250
+ str(e),
251
+ exc_info=True,
252
+ )
253
+ raise
254
+
255
+ def _asgi_to_internal_request(self, scope: _Scope, receive: _Receive) -> Request:
256
+ """
257
+ Convert an ASGI HTTP scope to an internal Request object.
258
+
259
+ Args:
260
+ scope: The ASGI scope dictionary.
261
+ receive: The ASGI receive callable.
262
+
263
+ Returns:
264
+ The internal Request object.
265
+ """
266
+ # Build URL
267
+ scheme = scope.get("scheme", "http")
268
+ server = scope.get("server")
269
+
270
+ if server is None:
271
+ server = ("localhost", 80)
272
+ logger.debug("No server info in scope, using default: localhost:80")
273
+
274
+ host = server[0]
275
+ port = server[1] if server[1] is not None else (443 if scheme == "https" else 80)
276
+
277
+ # Add port to host if non-standard
278
+ if (scheme == "http" and port != 80) or (scheme == "https" and port != 443):
279
+ host = f"{host}:{port}"
280
+
281
+ path = scope.get("path", "/")
282
+ query_string = scope.get("query_string", b"")
283
+ if query_string:
284
+ path = f"{path}?{query_string.decode('latin1')}"
285
+
286
+ url = f"{scheme}://{host}{path}"
287
+ method = scope.get("method", "GET")
288
+
289
+ # Extract headers
290
+ headers_dict = {key.decode("latin1"): value.decode("latin1") for key, value in scope.get("headers", [])}
291
+
292
+ logger.debug(
293
+ "Building internal request: method=%s url=%s headers_count=%d",
294
+ method,
295
+ url,
296
+ len(headers_dict),
297
+ )
298
+
299
+ # Create async iterator for request body that reads from ASGI receive
300
+ async def request_stream() -> AsyncIterator[bytes]:
301
+ while True:
302
+ message = await receive()
303
+ if message["type"] == "http.request":
304
+ body = message.get("body", b"")
305
+ if body:
306
+ yield body
307
+ if not message.get("more_body", False):
308
+ break
309
+ elif message["type"] == "http.disconnect":
310
+ logger.debug("Client disconnected during request body streaming")
311
+ break
312
+
313
+ return Request(
314
+ method=method,
315
+ url=url,
316
+ headers=Headers(headers_dict),
317
+ stream=request_stream(),
318
+ # Metadatas don't make sense in ASGI scope, so we leave it empty
319
+ metadata={},
320
+ )
321
+
322
+ async def _send_internal_response(self, response: Response, send: _Send) -> None:
323
+ """
324
+ Send an internal Response to the ASGI send callable.
325
+
326
+ Args:
327
+ response: The internal Response object.
328
+ send: The ASGI send callable.
329
+ """
330
+ logger.debug(
331
+ "Sending response to client: status=%d headers_count=%d",
332
+ response.status_code,
333
+ len(response.headers),
334
+ )
335
+
336
+ # Convert headers to ASGI format
337
+ headers: list[tuple[bytes, bytes]] = [
338
+ (key.encode("latin1"), value.encode("latin1")) for key, value in response.headers.items()
339
+ ]
340
+
341
+ try:
342
+ # Send response.start
343
+ await send(
344
+ {
345
+ "type": "http.response.start",
346
+ "status": response.status_code,
347
+ "headers": headers,
348
+ }
349
+ )
350
+ logger.debug("Response headers sent")
351
+
352
+ # Send response body in chunks
353
+ bytes_sent = 0
354
+ chunk_count = 0
355
+ async for chunk in response.aiter_stream():
356
+ await send(
357
+ {
358
+ "type": "http.response.body",
359
+ "body": chunk,
360
+ "more_body": True,
361
+ }
362
+ )
363
+ bytes_sent += len(chunk)
364
+ chunk_count += 1
365
+ logger.debug("Sent response chunk: size=%d bytes", len(chunk))
366
+
367
+ # Send final empty chunk to signal end
368
+ await send(
369
+ {
370
+ "type": "http.response.body",
371
+ "body": b"",
372
+ "more_body": False,
373
+ }
374
+ )
375
+ logger.info(
376
+ "Response fully sent: status=%d total_bytes=%d chunks=%d",
377
+ response.status_code,
378
+ bytes_sent,
379
+ chunk_count,
380
+ )
381
+
382
+ except Exception as e:
383
+ logger.error(
384
+ "Error sending response: status=%d error=%s",
385
+ response.status_code,
386
+ str(e),
387
+ exc_info=True,
388
+ )
389
+ raise
390
+
391
+ async def aclose(self) -> None:
392
+ """Close the storage backend and release resources."""
393
+ logger.info("Closing ASGICacheMiddleware and storage backend")
394
+ try:
395
+ if self.storage:
396
+ await self.storage.close()
397
+ logger.info("Storage backend closed successfully")
398
+ except Exception as e:
399
+ logger.error("Error closing storage backend: %s", str(e), exc_info=True)
400
+ raise
hishel/fastapi.py ADDED
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ from hishel._utils import generate_http_date
6
+
7
+ try:
8
+ import fastapi
9
+ except ImportError as e:
10
+ raise ImportError(
11
+ "fastapi is required to use hishel.fastapi module. "
12
+ "Please install hishel with the 'fastapi' extra, "
13
+ "e.g., 'pip install hishel[fastapi]'."
14
+ ) from e
15
+
16
+
17
+ def cache(
18
+ *,
19
+ max_age: int | None = None,
20
+ s_maxage: int | None = None,
21
+ public: bool = False,
22
+ private: bool | list[str] = False,
23
+ no_cache: bool | list[str] = False,
24
+ no_store: bool = False,
25
+ no_transform: bool = False,
26
+ must_revalidate: bool = False,
27
+ must_understand: bool = False,
28
+ proxy_revalidate: bool = False,
29
+ immutable: bool = False,
30
+ stale_while_revalidate: int | None = None,
31
+ stale_if_error: int | None = None,
32
+ ) -> t.Any:
33
+ """
34
+ Add HTTP Cache-Control headers to FastAPI responses.
35
+
36
+ This function provides a convenient way to set cache control directives
37
+ on FastAPI responses according to RFC 9111 (HTTP Caching) and related standards.
38
+
39
+ Args:
40
+ max_age: Maximum time in seconds a response can be cached.
41
+ [RFC 9111, Section 5.2.2.1]
42
+ Example: max_age=3600 sets "Cache-Control: max-age=3600"
43
+ Use for both private and shared caches.
44
+
45
+ s_maxage: Maximum time in seconds for shared caches (proxies, CDNs).
46
+ [RFC 9111, Section 5.2.2.10]
47
+ Overrides max_age for shared caches only.
48
+ Example: s_maxage=7200 sets "s-maxage=7200"
49
+ Private caches (browsers) ignore this directive.
50
+
51
+ public: Marks response as cacheable by any cache.
52
+ [RFC 9111, Section 5.2.2.9]
53
+ Explicitly allows caching even if Authorization header is present.
54
+ Example: public=True adds "public" to Cache-Control
55
+
56
+ private: Marks response as cacheable only by private caches (browsers).
57
+ [RFC 9111, Section 5.2.2.7]
58
+ Shared caches (proxies, CDNs) MUST NOT store the response.
59
+ Can be True (applies to entire response) or a list of field names
60
+ (applies only to specific headers).
61
+ Examples:
62
+ - private=True adds "private" to Cache-Control
63
+ - private=["Set-Cookie"] adds 'private="Set-Cookie"' to Cache-Control
64
+ Useful for user-specific data.
65
+
66
+ no_cache: Response can be cached but MUST be revalidated before use.
67
+ [RFC 9111, Section 5.2.2.4]
68
+ Cache MUST check with origin server before serving cached copy.
69
+ Can be True (requires revalidation for entire response) or a list
70
+ of field names (requires revalidation only for specific headers).
71
+ Examples:
72
+ - no_cache=True adds "no-cache" to Cache-Control
73
+ - no_cache=["Set-Cookie", "Authorization"] adds 'no-cache="Set-Cookie, Authorization"'
74
+ Different from no_store - allows caching with mandatory validation.
75
+
76
+ no_store: Response MUST NOT be stored in any cache.
77
+ [RFC 9111, Section 5.2.2.5]
78
+ Most restrictive directive - prevents all caching.
79
+ Example: no_store=True adds "no-store" to Cache-Control
80
+ Use for sensitive data (passwords, personal information).
81
+
82
+ no_transform: Prohibits any transformations to the response.
83
+ [RFC 9111, Section 5.2.2.6]
84
+ Prevents proxies from modifying content (compression, format conversion).
85
+ Example: no_transform=True adds "no-transform" to Cache-Control
86
+
87
+ must_revalidate: Cache MUST revalidate stale responses.
88
+ [RFC 9111, Section 5.2.2.2]
89
+ Prevents serving stale content even if client accepts it.
90
+ Example: must_revalidate=True adds "must-revalidate" to Cache-Control
91
+ Stronger than no-cache - applies only when response becomes stale.
92
+
93
+ must_understand: Cache MUST understand response status code to cache it.
94
+ [RFC 9111, Section 5.2.2.3]
95
+ Prevents caching of responses with unknown status codes.
96
+ Example: must_understand=True adds "must-understand" to Cache-Control
97
+
98
+ proxy_revalidate: Like must_revalidate but only for shared caches.
99
+ [RFC 9111, Section 5.2.2.8]
100
+ Shared caches MUST revalidate stale responses.
101
+ Private caches can serve stale content without revalidation.
102
+ Example: proxy_revalidate=True adds "proxy-revalidate" to Cache-Control
103
+
104
+ immutable: Response body will never change.
105
+ [RFC 8246]
106
+ Optimization hint that revalidation is unnecessary during freshness period.
107
+ Example: immutable=True adds "immutable" to Cache-Control
108
+ Useful for versioned assets (e.g., /static/app.v123.js)
109
+
110
+ stale_while_revalidate: Allow stale response while revalidating in background.
111
+ [RFC 5861, Section 3]
112
+ Time in seconds cache can serve stale content while fetching fresh copy.
113
+ Example: stale_while_revalidate=86400 adds "stale-while-revalidate=86400"
114
+ Improves performance by avoiding cache misses.
115
+
116
+ stale_if_error: Allow stale response if origin server returns error.
117
+ [RFC 5861, Section 4]
118
+ Time in seconds cache can serve stale content when origin is unavailable.
119
+ Example: stale_if_error=3600 adds "stale-if-error=3600"
120
+ Improves availability during server failures.
121
+
122
+ Returns:
123
+ A dependency function that adds Cache-Control headers to the response.
124
+
125
+ Examples:
126
+ >>> from fastapi import FastAPI
127
+ >>> from hishel.fastapi import cache
128
+ >>>
129
+ >>> app = FastAPI()
130
+ >>>
131
+ >>> # Static assets - cache for 1 year, immutable
132
+ >>> @app.get("/static/logo.png")
133
+ >>> async def get_logo(
134
+ ... _: None = cache(max_age=31536000, public=True, immutable=True)
135
+ ... ):
136
+ ... return {"image": "logo.png"}
137
+ >>>
138
+ >>> # API endpoint - cache for 5 minutes, private
139
+ >>> @app.get("/api/user/profile")
140
+ >>> async def get_profile(
141
+ ... _: None = cache(max_age=300, private=True)
142
+ ... ):
143
+ ... return {"name": "John"}
144
+ >>>
145
+ >>> # CDN with shared cache - different TTLs for browsers and proxies
146
+ >>> @app.get("/api/public/data")
147
+ >>> async def get_data(
148
+ ... _: None = cache(max_age=300, s_maxage=3600, public=True)
149
+ ... ):
150
+ ... return {"data": "public"}
151
+ >>>
152
+ >>> # Sensitive data - no caching
153
+ >>> @app.get("/api/secrets")
154
+ >>> async def get_secrets(
155
+ ... _: None = cache(no_store=True)
156
+ ... ):
157
+ ... return {"secret": "value"}
158
+ >>>
159
+ >>> # Cacheable but must revalidate
160
+ >>> @app.get("/api/critical")
161
+ >>> async def get_critical(
162
+ ... _: None = cache(max_age=3600, must_revalidate=True)
163
+ ... ):
164
+ ... return {"critical": "data"}
165
+ >>>
166
+ >>> # Stale-while-revalidate for better performance
167
+ >>> @app.get("/api/news")
168
+ >>> async def get_news(
169
+ ... _: None = cache(max_age=300, stale_while_revalidate=86400, public=True)
170
+ ... ):
171
+ ... return {"news": "articles"}
172
+ >>>
173
+ >>> # Private directive with specific field names
174
+ >>> @app.get("/api/user/data")
175
+ >>> async def get_user_data(
176
+ ... _: None = cache(max_age=600, private=["Set-Cookie"])
177
+ ... ):
178
+ ... return {"data": "user_specific"}
179
+ >>>
180
+ >>> # No-cache with field names - revalidate only specific headers
181
+ >>> @app.get("/api/conditional")
182
+ >>> async def get_conditional(
183
+ ... _: None = cache(max_age=3600, no_cache=["Set-Cookie", "Authorization"])
184
+ ... ):
185
+ ... return {"data": "conditional_cache"}
186
+
187
+ Notes:
188
+ - Conflicting directives (e.g., public and private) will both be set.
189
+ Choose appropriate combinations based on your caching strategy.
190
+ - no_store is the strongest directive and prevents all caching.
191
+ - For CDNs, use s_maxage to set different TTLs for proxies vs browsers.
192
+ - Use immutable with versioned URLs for maximum cache efficiency.
193
+ - Combine stale_while_revalidate with max_age for better UX.
194
+ - private and no_cache can accept field names to apply directives
195
+ selectively to specific headers rather than the entire response.
196
+
197
+ See Also:
198
+ - RFC 9111: HTTP Caching (https://www.rfc-editor.org/rfc/rfc9111.html)
199
+ - RFC 8246: HTTP Immutable Responses (https://www.rfc-editor.org/rfc/rfc8246.html)
200
+ - RFC 5861: HTTP Cache-Control Extensions (https://www.rfc-editor.org/rfc/rfc5861.html)
201
+ """
202
+
203
+ def add_cache_headers(response: fastapi.Response) -> t.Any:
204
+ """Add Cache-Control headers to the response."""
205
+ directives: list[str] = []
206
+
207
+ # IMPORTANT
208
+ response.headers["Date"] = generate_http_date()
209
+
210
+ # Add directives with values
211
+ if max_age is not None:
212
+ directives.append(f"max-age={max_age}")
213
+
214
+ if s_maxage is not None:
215
+ directives.append(f"s-maxage={s_maxage}")
216
+
217
+ if stale_while_revalidate is not None:
218
+ directives.append(f"stale-while-revalidate={stale_while_revalidate}")
219
+
220
+ if stale_if_error is not None:
221
+ directives.append(f"stale-if-error={stale_if_error}")
222
+
223
+ # Add boolean directives
224
+ if public:
225
+ directives.append("public")
226
+
227
+ # Handle private (can be bool or list of field names)
228
+ if private is True:
229
+ directives.append("private")
230
+ elif isinstance(private, list) and private:
231
+ field_names = ", ".join(private)
232
+ directives.append(f'private="{field_names}"')
233
+
234
+ # Handle no_cache (can be bool or list of field names)
235
+ if no_cache is True:
236
+ directives.append("no-cache")
237
+ elif isinstance(no_cache, list) and no_cache:
238
+ field_names = ", ".join(no_cache)
239
+ directives.append(f'no-cache="{field_names}"')
240
+
241
+ if no_store:
242
+ directives.append("no-store")
243
+
244
+ if no_transform:
245
+ directives.append("no-transform")
246
+
247
+ if must_revalidate:
248
+ directives.append("must-revalidate")
249
+
250
+ if must_understand:
251
+ directives.append("must-understand")
252
+
253
+ if proxy_revalidate:
254
+ directives.append("proxy-revalidate")
255
+
256
+ if immutable:
257
+ directives.append("immutable")
258
+
259
+ # Set the Cache-Control header if any directives were specified
260
+ if directives:
261
+ response.headers["Cache-Control"] = ", ".join(directives)
262
+
263
+ return fastapi.Depends(add_cache_headers)