hishel 1.0.0.dev1__py3-none-any.whl → 1.0.0.dev3__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,71 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import time
5
+ import typing as tp
6
+ import uuid
7
+
8
+ from ..models import Entry, Request, Response
9
+
10
+
11
+ class SyncBaseStorage(abc.ABC):
12
+ @abc.abstractmethod
13
+ def create_entry(self, request: Request, response: Response, key: str, id_: uuid.UUID | None = None) -> Entry:
14
+ raise NotImplementedError()
15
+
16
+ @abc.abstractmethod
17
+ def get_entries(self, key: str) -> tp.List[Entry]:
18
+ raise NotImplementedError()
19
+
20
+ @abc.abstractmethod
21
+ def update_entry(
22
+ self,
23
+ id: uuid.UUID,
24
+ new_entry: tp.Union[Entry, tp.Callable[[Entry], Entry]],
25
+ ) -> tp.Optional[Entry]:
26
+ raise NotImplementedError()
27
+
28
+ @abc.abstractmethod
29
+ def remove_entry(self, id: uuid.UUID) -> None:
30
+ raise NotImplementedError()
31
+
32
+ def close(self) -> None:
33
+ pass
34
+
35
+ def is_soft_deleted(self, pair: Entry) -> bool:
36
+ """
37
+ Check if a pair is soft deleted based on its metadata.
38
+
39
+ Args:
40
+ pair: The request pair to check.
41
+
42
+ Returns:
43
+ True if the pair is soft deleted, False otherwise.
44
+ """
45
+ return pair.meta.deleted_at is not None and pair.meta.deleted_at > 0
46
+
47
+ def is_safe_to_hard_delete(self, pair: Entry) -> bool:
48
+ """
49
+ Check if a pair is safe to hard delete based on its metadata.
50
+
51
+ If the pair has been soft deleted for more than 1 hour, it is considered safe to hard delete.
52
+
53
+ Args:
54
+ pair: The request pair to check.
55
+
56
+ Returns:
57
+ True if the pair is safe to hard delete, False otherwise.
58
+ """
59
+ return bool(pair.meta.deleted_at is not None and (pair.meta.deleted_at + 3600 < time.time()))
60
+
61
+ def mark_pair_as_deleted(self, pair: Entry) -> Entry:
62
+ """
63
+ Mark a pair as soft deleted by setting its deleted_at timestamp.
64
+
65
+ Args:
66
+ pair: The request pair to mark as deleted.
67
+ Returns:
68
+ The updated request pair with the deleted_at timestamp set.
69
+ """
70
+ pair.meta.deleted_at = time.time()
71
+ return pair
@@ -9,21 +9,19 @@ from typing import (
9
9
  Iterator,
10
10
  Callable,
11
11
  List,
12
- Literal,
13
12
  Optional,
14
13
  Union,
15
14
  )
16
15
 
17
- from hishel._core._base._storages._base import SyncBaseStorage, ensure_cache_dict
18
- from hishel._core._base._storages._packing import pack, unpack
16
+ from hishel._core._storages._sync_base import SyncBaseStorage
17
+ from hishel._core._storages._packing import pack, unpack
19
18
  from hishel._core.models import (
20
- CompletePair,
21
- IncompletePair,
22
- Pair,
23
- PairMeta,
19
+ Entry,
20
+ EntryMeta,
24
21
  Request,
25
22
  Response,
26
23
  )
24
+ from hishel._utils import ensure_cache_dict
27
25
 
28
26
  # Batch cleanup configuration
29
27
  # How often to run cleanup (seconds). Default: 1 hour.
@@ -38,7 +36,6 @@ try:
38
36
  import sqlite3
39
37
 
40
38
  class SyncSqliteStorage(SyncBaseStorage):
41
- _STREAM_KIND = {"request": 0, "response": 1}
42
39
  _COMPLETE_CHUNK_NUMBER = -1
43
40
 
44
41
  def __init__(
@@ -85,14 +82,13 @@ try:
85
82
  )
86
83
  """)
87
84
 
88
- # Table for storing stream chunks
85
+ # Table for storing response stream chunks only
89
86
  cursor.execute("""
