SPB Git

spb/findata

Public

archive: 7z fallback for Deflate64 zips (option chains ≥ 2023)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 2, 2026) parent 818844d Browse files

2 changed files +61 −1

modified connectors/market/src/findata_market_firstrate/archive.py +42 −1
@@ -37,7 +37,18 @@ def ticker_from_filename(name: str, contract: bool = False) -> str:
37 37
38 38
39 def iter_archive_files(zip_path: Path) -> Iterator[tuple[str, bytes]]: 39 def iter_archive_files(zip_path: Path) -> Iterator[tuple[str, bytes]]:
40− """Yield (member_name, bytes) for every data file, descending into nested zips.""" 40+ """Yield (member_name, bytes) for every data file, descending into nested zips.
41+
42+ Some vendor archives (option chains from 2023 on) use Deflate64, which Python's zipfile cannot decompress
43+ (`NotImplementedError: That compression method is not supported`). In that case the archive is extracted with
44+ the system `7z` into a temporary directory next to the archive and the files are streamed from disk."""
45+ try:
46+ yield from _iter_zip_members(zip_path)
47+ except NotImplementedError:
48+ yield from _iter_via_7z(zip_path)
49+
50+
51+def _iter_zip_members(zip_path: Path) -> Iterator[tuple[str, bytes]]:
41 with zipfile.ZipFile(zip_path) as zf: 52 with zipfile.ZipFile(zip_path) as zf:
42 for info in zf.infolist(): 53 for info in zf.infolist():
43 if info.is_dir(): 54 if info.is_dir():
@@ -53,6 +64,36 @@ def iter_archive_files(zip_path: Path) -> Iterator[tuple[str, bytes]]:
53 yield name, zf.read(info) 64 yield name, zf.read(info)
54 65
55 66
67+def _find_7z() -> str | None:
68+ import shutil
69+ for cand in ("7zz", "7z", "7za"):
70+ p = shutil.which(cand) or shutil.which(cand, path="/opt/homebrew/bin:/usr/local/bin")
71+ if p:
72+ return p
73+ return None
74+
75+
76+def _iter_via_7z(zip_path: Path) -> Iterator[tuple[str, bytes]]:
77+ import shutil
78+ import subprocess
79+ import tempfile
80+ tool = _find_7z()
81+ if not tool:
82+ raise RuntimeError(f"{zip_path.name}: unsupported zip compression (Deflate64) and no 7z binary found (brew install p7zip)")
83+ tmp = Path(tempfile.mkdtemp(prefix="unz-", dir=str(zip_path.parent)))
84+ try:
85+ r = subprocess.run([tool, "x", "-y", "-bd", "-bso0", "-bsp0", f"-o{tmp}", str(zip_path)], capture_output=True, text=True, timeout=6 * 3600)
86+ if r.returncode not in (0, 1): # 1 = warnings
87+ raise RuntimeError(f"7z extraction failed for {zip_path.name}: {r.stderr[-300:]}")
88+ for p in sorted(tmp.rglob("*")):
89+ if p.is_file() and p.suffix.lower() in (".txt", ".csv"):
90+ yield str(p.relative_to(tmp)), p.read_bytes()
91+ elif p.is_file() and p.suffix.lower() == ".zip":
92+ yield from ((f"{p.name}!/{n}", b) for n, b in iter_archive_files(p))
93+ finally:
94+ shutil.rmtree(tmp, ignore_errors=True)
95+
96+
56 def parse_bar_file(data: bytes, symbol: str, asset_class: AssetClass, interval: Interval, adjustment: Adjustment, 97 def parse_bar_file(data: bytes, symbol: str, asset_class: AssetClass, interval: Interval, adjustment: Adjustment,
57 source_id: str, ingested_at: datetime, source_tz: str = "America/New_York", 98 source_id: str, ingested_at: datetime, source_tz: str = "America/New_York",
58 currency: str = "USD", venue: str | None = None, revision_id: int = 1) -> tuple[pa.Table, int]: 99 currency: str = "USD", venue: str | None = None, revision_id: int = 1) -> tuple[pa.Table, int]:
modified tests/test_full_catalog.py +19 −0
@@ -124,3 +124,22 @@ def test_options_task_with_vendor_mock(vendor, lake, log, monkeypatch):
124 live = conn.execute("SELECT count(*) AS n, sum(row_count) AS rows FROM lake_files WHERE dataset_id='options.eod' AND version_removed IS NULL").fetchone() 124 live = conn.execute("SELECT count(*) AS n, sum(row_count) AS rows FROM lake_files WHERE dataset_id='options.eod' AND version_removed IS NULL").fetchone()
125 assert live["n"] == 2 and live["rows"] == 3 125 assert live["n"] == 2 and live["rows"] == 3
126 vendor.archive_override = None 126 vendor.archive_override = None
127+
128+
129+def test_deflate64_fallback_uses_7z(tmp_path, monkeypatch):
130+ import zipfile as zf_mod
131+
132+ from findata_market_firstrate import archive as A
133+ z = tmp_path / "a.zip"
134+ with zf_mod.ZipFile(z, "w") as zf:
135+ zf.writestr("X_option_chain.txt", "2026-10-01,33,2026-10-16,c,1,1,1,0,0,0,0,0,0,0,0,0\n")
136+ # simulate Python's zipfile refusing the compression method
137+ def boom(path):
138+ raise NotImplementedError("That compression method is not supported")
139+ yield # pragma: no cover
140+ monkeypatch.setattr(A, "_iter_zip_members", boom)
141+ if not A._find_7z():
142+ pytest.skip("7z not installed")
143+ out = list(A.iter_archive_files(z))
144+ assert out == [("X_option_chain.txt", b"2026-10-01,33,2026-10-16,c,1,1,1,0,0,0,0,0,0,0,0,0\n")]
145+ assert not list(tmp_path.glob("unz-*")) # temp dir cleaned
127 146
Menu

Theme