openai-sqlite-cache 0.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 openai_sqlite_cache contributors
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,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: openai_sqlite_cache
3
+ Version: 0.0.1
4
+ Summary: An unofficial drop-in OpenAI Python SDK with a local SQLite response cache
5
+ Author: openai_sqlite_cache contributors
6
+ License-Expression: MIT
7
+ Keywords: openai,cache,sqlite,llm,api
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: openai<3,>=1.66.2
22
+ Dynamic: license-file
23
+
24
+ # openai_sqlite_cache
25
+
26
+ `openai_sqlite_cache` 是非官方的 OpenAI Python SDK 透明 SQLite 缓存层。同一个 API、同一个模型、完全相同的输入再次调用时,会直接返回本机缓存的响应,不再请求上游。
27
+
28
+ PyPI 分发名是 `openai_sqlite_cache`(规范化名称为 `openai-sqlite-cache`);为满足只替换 import 的用法,Python 导入名仍然是 `cached_openai`。
29
+
30
+ ## 安装
31
+
32
+ 在当前仓库中安装:
33
+
34
+ ```bash
35
+ pip install ./openai_sqlite_cache
36
+ ```
37
+
38
+ 发布到你有权限的 Python 包索引后,安装形式为:
39
+
40
+ ```bash
41
+ pip install openai_sqlite_cache
42
+ ```
43
+
44
+ > 注意:本项目与公共 PyPI 上的 `cached-openai` 分发项目都提供 `cached_openai` 导入包,二者不能安全地安装在同一个 Python 环境中。请只安装其中一个。
45
+
46
+ ## 用法
47
+
48
+ 现有代码只需替换 import,并保留 `openai` 这个本地别名:
49
+
50
+ ```python
51
+ # 原来:import openai
52
+ import cached_openai as openai
53
+
54
+ client = openai.OpenAI()
55
+ response = client.responses.create(
56
+ model="gpt-5",
57
+ input="Explain SQLite in one sentence.",
58
+ )
59
+ print(response.output_text)
60
+ ```
61
+
62
+ 也可以直接使用包名:
63
+
64
+ ```python
65
+ import cached_openai
66
+
67
+ client = cached_openai.OpenAI()
68
+ completion = client.chat.completions.create(
69
+ model="gpt-4.1-mini",
70
+ messages=[{"role": "user", "content": "Hello"}],
71
+ )
72
+ ```
73
+
74
+ 以下官方 SDK 用法均保持不变:
75
+
76
+ - `OpenAI`、`AsyncOpenAI`、`AzureOpenAI` 和 `AsyncAzureOpenAI`;
77
+ - 模块级调用,如 `openai.chat.completions.create(...)`;
78
+ - `with_options()`、`with_raw_response`、`with_streaming_response`;
79
+ - 自定义 `base_url` 和自定义同步/异步 `http_client`;
80
+ - 官方 SDK 的响应模型和异常类型。
81
+
82
+ 流式响应会在第一次被完整消费时写入缓存;如果流被提前关闭,则不会缓存不完整内容。Realtime/WebSocket 不经过普通 HTTP 请求,因此不会缓存。
83
+
84
+ ## 缓存规则
85
+
86
+ 缓存键使用 SHA-256 计算,包含:
87
+
88
+ - HTTP 方法和规范化后的完整 API URL;
89
+ - 请求体(JSON 会按键排序,multipart 会忽略随机 boundary 并对文件内容取 hash);
90
+ - 模型名;
91
+ - organization、project、beta 等会影响语义的请求头;
92
+ - API 凭据的不可逆作用域 hash,防止不同 API key 之间共享响应。
93
+
94
+ 请求原文和 API key 都不会写入数据库。数据库只保存 hash、成功响应体、必要响应头和命中统计。默认永久保留,只缓存 2xx 响应。
95
+
96
+ 为避免有副作用的操作被错误去重,目前只缓存这些推理/生成端点:Responses、Chat Completions、Completions、Embeddings、Moderations、Images、Audio 和 Videos。Files、Uploads、Batches、Fine-tuning、Vector Stores 等管理类 API 会原样请求上游。
97
+
98
+ ## 配置
99
+
100
+ 默认数据库位置:
101
+
102
+ - macOS:`~/Library/Caches/openai-sqlite-cache/cache.sqlite3`
103
+ - Linux:`$XDG_CACHE_HOME/openai-sqlite-cache/cache.sqlite3`,未设置时使用 `~/.cache/...`
104
+ - Windows:`%LOCALAPPDATA%/openai-sqlite-cache/cache.sqlite3`
105
+
106
+ 环境变量:
107
+
108
+ ```bash
109
+ export CACHED_OPENAI_CACHE_PATH=/path/to/cache.sqlite3
110
+ export CACHED_OPENAI_TTL_SECONDS=86400
111
+ export CACHED_OPENAI_DISABLE=0
112
+ ```
113
+
114
+ 也可以在创建客户端前配置:
115
+
116
+ ```python
117
+ import cached_openai as openai
118
+
119
+ openai.configure_cache(
120
+ path="./.cache/openai.sqlite3",
121
+ ttl_seconds=24 * 60 * 60,
122
+ )
123
+ client = openai.OpenAI()
124
+ ```
125
+
126
+ 单个客户端可以覆盖全局配置:
127
+
128
+ ```python
129
+ client = openai.OpenAI(
130
+ cache_path="./project-cache.sqlite3",
131
+ cache_ttl=3600,
132
+ cache_enabled=True,
133
+ )
134
+ ```
135
+
136
+ 维护接口:
137
+
138
+ ```python
139
+ print(openai.cache_info())
140
+ removed = openai.clear_cache()
141
+ ```
142
+
143
+ 缓存文件可能包含模型响应中的敏感内容,应像其他本地应用数据一样保护。包会尽量把目录和数据库权限分别设为 `0700` 和 `0600`。
144
+
145
+ ## 开发与测试
146
+
147
+ ```bash
148
+ cd openai_sqlite_cache
149
+ python -m unittest discover -s tests -v
150
+ python -m pip wheel --no-deps . -w dist
151
+ ```
@@ -0,0 +1,128 @@
1
+ # openai_sqlite_cache
2
+
3
+ `openai_sqlite_cache` 是非官方的 OpenAI Python SDK 透明 SQLite 缓存层。同一个 API、同一个模型、完全相同的输入再次调用时,会直接返回本机缓存的响应,不再请求上游。
4
+
5
+ PyPI 分发名是 `openai_sqlite_cache`(规范化名称为 `openai-sqlite-cache`);为满足只替换 import 的用法,Python 导入名仍然是 `cached_openai`。
6
+
7
+ ## 安装
8
+
9
+ 在当前仓库中安装:
10
+
11
+ ```bash
12
+ pip install ./openai_sqlite_cache
13
+ ```
14
+
15
+ 发布到你有权限的 Python 包索引后,安装形式为:
16
+
17
+ ```bash
18
+ pip install openai_sqlite_cache
19
+ ```
20
+
21
+ > 注意:本项目与公共 PyPI 上的 `cached-openai` 分发项目都提供 `cached_openai` 导入包,二者不能安全地安装在同一个 Python 环境中。请只安装其中一个。
22
+
23
+ ## 用法
24
+
25
+ 现有代码只需替换 import,并保留 `openai` 这个本地别名:
26
+
27
+ ```python
28
+ # 原来:import openai
29
+ import cached_openai as openai
30
+
31
+ client = openai.OpenAI()
32
+ response = client.responses.create(
33
+ model="gpt-5",
34
+ input="Explain SQLite in one sentence.",
35
+ )
36
+ print(response.output_text)
37
+ ```
38
+
39
+ 也可以直接使用包名:
40
+
41
+ ```python
42
+ import cached_openai
43
+
44
+ client = cached_openai.OpenAI()
45
+ completion = client.chat.completions.create(
46
+ model="gpt-4.1-mini",
47
+ messages=[{"role": "user", "content": "Hello"}],
48
+ )
49
+ ```
50
+
51
+ 以下官方 SDK 用法均保持不变:
52
+
53
+ - `OpenAI`、`AsyncOpenAI`、`AzureOpenAI` 和 `AsyncAzureOpenAI`;
54
+ - 模块级调用,如 `openai.chat.completions.create(...)`;
55
+ - `with_options()`、`with_raw_response`、`with_streaming_response`;
56
+ - 自定义 `base_url` 和自定义同步/异步 `http_client`;
57
+ - 官方 SDK 的响应模型和异常类型。
58
+
59
+ 流式响应会在第一次被完整消费时写入缓存;如果流被提前关闭,则不会缓存不完整内容。Realtime/WebSocket 不经过普通 HTTP 请求,因此不会缓存。
60
+
61
+ ## 缓存规则
62
+
63
+ 缓存键使用 SHA-256 计算,包含:
64
+
65
+ - HTTP 方法和规范化后的完整 API URL;
66
+ - 请求体(JSON 会按键排序,multipart 会忽略随机 boundary 并对文件内容取 hash);
67
+ - 模型名;
68
+ - organization、project、beta 等会影响语义的请求头;
69
+ - API 凭据的不可逆作用域 hash,防止不同 API key 之间共享响应。
70
+
71
+ 请求原文和 API key 都不会写入数据库。数据库只保存 hash、成功响应体、必要响应头和命中统计。默认永久保留,只缓存 2xx 响应。
72
+
73
+ 为避免有副作用的操作被错误去重,目前只缓存这些推理/生成端点:Responses、Chat Completions、Completions、Embeddings、Moderations、Images、Audio 和 Videos。Files、Uploads、Batches、Fine-tuning、Vector Stores 等管理类 API 会原样请求上游。
74
+
75
+ ## 配置
76
+
77
+ 默认数据库位置:
78
+
79
+ - macOS:`~/Library/Caches/openai-sqlite-cache/cache.sqlite3`
80
+ - Linux:`$XDG_CACHE_HOME/openai-sqlite-cache/cache.sqlite3`,未设置时使用 `~/.cache/...`
81
+ - Windows:`%LOCALAPPDATA%/openai-sqlite-cache/cache.sqlite3`
82
+
83
+ 环境变量:
84
+
85
+ ```bash
86
+ export CACHED_OPENAI_CACHE_PATH=/path/to/cache.sqlite3
87
+ export CACHED_OPENAI_TTL_SECONDS=86400
88
+ export CACHED_OPENAI_DISABLE=0
89
+ ```
90
+
91
+ 也可以在创建客户端前配置:
92
+
93
+ ```python
94
+ import cached_openai as openai
95
+
96
+ openai.configure_cache(
97
+ path="./.cache/openai.sqlite3",
98
+ ttl_seconds=24 * 60 * 60,
99
+ )
100
+ client = openai.OpenAI()
101
+ ```
102
+
103
+ 单个客户端可以覆盖全局配置:
104
+
105
+ ```python
106
+ client = openai.OpenAI(
107
+ cache_path="./project-cache.sqlite3",
108
+ cache_ttl=3600,
109
+ cache_enabled=True,
110
+ )
111
+ ```
112
+
113
+ 维护接口:
114
+
115
+ ```python
116
+ print(openai.cache_info())
117
+ removed = openai.clear_cache()
118
+ ```
119
+
120
+ 缓存文件可能包含模型响应中的敏感内容,应像其他本地应用数据一样保护。包会尽量把目录和数据库权限分别设为 `0700` 和 `0600`。
121
+
122
+ ## 开发与测试
123
+
124
+ ```bash
125
+ cd openai_sqlite_cache
126
+ python -m unittest discover -s tests -v
127
+ python -m pip wheel --no-deps . -w dist
128
+ ```
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "openai_sqlite_cache"
7
+ version = "0.0.1"
8
+ description = "An unofficial drop-in OpenAI Python SDK with a local SQLite response cache"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "openai_sqlite_cache contributors" },
15
+ ]
16
+ keywords = ["openai", "cache", "sqlite", "llm", "api"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ ]
29
+ dependencies = [
30
+ "openai>=1.66.2,<3",
31
+ ]
32
+
33
+ [tool.setuptools]
34
+ package-dir = { "" = "src" }
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.setuptools.package-data]
40
+ cached_openai = ["py.typed"]
41
+
42
+ [tool.ruff]
43
+ target-version = "py39"
44
+ line-length = 88
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E", "F", "I"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,175 @@
1
+ """Drop-in OpenAI Python SDK with a persistent local SQLite cache.
2
+
3
+ Use ``import cached_openai as openai`` and keep the rest of an existing OpenAI
4
+ SDK integration unchanged.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ import types as _stdlib_types
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+
14
+ import openai as _openai
15
+ from openai import * # noqa: F403
16
+
17
+ from . import _client as _cached_client
18
+ from ._cache import SQLiteCache
19
+ from ._http import CachingSyncClient, wrap_sync_client
20
+ from ._module_alias import install_module_alias
21
+ from ._settings import _UNSET, configure_settings, get_settings
22
+ from ._version import PACKAGE_VERSION
23
+
24
+ OpenAI = _cached_client.OpenAI
25
+ AsyncOpenAI = _cached_client.AsyncOpenAI
26
+ Client = _cached_client.Client
27
+ AsyncClient = _cached_client.AsyncClient
28
+ AzureOpenAI = _cached_client.AzureOpenAI
29
+ AsyncAzureOpenAI = _cached_client.AsyncAzureOpenAI
30
+ CachedOpenAI = _cached_client.CachedOpenAI
31
+ AsyncCachedOpenAI = _cached_client.AsyncCachedOpenAI
32
+
33
+ __version__ = PACKAGE_VERSION
34
+ openai_version = _openai.__version__
35
+
36
+ _MODULE_CONFIG_NAMES = {
37
+ "api_key",
38
+ "organization",
39
+ "project",
40
+ "webhook_secret",
41
+ "base_url",
42
+ "timeout",
43
+ "max_retries",
44
+ "default_headers",
45
+ "default_query",
46
+ "http_client",
47
+ "api_type",
48
+ "api_version",
49
+ "azure_endpoint",
50
+ "azure_ad_token",
51
+ "azure_ad_token_provider",
52
+ }
53
+
54
+ _MODULE_RESOURCE_NAMES = {
55
+ "audio",
56
+ "batches",
57
+ "beta",
58
+ "chat",
59
+ "completions",
60
+ "containers",
61
+ "conversations",
62
+ "embeddings",
63
+ "evals",
64
+ "files",
65
+ "fine_tuning",
66
+ "images",
67
+ "models",
68
+ "moderations",
69
+ "realtime",
70
+ "responses",
71
+ "skills",
72
+ "uploads",
73
+ "vector_stores",
74
+ "videos",
75
+ "webhooks",
76
+ }
77
+
78
+
79
+ def _reset_upstream_module_client() -> None:
80
+ reset = getattr(_openai, "_reset_client", None)
81
+ if reset is not None:
82
+ reset()
83
+
84
+
85
+ def _prepare_module_client() -> None:
86
+ settings = get_settings()
87
+ if not settings.enabled:
88
+ return
89
+ current = getattr(_openai, "http_client", None)
90
+ if current is None:
91
+ current = _openai.DefaultHttpxClient()
92
+ wrapped = wrap_sync_client(current, settings)
93
+ if wrapped is not getattr(_openai, "http_client", None):
94
+ _openai.http_client = wrapped
95
+ _reset_upstream_module_client()
96
+
97
+
98
+ def configure_cache(
99
+ *,
100
+ path: Any = _UNSET,
101
+ ttl_seconds: Any = _UNSET,
102
+ enabled: Any = _UNSET,
103
+ ) -> dict:
104
+ """Configure defaults used by new clients and the module-level client."""
105
+
106
+ settings = configure_settings(path=path, ttl_seconds=ttl_seconds, enabled=enabled)
107
+ current = getattr(_openai, "http_client", None)
108
+ if isinstance(current, CachingSyncClient):
109
+ _openai.http_client = current._cached_openai_inner
110
+ _reset_upstream_module_client()
111
+ return {
112
+ "path": str(settings.path.expanduser().resolve()),
113
+ "ttl_seconds": settings.ttl_seconds,
114
+ "enabled": settings.enabled,
115
+ }
116
+
117
+
118
+ def clear_cache(path: Optional[Any] = None) -> int:
119
+ """Delete all cached response rows and return the number removed."""
120
+
121
+ cache_path = get_settings().path if path is None else Path(path).expanduser()
122
+ return SQLiteCache(cache_path).clear()
123
+
124
+
125
+ def cache_info(path: Optional[Any] = None) -> dict:
126
+ """Return cache path, row count, stored bytes, and cumulative hit count."""
127
+
128
+ cache_path = get_settings().path if path is None else Path(path).expanduser()
129
+ return SQLiteCache(cache_path).stats()
130
+
131
+
132
+ def __getattr__(name: str) -> Any:
133
+ if name in _MODULE_RESOURCE_NAMES:
134
+ _prepare_module_client()
135
+ return getattr(_openai, name)
136
+
137
+
138
+ def __dir__() -> list:
139
+ return sorted(set(globals()) | set(dir(_openai)))
140
+
141
+
142
+ class _CachedOpenAIModule(_stdlib_types.ModuleType):
143
+ def __setattr__(self, name: str, value: Any) -> None:
144
+ if name in _MODULE_CONFIG_NAMES:
145
+ if name == "http_client" and isinstance(value, CachingSyncClient):
146
+ value = value._cached_openai_inner
147
+ setattr(_openai, name, value)
148
+ _reset_upstream_module_client()
149
+ return
150
+ super().__setattr__(name, value)
151
+
152
+
153
+ sys.modules[__name__].__class__ = _CachedOpenAIModule
154
+
155
+ # Preserve class identity for imports such as
156
+ # ``from cached_openai.types.chat import ChatCompletion``.
157
+ types = install_module_alias(__name__ + ".types", "openai.types")
158
+
159
+ __all__ = sorted(
160
+ set(getattr(_openai, "__all__", []))
161
+ | {
162
+ "AsyncAzureOpenAI",
163
+ "AsyncCachedOpenAI",
164
+ "AsyncClient",
165
+ "AsyncOpenAI",
166
+ "AzureOpenAI",
167
+ "CachedOpenAI",
168
+ "Client",
169
+ "OpenAI",
170
+ "cache_info",
171
+ "clear_cache",
172
+ "configure_cache",
173
+ "openai_version",
174
+ }
175
+ )
@@ -0,0 +1,145 @@
1
+ """A small, process-safe SQLite store for HTTP response payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import time
9
+ from contextlib import closing
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from threading import RLock
13
+ from typing import List, Optional, Sequence, Tuple
14
+
15
+ HeaderList = List[Tuple[str, str]]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class CachedResponse:
20
+ status_code: int
21
+ headers: HeaderList
22
+ body: bytes
23
+
24
+
25
+ class SQLiteCache:
26
+ """Persistent cache whose rows contain responses, never request bodies."""
27
+
28
+ def __init__(self, path: Path):
29
+ self.path = path.expanduser().resolve()
30
+ self._lock = RLock()
31
+ self._initialize()
32
+
33
+ def _connect(self) -> sqlite3.Connection:
34
+ connection = sqlite3.connect(str(self.path), timeout=10.0)
35
+ connection.execute("PRAGMA busy_timeout = 10000")
36
+ return connection
37
+
38
+ def _initialize(self) -> None:
39
+ self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
40
+ with self._lock, closing(self._connect()) as connection, connection:
41
+ connection.execute("PRAGMA journal_mode = WAL")
42
+ connection.execute(
43
+ """
44
+ CREATE TABLE IF NOT EXISTS responses (
45
+ cache_key TEXT PRIMARY KEY,
46
+ status_code INTEGER NOT NULL,
47
+ headers_json TEXT NOT NULL,
48
+ body BLOB NOT NULL,
49
+ created_at REAL NOT NULL,
50
+ last_accessed_at REAL NOT NULL,
51
+ hit_count INTEGER NOT NULL DEFAULT 0
52
+ )
53
+ """
54
+ )
55
+ try:
56
+ os.chmod(self.path, 0o600)
57
+ except OSError:
58
+ pass
59
+
60
+ def get(
61
+ self, cache_key: str, ttl_seconds: Optional[float]
62
+ ) -> Optional[CachedResponse]:
63
+ now = time.time()
64
+ with self._lock, closing(self._connect()) as connection, connection:
65
+ row = connection.execute(
66
+ """
67
+ SELECT status_code, headers_json, body, created_at
68
+ FROM responses
69
+ WHERE cache_key = ?
70
+ """,
71
+ (cache_key,),
72
+ ).fetchone()
73
+ if row is None:
74
+ return None
75
+ if ttl_seconds is not None and now - float(row[3]) >= ttl_seconds:
76
+ connection.execute(
77
+ "DELETE FROM responses WHERE cache_key = ?", (cache_key,)
78
+ )
79
+ return None
80
+ connection.execute(
81
+ """
82
+ UPDATE responses
83
+ SET last_accessed_at = ?, hit_count = hit_count + 1
84
+ WHERE cache_key = ?
85
+ """,
86
+ (now, cache_key),
87
+ )
88
+ raw_headers = json.loads(str(row[1]))
89
+ headers = [(str(name), str(value)) for name, value in raw_headers]
90
+ return CachedResponse(
91
+ status_code=int(row[0]), headers=headers, body=bytes(row[2])
92
+ )
93
+
94
+ def put(
95
+ self,
96
+ cache_key: str,
97
+ status_code: int,
98
+ headers: Sequence[Tuple[str, str]],
99
+ body: bytes,
100
+ ) -> None:
101
+ now = time.time()
102
+ headers_json = json.dumps(
103
+ list(headers), ensure_ascii=False, separators=(",", ":")
104
+ )
105
+ with self._lock, closing(self._connect()) as connection, connection:
106
+ connection.execute(
107
+ """
108
+ INSERT INTO responses (
109
+ cache_key, status_code, headers_json, body,
110
+ created_at, last_accessed_at, hit_count
111
+ ) VALUES (?, ?, ?, ?, ?, ?, 0)
112
+ ON CONFLICT(cache_key) DO UPDATE SET
113
+ status_code = excluded.status_code,
114
+ headers_json = excluded.headers_json,
115
+ body = excluded.body,
116
+ created_at = excluded.created_at,
117
+ last_accessed_at = excluded.last_accessed_at,
118
+ hit_count = 0
119
+ """,
120
+ (cache_key, status_code, headers_json, sqlite3.Binary(body), now, now),
121
+ )
122
+
123
+ def clear(self) -> int:
124
+ with self._lock, closing(self._connect()) as connection, connection:
125
+ count = int(
126
+ connection.execute("SELECT COUNT(*) FROM responses").fetchone()[0]
127
+ )
128
+ connection.execute("DELETE FROM responses")
129
+ return count
130
+
131
+ def stats(self) -> dict:
132
+ with self._lock, closing(self._connect()) as connection, connection:
133
+ row = connection.execute(
134
+ """
135
+ SELECT COUNT(*), COALESCE(SUM(LENGTH(body)), 0),
136
+ COALESCE(SUM(hit_count), 0)
137
+ FROM responses
138
+ """
139
+ ).fetchone()
140
+ return {
141
+ "path": str(self.path),
142
+ "entries": int(row[0]),
143
+ "response_bytes": int(row[1]),
144
+ "hits": int(row[2]),
145
+ }