90
87
  CREATE TABLE IF NOT EXISTS streams (
91
88
  entry_id BLOB NOT NULL,
92
- kind INTEGER NOT NULL,
93
89
  chunk_number INTEGER NOT NULL,
94
90
  chunk_data BLOB NOT NULL,
95
- PRIMARY KEY (entry_id, kind, chunk_number),
91
+ PRIMARY KEY (entry_id, chunk_number),
96
92
  FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
97
93
  )
98
94
  """)
@@ -100,85 +96,48 @@ try:
100
96
  # Indexes for performance
101
97
  cursor.execute("CREATE INDEX IF NOT EXISTS idx_entries_deleted_at ON entries(deleted_at)")
102
98
  cursor.execute("CREATE INDEX IF NOT EXISTS idx_entries_cache_key ON entries(cache_key)")
103
- # Note: PRIMARY KEY (entry_id, kind, chunk_number) already provides an index
104
- # for queries like: entry_id = ? AND kind = ? AND chunk_number = ?
105
99
 
106
100
  self.connection.commit()
107
101
 
108
- def create_pair(
109
- self,
110
- request: Request,
111
- id: uuid.UUID | None = None,
112
- ) -> IncompletePair:
113
- pair_id = id if id is not None else uuid.uuid4()
114
- pair_meta = PairMeta(
115
- created_at=time.time(),
116
- )
117
-
118
- pair = IncompletePair(id=pair_id, request=request, meta=pair_meta)
119
-
120
- packed_pair = pack(pair, kind="pair")
102
+ def create_entry(
103
+ self, request: Request, response: Response, key: str, id_: uuid.UUID | None = None
104
+ ) -> Entry:
105
+ key_bytes = key.encode("utf-8")
121
106
 
122
107
  connection = self._ensure_connection()
123
108
  cursor = connection.cursor()
124
- cursor.execute(
125
- "INSERT INTO entries (id, cache_key, data, created_at, deleted_at) VALUES (?, ?, ?, ?, ?)",
126
- (pair_id.bytes, None, packed_pair, pair_meta.created_at, None),
127
- )
128
- connection.commit()
129
-
130
- assert isinstance(request.stream, Iterable), "Request stream must be an Iterable, not Iterable"
131
109
 
132
- request = Request(
133
- method=request.method,
134
- url=request.url,
135
- headers=request.headers,
136
- metadata=request.metadata,
137
- stream=self._save_stream(request.stream, pair_id.bytes, "request"),
110
+ # Create a new entry directly with both request and response
111
+ pair_id = id_ if id_ is not None else uuid.uuid4()
112
+ pair_meta = EntryMeta(
113
+ created_at=time.time(),
138
114
  )
139
115
 
140
- return replace(pair, request=request)
141
-
142
- def add_response(
143
- self,
144
- pair_id: uuid.UUID,
145
- response: Response,
146
- key: str | bytes,
147
- ) -> CompletePair:
148
- if isinstance(key, str):
149
- key = key.encode("utf-8")
150
-
151
- connection = self._ensure_connection()
152
- cursor = connection.cursor()
153
-
154
- # Get the existing pair
155
- cursor.execute("SELECT data FROM entries WHERE id = ?", (pair_id.bytes,))
156
- result = cursor.fetchone()
157
-
158
- if result is None:
159
- raise ValueError(f"Entry with ID {pair_id} not found.")
160
-
161
- pair = unpack(result[0], kind="pair")
162
-
163
116
  assert isinstance(response.stream, (Iterator, Iterable))
164
- response = replace(response, stream=self._save_stream(response.stream, pair_id.bytes, "response"))
117
+ response_with_stream = replace(
118
+ response,
119
+ stream=self._save_stream(response.stream, pair_id.bytes),
120
+ )
165
121
 
166
- self._delete_stream(pair.id.bytes, cursor, type="response")
167
- complete_pair = CompletePair(
168
- id=pair.id, request=pair.request, response=response, meta=pair.meta, cache_key=key
122
+ complete_entry = Entry(
123
+ id=pair_id,
124
+ request=request,
125
+ response=response_with_stream,
126
+ meta=pair_meta,
127
+ cache_key=key_bytes,
169
128
  )
170
129
 
171
- # Update the entry with the complete pair and set cache_key
130
+ # Insert the complete entry into the database
172
131
  cursor.execute(
173
- "UPDATE entries SET data = ?, cache_key = ? WHERE id = ?",
174
- (pack(complete_pair, kind="pair"), key, pair_id.bytes),
132
+ "INSERT INTO entries (id, cache_key, data, created_at, deleted_at) VALUES (?, ?, ?, ?, ?)",
133
+ (pair_id.bytes, key_bytes, pack(complete_entry, kind="pair"), pair_meta.created_at, None),
175
134
  )
176
135
  connection.commit()
177
136
 
178
- return complete_pair
137
+ return complete_entry
179
138
 
180
- def get_pairs(self, key: str) -> List[CompletePair]:
181
- final_pairs: List[CompletePair] = []
139
+ def get_entries(self, key: str) -> List[Entry]:
140
+ final_pairs: List[Entry] = []
182
141
 
183
142
  now = time.time()
184
143
  if now - self.last_cleanup >= BATCH_CLEANUP_INTERVAL:
@@ -191,39 +150,40 @@ try:
191
150
  connection = self._ensure_connection()
192
151
  cursor = connection.cursor()
193
152
  # Query entries directly by cache_key
194
- cursor.execute("SELECT id, data FROM entries WHERE cache_key = ?", (key.encode("utf-8"),))
153
+ cursor.execute(
154
+ "SELECT id, data FROM entries WHERE cache_key = ?",
155
+ (key.encode("utf-8"),),
156
+ )
195
157
 
196
158
  for row in cursor.fetchall():
197
159
  pair_data = unpack(row[1], kind="pair")
198
160
 
199
- if isinstance(pair_data, IncompletePair):
161
+ # Skip entries without a response (incomplete)
162
+ if not isinstance(pair_data, Entry) or pair_data.response is None:
200
163
  continue
201
164
 
202
165
  final_pairs.append(pair_data)
203
166
 
204
- pairs_with_streams: List[CompletePair] = []
167
+ pairs_with_streams: List[Entry] = []
205
168
 
169
+ # Only restore response streams from cache
206
170
  for pair in final_pairs:
207
171
  pairs_with_streams.append(
208
172
  replace(
209
173
  pair,
210
174
  response=replace(
211
175
  pair.response,
212
- stream=self._stream_data_from_cache(pair.id.bytes, "response"),
213
- ),
214
- request=replace(
215
- pair.request,
216
- stream=self._stream_data_from_cache(pair.id.bytes, "request"),
176
+ stream=self._stream_data_from_cache(pair.id.bytes),
217
177
  ),
218
178
  )
219
179
  )
220
180
  return pairs_with_streams
221
181
 
222
- def update_pair(
182
+ def update_entry(
223
183
  self,
224
184
  id: uuid.UUID,
225
- new_pair: Union[CompletePair, Callable[[CompletePair], CompletePair]],
226
- ) -> Optional[CompletePair]:
185
+ new_pair: Union[Entry, Callable[[Entry], Entry]],
186
+ ) -> Optional[Entry]:
227
187
  connection = self._ensure_connection()
228
188
  cursor = connection.cursor()
229
189
  cursor.execute("SELECT data FROM entries WHERE id = ?", (id.bytes,))
@@ -234,10 +194,11 @@ try:
234
194
 
235
195
  pair = unpack(result[0], kind="pair")
236
196
 
237
- if isinstance(pair, IncompletePair):
197
+ # Skip entries without a response (incomplete)
198
+ if not isinstance(pair, Entry) or pair.response is None:
238
199
  return None
239
200
 
240
- if isinstance(new_pair, CompletePair):
201
+ if isinstance(new_pair, Entry):
241
202
  complete_pair = new_pair
242
203
  else:
243
204
  complete_pair = new_pair(pair)
@@ -246,7 +207,8 @@ try:
246
207
  raise ValueError("Pair ID mismatch")
247
208
 
248
209
  cursor.execute(
249
- "UPDATE entries SET data = ? WHERE id = ?", (pack(complete_pair, kind="pair"), id.bytes)
210
+ "UPDATE entries SET data = ? WHERE id = ?",
211
+ (pack(complete_pair, kind="pair"), id.bytes),
250
212
  )
251
213
 
252
214
  if pair.cache_key != complete_pair.cache_key:
@@ -259,7 +221,7 @@ try:
259
221
 
260
222
  return complete_pair
261
223
 
262
- def remove(self, id: uuid.UUID) -> None:
224
+ def remove_entry(self, id: uuid.UUID) -> None:
263
225
  connection = self._ensure_connection()
264
226
  cursor = connection.cursor()
265
227
  cursor.execute("SELECT data FROM entries WHERE id = ?", (id.bytes,))
@@ -272,28 +234,33 @@ try:
272
234
  self._soft_delete_pair(pair, cursor)
273
235
  connection.commit()
274
236
 
275
- def _is_stream_complete(
276
- self, kind: Literal["request", "response"], pair_id: uuid.UUID, cursor: sqlite3.Cursor
277
- ) -> bool:
278
- kind_id = self._STREAM_KIND[kind]
279
- # Check if there's a completion marker (chunk_number = -1)
237
+ def _is_stream_complete(self, pair_id: uuid.UUID, cursor: sqlite3.Cursor) -> bool:
238
+ # Check if there's a completion marker (chunk_number = -1) for response stream
280
239
  cursor.execute(
281
- "SELECT 1 FROM streams WHERE entry_id = ? AND kind = ? AND chunk_number = ? LIMIT 1",
282
- (pair_id.bytes, kind_id, self._COMPLETE_CHUNK_NUMBER),
240
+ "SELECT 1 FROM streams WHERE entry_id = ? AND chunk_number = ? LIMIT 1",
241
+ (pair_id.bytes, self._COMPLETE_CHUNK_NUMBER),
283
242
  )
284
243
  return cursor.fetchone() is not None
285
244
 
286
- def _soft_delete_pair(self, pair: Union[CompletePair, IncompletePair], cursor: sqlite3.Cursor) -> None:
245
+ def _soft_delete_pair(
246
+ self,
247
+ pair: Entry,
248
+ cursor: sqlite3.Cursor,
249
+ ) -> None:
287
250
  """
