wayback2warc 1.0.0__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 tmctmt
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,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: wayback2warc
3
+ Version: 1.0.0
4
+ Summary: WARC exporter for the Wayback Machine
5
+ Author-email: tmctmt <tmctmt@proton.me>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 tmctmt
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: Internet Archive,Wayback Machine,WARC
29
+ Classifier: Development Status :: 5 - Production/Stable
30
+ Classifier: Environment :: Console
31
+ Classifier: Intended Audience :: System Administrators
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Natural Language :: English
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Programming Language :: Python
36
+ Classifier: Topic :: Utilities
37
+ Requires-Python: >=3.8
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: httpx[socks]==0.28.1
41
+ Requires-Dist: tqdm==4.67.3
42
+ Requires-Dist: warcio==1.8.1
43
+ Dynamic: license-file
44
+
45
+ # wayback2warc
46
+ A CLI tool that exports captures from the Wayback Machine and packs them into WARC files.
47
+
48
+ ```
49
+ usage: wayback2warc [-h] [-c COLLAPSE] [-f FILTER] [-m] [-p PROXY] [-t THREADS] [-w WARC_SIZE] url [prefix]
50
+
51
+ positional arguments:
52
+ url *.example.org or example.org/*
53
+ prefix output path prefix
54
+
55
+ options:
56
+ -h, --help show this help message and exit
57
+ -c, --collapse COLLAPSE
58
+ dedupe records based on returned key
59
+ -f, --filter FILTER filter records based on returned boolean
60
+ -m, --meta dump cdx metadata into jsonl file
61
+ -p, --proxy PROXY url formatted proxy, can be a single value or a file
62
+ -t, --threads THREADS
63
+ concurrent downloads (default: 2)
64
+ -w, --warc-size WARC_SIZE
65
+ max size of produced warc files in MB (default: 1024)
66
+ ```
67
+
68
+ ## Install
69
+ `pip install wayback2warc`
70
+
71
+ ## Usage
72
+
73
+ Download all subdomains, all pages, and all captures for a domain:
74
+
75
+ `wayback2warc '*.example.org'`
76
+
77
+ Download all pages and all captures for a prefix:
78
+
79
+ `wayback2warc 'example.org/*'`
80
+
81
+ Download all captures for a single page:
82
+
83
+ `wayback2warc 'example.org'`
84
+
85
+ Download all pages but only a yearly capture of each:
86
+
87
+ `wayback2warc 'example.org/*' --collapse 'lambda m: (m.urlkey, m.timestamp[:4])'`
88
+
89
+ ## Filtering and collapsing
90
+ This tool implements filtering/collapsing functionality on the client-side rather than relying on the CDX server to do so.
91
+ This is a necessity due to the way CDX paging works, but on the bright side it allows for much more granular queries.
92
+
93
+ Instead of a query language, lambda functions are used to narrow down captures for downloading.
94
+ A [CaptureMetadata](https://github.com/tmctmt/wayback2warc/blob/main/wayback2warc.py#L28) instance is passed to both functions, and the returned value is used as a key for collapsing, or as a boolean for filtering.
95
+
96
+ Collapse captures by URL excluding query:
97
+ `--collapse 'lambda m: m.urlkey.split("?")[0]'`
98
+
99
+ Filter captures before 2020 and exclude images:
100
+ `--filter 'lambda m: m.timestamp < "2020" and "image/" not in m.mimetype'`
101
+
@@ -0,0 +1,57 @@
1
+ # wayback2warc
2
+ A CLI tool that exports captures from the Wayback Machine and packs them into WARC files.
3
+
4
+ ```
5
+ usage: wayback2warc [-h] [-c COLLAPSE] [-f FILTER] [-m] [-p PROXY] [-t THREADS] [-w WARC_SIZE] url [prefix]
6
+
7
+ positional arguments:
8
+ url *.example.org or example.org/*
9
+ prefix output path prefix
10
+
11
+ options:
12
+ -h, --help show this help message and exit
13
+ -c, --collapse COLLAPSE
14
+ dedupe records based on returned key
15
+ -f, --filter FILTER filter records based on returned boolean
16
+ -m, --meta dump cdx metadata into jsonl file
17
+ -p, --proxy PROXY url formatted proxy, can be a single value or a file
18
+ -t, --threads THREADS
19
+ concurrent downloads (default: 2)
20
+ -w, --warc-size WARC_SIZE
21
+ max size of produced warc files in MB (default: 1024)
22
+ ```
23
+
24
+ ## Install
25
+ `pip install wayback2warc`
26
+
27
+ ## Usage
28
+
29
+ Download all subdomains, all pages, and all captures for a domain:
30
+
31
+ `wayback2warc '*.example.org'`
32
+
33
+ Download all pages and all captures for a prefix:
34
+
35
+ `wayback2warc 'example.org/*'`
36
+
37
+ Download all captures for a single page:
38
+
39
+ `wayback2warc 'example.org'`
40
+
41
+ Download all pages but only a yearly capture of each:
42
+
43
+ `wayback2warc 'example.org/*' --collapse 'lambda m: (m.urlkey, m.timestamp[:4])'`
44
+
45
+ ## Filtering and collapsing
46
+ This tool implements filtering/collapsing functionality on the client-side rather than relying on the CDX server to do so.
47
+ This is a necessity due to the way CDX paging works, but on the bright side it allows for much more granular queries.
48
+
49
+ Instead of a query language, lambda functions are used to narrow down captures for downloading.
50
+ A [CaptureMetadata](https://github.com/tmctmt/wayback2warc/blob/main/wayback2warc.py#L28) instance is passed to both functions, and the returned value is used as a key for collapsing, or as a boolean for filtering.
51
+
52
+ Collapse captures by URL excluding query:
53
+ `--collapse 'lambda m: m.urlkey.split("?")[0]'`
54
+
55
+ Filter captures before 2020 and exclude images:
56
+ `--filter 'lambda m: m.timestamp < "2020" and "image/" not in m.mimetype'`
57
+
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wayback2warc"
7
+ version = "1.0.0"
8
+ description = "WARC exporter for the Wayback Machine"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "tmctmt", email = "tmctmt@proton.me" },
12
+ ]
13
+ license = { file = "LICENSE" }
14
+ requires-python = ">=3.8"
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Environment :: Console",
18
+ "Intended Audience :: System Administrators",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Natural Language :: English",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python",
23
+ "Topic :: Utilities",
24
+ ]
25
+ keywords = ["Internet Archive", "Wayback Machine", "WARC"]
26
+ dependencies = [
27
+ "httpx[socks]==0.28.1",
28
+ "tqdm==4.67.3",
29
+ "warcio==1.8.1",
30
+ ]
31
+ [project.scripts]
32
+ wayback2warc = "wayback2warc.py"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: wayback2warc
3
+ Version: 1.0.0
4
+ Summary: WARC exporter for the Wayback Machine
5
+ Author-email: tmctmt <tmctmt@proton.me>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 tmctmt
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: Internet Archive,Wayback Machine,WARC
29
+ Classifier: Development Status :: 5 - Production/Stable
30
+ Classifier: Environment :: Console
31
+ Classifier: Intended Audience :: System Administrators
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Natural Language :: English
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Programming Language :: Python
36
+ Classifier: Topic :: Utilities
37
+ Requires-Python: >=3.8
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: httpx[socks]==0.28.1
41
+ Requires-Dist: tqdm==4.67.3
42
+ Requires-Dist: warcio==1.8.1
43
+ Dynamic: license-file
44
+
45
+ # wayback2warc
46
+ A CLI tool that exports captures from the Wayback Machine and packs them into WARC files.
47
+
48
+ ```
49
+ usage: wayback2warc [-h] [-c COLLAPSE] [-f FILTER] [-m] [-p PROXY] [-t THREADS] [-w WARC_SIZE] url [prefix]
50
+
51
+ positional arguments:
52
+ url *.example.org or example.org/*
53
+ prefix output path prefix
54
+
55
+ options:
56
+ -h, --help show this help message and exit
57
+ -c, --collapse COLLAPSE
58
+ dedupe records based on returned key
59
+ -f, --filter FILTER filter records based on returned boolean
60
+ -m, --meta dump cdx metadata into jsonl file
61
+ -p, --proxy PROXY url formatted proxy, can be a single value or a file
62
+ -t, --threads THREADS
63
+ concurrent downloads (default: 2)
64
+ -w, --warc-size WARC_SIZE
65
+ max size of produced warc files in MB (default: 1024)
66
+ ```
67
+
68
+ ## Install
69
+ `pip install wayback2warc`
70
+
71
+ ## Usage
72
+
73
+ Download all subdomains, all pages, and all captures for a domain:
74
+
75
+ `wayback2warc '*.example.org'`
76
+
77
+ Download all pages and all captures for a prefix:
78
+
79
+ `wayback2warc 'example.org/*'`
80
+
81
+ Download all captures for a single page:
82
+
83
+ `wayback2warc 'example.org'`
84
+
85
+ Download all pages but only a yearly capture of each:
86
+
87
+ `wayback2warc 'example.org/*' --collapse 'lambda m: (m.urlkey, m.timestamp[:4])'`
88
+
89
+ ## Filtering and collapsing
90
+ This tool implements filtering/collapsing functionality on the client-side rather than relying on the CDX server to do so.
91
+ This is a necessity due to the way CDX paging works, but on the bright side it allows for much more granular queries.
92
+
93
+ Instead of a query language, lambda functions are used to narrow down captures for downloading.
94
+ A [CaptureMetadata](https://github.com/tmctmt/wayback2warc/blob/main/wayback2warc.py#L28) instance is passed to both functions, and the returned value is used as a key for collapsing, or as a boolean for filtering.
95
+
96
+ Collapse captures by URL excluding query:
97
+ `--collapse 'lambda m: m.urlkey.split("?")[0]'`
98
+
99
+ Filter captures before 2020 and exclude images:
100
+ `--filter 'lambda m: m.timestamp < "2020" and "image/" not in m.mimetype'`
101
+
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ wayback2warc.py
5
+ wayback2warc.egg-info/PKG-INFO
6
+ wayback2warc.egg-info/SOURCES.txt
7
+ wayback2warc.egg-info/dependency_links.txt
8
+ wayback2warc.egg-info/entry_points.txt
9
+ wayback2warc.egg-info/requires.txt
10
+ wayback2warc.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wayback2warc = wayback2warc.py
@@ -0,0 +1,3 @@
1
+ httpx[socks]==0.28.1
2
+ tqdm==4.67.3
3
+ warcio==1.8.1
@@ -0,0 +1 @@
1
+ wayback2warc
@@ -0,0 +1,227 @@
1
+ from collections import deque
2
+ from concurrent.futures import ThreadPoolExecutor, as_completed
3
+ from dataclasses import dataclass, astuple
4
+ from datetime import datetime, timezone
5
+ from io import BytesIO
6
+ from operator import attrgetter
7
+ import argparse
8
+ import json
9
+ import os.path
10
+ import random
11
+ import re
12
+ import sys
13
+ import uuid
14
+
15
+ from tqdm import tqdm
16
+ from warcio import WARCWriter, StatusAndHeaders
17
+ import httpx
18
+
19
+ @dataclass(frozen=True)
20
+ class Capture:
21
+ url: str
22
+ date: datetime
23
+ statusline: str
24
+ headers: list[tuple[str, str]]
25
+ content: bytes
26
+
27
+ @dataclass(frozen=True)
28
+ class CaptureMetadata:
29
+ urlkey: str
30
+ timestamp: str
31
+ original: str
32
+ mimetype: str
33
+ statuscode: str
34
+ digest: str
35
+ length: str
36
+
37
+ class PooledClient:
38
+ base_url = 'https://web.archive.org'
39
+ timeout = httpx.Timeout(30, connect=5, read=30)
40
+ max_retries = 30
41
+
42
+ def __init__(self, proxy_list: list[str | None]):
43
+ self.proxy_list = proxy_list
44
+ self.session_pool = deque()
45
+
46
+ def request(self, method: str, url: str, retry: int = 1, **kwargs):
47
+ try:
48
+ session = self.session_pool.popleft()
49
+ except IndexError:
50
+ proxy = random.choice(self.proxy_list)
51
+ session = httpx.Client(base_url=self.base_url, timeout=self.timeout, proxy=proxy)
52
+
53
+ try:
54
+ resp = session.request(method, url, **kwargs)
55
+ # we're blocked if this header is not present
56
+ # exempt error 400 from retries as the cause is usually url encoding issues on our part
57
+ assert 'x-app-server' in resp.headers or resp.status_code == 400
58
+ except:
59
+ session.close()
60
+ if retry > self.max_retries:
61
+ raise
62
+ return self.request(method, url, retry+1, **kwargs)
63
+ else:
64
+ self.session_pool.append(session)
65
+ return resp
66
+
67
+ def get_cdx_page_count(self, url: str):
68
+ resp = self.request('GET', '/cdx/search/cdx', params={
69
+ 'url': url,
70
+ 'showNumPages': True
71
+ })
72
+ return int(resp.text)
73
+
74
+ def get_cdx_page(self, url: str, page: int):
75
+ resp = self.request('GET', '/cdx/search/cdx', params={
76
+ 'url': url,
77
+ 'page': page,
78
+ 'output': 'json'
79
+ })
80
+ rows = resp.json()
81
+ return [CaptureMetadata(*row) for row in rows[1:]]
82
+
83
+ def get_capture(self, url: str, timestamp: str):
84
+ try:
85
+ resp = self.request('GET', f'/web/{timestamp}id_/{url}')
86
+ except:
87
+ # .request already handles retries
88
+ return
89
+
90
+ # capture is not available
91
+ if 'memento-datetime' not in resp.headers:
92
+ return
93
+
94
+ headers = [('content-length', f'{len(resp.content)}')]
95
+ for header, value in resp.headers.multi_items():
96
+ if header.startswith('x-archive-orig-'):
97
+ header = header[15:]
98
+ elif header == 'location':
99
+ value = re.sub(r'^(https?://web\.archive\.org)?/web/\d{14}\w*/', '', value)
100
+ elif header == 'content-type':
101
+ pass
102
+ else:
103
+ continue
104
+ if header not in ('content-length', 'content-encoding', 'transfer-encoding'):
105
+ headers.append((header, value))
106
+
107
+ try:
108
+ original = resp.links['original']['url']
109
+ except KeyError:
110
+ original = url
111
+ date = datetime.strptime(timestamp, '%Y%m%d%H%M%S').replace(tzinfo=timezone.utc)
112
+ statusline = f'HTTP/1.1 {resp.status_code} {resp.reason_phrase}'
113
+
114
+ return Capture(original, date, statusline, headers, resp.content)
115
+
116
+ def unordered_map(pool, fn, iterable):
117
+ return (future.result() for future in as_completed(pool.submit(fn, item) for item in iterable))
118
+
119
+ if __name__ == '__main__':
120
+ arg_parser = argparse.ArgumentParser(
121
+ description='WARC exporter for the Wayback Machine',
122
+ epilog=(
123
+ "capture fields: urlkey, timestamp, original, mimetype, statuscode, digest, length\n"
124
+ "\n"
125
+ "examples:\n"
126
+ "%(prog)s '*.example.org' --collapse 'lambda m: m.digest'\n"
127
+ "%(prog)s 'twitter.com/elonmusk/status/*' -c 'lambda m: m.urlkey.split(\"?\")[0]' -f 'lambda m: m.timestamp < \"2022\"'"
128
+ ),
129
+ formatter_class=argparse.RawDescriptionHelpFormatter
130
+ )
131
+ arg_parser.add_argument('url',
132
+ help='*.example.org or example.org/*')
133
+ arg_parser.add_argument('prefix', nargs='?', default='',
134
+ help='output path prefix')
135
+ arg_parser.add_argument('-c', '--collapse', type=eval,
136
+ help='dedupe records based on returned key')
137
+ arg_parser.add_argument('-f', '--filter', type=eval,
138
+ help='filter records based on returned boolean')
139
+ arg_parser.add_argument('-m', '--meta', action='store_true',
140
+ help='dump cdx metadata into jsonl file')
141
+ arg_parser.add_argument('-p', '--proxy',
142
+ help='url formatted proxy, can be a single value or a file')
143
+ arg_parser.add_argument('-t', '--threads', type=int, default=2,
144
+ help='concurrent downloads (default: %(default)s)')
145
+ arg_parser.add_argument('-w', '--warc-size', type=int, default=1024,
146
+ help='max size of produced warc files in MB (default: %(default)s)')
147
+ args = arg_parser.parse_args()
148
+
149
+ if not args.proxy:
150
+ proxy_list = [None]
151
+ elif os.path.exists(args.proxy):
152
+ with open(args.proxy) as file:
153
+ proxy_list = file.read().splitlines()
154
+ else:
155
+ proxy_list = [args.proxy]
156
+
157
+ client = PooledClient(proxy_list)
158
+ pages = client.get_cdx_page_count(args.url)
159
+ pool = ThreadPoolExecutor(max_workers=args.threads)
160
+ queue: list[CaptureMetadata] = []
161
+
162
+ with tqdm(total=pages, desc='listing cdx') as progress:
163
+ keys = set()
164
+
165
+ for chunk in unordered_map(pool, lambda page: client.get_cdx_page(args.url, page), range(pages)):
166
+ for meta in chunk:
167
+ if int(meta.length) > 1024**2 * 100:
168
+ print(f'skipping large file: web.archive.org/web/{meta.timestamp}/{meta.original}', file=sys.stderr)
169
+ continue
170
+
171
+ if args.filter and not args.filter(meta):
172
+ continue
173
+
174
+ if args.collapse:
175
+ key = args.collapse(meta)
176
+ if key in keys:
177
+ continue
178
+ keys.add(key)
179
+
180
+ queue.append(meta)
181
+
182
+ progress.set_postfix({'queued': len(queue)})
183
+ progress.update()
184
+
185
+ del keys
186
+
187
+ queue.sort(key=attrgetter('timestamp'))
188
+
189
+ if args.meta:
190
+ with open(f'{args.prefix}meta.jsonl', 'w') as file:
191
+ for meta in queue:
192
+ file.write(json.dumps(astuple(meta)) + '\n')
193
+ sys.exit()
194
+
195
+ with tqdm(total=len(queue), desc='downloading captures') as progress:
196
+ file = None
197
+ skipped = 0
198
+
199
+ for capture in unordered_map(pool, lambda meta: client.get_capture(meta.original, meta.timestamp), queue):
200
+ if not capture:
201
+ skipped += 1
202
+ progress.set_postfix({'skipped': skipped})
203
+ progress.update()
204
+ continue
205
+
206
+ if not file or file.tell() > 1024**2 * args.warc_size:
207
+ if file:
208
+ file.close()
209
+ file = open(f'{args.prefix}{uuid.uuid4()}.warc.gz', 'xb')
210
+ writer = WARCWriter(file, gzip=True)
211
+
212
+ record = writer.create_warc_record(
213
+ uri=capture.url,
214
+ record_type='response',
215
+ payload=BytesIO(capture.content),
216
+ warc_headers_dict={'WARC-Date': capture.date.isoformat().replace('+00:00', 'Z')},
217
+ http_headers=StatusAndHeaders(capture.statusline, capture.headers)
218
+ )
219
+ deterministic_uuid = uuid.uuid5(uuid.NAMESPACE_URL, f'{capture.url}|{capture.date.isoformat()}')
220
+ record.rec_headers.replace_header('WARC-Record-ID', f'<urn:uuid:{deterministic_uuid}>')
221
+ writer.write_record(record)
222
+ progress.update()
223
+
224
+ if file:
225
+ file.close()
226
+
227
+ pool.shutdown()