fastapi-memory 0.1.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.
- fastapi_memory-0.1.0/LICENSE +21 -0
- fastapi_memory-0.1.0/MANIFEST.in +3 -0
- fastapi_memory-0.1.0/PKG-INFO +233 -0
- fastapi_memory-0.1.0/README.md +192 -0
- fastapi_memory-0.1.0/fastapi_memory/__init__.py +74 -0
- fastapi_memory-0.1.0/fastapi_memory/caching.py +117 -0
- fastapi_memory-0.1.0/fastapi_memory/config.py +34 -0
- fastapi_memory-0.1.0/fastapi_memory/http.py +170 -0
- fastapi_memory-0.1.0/fastapi_memory/py.typed +0 -0
- fastapi_memory-0.1.0/fastapi_memory/resilience.py +116 -0
- fastapi_memory-0.1.0/fastapi_memory.egg-info/PKG-INFO +233 -0
- fastapi_memory-0.1.0/fastapi_memory.egg-info/SOURCES.txt +16 -0
- fastapi_memory-0.1.0/fastapi_memory.egg-info/dependency_links.txt +1 -0
- fastapi_memory-0.1.0/fastapi_memory.egg-info/requires.txt +17 -0
- fastapi_memory-0.1.0/fastapi_memory.egg-info/top_level.txt +1 -0
- fastapi_memory-0.1.0/pyproject.toml +53 -0
- fastapi_memory-0.1.0/setup.cfg +4 -0
- fastapi_memory-0.1.0/tests/test_imports.py +158 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Your Name
|
|
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,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-memory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Caching, retry and resilient-HTTP helpers for FastAPI services.
|
|
5
|
+
Author-email: Alexander Stankovic <alexdarka@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alexdarka/fastapi-memory
|
|
8
|
+
Project-URL: Repository, https://github.com/alexdarka/fastapi-memory
|
|
9
|
+
Keywords: fastapi,cache,caching,retry,tenacity,httpx,resilience,redis
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: fastapi>=0.115.0
|
|
27
|
+
Requires-Dist: fastapi-cache2>=0.2.2
|
|
28
|
+
Requires-Dist: tenacity>=8.3.0
|
|
29
|
+
Requires-Dist: httpx>=0.27.0
|
|
30
|
+
Requires-Dist: jinja2>=3.1.0
|
|
31
|
+
Provides-Extra: redis
|
|
32
|
+
Requires-Dist: redis>=5.0.0; extra == "redis"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
38
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
39
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# fastapi-memory
|
|
43
|
+
|
|
44
|
+
Caching, retry, and resilient-HTTP helpers for FastAPI services.
|
|
45
|
+
|
|
46
|
+
A single package that provides response caching, retry policies, a resilient
|
|
47
|
+
async HTTP client, and cached config singletons — tailored for FastAPI
|
|
48
|
+
projects that talk to upstream APIs.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fastapi_memory import (
|
|
52
|
+
FmCacheManager, FmMemoryBackend, memorize,
|
|
53
|
+
retry, stop_after_retries, exponential_backoff, retry_on_error,
|
|
54
|
+
fm_lru,
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why
|
|
59
|
+
|
|
60
|
+
Most FastAPI services that proxy slower or less-reliable upstream APIs end up
|
|
61
|
+
re-writing the same patterns:
|
|
62
|
+
|
|
63
|
+
1. A **cache** for endpoints or data that don't change often.
|
|
64
|
+
2. A **retry policy** for upstream calls — retry network errors and 5xx, but not 4xx.
|
|
65
|
+
3. A **cached singleton** config object (build once, reuse everywhere).
|
|
66
|
+
4. A **resilient HTTP client** with connection pooling and retries baked in.
|
|
67
|
+
|
|
68
|
+
`fastapi-memory` consolidates all of these into one import, with ready-made
|
|
69
|
+
helpers so you don't have to re-derive common patterns every time.
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Editable install (local development)
|
|
75
|
+
pip install -e /path/to/fastapi-memory
|
|
76
|
+
|
|
77
|
+
# From PyPI (once published)
|
|
78
|
+
pip install fastapi-memory
|
|
79
|
+
|
|
80
|
+
# With optional Redis cache backend support
|
|
81
|
+
pip install "fastapi-memory[redis]"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## What's inside
|
|
85
|
+
|
|
86
|
+
| Module | Provides | Adds |
|
|
87
|
+
|--------------|-------------------------------------------------------------------|-----------------------------------------------------------|
|
|
88
|
+
| `caching` | `FmCacheManager`, `FmMemoryBackend`, `memorize`, `FmRedisBackend` | `init_cache()`, `clear_cache()` |
|
|
89
|
+
| `resilience` | `retry`, `stop_after_retries`, `exponential_backoff`, `retry_on_error` | `default_retry()`, `is_retryable_httpx_error()` |
|
|
90
|
+
| `config` | `fm_lru` | `cached_singleton` |
|
|
91
|
+
| `http` | — | `FmResilientClient` |
|
|
92
|
+
|
|
93
|
+
Everything above is re-exported from the top-level `fastapi_memory` package,
|
|
94
|
+
so `from fastapi_memory import <anything in the table>` works.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Quick start
|
|
99
|
+
|
|
100
|
+
### Caching — response caching with a single call
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from contextlib import asynccontextmanager
|
|
104
|
+
from fastapi import FastAPI
|
|
105
|
+
from fastapi_memory import init_cache, clear_cache, memorize
|
|
106
|
+
|
|
107
|
+
@asynccontextmanager
|
|
108
|
+
async def lifespan(app: FastAPI):
|
|
109
|
+
init_cache(prefix="app-cache") # in-memory (default)
|
|
110
|
+
yield
|
|
111
|
+
|
|
112
|
+
app = FastAPI(lifespan=lifespan)
|
|
113
|
+
|
|
114
|
+
@app.get("/api/data")
|
|
115
|
+
@memorize(expire=60)
|
|
116
|
+
async def get_data():
|
|
117
|
+
return {"data": "computed result"}
|
|
118
|
+
|
|
119
|
+
@app.post("/api/cache/invalidate")
|
|
120
|
+
async def invalidate_cache():
|
|
121
|
+
await clear_cache()
|
|
122
|
+
return {"ok": True}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Switching to Redis later
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
init_cache(backend="redis", prefix="app-cache", redis_url="redis://localhost:6379")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Retry — exponential backoff with sensible defaults
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from fastapi_memory import default_retry
|
|
135
|
+
|
|
136
|
+
@default_retry()
|
|
137
|
+
async def call_upstream():
|
|
138
|
+
resp = await client.get(url)
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
return resp.json()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`default_retry()` retries up to 3 times with exponential backoff (2s–10s),
|
|
144
|
+
skip 4xx, reraise on final failure. Override any piece:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
@default_retry(attempts=5, wait_max=30)
|
|
148
|
+
async def flaky_call():
|
|
149
|
+
...
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Config — cached singleton
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from fastapi_memory import cached_singleton
|
|
156
|
+
|
|
157
|
+
class Settings:
|
|
158
|
+
BASE_URL: str = "http://api.example.com:8080"
|
|
159
|
+
CACHE_TTL: int = 300
|
|
160
|
+
|
|
161
|
+
@cached_singleton
|
|
162
|
+
def get_settings() -> Settings:
|
|
163
|
+
return Settings()
|
|
164
|
+
|
|
165
|
+
config = get_settings()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Resilient HTTP client
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from contextlib import asynccontextmanager
|
|
172
|
+
from fastapi import FastAPI
|
|
173
|
+
from fastapi_memory import FmResilientClient, init_cache
|
|
174
|
+
|
|
175
|
+
upstream = FmResilientClient(
|
|
176
|
+
base_url="http://api.example.com:8080",
|
|
177
|
+
timeout=30.0,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@asynccontextmanager
|
|
181
|
+
async def lifespan(app: FastAPI):
|
|
182
|
+
await upstream.start()
|
|
183
|
+
init_cache(prefix="app-cache")
|
|
184
|
+
yield
|
|
185
|
+
await upstream.aclose()
|
|
186
|
+
|
|
187
|
+
app = FastAPI(lifespan=lifespan)
|
|
188
|
+
|
|
189
|
+
@app.get("/api/data")
|
|
190
|
+
async def get_data():
|
|
191
|
+
return await upstream.get_json("data")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`upstream.get_raw(path, params)` is also available if you want the raw
|
|
195
|
+
exceptions instead of `HTTPException`.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Project layout
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
fastapi-memory/
|
|
203
|
+
├── pyproject.toml
|
|
204
|
+
├── setup.py
|
|
205
|
+
├── README.md
|
|
206
|
+
├── LICENSE
|
|
207
|
+
├── fastapi_memory/
|
|
208
|
+
│ ├── __init__.py # re-exports everything
|
|
209
|
+
│ ├── caching.py # FmCacheManager, FmMemoryBackend, memorize, init_cache, clear_cache
|
|
210
|
+
│ ├── resilience.py # retry + default_retry, is_retryable_httpx_error
|
|
211
|
+
│ ├── config.py # fm_lru + cached_singleton
|
|
212
|
+
│ └── http.py # FmResilientClient
|
|
213
|
+
└── tests/
|
|
214
|
+
└── test_imports.py
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Updating `requirements.txt`
|
|
218
|
+
|
|
219
|
+
Replace:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
tenacity>=8.3.0
|
|
223
|
+
fastapi-cache2>=0.2.2
|
|
224
|
+
redis>=5.0.0
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
with:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
fastapi-memory @ file:///path/to/fastapi-memory
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
(or once published: `fastapi-memory>=0.1.0`, optionally `fastapi-memory[redis]`.)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# fastapi-memory
|
|
2
|
+
|
|
3
|
+
Caching, retry, and resilient-HTTP helpers for FastAPI services.
|
|
4
|
+
|
|
5
|
+
A single package that provides response caching, retry policies, a resilient
|
|
6
|
+
async HTTP client, and cached config singletons — tailored for FastAPI
|
|
7
|
+
projects that talk to upstream APIs.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from fastapi_memory import (
|
|
11
|
+
FmCacheManager, FmMemoryBackend, memorize,
|
|
12
|
+
retry, stop_after_retries, exponential_backoff, retry_on_error,
|
|
13
|
+
fm_lru,
|
|
14
|
+
)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
Most FastAPI services that proxy slower or less-reliable upstream APIs end up
|
|
20
|
+
re-writing the same patterns:
|
|
21
|
+
|
|
22
|
+
1. A **cache** for endpoints or data that don't change often.
|
|
23
|
+
2. A **retry policy** for upstream calls — retry network errors and 5xx, but not 4xx.
|
|
24
|
+
3. A **cached singleton** config object (build once, reuse everywhere).
|
|
25
|
+
4. A **resilient HTTP client** with connection pooling and retries baked in.
|
|
26
|
+
|
|
27
|
+
`fastapi-memory` consolidates all of these into one import, with ready-made
|
|
28
|
+
helpers so you don't have to re-derive common patterns every time.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Editable install (local development)
|
|
34
|
+
pip install -e /path/to/fastapi-memory
|
|
35
|
+
|
|
36
|
+
# From PyPI (once published)
|
|
37
|
+
pip install fastapi-memory
|
|
38
|
+
|
|
39
|
+
# With optional Redis cache backend support
|
|
40
|
+
pip install "fastapi-memory[redis]"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What's inside
|
|
44
|
+
|
|
45
|
+
| Module | Provides | Adds |
|
|
46
|
+
|--------------|-------------------------------------------------------------------|-----------------------------------------------------------|
|
|
47
|
+
| `caching` | `FmCacheManager`, `FmMemoryBackend`, `memorize`, `FmRedisBackend` | `init_cache()`, `clear_cache()` |
|
|
48
|
+
| `resilience` | `retry`, `stop_after_retries`, `exponential_backoff`, `retry_on_error` | `default_retry()`, `is_retryable_httpx_error()` |
|
|
49
|
+
| `config` | `fm_lru` | `cached_singleton` |
|
|
50
|
+
| `http` | — | `FmResilientClient` |
|
|
51
|
+
|
|
52
|
+
Everything above is re-exported from the top-level `fastapi_memory` package,
|
|
53
|
+
so `from fastapi_memory import <anything in the table>` works.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
### Caching — response caching with a single call
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from contextlib import asynccontextmanager
|
|
63
|
+
from fastapi import FastAPI
|
|
64
|
+
from fastapi_memory import init_cache, clear_cache, memorize
|
|
65
|
+
|
|
66
|
+
@asynccontextmanager
|
|
67
|
+
async def lifespan(app: FastAPI):
|
|
68
|
+
init_cache(prefix="app-cache") # in-memory (default)
|
|
69
|
+
yield
|
|
70
|
+
|
|
71
|
+
app = FastAPI(lifespan=lifespan)
|
|
72
|
+
|
|
73
|
+
@app.get("/api/data")
|
|
74
|
+
@memorize(expire=60)
|
|
75
|
+
async def get_data():
|
|
76
|
+
return {"data": "computed result"}
|
|
77
|
+
|
|
78
|
+
@app.post("/api/cache/invalidate")
|
|
79
|
+
async def invalidate_cache():
|
|
80
|
+
await clear_cache()
|
|
81
|
+
return {"ok": True}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Switching to Redis later
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
init_cache(backend="redis", prefix="app-cache", redis_url="redis://localhost:6379")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Retry — exponential backoff with sensible defaults
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from fastapi_memory import default_retry
|
|
94
|
+
|
|
95
|
+
@default_retry()
|
|
96
|
+
async def call_upstream():
|
|
97
|
+
resp = await client.get(url)
|
|
98
|
+
resp.raise_for_status()
|
|
99
|
+
return resp.json()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`default_retry()` retries up to 3 times with exponential backoff (2s–10s),
|
|
103
|
+
skip 4xx, reraise on final failure. Override any piece:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
@default_retry(attempts=5, wait_max=30)
|
|
107
|
+
async def flaky_call():
|
|
108
|
+
...
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Config — cached singleton
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from fastapi_memory import cached_singleton
|
|
115
|
+
|
|
116
|
+
class Settings:
|
|
117
|
+
BASE_URL: str = "http://api.example.com:8080"
|
|
118
|
+
CACHE_TTL: int = 300
|
|
119
|
+
|
|
120
|
+
@cached_singleton
|
|
121
|
+
def get_settings() -> Settings:
|
|
122
|
+
return Settings()
|
|
123
|
+
|
|
124
|
+
config = get_settings()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Resilient HTTP client
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from contextlib import asynccontextmanager
|
|
131
|
+
from fastapi import FastAPI
|
|
132
|
+
from fastapi_memory import FmResilientClient, init_cache
|
|
133
|
+
|
|
134
|
+
upstream = FmResilientClient(
|
|
135
|
+
base_url="http://api.example.com:8080",
|
|
136
|
+
timeout=30.0,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@asynccontextmanager
|
|
140
|
+
async def lifespan(app: FastAPI):
|
|
141
|
+
await upstream.start()
|
|
142
|
+
init_cache(prefix="app-cache")
|
|
143
|
+
yield
|
|
144
|
+
await upstream.aclose()
|
|
145
|
+
|
|
146
|
+
app = FastAPI(lifespan=lifespan)
|
|
147
|
+
|
|
148
|
+
@app.get("/api/data")
|
|
149
|
+
async def get_data():
|
|
150
|
+
return await upstream.get_json("data")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`upstream.get_raw(path, params)` is also available if you want the raw
|
|
154
|
+
exceptions instead of `HTTPException`.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Project layout
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
fastapi-memory/
|
|
162
|
+
├── pyproject.toml
|
|
163
|
+
├── setup.py
|
|
164
|
+
├── README.md
|
|
165
|
+
├── LICENSE
|
|
166
|
+
├── fastapi_memory/
|
|
167
|
+
│ ├── __init__.py # re-exports everything
|
|
168
|
+
│ ├── caching.py # FmCacheManager, FmMemoryBackend, memorize, init_cache, clear_cache
|
|
169
|
+
│ ├── resilience.py # retry + default_retry, is_retryable_httpx_error
|
|
170
|
+
│ ├── config.py # fm_lru + cached_singleton
|
|
171
|
+
│ └── http.py # FmResilientClient
|
|
172
|
+
└── tests/
|
|
173
|
+
└── test_imports.py
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Updating `requirements.txt`
|
|
177
|
+
|
|
178
|
+
Replace:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
tenacity>=8.3.0
|
|
182
|
+
fastapi-cache2>=0.2.2
|
|
183
|
+
redis>=5.0.0
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
with:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
fastapi-memory @ file:///path/to/fastapi-memory
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
(or once published: `fastapi-memory>=0.1.0`, optionally `fastapi-memory[redis]`.)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fastapi-memory
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
Caching, retry, and resilient-HTTP helpers for FastAPI services.
|
|
6
|
+
|
|
7
|
+
This package provides response caching, retry policies, a resilient async
|
|
8
|
+
HTTP client, and cached config singletons — all in one import, designed for
|
|
9
|
+
FastAPI services that talk to slower upstream APIs.
|
|
10
|
+
|
|
11
|
+
Import from this package instead of juggling multiple dependencies directly::
|
|
12
|
+
|
|
13
|
+
from fastapi_memory import (
|
|
14
|
+
FmCacheManager, FmMemoryBackend, memorize,
|
|
15
|
+
retry, stop_after_retries, exponential_backoff, retry_on_error,
|
|
16
|
+
fm_lru,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Higher-level helpers
|
|
20
|
+
---------------------
|
|
21
|
+
On top of the re-exports, fastapi-memory adds a few small conveniences:
|
|
22
|
+
|
|
23
|
+
- ``init_cache()`` -> one-line ``FmCacheManager`` setup (memory or Redis)
|
|
24
|
+
- ``clear_cache()`` -> ``await FmCacheManager.clear()``
|
|
25
|
+
- ``default_retry()`` -> the "3 attempts, exponential backoff, skip 4xx" policy
|
|
26
|
+
- ``is_retryable_httpx_error`` -> the retry predicate behind ``default_retry``
|
|
27
|
+
- ``cached_singleton`` -> ``@fm_lru(maxsize=1)`` for settings-style singletons
|
|
28
|
+
- ``FmResilientClient`` -> persistent ``httpx.AsyncClient`` + retries + JSON helpers
|
|
29
|
+
|
|
30
|
+
See the README for full usage examples and a migration guide.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from .caching import (
|
|
34
|
+
FmCacheManager,
|
|
35
|
+
FmMemoryBackend,
|
|
36
|
+
FmRedisBackend,
|
|
37
|
+
memorize,
|
|
38
|
+
clear_cache,
|
|
39
|
+
init_cache,
|
|
40
|
+
)
|
|
41
|
+
from .config import cached_singleton, fm_lru
|
|
42
|
+
from .http import FmResilientClient
|
|
43
|
+
from .resilience import (
|
|
44
|
+
default_retry,
|
|
45
|
+
is_retryable_httpx_error,
|
|
46
|
+
retry,
|
|
47
|
+
retry_on_error,
|
|
48
|
+
stop_after_retries,
|
|
49
|
+
exponential_backoff,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__version__ = "0.1.0"
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
# caching
|
|
56
|
+
"FmCacheManager",
|
|
57
|
+
"FmMemoryBackend",
|
|
58
|
+
"FmRedisBackend",
|
|
59
|
+
"memorize",
|
|
60
|
+
"init_cache",
|
|
61
|
+
"clear_cache",
|
|
62
|
+
# resilience
|
|
63
|
+
"retry",
|
|
64
|
+
"stop_after_retries",
|
|
65
|
+
"exponential_backoff",
|
|
66
|
+
"retry_on_error",
|
|
67
|
+
"default_retry",
|
|
68
|
+
"is_retryable_httpx_error",
|
|
69
|
+
# config
|
|
70
|
+
"fm_lru",
|
|
71
|
+
"cached_singleton",
|
|
72
|
+
# http
|
|
73
|
+
"FmResilientClient",
|
|
74
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thin convenience layer around fastapi-cache2.
|
|
3
|
+
|
|
4
|
+
Re-exports the pieces you already use directly (``FmCacheManager``,
|
|
5
|
+
``FmMemoryBackend``, ``memorize``) and adds two small helpers:
|
|
6
|
+
|
|
7
|
+
- :func:`init_cache` - one-line setup for an in-memory or Redis-backed cache
|
|
8
|
+
- :func:`clear_cache` - ``await FmCacheManager.clear()``
|
|
9
|
+
|
|
10
|
+
The Redis backend is optional. Install it with::
|
|
11
|
+
|
|
12
|
+
pip install "fastapi-memory[redis]"
|
|
13
|
+
|
|
14
|
+
If ``redis`` isn't installed, ``FmRedisBackend`` is simply ``None`` and
|
|
15
|
+
``init_cache(backend="redis", ...)`` raises a clear ``RuntimeError``
|
|
16
|
+
explaining how to fix it. The default ``backend="memory"`` works with no
|
|
17
|
+
extra dependencies, exactly like the original::
|
|
18
|
+
|
|
19
|
+
FmCacheManager.init(FmMemoryBackend(), prefix="app-cache")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from fastapi_cache import FastAPICache
|
|
27
|
+
from fastapi_cache.backends.inmemory import InMemoryBackend
|
|
28
|
+
from fastapi_cache.decorator import cache
|
|
29
|
+
|
|
30
|
+
# Aliases under fastapi-memory namespace
|
|
31
|
+
FmCacheManager = FastAPICache
|
|
32
|
+
FmMemoryBackend = InMemoryBackend
|
|
33
|
+
memorize = cache
|
|
34
|
+
|
|
35
|
+
try: # pragma: no cover - exercised only when the `redis` extra is installed
|
|
36
|
+
from fastapi_cache.backends.redis import RedisBackend
|
|
37
|
+
from redis.asyncio import from_url as _redis_from_url
|
|
38
|
+
|
|
39
|
+
FmRedisBackend = RedisBackend
|
|
40
|
+
_REDIS_AVAILABLE = True
|
|
41
|
+
except ImportError: # pragma: no cover - redis extra not installed
|
|
42
|
+
FmRedisBackend = None # type: ignore[assignment, misc]
|
|
43
|
+
_redis_from_url = None
|
|
44
|
+
_REDIS_AVAILABLE = False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def init_cache(
|
|
48
|
+
backend: str = "memory",
|
|
49
|
+
*,
|
|
50
|
+
prefix: str = "fastapi-cache",
|
|
51
|
+
redis_url: Optional[str] = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Initialise :class:`FmCacheManager` with either an in-memory backend
|
|
55
|
+
(default) or a Redis backend.
|
|
56
|
+
|
|
57
|
+
Call this once during application startup, typically inside a
|
|
58
|
+
``lifespan`` handler::
|
|
59
|
+
|
|
60
|
+
from contextlib import asynccontextmanager
|
|
61
|
+
from fastapi import FastAPI
|
|
62
|
+
from fastapi_memory import init_cache
|
|
63
|
+
|
|
64
|
+
@asynccontextmanager
|
|
65
|
+
async def lifespan(app: FastAPI):
|
|
66
|
+
init_cache(prefix="app-cache") # in-memory (default)
|
|
67
|
+
yield
|
|
68
|
+
|
|
69
|
+
app = FastAPI(lifespan=lifespan)
|
|
70
|
+
|
|
71
|
+
For Redis, pass ``backend="redis"`` and a connection URL::
|
|
72
|
+
|
|
73
|
+
init_cache(backend="redis", prefix="app-cache", redis_url="redis://localhost:6379")
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
backend:
|
|
78
|
+
``"memory"`` (default) or ``"redis"``.
|
|
79
|
+
prefix:
|
|
80
|
+
Cache-key prefix, passed straight through to ``FmCacheManager.init``.
|
|
81
|
+
redis_url:
|
|
82
|
+
Connection string for Redis, e.g. ``redis://localhost:6379``.
|
|
83
|
+
Required when ``backend="redis"``.
|
|
84
|
+
"""
|
|
85
|
+
if backend == "memory":
|
|
86
|
+
FmCacheManager.init(FmMemoryBackend(), prefix=prefix)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if backend == "redis":
|
|
90
|
+
if not _REDIS_AVAILABLE:
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
"Redis backend requested but the 'redis' package is not "
|
|
93
|
+
"installed. Install it with: pip install fastapi-memory[redis]"
|
|
94
|
+
)
|
|
95
|
+
if not redis_url:
|
|
96
|
+
raise ValueError("redis_url is required when backend='redis'")
|
|
97
|
+
|
|
98
|
+
client = _redis_from_url(redis_url, encoding="utf8", decode_responses=False)
|
|
99
|
+
FmCacheManager.init(FmRedisBackend(client), prefix=prefix)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
raise ValueError(f"Unknown backend {backend!r}, expected 'memory' or 'redis'")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def clear_cache() -> None:
|
|
106
|
+
"""Clear the entire cache - thin wrapper around ``await FmCacheManager.clear()``."""
|
|
107
|
+
await FmCacheManager.clear()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"FmCacheManager",
|
|
112
|
+
"FmMemoryBackend",
|
|
113
|
+
"FmRedisBackend",
|
|
114
|
+
"memorize",
|
|
115
|
+
"init_cache",
|
|
116
|
+
"clear_cache",
|
|
117
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tiny helper built on top of ``functools.lru_cache`` for the
|
|
3
|
+
"build-it-once-and-reuse-it" settings/config pattern.
|
|
4
|
+
|
|
5
|
+
Re-exports ``lru_cache`` as ``fm_lru``, plus :func:`cached_singleton`, a
|
|
6
|
+
small shorthand for the common::
|
|
7
|
+
|
|
8
|
+
@fm_lru(maxsize=1)
|
|
9
|
+
def get_settings() -> Settings:
|
|
10
|
+
return Settings()
|
|
11
|
+
|
|
12
|
+
pattern.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from typing import Callable, TypeVar
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
# Alias under fastapi-memory namespace
|
|
23
|
+
fm_lru = lru_cache
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cached_singleton(func: Callable[[], T]) -> Callable[[], T]:
|
|
27
|
+
"""
|
|
28
|
+
Shorthand for ``@fm_lru(maxsize=1)`` - turns a zero-argument factory
|
|
29
|
+
function into a cached singleton getter.
|
|
30
|
+
"""
|
|
31
|
+
return fm_lru(maxsize=1)(func)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["fm_lru", "cached_singleton"]
|