288
251
  Mark the pair as deleted by setting the deleted_at timestamp.
289
252
  """
290
253
  marked_pair = self.mark_pair_as_deleted(pair)
291
254
  cursor.execute(
292
255
  "UPDATE entries SET data = ?, deleted_at = ? WHERE id = ?",
293
- (pack(marked_pair, kind="pair"), marked_pair.meta.deleted_at, pair.id.bytes),
256
+ (
257
+ pack(marked_pair, kind="pair"),
258
+ marked_pair.meta.deleted_at,
259
+ pair.id.bytes,
260
+ ),
294
261
  )
295
262
 
296
- def _is_pair_expired(self, pair: Pair, cursor: sqlite3.Cursor) -> bool:
263
+ def _is_pair_expired(self, pair: Entry, cursor: sqlite3.Cursor) -> bool:
297
264
  """
298
265
  Check if the pair is expired.
299
266
  """
@@ -307,10 +274,10 @@ try:
307
274
  self,
308
275
  ) -> None:
309
276
  """
310
- Cleanup expired pairs in the database.
277
+ Cleanup expired entries in the database.
311
278
  """
312
- should_mark_as_deleted: List[Union[CompletePair, IncompletePair]] = []
313
- should_hard_delete: List[Union[CompletePair, IncompletePair]] = []
279
+ should_mark_as_deleted: List[Entry] = []
280
+ should_hard_delete: List[Entry] = []
314
281
 
315
282
  connection = self._ensure_connection()
316
283
  cursor = connection.cursor()
@@ -319,7 +286,10 @@ try:
319
286
  chunk_size = BATCH_CLEANUP_CHUNK_SIZE
320
287
  offset = 0
321
288
  while True:
322
- cursor.execute("SELECT id, data FROM entries LIMIT ? OFFSET ?", (chunk_size, offset))
289
+ cursor.execute(
290
+ "SELECT id, data FROM entries LIMIT ? OFFSET ?",
291
+ (chunk_size, offset),
292
+ )
323
293
  rows = cursor.fetchall()
324
294
  if not rows:
325
295
  break
@@ -350,61 +320,56 @@ try:
350
320
 
351
321
  connection.commit()
352
322
 
353
- def _is_corrupted(self, pair: IncompletePair | CompletePair, cursor: sqlite3.Cursor) -> bool:
354
- # if pair was created more than 1 hour ago and still not completed
355
- if pair.meta.created_at + 3600 < time.time() and isinstance(pair, IncompletePair):
323
+ def _is_corrupted(self, pair: Entry, cursor: sqlite3.Cursor) -> bool:
324
+ # if entry was created more than 1 hour ago and still has no response (incomplete)
325
+ if pair.meta.created_at + 3600 < time.time() and pair.response is None:
356
326
  return True
357
327
 
358
- if isinstance(pair, CompletePair) and not self._is_stream_complete("request", pair.id, cursor):
328
+ # Check if response stream is complete for Entry with response
329
+ if (
330
+ isinstance(pair, Entry)
331
+ and pair.response is not None
332
+ and not self._is_stream_complete(pair.id, cursor)
333
+ ):
359
334
  return True
360
335
  return False
361
336
 
362
- def _hard_delete_pair(self, pair: CompletePair | IncompletePair, cursor: sqlite3.Cursor) -> None:
337
+ def _hard_delete_pair(self, pair: Entry, cursor: sqlite3.Cursor) -> None:
363
338
  """
