"""Repackage official Waitress 3.0.2 metadata for the tested Python 3.8 build. The HTTP server and security fixes are kept byte-for-byte. Only the CLI name resolver gets a Python 3.8 fallback. The local version identifies the adaptation. """ import base64 import csv import hashlib import io from pathlib import Path import zipfile UPSTREAM_SHA256 = 'c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e' VERSION = '3.0.2+win7.1' def repackage(source, destination): source, destination = Path(source), Path(destination) if hashlib.sha256(source.read_bytes()).hexdigest() != UPSTREAM_SHA256: raise ValueError('Unexpected upstream Waitress wheel checksum') old_info, new_info = 'waitress-3.0.2.dist-info', 'waitress-%s.dist-info' % VERSION destination.mkdir(parents=True, exist_ok=True) target = destination / ('waitress-%s-py3-none-any.whl' % VERSION) records = [] with zipfile.ZipFile(source) as src, zipfile.ZipFile(target, 'w', zipfile.ZIP_DEFLATED) as out: for name in src.namelist(): if name.endswith('/RECORD'): continue content = src.read(name) new_name = name.replace(old_info, new_info) if name == 'waitress/runner.py': assert b'import pkgutil\n' in content and b'pkgutil.resolve_name(args[0])' in content content = content.replace(b'import pkgutil\n', b'try:\n from pkgutil import resolve_name\nexcept ImportError:\n from pkgutil_resolve_name import resolve_name\n') content = content.replace(b'pkgutil.resolve_name(args[0])', b'resolve_name(args[0])') if name == old_info + '/METADATA': content = content.replace(b'Version: 3.0.2\n', ('Version: %s\n' % VERSION).encode()) content = content.replace(b'Requires-Python: >=3.9.0\n', b'Requires-Python: >=3.8.0\nRequires-Dist: pkgutil-resolve-name==1.3.10; python_version < "3.9"\n') assert b'Requires-Python: >=3.8.0\n' in content out.writestr(new_name, content) digest = base64.urlsafe_b64encode(hashlib.sha256(content).digest()).rstrip(b'=').decode() records.append((new_name, 'sha256=' + digest, str(len(content)))) records.append((new_info + '/RECORD', '', '')) record = io.StringIO(newline='') csv.writer(record, lineterminator='\n').writerows(records) out.writestr(new_info + '/RECORD', record.getvalue()) with zipfile.ZipFile(source) as src, zipfile.ZipFile(target) as dest: assert all(src.read(name) == dest.read(name) for name in src.namelist() if name.startswith('waitress/') and name != 'waitress/runner.py') return target if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('source', type=Path) parser.add_argument('destination', type=Path) args = parser.parse_args() print(repackage(args.source, args.destination))