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 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 52 with zipfile.ZipFile(zip_path) as zf:
42 53 for info in zf.infolist():
43 54 if info.is_dir():
@@ -53,6 +64,36 @@ def iter_archive_files(zip_path: Path) -> Iterator[tuple[str, bytes]]:
53 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 97 def parse_bar_file(data: bytes, symbol: str, asset_class: AssetClass, interval: Interval, adjustment: Adjustment,
57 98 source_id: str, ingested_at: datetime, source_tz: str = "America/New_York",
58 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 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 125 assert live["n"] == 2 and live["rows"] == 3
126 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