364
339
  Permanently delete the pair from the database.
365
340
  """
366
341
  cursor.execute("DELETE FROM entries WHERE id = ?", (pair.id.bytes,))
367
342
 
368
- # Delete all streams (both request and response) for this entry
343
+ # Delete response stream for this entry
369
344
  self._delete_stream(pair.id.bytes, cursor)
370
345
 
371
346
  def _delete_stream(
372
347
  self,
373
348
  entry_id: bytes,
374
349
  cursor: sqlite3.Cursor,
375
- type: Literal["request", "response", "all"] = "all",
376
350
  ) -> None:
377
351
  """
378
- Delete all streams (both request and response) associated with the given entry ID.
352
+ Delete response stream associated with the given entry ID.
379
353
  """
380
- if type == "request":
381
- cursor.execute(
382
- "DELETE FROM streams WHERE entry_id = ? AND kind = ?", (entry_id, self._STREAM_KIND["request"])
383
- )
384
- elif type == "response":
385
- cursor.execute(
386
- "DELETE FROM streams WHERE entry_id = ? AND kind = ?", (entry_id, self._STREAM_KIND["response"])
387
- )
388
- elif type == "all":
389
- cursor.execute("DELETE FROM streams WHERE entry_id = ?", (entry_id,))
354
+ cursor.execute("DELETE FROM streams WHERE entry_id = ?", (entry_id,))
390
355
 
391
356
  def _save_stream(
392
357
  self,
393
358
  stream: Iterator[bytes],
394
359
  entry_id: bytes,
395
- kind: Literal["response", "request"],
396
360
  ) -> Iterator[bytes]:
397
361
  """
