fastapi-memory 0.1.0__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.
@@ -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"]
fastapi_memory/http.py ADDED
@@ -0,0 +1,170 @@
1
+ """
2
+ A small resilient async HTTP client: a persistent ``httpx.AsyncClient`` with
3
+ connection-pooling defaults plus automatic retries via
4
+ :func:`fastapi_memory.resilience.default_retry`.
5
+
6
+ This mirrors the ``_upstream_get_raw`` / ``_upstream_get`` pattern: a single
7
+ shared client, retried with exponential backoff on network errors and 5xx
8
+ responses, with a thin layer that turns final failures into FastAPI
9
+ ``HTTPException``s.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Optional
15
+
16
+ import httpx
17
+ from fastapi import HTTPException
18
+
19
+ from .resilience import default_retry
20
+
21
+
22
+ class FmResilientClient:
23
+ """
24
+ A persistent, retrying async HTTP client for talking to an upstream API.
25
+
26
+ Example
27
+ -------
28
+ from contextlib import asynccontextmanager
29
+ from fastapi import FastAPI
30
+ from fastapi_memory import FmResilientClient
31
+
32
+ upstream = FmResilientClient(
33
+ base_url="http://api.example.com:8080",
34
+ timeout=30.0,
35
+ )
36
+
37
+ @asynccontextmanager
38
+ async def lifespan(app: FastAPI):
39
+ await upstream.start()
40
+ yield
41
+ await upstream.aclose()
42
+
43
+ app = FastAPI(lifespan=lifespan)
44
+
45
+ @app.get("/api/data")
46
+ async def get_data():
47
+ return await upstream.get_json("data")
48
+
49
+ Parameters
50
+ ----------
51
+ base_url:
52
+ Prepended to every ``path`` passed to :meth:`get_raw` / :meth:`get_json`.
53
+ Leave empty to pass full URLs directly.
54
+ timeout:
55
+ Per-request timeout in seconds, forwarded to ``httpx.AsyncClient``.
56
+ verify:
57
+ TLS verification, forwarded to ``httpx.AsyncClient``. Set to
58
+ ``False`` for self-signed/internal endpoints (matches
59
+ ``httpx.AsyncClient(verify=False)`` in the original code).
60
+ max_connections / max_keepalive_connections:
61
+ Forwarded to ``httpx.Limits``.
62
+ retry_attempts / retry_wait_min / retry_wait_max:
63
+ Forwarded to :func:`default_retry` for every request made through
64
+ this client.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ base_url: str = "",
70
+ *,
71
+ timeout: float = 30.0,
72
+ verify: bool = True,
73
+ max_connections: int = 20,
74
+ max_keepalive_connections: int = 10,
75
+ retry_attempts: int = 3,
76
+ retry_wait_min: float = 2,
77
+ retry_wait_max: float = 10,
78
+ ) -> None:
79
+ self.base_url = base_url.rstrip("/")
80
+ self._timeout = timeout
81
+ self._verify = verify
82
+ self._limits = httpx.Limits(
83
+ max_connections=max_connections,
84
+ max_keepalive_connections=max_keepalive_connections,
85
+ )
86
+ self._retry_attempts = retry_attempts
87
+ self._retry_wait_min = retry_wait_min
88
+ self._retry_wait_max = retry_wait_max
89
+ self._client: Optional[httpx.AsyncClient] = None
90
+
91
+ async def start(self) -> None:
92
+ """Create the underlying ``httpx.AsyncClient``. Call once on startup."""
93
+ self._client = httpx.AsyncClient(
94
+ timeout=self._timeout,
95
+ verify=self._verify,
96
+ limits=self._limits,
97
+ )
98
+
99
+ async def aclose(self) -> None:
100
+ """Close the underlying client. Call once on shutdown."""
101
+ if self._client is not None:
102
+ await self._client.aclose()
103
+ self._client = None
104
+
105
+ async def __aenter__(self) -> "FmResilientClient":
106
+ await self.start()
107
+ return self
108
+
109
+ async def __aexit__(self, *exc_info: object) -> None:
110
+ await self.aclose()
111
+
112
+ @property
113
+ def client(self) -> httpx.AsyncClient:
114
+ """The underlying ``httpx.AsyncClient``. Raises if not started yet."""
115
+ if self._client is None:
116
+ raise RuntimeError(
117
+ "FmResilientClient is not started - call `await client.start()` "
118
+ "during application startup (e.g. in your lifespan handler), "
119
+ "or use it as `async with FmResilientClient(...) as client:`."
120
+ )
121
+ return self._client
122
+
123
+ def _url(self, path: str) -> str:
124
+ if self.base_url and not path.startswith(("http://", "https://")):
125
+ return f"{self.base_url}/{path.lstrip('/')}"
126
+ return path
127
+
128
+ async def get_raw(self, path: str, params: Optional[dict] = None) -> Any:
129
+ """
130
+ ``GET path`` with retries (see :func:`default_retry`).
131
+
132
+ Returns the parsed JSON body if possible, otherwise the raw response
133
+ text. Raises the underlying ``httpx`` exception on final failure -
134
+ use :meth:`get_json` if you'd rather get a FastAPI ``HTTPException``.
135
+ """
136
+
137
+ @default_retry(
138
+ attempts=self._retry_attempts,
139
+ wait_min=self._retry_wait_min,
140
+ wait_max=self._retry_wait_max,
141
+ )
142
+ async def _do_request() -> Any:
143
+ resp = await self.client.get(self._url(path), params=params)
144
+ resp.raise_for_status()
145
+ try:
146
+ return resp.json()
147
+ except ValueError:
148
+ return resp.text or ""
149
+
150
+ return await _do_request()
151
+
152
+ async def get_json(self, path: str, params: Optional[dict] = None) -> Any:
153
+ """
154
+ Like :meth:`get_raw`, but converts ``httpx`` errors (after retries
155
+ are exhausted) into FastAPI ``HTTPException``s:
156
+
157
+ - ``httpx.HTTPStatusError`` -> ``HTTPException(status_code=<upstream status>)``
158
+ - ``httpx.RequestError`` -> ``HTTPException(status_code=502)``
159
+ """
160
+ try:
161
+ return await self.get_raw(path, params)
162
+ except httpx.HTTPStatusError as exc:
163
+ raise HTTPException(
164
+ status_code=exc.response.status_code, detail="Upstream request failed"
165
+ ) from exc
166
+ except httpx.RequestError as exc:
167
+ raise HTTPException(status_code=502, detail="Upstream request error") from exc
168
+
169
+
170
+ __all__ = ["FmResilientClient"]
File without changes
@@ -0,0 +1,116 @@
1
+ """
2
+ Retry helpers built on top of tenacity.
3
+
4
+ Re-exports the building blocks you already use directly (``retry``,
5
+ ``stop_after_retries``, ``exponential_backoff``, ``retry_on_error``) and
6
+ adds two small helpers for retry policies on flaky upstream services:
7
+
8
+ - :func:`is_retryable_httpx_error` - retry network errors and 5xx responses,
9
+ but never 4xx client errors.
10
+ - :func:`default_retry` - a ready-made decorator factory for "retry up to 3
11
+ times with exponential backoff (2s..10s), reraising on final failure".
12
+
13
+ These two together are exactly equivalent to::
14
+
15
+ def _should_retry(exc):
16
+ if isinstance(exc, httpx.RequestError):
17
+ return True
18
+ if isinstance(exc, httpx.HTTPStatusError):
19
+ return exc.response.status_code >= 500
20
+ return False
21
+
22
+ @retry(
23
+ stop=stop_after_retries(3),
24
+ wait=exponential_backoff(multiplier=1, min=2, max=10),
25
+ retry=retry_on_error(_should_retry),
26
+ reraise=True,
27
+ )
28
+ async def call_upstream(...):
29
+ ...
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import Callable, Optional
35
+
36
+ import httpx
37
+ from tenacity import (
38
+ retry,
39
+ retry_if_exception,
40
+ stop_after_attempt,
41
+ wait_exponential,
42
+ )
43
+
44
+ # Aliases under fastapi-memory namespace
45
+ stop_after_retries = stop_after_attempt
46
+ exponential_backoff = wait_exponential
47
+ retry_on_error = retry_if_exception
48
+
49
+ RetryPredicate = Callable[[BaseException], bool]
50
+
51
+
52
+ def is_retryable_httpx_error(exc: BaseException) -> bool:
53
+ """
54
+ Default retry policy for httpx calls:
55
+
56
+ - ``httpx.RequestError`` (timeouts, connection errors, DNS failures, ...) -> retry
57
+ - ``httpx.HTTPStatusError`` with a 5xx response -> retry
58
+ - ``httpx.HTTPStatusError`` with a 4xx response -> do NOT retry
59
+ - anything else -> do NOT retry
60
+ """
61
+ if isinstance(exc, httpx.RequestError):
62
+ return True
63
+ if isinstance(exc, httpx.HTTPStatusError):
64
+ return exc.response.status_code >= 500
65
+ return False
66
+
67
+
68
+ def default_retry(
69
+ attempts: int = 3,
70
+ *,
71
+ wait_min: float = 2,
72
+ wait_max: float = 10,
73
+ multiplier: float = 1,
74
+ retry_on: Optional[RetryPredicate] = None,
75
+ reraise: bool = True,
76
+ ):
77
+ """
78
+ A pre-configured :func:`tenacity.retry` decorator factory.
79
+
80
+ Calling ``default_retry()`` with no arguments is equivalent to::
81
+
82
+ @retry(
83
+ stop=stop_after_retries(3),
84
+ wait=exponential_backoff(multiplier=1, min=2, max=10),
85
+ retry=retry_on_error(is_retryable_httpx_error),
86
+ reraise=True,
87
+ )
88
+
89
+ Example
90
+ -------
91
+ @default_retry()
92
+ async def call_upstream():
93
+ resp = await client.get(url)
94
+ resp.raise_for_status()
95
+ return resp.json()
96
+
97
+ Override any piece as needed, e.g. ``default_retry(attempts=5)`` or
98
+ ``default_retry(retry_on=my_predicate)``.
99
+ """
100
+ condition = retry_on or is_retryable_httpx_error
101
+ return retry(
102
+ stop=stop_after_retries(attempts),
103
+ wait=exponential_backoff(multiplier=multiplier, min=wait_min, max=wait_max),
104
+ retry=retry_on_error(condition),
105
+ reraise=reraise,
106
+ )
107
+
108
+
109
+ __all__ = [
110
+ "retry",
111
+ "stop_after_retries",
112
+ "exponential_backoff",
113
+ "retry_on_error",
114
+ "default_retry",
115
+ "is_retryable_httpx_error",
116
+ ]
@@ -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,11 @@
1
+ fastapi_memory/__init__.py,sha256=IfdgrQx-vtTVeaBe5cmfwdY0Sk-QdL4x3lNILwfznDU,2047
2
+ fastapi_memory/caching.py,sha256=Jpk6edAsJmdlu-8d_x9wvw_1EfsuxDwYr5BJ2n2DUWw,3673
3
+ fastapi_memory/config.py,sha256=GhAGG3aTw_-Av0py7x_1DZkmRP6FwiPTGCH1sgpcmkI,785
4
+ fastapi_memory/http.py,sha256=05WQ2w2CV6YX7rf8mwc4OxxXf92cFfNOdPhFyCRPqtI,5846
5
+ fastapi_memory/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ fastapi_memory/resilience.py,sha256=pqG9TzCebTs90gAy-GBE1EDtA-4Ft56Qn5kqQ2wQ8PI,3393
7
+ fastapi_memory-0.1.0.dist-info/licenses/LICENSE,sha256=v2spsd7N1pKFFh2G8wGP_45iwe5S0DYiJzG4im8Rupc,1066
8
+ fastapi_memory-0.1.0.dist-info/METADATA,sha256=M7jeNnKPKepT_BUgL3TtMlHDEbUEQk5_DfB509-zSEQ,7126
9
+ fastapi_memory-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ fastapi_memory-0.1.0.dist-info/top_level.txt,sha256=SlnFpu4h_DxWXWff3_wTcdv64tx5xEKSd_7j7wN2DgA,15
11
+ fastapi_memory-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ fastapi_memory