hishel 0.1.5__py3-none-any.whl → 1.0.0.dev0__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.
- hishel/__init__.py +55 -53
- hishel/{beta/_async_cache.py → _async_cache.py} +3 -3
- hishel/{beta → _core}/__init__.py +6 -6
- hishel/{beta/_core → _core}/_async/_storages/_sqlite.py +3 -3
- hishel/{beta/_core → _core}/_base/_storages/_base.py +1 -1
- hishel/{beta/_core → _core}/_base/_storages/_packing.py +5 -5
- hishel/{beta/_core → _core}/_spec.py +89 -2
- hishel/{beta/_core → _core}/_sync/_storages/_sqlite.py +3 -3
- hishel/{beta/_core → _core}/models.py +1 -1
- hishel/{beta/_sync_cache.py → _sync_cache.py} +3 -3
- hishel/{beta/httpx.py → httpx.py} +6 -6
- hishel/{beta/requests.py → requests.py} +5 -5
- hishel-1.0.0.dev0.dist-info/METADATA +321 -0
- hishel-1.0.0.dev0.dist-info/RECORD +19 -0
- hishel/_async/__init__.py +0 -5
- hishel/_async/_client.py +0 -30
- hishel/_async/_mock.py +0 -43
- hishel/_async/_pool.py +0 -201
- hishel/_async/_storages.py +0 -768
- hishel/_async/_transports.py +0 -282
- hishel/_controller.py +0 -581
- hishel/_exceptions.py +0 -10
- hishel/_files.py +0 -54
- hishel/_headers.py +0 -215
- hishel/_lfu_cache.py +0 -71
- hishel/_lmdb_types_.pyi +0 -53
- hishel/_s3.py +0 -122
- hishel/_serializers.py +0 -329
- hishel/_sync/__init__.py +0 -5
- hishel/_sync/_client.py +0 -30
- hishel/_sync/_mock.py +0 -43
- hishel/_sync/_pool.py +0 -201
- hishel/_sync/_storages.py +0 -768
- hishel/_sync/_transports.py +0 -282
- hishel/_synchronization.py +0 -37
- hishel/beta/_core/__init__.py +0 -0
- hishel-0.1.5.dist-info/METADATA +0 -258
- hishel-0.1.5.dist-info/RECORD +0 -41
- /hishel/{beta/_core → _core}/_headers.py +0 -0
- {hishel-0.1.5.dist-info → hishel-1.0.0.dev0.dist-info}/WHEEL +0 -0
- {hishel-0.1.5.dist-info → hishel-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hishel
|
|
3
|
+
Version: 1.0.0.dev0
|
|
4
|
+
Summary: Elegant HTTP Caching for Python
|
|
5
|
+
Project-URL: Homepage, https://hishel.com
|
|
6
|
+
Project-URL: Source, https://github.com/karpetrosyan/hishel
|
|
7
|
+
Author-email: Kar Petrosyan <kar.petrosyanpy@gmail.com>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Framework :: Trio
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: anyio>=4.9.0
|
|
28
|
+
Requires-Dist: anysqlite>=0.0.5
|
|
29
|
+
Requires-Dist: httpx>=0.28.0
|
|
30
|
+
Requires-Dist: msgpack>=1.1.2
|
|
31
|
+
Requires-Dist: typing-extensions>=4.14.1
|
|
32
|
+
Provides-Extra: httpx
|
|
33
|
+
Requires-Dist: httpx>=0.28.1; extra == 'httpx'
|
|
34
|
+
Provides-Extra: requests
|
|
35
|
+
Requires-Dist: requests>=2.32.5; extra == 'requests'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_yellow.png#gh-dark-mode-only">
|
|
40
|
+
<img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_black.png#gh-light-mode-only">
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<h1 align="center">Hishel</h1>
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<strong>Elegant HTTP Caching for Python</strong>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<a href="https://pypi.org/project/hishel">
|
|
51
|
+
<img src="https://img.shields.io/pypi/v/hishel.svg" alt="PyPI version">
|
|
52
|
+
</a>
|
|
53
|
+
<a href="https://pypi.org/project/hishel">
|
|
54
|
+
<img src="https://img.shields.io/pypi/pyversions/hishel.svg" alt="Python versions">
|
|
55
|
+
</a>
|
|
56
|
+
<a href="https://github.com/karpetrosyan/hishel/blob/master/LICENSE">
|
|
57
|
+
<img src="https://img.shields.io/pypi/l/hishel" alt="License">
|
|
58
|
+
</a>
|
|
59
|
+
<a href="https://coveralls.io/github/karpetrosyan/hishel">
|
|
60
|
+
<img src="https://img.shields.io/coverallsCoverage/github/karpetrosyan/hishel" alt="Coverage">
|
|
61
|
+
</a>
|
|
62
|
+
<a href="https://static.pepy.tech/badge/hishel/month">
|
|
63
|
+
<img src="https://static.pepy.tech/badge/hishel/month" alt="Downloads">
|
|
64
|
+
</a>
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
**Hishel** (հիշել, *to remember* in Armenian) is a modern HTTP caching library for Python that implements [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) specifications. It provides seamless caching integration for popular HTTP clients with minimal code changes.
|
|
70
|
+
|
|
71
|
+
## ✨ Features
|
|
72
|
+
|
|
73
|
+
- 🎯 **RFC 9111 Compliant** - Fully compliant with the latest HTTP caching specification
|
|
74
|
+
- 🔌 **Easy Integration** - Drop-in support for HTTPX and Requests
|
|
75
|
+
- 💾 **Flexible Storage** - SQLite backend with more coming soon
|
|
76
|
+
- ⚡ **High Performance** - Efficient caching with minimal overhead
|
|
77
|
+
- 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows
|
|
78
|
+
- 🎨 **Type Safe** - Fully typed with comprehensive type hints
|
|
79
|
+
- 🧪 **Well Tested** - Extensive test coverage and battle-tested
|
|
80
|
+
- 🎛️ **Configurable** - Fine-grained control over caching behavior
|
|
81
|
+
- 🌐 **Future Ready** - Designed for easy integration with any HTTP client/server
|
|
82
|
+
|
|
83
|
+
## 📦 Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install hishel
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Optional Dependencies
|
|
90
|
+
|
|
91
|
+
Install with specific HTTP client support:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install hishel[httpx] # For HTTPX support
|
|
95
|
+
pip install hishel[requests] # For Requests support
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or install both:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install hishel[httpx,requests]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 🚀 Quick Start
|
|
105
|
+
|
|
106
|
+
### With HTTPX
|
|
107
|
+
|
|
108
|
+
**Synchronous:**
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from hishel.httpx import SyncCacheClient
|
|
112
|
+
|
|
113
|
+
client = SyncCacheClient()
|
|
114
|
+
|
|
115
|
+
# First request - fetches from origin
|
|
116
|
+
response = client.get("https://api.example.com/data")
|
|
117
|
+
print(response.extensions["hishel_from_cache"]) # False
|
|
118
|
+
|
|
119
|
+
# Second request - served from cache
|
|
120
|
+
response = client.get("https://api.example.com/data")
|
|
121
|
+
print(response.extensions["hishel_from_cache"]) # True
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Asynchronous:**
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from hishel.httpx import AsyncCacheClient
|
|
128
|
+
|
|
129
|
+
async with AsyncCacheClient() as client:
|
|
130
|
+
# First request - fetches from origin
|
|
131
|
+
response = await client.get("https://api.example.com/data")
|
|
132
|
+
print(response.extensions["hishel_from_cache"]) # False
|
|
133
|
+
|
|
134
|
+
# Second request - served from cache
|
|
135
|
+
response = await client.get("https://api.example.com/data")
|
|
136
|
+
print(response.extensions["hishel_from_cache"]) # True
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### With Requests
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import requests
|
|
143
|
+
from hishel.requests import CacheAdapter
|
|
144
|
+
|
|
145
|
+
session = requests.Session()
|
|
146
|
+
session.mount("https://", CacheAdapter())
|
|
147
|
+
session.mount("http://", CacheAdapter())
|
|
148
|
+
|
|
149
|
+
# First request - fetches from origin
|
|
150
|
+
response = session.get("https://api.example.com/data")
|
|
151
|
+
|
|
152
|
+
# Second request - served from cache
|
|
153
|
+
response = session.get("https://api.example.com/data")
|
|
154
|
+
print(response.headers.get("X-Hishel-From-Cache")) # "True"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 🎛️ Advanced Configuration
|
|
158
|
+
|
|
159
|
+
### Custom Cache Options
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from hishel import CacheOptions
|
|
163
|
+
from hishel.httpx import SyncCacheClient
|
|
164
|
+
|
|
165
|
+
client = SyncCacheClient(
|
|
166
|
+
cache_options=CacheOptions(
|
|
167
|
+
shared=False, # Use as private cache (browser-like)
|
|
168
|
+
supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
|
|
169
|
+
allow_stale=True # Allow serving stale responses
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Custom Storage Backend
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from hishel import SyncSqliteStorage
|
|
178
|
+
from hishel.httpx import SyncCacheClient
|
|
179
|
+
|
|
180
|
+
storage = SyncSqliteStorage(
|
|
181
|
+
database_path="my_cache.db",
|
|
182
|
+
default_ttl=7200.0, # Cache entries expire after 2 hours
|
|
183
|
+
refresh_ttl_on_access=True # Reset TTL when accessing cached entries
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
client = SyncCacheClient(storage=storage)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 🏗️ Architecture
|
|
190
|
+
|
|
191
|
+
Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations:
|
|
192
|
+
|
|
193
|
+
- ✅ **Correct** - Fully RFC 9111 compliant
|
|
194
|
+
- ✅ **Testable** - Easy to test without network dependencies
|
|
195
|
+
- ✅ **Flexible** - Works with any HTTP client or server
|
|
196
|
+
- ✅ **Type Safe** - Clear state transitions with full type hints
|
|
197
|
+
|
|
198
|
+
## 🔮 Roadmap
|
|
199
|
+
|
|
200
|
+
While Hishel currently supports HTTPX and Requests, we're actively working on:
|
|
201
|
+
|
|
202
|
+
- 🎯 Additional HTTP client integrations
|
|
203
|
+
- 🎯 Server-side caching support
|
|
204
|
+
- 🎯 More storage backends
|
|
205
|
+
- 🎯 Advanced caching strategies
|
|
206
|
+
- 🎯 Performance optimizations
|
|
207
|
+
|
|
208
|
+
## 📚 Documentation
|
|
209
|
+
|
|
210
|
+
Comprehensive documentation is available at [https://hishel.com/dev](https://hishel.com/dev)
|
|
211
|
+
|
|
212
|
+
- [Getting Started](https://hishel.com)
|
|
213
|
+
- [HTTPX Integration](https://hishel.com/dev/integrations/httpx)
|
|
214
|
+
- [Requests Integration](https://hishel.com/dev/integrations/requests)
|
|
215
|
+
- [Storage Backends](https://hishel.com/dev/storages)
|
|
216
|
+
- [RFC 9111 Specification](https://hishel.com/dev/specification)
|
|
217
|
+
|
|
218
|
+
## 🤝 Contributing
|
|
219
|
+
|
|
220
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
221
|
+
|
|
222
|
+
See our [Contributing Guide](https://hishel.com/dev/contributing) for more details.
|
|
223
|
+
|
|
224
|
+
## 📄 License
|
|
225
|
+
|
|
226
|
+
This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details.
|
|
227
|
+
|
|
228
|
+
## 💖 Support
|
|
229
|
+
|
|
230
|
+
If you find Hishel useful, please consider:
|
|
231
|
+
|
|
232
|
+
- ⭐ Starring the repository
|
|
233
|
+
- 🐛 Reporting bugs and issues
|
|
234
|
+
- 💡 Suggesting new features
|
|
235
|
+
- 📖 Improving documentation
|
|
236
|
+
- ☕ [Buying me a coffee](https://buymeacoffee.com/karpetrosyan)
|
|
237
|
+
|
|
238
|
+
## 🙏 Acknowledgments
|
|
239
|
+
|
|
240
|
+
Hishel is inspired by and builds upon the excellent work in the Python HTTP ecosystem, particularly:
|
|
241
|
+
|
|
242
|
+
- [HTTPX](https://github.com/encode/httpx) - A next-generation HTTP client for Python
|
|
243
|
+
- [Requests](https://github.com/psf/requests) - The classic HTTP library for Python
|
|
244
|
+
- [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) - HTTP Caching specification
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
<p align="center">
|
|
249
|
+
<strong>Made with ❤️ by <a href="https://github.com/karpetrosyan">Kar Petrosyan</a></strong>
|
|
250
|
+
</p>
|
|
251
|
+
|
|
252
|
+
## [1.0.0dev0] - 2025-10-19
|
|
253
|
+
|
|
254
|
+
### ⚙️ Miscellaneous Tasks
|
|
255
|
+
|
|
256
|
+
- *(docs)* Use mike powered versioning
|
|
257
|
+
- *(docs)* Improve docs versioning, deploy dev doc on ci
|
|
258
|
+
## [0.1.5] - 2025-10-18
|
|
259
|
+
|
|
260
|
+
### 🚀 Features
|
|
261
|
+
|
|
262
|
+
- *(perf)* Set chunk size to 128KB for httpx to reduce SQLite read/writes
|
|
263
|
+
- Better cache-control parsing
|
|
264
|
+
- Add close method to storages API (#384)
|
|
265
|
+
- *(perf)* Increase requests buffer size to 128KB, disable charset detection
|
|
266
|
+
|
|
267
|
+
### 🐛 Bug Fixes
|
|
268
|
+
|
|
269
|
+
- *(docs)* Fix some line breaks
|
|
270
|
+
|
|
271
|
+
### ⚙️ Miscellaneous Tasks
|
|
272
|
+
|
|
273
|
+
- Remove some redundant files from repo
|
|
274
|
+
## [0.1.4] - 2025-10-14
|
|
275
|
+
|
|
276
|
+
### 🚀 Features
|
|
277
|
+
|
|
278
|
+
- Add support for a sans-IO API (#366)
|
|
279
|
+
- Allow already consumed streams with `CacheTransport` (#377)
|
|
280
|
+
- Add sqlite storage for beta storages
|
|
281
|
+
- Get rid of some locks from sqlite storage
|
|
282
|
+
- Better async implemetation for sqlite storage
|
|
283
|
+
|
|
284
|
+
### 🐛 Bug Fixes
|
|
285
|
+
|
|
286
|
+
- Create an sqlite file in a cache folder
|
|
287
|
+
- Fix beta imports
|
|
288
|
+
|
|
289
|
+
### ⚙️ Miscellaneous Tasks
|
|
290
|
+
|
|
291
|
+
- Improve CI (#369)
|
|
292
|
+
- *(internal)* Remove src folder (#373)
|
|
293
|
+
- *(internal)* Temporary remove python3.14 from CI
|
|
294
|
+
- *(tests)* Add sqlite tests for new storage
|
|
295
|
+
- *(tests)* Move some tests to beta
|
|
296
|
+
## [0.1.3] - 2025-07-06
|
|
297
|
+
|
|
298
|
+
### 🚀 Features
|
|
299
|
+
|
|
300
|
+
- Support providing a path prefix to S3 storage (#342)
|
|
301
|
+
|
|
302
|
+
### 📚 Documentation
|
|
303
|
+
|
|
304
|
+
- Update link to httpx transports page (#337)
|
|
305
|
+
## [0.1.2] - 2025-04-04
|
|
306
|
+
|
|
307
|
+
### 🐛 Bug Fixes
|
|
308
|
+
|
|
309
|
+
- Requirements.txt to reduce vulnerabilities (#263)
|
|
310
|
+
## [0.0.30] - 2024-07-12
|
|
311
|
+
|
|
312
|
+
### 🐛 Bug Fixes
|
|
313
|
+
|
|
314
|
+
- Requirements.txt to reduce vulnerabilities (#245)
|
|
315
|
+
- Requirements.txt to reduce vulnerabilities (#255)
|
|
316
|
+
## [0.0.27] - 2024-05-31
|
|
317
|
+
|
|
318
|
+
### 🐛 Bug Fixes
|
|
319
|
+
|
|
320
|
+
- *(redis)* Do not update metadata with negative ttl (#231)
|
|
321
|
+
## [0.0.1] - 2023-07-22
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
hishel/__init__.py,sha256=byj_IhCjFMaBcp6R8iyRlQV-3R4uTfH44PQzB4lVe1g,1447
|
|
2
|
+
hishel/_async_cache.py,sha256=gE5CygC7FG9htBMfxul7carRRNph8zcMlSoOcB_LNTY,6792
|
|
3
|
+
hishel/_sync_cache.py,sha256=lfkWHJFK527peESMaufjKSbXBriidc09tOwBwub2t34,6538
|
|
4
|
+
hishel/_utils.py,sha256=uO8PcY_E1sHSgBGzZ2GNB4kpKqAlzmnzPCc3s-yDd44,13826
|
|
5
|
+
hishel/httpx.py,sha256=HcJ5iO9PgkEOp92ti8013N6m1IotLajwd9M_DLsmrX0,10997
|
|
6
|
+
hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
hishel/requests.py,sha256=eiWcwCId04DucnquCsU12tj9cDZcn-cjZ9MYniVuNeo,6429
|
|
8
|
+
hishel/_core/__init__.py,sha256=byj_IhCjFMaBcp6R8iyRlQV-3R4uTfH44PQzB4lVe1g,1447
|
|
9
|
+
hishel/_core/_headers.py,sha256=ii4x2L6GoQFpqpgg28OtFh7p2DoM9mhE4q6CjW6xUWc,17473
|
|
10
|
+
hishel/_core/_spec.py,sha256=d2ZnTXttyT4zuVq9xHAO86VGJxAEBxD2a8WMyEgOuAo,102702
|
|
11
|
+
hishel/_core/models.py,sha256=5qwo1WifrDeZdXag7M5rh0hJuVsm1N-sF3UagQ5LcLc,5519
|
|
12
|
+
hishel/_core/_async/_storages/_sqlite.py,sha256=wIO0UaFzal9qoVqDVczzcsW0kGUjBQD-ikauc_MN414,14704
|
|
13
|
+
hishel/_core/_base/_storages/_base.py,sha256=xLJGTBlFK8DVrQMgRMtGXJnYRUmNB-iYkk7S-BtMx8s,8516
|
|
14
|
+
hishel/_core/_base/_storages/_packing.py,sha256=NFMpSvYYTDBNkzwpjj5l4w-JOPLc19oAEDqDEQJ7VZI,4873
|
|
15
|
+
hishel/_core/_sync/_storages/_sqlite.py,sha256=TDm9jXIWtd54m4_8AiVApxZVmbBoeFVi3E6s-vGzDjs,14138
|
|
16
|
+
hishel-1.0.0.dev0.dist-info/METADATA,sha256=EpqEHRIGfzVXqMiRefCa_NZ9AlbjzVToXfnK-GBrs9o,9993
|
|
17
|
+
hishel-1.0.0.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
hishel-1.0.0.dev0.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
|
|
19
|
+
hishel-1.0.0.dev0.dist-info/RECORD,,
|
hishel/_async/__init__.py
DELETED
hishel/_async/_client.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import typing as tp
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
|
|
5
|
-
from ._transports import AsyncCacheTransport
|
|
6
|
-
|
|
7
|
-
__all__ = ("AsyncCacheClient",)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class AsyncCacheClient(httpx.AsyncClient):
|
|
11
|
-
def __init__(self, *args: tp.Any, **kwargs: tp.Any):
|
|
12
|
-
self._storage = kwargs.pop("storage") if "storage" in kwargs else None
|
|
13
|
-
self._controller = kwargs.pop("controller") if "controller" in kwargs else None
|
|
14
|
-
super().__init__(*args, **kwargs)
|
|
15
|
-
|
|
16
|
-
def _init_transport(self, *args, **kwargs) -> AsyncCacheTransport: # type: ignore
|
|
17
|
-
_transport = super()._init_transport(*args, **kwargs)
|
|
18
|
-
return AsyncCacheTransport(
|
|
19
|
-
transport=_transport,
|
|
20
|
-
storage=self._storage,
|
|
21
|
-
controller=self._controller,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
def _init_proxy_transport(self, *args, **kwargs) -> AsyncCacheTransport: # type: ignore
|
|
25
|
-
_transport = super()._init_proxy_transport(*args, **kwargs) # pragma: no cover
|
|
26
|
-
return AsyncCacheTransport( # pragma: no cover
|
|
27
|
-
transport=_transport,
|
|
28
|
-
storage=self._storage,
|
|
29
|
-
controller=self._controller,
|
|
30
|
-
)
|
hishel/_async/_mock.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import typing as tp
|
|
2
|
-
from types import TracebackType
|
|
3
|
-
|
|
4
|
-
import httpcore
|
|
5
|
-
import httpx
|
|
6
|
-
from httpcore._async.interfaces import AsyncRequestInterface
|
|
7
|
-
|
|
8
|
-
if tp.TYPE_CHECKING: # pragma: no cover
|
|
9
|
-
from typing_extensions import Self
|
|
10
|
-
|
|
11
|
-
__all__ = ("MockAsyncConnectionPool", "MockAsyncTransport")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class MockAsyncConnectionPool(AsyncRequestInterface):
|
|
15
|
-
async def handle_async_request(self, request: httpcore.Request) -> httpcore.Response:
|
|
16
|
-
assert isinstance(request.stream, tp.AsyncIterable)
|
|
17
|
-
data = b"".join([chunk async for chunk in request.stream]) # noqa: F841
|
|
18
|
-
return self.mocked_responses.pop(0)
|
|
19
|
-
|
|
20
|
-
def add_responses(self, responses: tp.List[httpcore.Response]) -> None:
|
|
21
|
-
if not hasattr(self, "mocked_responses"):
|
|
22
|
-
self.mocked_responses = []
|
|
23
|
-
self.mocked_responses.extend(responses)
|
|
24
|
-
|
|
25
|
-
async def __aenter__(self) -> "Self":
|
|
26
|
-
return self
|
|
27
|
-
|
|
28
|
-
async def __aexit__(
|
|
29
|
-
self,
|
|
30
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
31
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
32
|
-
traceback: tp.Optional[TracebackType] = None,
|
|
33
|
-
) -> None: ...
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class MockAsyncTransport(httpx.AsyncBaseTransport):
|
|
37
|
-
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
|
38
|
-
return self.mocked_responses.pop(0)
|
|
39
|
-
|
|
40
|
-
def add_responses(self, responses: tp.List[httpx.Response]) -> None:
|
|
41
|
-
if not hasattr(self, "mocked_responses"):
|
|
42
|
-
self.mocked_responses = []
|
|
43
|
-
self.mocked_responses.extend(responses)
|
hishel/_async/_pool.py
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import types
|
|
4
|
-
import typing as tp
|
|
5
|
-
|
|
6
|
-
from httpcore._async.interfaces import AsyncRequestInterface
|
|
7
|
-
from httpcore._exceptions import ConnectError
|
|
8
|
-
from httpcore._models import Request, Response
|
|
9
|
-
|
|
10
|
-
from .._controller import Controller, allowed_stale
|
|
11
|
-
from .._headers import parse_cache_control
|
|
12
|
-
from .._serializers import JSONSerializer, Metadata
|
|
13
|
-
from .._utils import extract_header_values_decoded
|
|
14
|
-
from ._storages import AsyncBaseStorage, AsyncFileStorage
|
|
15
|
-
|
|
16
|
-
T = tp.TypeVar("T")
|
|
17
|
-
|
|
18
|
-
__all__ = ("AsyncCacheConnectionPool",)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
async def fake_stream(content: bytes) -> tp.AsyncIterable[bytes]:
|
|
22
|
-
yield content
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def generate_504() -> Response:
|
|
26
|
-
return Response(status=504)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class AsyncCacheConnectionPool(AsyncRequestInterface):
|
|
30
|
-
"""An HTTP Core Connection Pool that supports HTTP caching.
|
|
31
|
-
|
|
32
|
-
:param pool: `Connection Pool` that our class wraps in order to add an HTTP Cache layer on top of
|
|
33
|
-
:type pool: AsyncRequestInterface
|
|
34
|
-
:param storage: Storage that handles how the responses should be saved., defaults to None
|
|
35
|
-
:type storage: tp.Optional[AsyncBaseStorage], optional
|
|
36
|
-
:param controller: Controller that manages the cache behavior at the specification level, defaults to None
|
|
37
|
-
:type controller: tp.Optional[Controller], optional
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
def __init__(
|
|
41
|
-
self,
|
|
42
|
-
pool: AsyncRequestInterface,
|
|
43
|
-
storage: tp.Optional[AsyncBaseStorage] = None,
|
|
44
|
-
controller: tp.Optional[Controller] = None,
|
|
45
|
-
) -> None:
|
|
46
|
-
self._pool = pool
|
|
47
|
-
|
|
48
|
-
self._storage = storage if storage is not None else AsyncFileStorage(serializer=JSONSerializer())
|
|
49
|
-
|
|
50
|
-
if not isinstance(self._storage, AsyncBaseStorage): # pragma: no cover
|
|
51
|
-
raise TypeError(f"Expected subclass of `AsyncBaseStorage` but got `{storage.__class__.__name__}`")
|
|
52
|
-
|
|
53
|
-
self._controller = controller if controller is not None else Controller()
|
|
54
|
-
|
|
55
|
-
async def handle_async_request(self, request: Request) -> Response:
|
|
56
|
-
"""
|
|
57
|
-
Handles HTTP requests while also implementing HTTP caching.
|
|
58
|
-
|
|
59
|
-
:param request: An HTTP request
|
|
60
|
-
:type request: httpcore.Request
|
|
61
|
-
:return: An HTTP response
|
|
62
|
-
:rtype: httpcore.Response
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
if request.extensions.get("cache_disabled", False):
|
|
66
|
-
request.headers.extend([(b"cache-control", b"no-cache"), (b"cache-control", b"max-age=0")])
|
|
67
|
-
|
|
68
|
-
if request.method.upper() not in [b"GET", b"HEAD"]:
|
|
69
|
-
# If the HTTP method is, for example, POST,
|
|
70
|
-
# we must also use the request data to generate the hash.
|
|
71
|
-
assert isinstance(request.stream, tp.AsyncIterable)
|
|
72
|
-
body_for_key = b"".join([chunk async for chunk in request.stream])
|
|
73
|
-
request.stream = fake_stream(body_for_key)
|
|
74
|
-
else:
|
|
75
|
-
body_for_key = b""
|
|
76
|
-
|
|
77
|
-
key = self._controller._key_generator(request, body_for_key)
|
|
78
|
-
stored_data = await self._storage.retrieve(key)
|
|
79
|
-
|
|
80
|
-
request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"Cache-Control"))
|
|
81
|
-
|
|
82
|
-
if request_cache_control.only_if_cached and not stored_data:
|
|
83
|
-
return generate_504()
|
|
84
|
-
|
|
85
|
-
if stored_data:
|
|
86
|
-
# Try using the stored response if it was discovered.
|
|
87
|
-
|
|
88
|
-
stored_response, stored_request, metadata = stored_data
|
|
89
|
-
|
|
90
|
-
# Immediately read the stored response to avoid issues when trying to access the response body.
|
|
91
|
-
stored_response.read()
|
|
92
|
-
|
|
93
|
-
res = self._controller.construct_response_from_cache(
|
|
94
|
-
request=request,
|
|
95
|
-
response=stored_response,
|
|
96
|
-
original_request=stored_request,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
if isinstance(res, Response):
|
|
100
|
-
# Simply use the response if the controller determines it is ready for use.
|
|
101
|
-
return await self._create_hishel_response(
|
|
102
|
-
key=key,
|
|
103
|
-
response=stored_response,
|
|
104
|
-
request=request,
|
|
105
|
-
metadata=metadata,
|
|
106
|
-
cached=True,
|
|
107
|
-
revalidated=False,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
if request_cache_control.only_if_cached:
|
|
111
|
-
return generate_504()
|
|
112
|
-
|
|
113
|
-
if isinstance(res, Request):
|
|
114
|
-
# Controller has determined that the response needs to be re-validated.
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
revalidation_response = await self._pool.handle_async_request(res)
|
|
118
|
-
except ConnectError:
|
|
119
|
-
# If there is a connection error, we can use the stale response if allowed.
|
|
120
|
-
if self._controller._allow_stale and allowed_stale(response=stored_response):
|
|
121
|
-
return await self._create_hishel_response(
|
|
122
|
-
key=key,
|
|
123
|
-
response=stored_response,
|
|
124
|
-
request=request,
|
|
125
|
-
metadata=metadata,
|
|
126
|
-
cached=True,
|
|
127
|
-
revalidated=False,
|
|
128
|
-
)
|
|
129
|
-
raise # pragma: no cover
|
|
130
|
-
# Merge headers with the stale response.
|
|
131
|
-
final_response = self._controller.handle_validation_response(
|
|
132
|
-
old_response=stored_response, new_response=revalidation_response
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
await final_response.aread()
|
|
136
|
-
|
|
137
|
-
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
138
|
-
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
139
|
-
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
140
|
-
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
141
|
-
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
142
|
-
if revalidation_response.status != 304 and self._controller.is_cachable(
|
|
143
|
-
request=request, response=final_response
|
|
144
|
-
):
|
|
145
|
-
await self._storage.store(key, response=final_response, request=request)
|
|
146
|
-
|
|
147
|
-
return await self._create_hishel_response(
|
|
148
|
-
key=key,
|
|
149
|
-
response=final_response,
|
|
150
|
-
request=request,
|
|
151
|
-
cached=revalidation_response.status == 304,
|
|
152
|
-
revalidated=True,
|
|
153
|
-
metadata=metadata,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
regular_response = await self._pool.handle_async_request(request)
|
|
157
|
-
await regular_response.aread()
|
|
158
|
-
|
|
159
|
-
if self._controller.is_cachable(request=request, response=regular_response):
|
|
160
|
-
await self._storage.store(key, response=regular_response, request=request)
|
|
161
|
-
|
|
162
|
-
return await self._create_hishel_response(
|
|
163
|
-
key=key, response=regular_response, request=request, cached=False, revalidated=False
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
async def _create_hishel_response(
|
|
167
|
-
self,
|
|
168
|
-
key: str,
|
|
169
|
-
response: Response,
|
|
170
|
-
request: Request,
|
|
171
|
-
cached: bool,
|
|
172
|
-
revalidated: bool,
|
|
173
|
-
metadata: Metadata | None = None,
|
|
174
|
-
) -> Response:
|
|
175
|
-
if cached:
|
|
176
|
-
assert metadata
|
|
177
|
-
metadata["number_of_uses"] += 1
|
|
178
|
-
await self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata)
|
|
179
|
-
response.extensions["from_cache"] = True # type: ignore[index]
|
|
180
|
-
response.extensions["cache_metadata"] = metadata # type: ignore[index]
|
|
181
|
-
else:
|
|
182
|
-
response.extensions["from_cache"] = False # type: ignore[index]
|
|
183
|
-
response.extensions["revalidated"] = revalidated # type: ignore[index]
|
|
184
|
-
return response
|
|
185
|
-
|
|
186
|
-
async def aclose(self) -> None:
|
|
187
|
-
await self._storage.aclose()
|
|
188
|
-
|
|
189
|
-
if hasattr(self._pool, "aclose"): # pragma: no cover
|
|
190
|
-
await self._pool.aclose()
|
|
191
|
-
|
|
192
|
-
async def __aenter__(self: T) -> T:
|
|
193
|
-
return self
|
|
194
|
-
|
|
195
|
-
async def __aexit__(
|
|
196
|
-
self,
|
|
197
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
198
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
199
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
200
|
-
) -> None:
|
|
201
|
-
await self.aclose()
|