fastapi-caching-route 0.6.0__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_caching_route-0.6.0/.github/workflows/coverage.yaml +34 -0
  2. fastapi_caching_route-0.6.0/.github/workflows/deploy-docs.yaml +44 -0
  3. fastapi_caching_route-0.6.0/.github/workflows/deploy-pypi.yaml +42 -0
  4. fastapi_caching_route-0.6.0/.github/workflows/pr.yaml +85 -0
  5. fastapi_caching_route-0.6.0/.gitignore +6 -0
  6. fastapi_caching_route-0.6.0/.vscode/settings.json +3 -0
  7. fastapi_caching_route-0.6.0/.zed/settings.json +10 -0
  8. fastapi_caching_route-0.6.0/LICENSE +21 -0
  9. fastapi_caching_route-0.6.0/PKG-INFO +250 -0
  10. fastapi_caching_route-0.6.0/README.md +230 -0
  11. fastapi_caching_route-0.6.0/docs/api_reference/caching_route.md +1 -0
  12. fastapi_caching_route-0.6.0/docs/api_reference/fastapi_cache.md +8 -0
  13. fastapi_caching_route-0.6.0/docs/api_reference/typing.md +5 -0
  14. fastapi_caching_route-0.6.0/docs/index.md +1 -0
  15. fastapi_caching_route-0.6.0/examples/README.md +155 -0
  16. fastapi_caching_route-0.6.0/examples/complex.py +77 -0
  17. fastapi_caching_route-0.6.0/examples/curl-format.txt +7 -0
  18. fastapi_caching_route-0.6.0/examples/invalidate.py +68 -0
  19. fastapi_caching_route-0.6.0/examples/ruff.toml +4 -0
  20. fastapi_caching_route-0.6.0/examples/simple.py +18 -0
  21. fastapi_caching_route-0.6.0/fastapi_caching_route/__init__.py +6 -0
  22. fastapi_caching_route-0.6.0/fastapi_caching_route/_version.py +1 -0
  23. fastapi_caching_route-0.6.0/fastapi_caching_route/_version.pyi +1 -0
  24. fastapi_caching_route-0.6.0/fastapi_caching_route/main.py +590 -0
  25. fastapi_caching_route-0.6.0/fastapi_caching_route/py.typed +0 -0
  26. fastapi_caching_route-0.6.0/justfile +19 -0
  27. fastapi_caching_route-0.6.0/mkdocs.yml +32 -0
  28. fastapi_caching_route-0.6.0/pyproject.toml +111 -0
  29. fastapi_caching_route-0.6.0/tests/ruff.toml +4 -0
  30. fastapi_caching_route-0.6.0/tests/test_main.py +391 -0
  31. fastapi_caching_route-0.6.0/uv.lock +1558 -0