398
- Wrapper around an async iterator that also saves the data to the cache in chunks.
362
+ Wrapper around an async iterator that also saves the response data to the cache in chunks.
399
363
  """
400
- kind_id = self._STREAM_KIND[kind]
401
364
  chunk_number = 0
365
+ content_length = 0
402
366
  for chunk in stream:
367
+ content_length += len(chunk)
403
368
  connection = self._ensure_connection()
404
369
  cursor = connection.cursor()
405
370
  cursor.execute(
406
- "INSERT INTO streams (entry_id, kind, chunk_number, chunk_data) VALUES (?, ?, ?, ?)",
407
- (entry_id, kind_id, chunk_number, chunk),
371
+ "INSERT INTO streams (entry_id, chunk_number, chunk_data) VALUES (?, ?, ?)",
372
+ (entry_id, chunk_number, chunk),
408
373
  )
409
374
  connection.commit()
410
375
  chunk_number += 1
@@ -414,28 +379,26 @@ try:
414
379
  connection = self._ensure_connection()
415
380
  cursor = connection.cursor()
416
381
  cursor.execute(
417
- "INSERT INTO streams (entry_id, kind, chunk_number, chunk_data) VALUES (?, ?, ?, ?)",
418
- (entry_id, kind_id, self._COMPLETE_CHUNK_NUMBER, b""),
382
+ "INSERT INTO streams (entry_id, chunk_number, chunk_data) VALUES (?, ?, ?)",
383
+ (entry_id, self._COMPLETE_CHUNK_NUMBER, b""),
419
384
  )
420
385
  connection.commit()
421
386
 
422
387
  def _stream_data_from_cache(
423
388
  self,
424
389
  entry_id: bytes,
425
- kind: Literal["response", "request"],
426
390
  ) -> Iterator[bytes]:
427
391
  """
