Yield one parsed object per line; skip blank/unparseable lines.
(self)
| 273 | return self._iterate() |
| 274 | |
| 275 | def _iterate(self) -> Iterator[Any]: |
| 276 | """Yield one parsed object per line; skip blank/unparseable lines.""" |
| 277 | try: |
| 278 | handle = open(self._path, "rb") |
| 279 | except FileNotFoundError: |
| 280 | return |
| 281 | try: |
| 282 | for raw_line in handle: |
| 283 | # raw_line still has its trailing newline; strip and skip |
| 284 | # blanks. Decode is utf-8 with replacement so a corrupt |
| 285 | # byte doesn't crash the iterator. |
| 286 | # |
| 287 | # N2 caveat (Chunk-D fold-in): ``errors="replace"`` maps |
| 288 | # corrupt bytes to U+FFFD. In theory a corrupted line |
| 289 | # could still parse as JSON if the U+FFFD substitutions |
| 290 | # land inside string literals — yielding "garbage but |
| 291 | # technically valid JSON". For chapter-10 transcripts |
| 292 | # the writer is the only producer and uses utf-8 |
| 293 | # round-trip, so the regime is safe; downstream replay |
| 294 | # consumers should validate message shape post-parse. |
| 295 | line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n") |
| 296 | if not line: |
| 297 | continue |
| 298 | try: |
| 299 | yield json.loads(line) |
| 300 | except json.JSONDecodeError: |
| 301 | if not self._logged_partial: |
| 302 | # Log once per reader instance — a corrupt |
| 303 | # transcript shouldn't spam the log. |
| 304 | logger.warning( |
| 305 | "skipping unparseable transcript line in %s " |
| 306 | "(file may have a trailing partial-write)", |
| 307 | self._path, |
| 308 | ) |
| 309 | self._logged_partial = True |
| 310 | continue |
| 311 | finally: |
| 312 | handle.close() |
| 313 | |
| 314 | def read_all(self) -> list[Any]: |
| 315 | """Materialize every parseable line into a list.""" |