Skip to content

Commit 1491db1

Browse files
committed
Support disk/embedded/remote store via libsql
1 parent 2be6a8c commit 1491db1

File tree

6 files changed

+138
-5
lines changed

6 files changed

+138
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang
1010

1111
## 0.2
1212

13+
### 0.2.2
14+
15+
- Support `libsql` backend.
16+
1317
### 0.2.1
1418
- Fix picklecoder
1519
- Fix connection failure transparency and add logging

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
## Introduction
88

99
`fastapi-cache` is a tool to cache FastAPI endpoint and function results, with
10-
backends supporting Redis, Memcached, and Amazon DynamoDB.
10+
backends supporting Redis, Memcached, libsql and Amazon DynamoDB.
1111

1212
## Features
1313

14-
- Supports `redis`, `memcache`, `dynamodb`, and `in-memory` backends.
14+
- Supports `redis`, `memcache`, `dynamodb`, `libsql` and `in-memory` backends.
1515
- Easy integration with [FastAPI](https://fastapi.tiangolo.com/).
1616
- Support for HTTP cache headers like `ETag` and `Cache-Control`, as well as conditional `If-Match-None` requests.
1717

@@ -21,6 +21,7 @@ backends supporting Redis, Memcached, and Amazon DynamoDB.
2121
- `redis` when using `RedisBackend`.
2222
- `memcache` when using `MemcacheBackend`.
2323
- `aiobotocore` when using `DynamoBackend`.
24+
- `libsql-client` when using `libsql`
2425

2526
## Install
2627

@@ -46,6 +47,10 @@ or
4647
> pip install "fastapi-cache2[dynamodb]"
4748
```
4849

50+
```shell
51+
> pip install "fastapi-cache2[libsql]"
52+
```
53+
4954
## Usage
5055

5156
### Quick Start

fastapi_cache/backends/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,10 @@
2626
pass
2727
else:
2828
__all__ += ["redis"]
29+
30+
try:
31+
from fastapi_cache.backends import libsql
32+
except ImportError:
33+
pass
34+
else:
35+
__all__ += ["libsql"]

fastapi_cache/backends/libsql.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import time
2+
from typing import Optional, Tuple
3+
4+
import libsql_client
5+
from libsql_client import ResultSet
6+
7+
from fastapi_cache.types import Backend
8+
9+
EmptyResultSet = ResultSet(
10+
columns=(),
11+
rows=[],
12+
rows_affected=0,
13+
last_insert_rowid=0)
14+
15+
class LibsqlBackend(Backend):
16+
"""
17+
libsql backend provider
18+
19+
This backend requires a table name to be passed during initialization. The table
20+
will be created if it does not exist. If the table does exists, it will be emptied during init
21+
22+
Note that this backend does not fully support TTL. It will only delete outdated objects on get.
23+
24+
Usage:
25+
>> libsql_url = "file:local.db"
26+
>> cache = LibsqlBackend(libsql_url=libsql_url, table_name="your-cache")
27+
>> cache.create_and_flush()
28+
>> FastAPICache.init(cache)
29+
"""
30+
31+
# client: libsql_client.Client
32+
table_name: str
33+
libsql_url: str
34+
35+
def __init__(self, libsql_url: str, table_name: str):
36+
self.libsql_url = libsql_url
37+
self.table_name = table_name
38+
39+
@property
40+
def now(self) -> int:
41+
return int(time.time())
42+
43+
async def _make_request(self, request: str) -> ResultSet:
44+
# TODO: Exception handling. Return EmptyResultSet on error?
45+
async with libsql_client.create_client(self.libsql_url) as client:
46+
return await client.execute(request)
47+
48+
49+
async def create_and_flush(self) -> None:
50+
await self._make_request("CREATE TABLE IF NOT EXISTS `{}` "
51+
"(key STRING PRIMARY KEY, value BLOB, expire INTEGER);"
52+
.format(self.table_name))
53+
await self._make_request("DELETE FROM `{}`;".format(self.table_name))
54+
55+
return None
56+
57+
async def _get(self, key: str) -> Tuple[int, Optional[bytes]]:
58+
result_set = await self._make_request("SELECT * from `{}` WHERE key = \"{}\""
59+
.format(self.table_name,key))
60+
if len(result_set.rows) == 0:
61+
return (0,None)
62+
63+
value = result_set.rows[0]["value"]
64+
ttl_ts = result_set.rows[0]["expire"]
65+
66+
if not value:
67+
return (0,None)
68+
if ttl_ts < self.now:
69+
await self._make_request("DELETE FROM `{}` WHERE key = \"{}\""
70+
.format(self.table_name, key))
71+
return (0, None)
72+
73+
return(ttl_ts, value) # type: ignore[union-attr,no-any-return]
74+
75+
async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
76+
return await self._get(key)
77+
78+
async def get(self, key: str) -> Optional[bytes]:
79+
_, value = await self._get(key)
80+
return value
81+
82+
async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> None:
83+
ttl = self.now + expire if expire else 0
84+
await self._make_request("INSERT OR REPLACE INTO `{}`(\"key\", \"value\", \"expire\") "
85+
"VALUES('{}','{}',{});"
86+
.format(self.table_name, key, value.decode("utf-8"), ttl))
87+
return None
88+
89+
async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None) -> int:
90+
91+
if namespace:
92+
result_set = await self._make_request("DELETE FROM `{}` WHERE key = \"{}%\""
93+
.format(self.table_name, namespace))
94+
return result_set.rowcount # type: ignore
95+
elif key:
96+
result_set = await self._make_request("DELETE FROM `{}` WHERE key = \"{}\""
97+
.format(self.table_name, key))
98+
return result_set.rowcount # type: ignore
99+
return 0

poetry.lock

Lines changed: 18 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pendulum = "*"
2424
aiobotocore = { version = ">=1.4.1,<3.0.0", optional = true }
2525
typing-extensions = { version = ">=4.1.0" }
2626
importlib-metadata = {version = "^6.6.0", python = "<3.8"}
27+
libsql-client = { version = "^0.3.0", optional = true }
2728

2829
[tool.poetry.group.linting]
2930
optional = true
@@ -53,7 +54,8 @@ twine = { version = "^4.0.2", python = "^3.10" }
5354
redis = ["redis"]
5455
memcache = ["aiomcache"]
5556
dynamodb = ["aiobotocore"]
56-
all = ["redis", "aiomcache", "aiobotocore"]
57+
libsql = ["libsql-client"]
58+
all = ["redis", "aiomcache", "aiobotocore", "libsql-client"]
5759

5860
[tool.mypy]
5961
files = ["."]

0 commit comments

Comments
 (0)