@@ -0,0 +1,34 @@
1
+ name: Coverage
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ coverage:
10
+ name: Coverage
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v7
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v6
18
+ with:
19
+ python-version-file: pyproject.toml
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
23
+
24
+ - name: Install the project
25
+ run: uv sync --frozen
26
+
27
+ - name: Coverage
28
+ run: uv run --no-sync pytest --cov-report=xml --cov-report=term
29
+
30
+ - name: Upload coverage to Coveralls
31
+ uses: coverallsapp/github-action@v2
32
+ with:
33
+ format: cobertura
34
+ file: coverage.xml
@@ -0,0 +1,44 @@
1
+ name: Deploy documentation to Pages
2
+
3
+ on:
4
+ release:
5
+ types:
6
+ - published
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ pages: write
12
+ id-token: write
13
+
14
+ concurrency:
15
+ group: 'pages'
16
+ cancel-in-progress: true
17
+
18
+ jobs:
19
+ deploy:
20
+ environment:
21
+ name: github-pages
22
+ url: ${{ steps.deployment.outputs.page_url }}
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v7
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
30
+
31
+ - name: Build site
32
+ run: uv run --frozen --group docs mkdocs build
33
+
34
+ - name: Setup Pages
35
+ uses: actions/configure-pages@v6
36
+
37
+ - name: Upload artifact
38
+ uses: actions/upload-pages-artifact@v5
39
+ with:
40
+ path: ./site
41
+
42
+ - name: Deploy to GitHub Pages
43
+ id: deployment
44
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,42 @@
1
+ name: Deploy to PyPI
2
+
3
+ on:
4
+ release:
5
+ types:
6
+ - published
7
+
8
+ jobs:
9
+ build:
10
+ name: Build python packages
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v7
15
+ with:
16
+ fetch-depth: 0
17
+
18
+ - name: Build
19
+ run: pipx run build
20
+
21
+ - uses: actions/upload-artifact@v7
22
+ with:
23
+ path: dist/*
24
+
25
+ publish:
26
+ name: Publish to PyPI
27
+ needs: [build]
28
+ environment:
29
+ name: pypi
30
+ url: https://pypi.org/p/fastapi-caching-route
31
+ permissions:
32
+ id-token: write
33
+ runs-on: ubuntu-latest
34
+ if: github.event_name == 'release' && github.event.action == 'published'
35
+ steps:
36
+ - name: Download artifacts
37
+ uses: actions/download-artifact@v8
38
+ with:
39
+ path: dist
40
+
41
+ - name: Upload to PyPI
42
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,85 @@
1
+ name: PR Checks
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ pytest:
10
+ name: Tests Python ${{ matrix.python-version }}
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.14t']
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v7
19
+
20
+ - uses: actions/setup-python@v6
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
26
+
27
+ - name: Install the project
28
+ run: uv sync --frozen
29
+
30
+ - name: Run pytest
31
+ run: uv run --no-sync pytest --no-cov
32
+
33
+ code-quality:
34
+ name: Code Quality
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - name: Checkout
38
+ uses: actions/checkout@v7
39
+
40
+ - name: Set up Python
41
+ uses: actions/setup-python@v6
42
+ with:
43
+ python-version-file: pyproject.toml
44
+
45
+ - name: Install uv
46
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
47
+
48
+ - name: Check uv.lock
49
+ run: uv lock --check
50
+
51
+ - name: Install the project
52
+ run: uv sync --frozen
53
+
54
+ - name: Ruff check
55
+ id: ruff-check
56
+ continue-on-error: true
57
+ run: uv run --no-sync ruff check --output-format=github
58
+
59
+ - name: Ruff format
60
+ id: ruff-format
61
+ continue-on-error: true
62
+ run: uv run --no-sync ruff format --check --output-format=github
63
+
64
+ - name: Type check
65
+ id: ty
66
+ continue-on-error: true
67
+ run: uv run --no-sync ty check --output-format=github
68
+
69
+ - name: Fail if any step failed
70
+ if: |
71
+ steps.ruff-check.outcome == 'failure' ||
72
+ steps.ruff-format.outcome == 'failure' ||
73
+ steps.ty.outcome == 'failure'
74
+ run: exit 1
75
+
76
+ summary:
77
+ name: PR Checks Summary
78
+ runs-on: ubuntu-latest
79
+ if: always()
80
+ needs: [pytest, code-quality]
81
+ steps:
82
+ - run: >
83
+ test
84
+ "${{ needs.pytest.result }}" = "success" -a
85
+ "${{ needs.code-quality.result }}" = "success"
@@ -0,0 +1,6 @@
1
+ __pycache__
2
+ /dist
3
+ /fastapi_caching_route/_version.py
4
+ /site
5
+
6
+ .coverage*
@@ -0,0 +1,3 @@
1
+ {
2
+ "mypy-type-checker.importStrategy": "fromEnvironment"
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "languages": {
3
+ "Python": {
4
+ "language_servers": [
5
+ "ty",
6
+ "ruff"
7
+ ]
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Petr Tsymbarovich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,250 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-caching-route
3
+ Version: 0.6.0
4
+ Summary: FastAPI route for efficient caching.
5
+ Project-URL: Homepage, https://github.com/AgroDT/fastapi-caching-route
6
+ Project-URL: Repository, https://github.com/AgroDT/fastapi-caching-route.git
7
+ Project-URL: Issues, https://github.com/AgroDT/fastapi-caching-route/issues
8
+ Project-URL: Documentation, https://agrodt.github.io/fastapi-caching-route/
9
+ Project-URL: Coverage, https://coveralls.io/github/AgroDT/fastapi-caching-route
10
+ Author-email: Petr Tsymbarovich <petr@tsymbarovich.ru>
11
+ License-File: LICENSE
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: aiocache<1.0,>=0.12
19
+ Description-Content-Type: text/markdown
20
+
21
+ # FastAPI Caching Route
22
+
23
+ [![PR Checks](https://github.com/AgroDT/fastapi-caching-route/actions/workflows/pr.yaml/badge.svg)](https://github.com/AgroDT/fastapi-caching-route/actions/workflows/pr.yaml)
24
+ [![Coverage](https://github.com/AgroDT/fastapi-caching-route/actions/workflows/coverage.yaml/badge.svg)](https://github.com/AgroDT/fastapi-caching-route/actions/workflows/coverage.yaml)
25
+ [![Coverage Status](https://coveralls.io/repos/github/AgroDT/fastapi-caching-route/badge.svg)](https://coveralls.io/github/AgroDT/fastapi-caching-route)
26
+ [![Docs](https://github.com/AgroDT/fastapi-caching-route/actions/workflows/deploy-docs.yaml/badge.svg)](https://github.com/AgroDT/fastapi-caching-route/actions/workflows/deploy-docs.yaml)
27
+ [![PyPI](https://img.shields.io/pypi/v/fastapi-caching-route.svg)](https://pypi.org/project/fastapi-caching-route/)
28
+
29
+ FastAPI route class for response caching before entering the endpoint handler.
30
+
31
+ `fastapi-caching-route` plugs into `fastapi.APIRouter`, stores complete response
32
+ payloads in an `aiocache` backend, and serves cache hits directly from the route
33
+ handler. It is useful for expensive read endpoints where the cache key can be
34
+ derived from the request path, query parameters, or a custom key builder.
35
+
36
+ **⚠️ This project is a proof of concept and is not yet recommended for production use!**
37
+
38
+ ## Features
39
+
40
+ - Cache regular FastAPI responses and `StreamingResponse` bodies.
41
+ - Return `X-Cache: MISS` for stored responses and `X-Cache: HIT` for cache hits.
42
+ - Generate `ETag` headers for cached responses.
43
+ - Return `304 Not Modified` for matching `If-None-Match` requests.
44
+ - Build default cache keys from the route path and declared query parameters.
45
+ - Provide custom key builders for path parameters or application-specific keys.
46
+ - Include selected request headers in default cache keys for negotiated responses.
47
+ - Run explicitly configured dependencies before cache lookup, for example API key
48
+ checks.
49
+ - Pass `namespace` and `ttl` through to the underlying `aiocache` backend.
50
+ - Manually invalidate cached values through `FastAPICache.invalidate_cached()`.
51
+
52
+ ## Installation
53
+
54
+ ```sh
55
+ uv add fastapi-caching-route
56
+ ```
57
+
58
+ ```sh
59
+ pip install fastapi-caching-route
60
+ ```
61
+
62
+ Install FastAPI and a cache backend that matches your application. For local
63
+ development or tests, `aiocache.SimpleMemoryCache` is enough:
64
+
65
+ ```sh
66
+ uv add fastapi aiocache
67
+ ```
68
+
69
+ ```sh
70
+ pip install fastapi aiocache
71
+ ```
72
+
73
+ ## Basic Usage
74
+
75
+ Use `CachingRoute` as the router route class and decorate endpoints with
76
+ `FastAPICache`.
77
+
78
+ ```py
79
+ from aiocache import SimpleMemoryCache
80
+ from fastapi import APIRouter, FastAPI
81
+ from fastapi_caching_route import CachingRoute, FastAPICache
82
+
83
+
84
+ router = APIRouter(route_class=CachingRoute)
85
+ cache = FastAPICache(SimpleMemoryCache())
86
+
87
+
88
+ @cache()
89
+ @router.get('/')
90
+ def cached() -> str:
91
+ return 'Hello, World!'
92
+
93
+
94
+ app = FastAPI()
95
+ app.include_router(router)
96
+ ```
97
+
98
+ Start an app with this route and call it twice. The first response is produced by
99
+ the endpoint and stored in the cache:
100
+
101
+ ```text
102
+ X-Cache: MISS
103
+ ```
104
+
105
+ The second response is returned from the cache before the endpoint handler is
106
+ called:
107
+
108
+ ```text
109
+ X-Cache: HIT
110
+ ```
111
+
112
+ Routes decorated with `@cache()` must be registered on a router that uses
113
+ `CachingRoute`. A plain `APIRoute` ignores the cache configuration.
114
+
115
+ ## Cache Keys
116
+
117
+ By default, the cache key is built from the request path and declared query
118
+ parameters. Query parameter order is normalized, so these two requests hit the
119
+ same cache entry when the endpoint declares `a` and `b`:
120
+
121
+ ```text
122
+ /query?a=a&b=b
123
+ /query?b=b&a=a
124
+ ```
125
+
126
+ Use a custom key builder when the cache key should be based on path parameters,
127
+ headers, user context, or another application-specific value.
128
+
129
+ ```py
130
+ from fastapi import Request
131
+
132
+
133
+ def user_key_builder(request: Request) -> str:
134
+ user_id = request.scope['path_params']['user_id']
135
+ return f'user:{user_id}'
136
+
137
+
138
+ @cache(key_builder=user_key_builder, ttl=60, namespace='users')
139
+ @router.get('/users/{user_id}')
140
+ def get_user(user_id: int) -> dict[str, int]:
141
+ return {'id': user_id}
142
+ ```
143
+
144
+ `ttl` and `namespace` are passed to `aiocache`. When the underlying cache instance
145
+ also has a namespace, `FastAPICache` concatenates the root and endpoint namespace
146
+ by default:
147
+
148
+ ```py
149
+ cache = FastAPICache(RedisCache(namespace='api'))
150
+
151
+
152
+ @cache(namespace='users')
153
+ @router.get('/users/{user_id}')
154
+ def get_user(user_id: int): ...
155
+
156
+
157
+ # Resulting namespace: "api:users"
158
+ ```
159
+
160
+ Pass `namespace_policy="replace"` to `FastAPICache` if endpoint namespaces should
161
+ replace the root namespace instead.
162
+
163
+ If the response representation depends on request headers, include those headers
164
+ in the default cache key with `vary_headers`. The route also returns a matching
165
+ `Vary` header:
166
+
167
+ ```py
168
+ @cache(vary_headers=['Accept-Language'])
169
+ @router.get('/localized')
170
+ def localized(request: Request) -> str:
171
+ return request.headers.get('accept-language', 'en')
172
+ ```
173
+
174
+ Responses with `Vary: *` are not cached.
175
+
176
+ ## Dependencies Before Cache Lookup
177
+
178
+ FastAPI dependencies on the route still run on cache misses. If a dependency
179
+ must also be resolved before cache lookup, pass it to `@cache(dependencies=...)`.
180
+ This is mainly useful for security dependencies that should reject unauthorized
181
+ requests before a cached response can be served.
182
+
183
+ ```py
184
+ from fastapi import Depends
185
+ from fastapi.security import APIKeyHeader
186
+
187
+
188
+ api_key = Depends(APIKeyHeader(name='X-Key'))
189
+
190
+
191
+ @cache(dependencies=[api_key])
192
+ @router.get('/private', dependencies=[api_key])
193
+ def private_data() -> str:
194
+ return 'secret'
195
+ ```
196
+
197
+ Keep the dependency on the route as well if it must be enforced for cache misses.
198
+
199
+ ## Conditional Requests
200
+
201
+ Cached responses without an existing `ETag` get one based on the response body.
202
+ If the endpoint already sets `ETag`, that value is preserved. On a cache hit,
203
+ requests with a matching `If-None-Match` header return `304 Not Modified` with an
204
+ empty body. `If-None-Match` supports weak tags, tag lists, and `*`.
205
+
206
+ ```sh
207
+ curl http://127.0.0.1:8000/cached -H 'If-None-Match: "..."'
208
+ ```
209
+
210
+ ## Invalidation
211
+
212
+ Use the same key builder logic when invalidating cache entries after writes.
213
+
214
+ ```py
215
+ from fastapi import Request
216
+
217
+
218
+ def user_cache_key(user_id: int) -> str:
219
+ return f'user:{user_id}'
220
+
221
+
222
+ def user_key_builder(request: Request) -> str:
223
+ return user_cache_key(request.scope['path_params']['user_id'])
224
+
225
+
226
+ @cache(key_builder=user_key_builder)
227
+ @router.get('/users/{user_id}')
228
+ def get_user(user_id: int): ...
229
+
230
+
231
+ @router.patch('/users/{user_id}')
232
+ async def update_user(user_id: int) -> dict[str, int]:
233
+ await cache.invalidate_cached(user_cache_key(user_id))
234
+ return {'id': user_id}
235
+ ```
236
+
237
+ `invalidate_cached()` returns the number of deleted keys reported by the
238
+ underlying cache backend.
239
+
240
+ ## Examples
241
+
242
+ The repository contains runnable examples:
243
+
244
+ - `examples/simple.py`: minimal cached route.
245
+ - `examples/complex.py`: cache hits and misses, auth dependency, ETag handling,
246
+ streaming response caching, query parameter keys, and non-cacheable responses.
247
+ - `examples/invalidate.py`: custom key builder and manual invalidation after an
248
+ update.
249
+
250
+ Follow the detailed walkthrough in the examples README.