428
- Get an async iterator that yields the stream data from the cache.
392
+ Get an async iterator that yields the response stream data from the cache.
429
393
  """
430
- kind_id = self._STREAM_KIND[kind]
431
394
  chunk_number = 0
432
395
 
433
396
  connection = self._ensure_connection()
434
397
  while True:
435
398
  cursor = connection.cursor()
436
399
  cursor.execute(
437
- "SELECT chunk_data FROM streams WHERE entry_id = ? AND kind = ? AND chunk_number = ?",
438
- (entry_id, kind_id, chunk_number),
400
+ "SELECT chunk_data FROM streams WHERE entry_id = ? AND chunk_number = ?",
401
+ (entry_id, chunk_number),
439
402
  )
440
403
  result = cursor.fetchone()
441
404
 
@@ -449,7 +412,7 @@ try:
449
412
  chunk_number += 1
450
413
  except ImportError:
451
414
 
452
- class SyncSqliteStorage(SyncBaseStorage): # type: ignore[no-redef]
415
+ class SyncSqliteStorage: # type: ignore[no-redef]
453
416
  def __init__(self, *args: Any, **kwargs: Any) -> None:
454
417
  raise ImportError(
455
418
  "The 'sqlite3' library is required to use the `SyncSqliteStorage` integration. "
hishel/_core/models.py CHANGED
@@ -64,7 +64,9 @@ class RequestMetadata(TypedDict, total=False):
64
64
  """
65
65
 
66
66
 
67
- def extract_metadata_from_headers(headers: Mapping[str, str]) -> RequestMetadata:
67
+ def extract_metadata_from_headers(
68
+ headers: Mapping[str, str],
69
+ ) -> RequestMetadata:
68
70
  metadata: RequestMetadata = {}
69
71
  if "X-Hishel-Ttl" in headers:
70
72
  try:
@@ -109,18 +111,21 @@ class Request:
109
111
 
110
112
  class ResponseMetadata(TypedDict, total=False):
111
113
  # All the names here should be prefixed with "hishel_" to avoid collisions with user data
112
- hishel_from_cache: bool | None
114
+ hishel_from_cache: bool
113
115
  """Indicates whether the response was served from cache."""
114
116
 
115
- hishel_revalidated: bool | None
117
+ hishel_revalidated: bool
116
118
  """Indicates whether the response was revalidated with the origin server."""
117
119
 
118
- hishel_spec_ignored: bool | None
120
+ hishel_spec_ignored: bool
119
121
  """Indicates whether the caching specification was ignored for this response."""
120
122
 
121
- hishel_stored: bool | None
123
+ hishel_stored: bool
122
124
  """Indicates whether the response was stored in cache."""
123
125
 
126
+ hishel_created_at: float
127
+ """Timestamp when the response was cached."""
128
+
124
129
 
125
130
  @dataclass
126
131
  class Response:
@@ -143,34 +148,16 @@ class Response:
143
148
 
144
149
 
145
150
  @dataclass
146
- class PairMeta:
151
+ class EntryMeta:
147
152
  created_at: float = field(default_factory=time.time)
148
153
  deleted_at: Optional[float] = None
149
154
 
150
155
 
151
156
  @dataclass
152
- class Pair:
157
+ class Entry:
153
158
  id: uuid.UUID
154
159
  request: Request
155
- meta: PairMeta
156
-
157
-
158
- # class used by storage
159
- @dataclass
160
- class IncompletePair(Pair):
161
- extra: Mapping[str, Any] = field(default_factory=dict)
162
-
163
-
164
- @dataclass
165
- class CompletePair(Pair):
160
+ meta: EntryMeta
166
161
  response: Response
167
162
  cache_key: bytes
168
163
  extra: Mapping[str, Any] = field(default_factory=dict)
169
-
170
- @classmethod
171
- def create(
172
- cls,
173
- response: Response,
174
- request: Request,
175
- ) -> "CompletePair": # pragma: nocover
176
- return cls(id=uuid.uuid4(), request=request, response=response, meta=PairMeta(), cache_key=b"")