feat: add streaming of rdf xml dataset with resume functionality #44
@@ -32,6 +32,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
@@ -42,6 +43,7 @@ import time
|
||||
import zipfile
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
@@ -51,7 +53,19 @@ from huggingface_hub import HfApi, hf_hub_download
|
||||
from rdflib import Graph, Literal, URIRef
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from src.streaming.extract_stream import (
|
||||
|
||||
# Ensure project root is on sys.path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from convert_rdf_to_hf_dataset_streaming_parallel import stream_rdf_chunks # noqa: E402
|
||||
from dataset_registry import ( # noqa: E402
|
||||
DATASET_REGISTRY,
|
||||
DatasetInfo,
|
||||
get_dataset_config,
|
||||
)
|
||||
from src.streaming.extract_stream import ( # noqa: E402
|
||||
ExtractConfig,
|
||||
_infer_name_from_url,
|
||||
iter_decompressed_bytes,
|
||||
@@ -60,12 +74,7 @@ from src.streaming.extract_stream import (
|
||||
spool_zip_to_tempfile,
|
||||
detect_compression_format,
|
||||
)
|
||||
from src.streaming.http_stream import HTTPStreamConfig, stream_http_bytes
|
||||
|
||||
# Ensure project root is on sys.path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
from src.streaming.http_stream import HTTPStreamConfig, stream_http_bytes # noqa: E402
|
||||
|
||||
|
||||
# Setup logger
|
||||
@@ -135,19 +144,17 @@ def stream_turtle_chunks_from_lines(
|
||||
|
||||
current_chunk: list[str] = []
|
||||
triple_count = 0
|
||||
line_count = 0
|
||||
prefix_lines: list[str] = []
|
||||
in_prefixes = True
|
||||
|
||||
for line in lines:
|
||||
line_count += 1
|
||||
for line_count, line in enumerate(lines, start=1):
|
||||
stripped = line.strip()
|
||||
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
|
||||
if (in_prefixes
|
||||
and (stripped.startswith("@prefix") or stripped.startswith("@base"))):
|
||||
is_prefix = stripped.startswith("@prefix") or stripped.startswith("@base")
|
||||
if in_prefixes and is_prefix:
|
||||
prefix_lines.append(line)
|
||||
continue
|
||||
elif in_prefixes:
|
||||
@@ -158,17 +165,16 @@ def stream_turtle_chunks_from_lines(
|
||||
|
||||
if triple_count >= chunk_size:
|
||||
chunk_text = (
|
||||
"".join(prefix_lines)
|
||||
+ "\n"
|
||||
+ "".join(current_chunk)
|
||||
+ "\n"
|
||||
+ line
|
||||
"".join(prefix_lines) + "\n" +
|
||||
"".join(current_chunk) + "\n" + line
|
||||
)
|
||||
try:
|
||||
graph = Graph()
|
||||
graph.parse(data=chunk_text, format="turtle")
|
||||
triples = [_convert_rdf_triple_to_dict(s, p, o)
|
||||
for s, p, o in graph]
|
||||
triples = [
|
||||
_convert_rdf_triple_to_dict(s, p, o)
|
||||
for s, p, o in graph
|
||||
]
|
||||
if triples:
|
||||
yield triples
|
||||
except Exception as e:
|
||||
@@ -227,6 +233,179 @@ def stream_ntriples_from_lines(lines: Iterator[str]) -> Iterator[list[dict]]:
|
||||
yield batch
|
||||
|
||||
|
||||
class _LineIteratorReader(io.RawIOBase):
|
||||
"""Adapter to convert line iterator to file-like object for XML parsing."""
|
||||
|
||||
def __init__(self, lines: Iterator[str]):
|
||||
self._lines = lines
|
||||
self._buffer = b''
|
||||
self._exhausted = False
|
||||
|
||||
def readable(self) -> bool:
|
||||
return True
|
||||
|
||||
def readinto(self, b): # type: ignore[override]
|
||||
"""Read bytes from line iterator into buffer."""
|
||||
if self._exhausted and not self._buffer:
|
||||
return 0
|
||||
|
||||
# Fill buffer from line iterator
|
||||
while len(self._buffer) < len(b) and not self._exhausted:
|
||||
try:
|
||||
line = next(self._lines)
|
||||
self._buffer += line.encode('utf-8')
|
||||
except StopIteration:
|
||||
self._exhausted = True
|
||||
break
|
||||
|
||||
# Copy buffer to output
|
||||
n = min(len(self._buffer), len(b))
|
||||
b[:n] = self._buffer[:n]
|
||||
self._buffer = self._buffer[n:]
|
||||
return n
|
||||
|
||||
|
||||
def _wrap_description_xml(description_xml: str) -> str:
|
||||
"""Wrap a Description element in a minimal valid XML document.
|
||||
|
||||
ElementTree preserves namespace URIs when serializing elements, so we
|
||||
only need to provide the minimal RDF namespace declaration. Additional
|
||||
namespaces are already included in the serialized element.
|
||||
|
|
||||
|
||||
Args:
|
||||
description_xml: Serialized Description element
|
||||
|
||||
Returns:
|
||||
Complete XML document string
|
||||
"""
|
||||
# Minimal RDF/XML wrapper - ElementTree preserves other namespaces
|
||||
wrapped = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n'
|
||||
f'{description_xml}\n'
|
||||
'</rdf:RDF>'
|
||||
)
|
||||
return wrapped
|
||||
|
||||
|
||||
def _parse_description_xml(xml_content: str) -> list[dict]:
|
||||
"""Parse a Description XML fragment and extract all triples.
|
||||
|
||||
Args:
|
||||
xml_content: Complete XML document containing one Description element
|
||||
|
||||
Returns:
|
||||
List of triple dictionaries
|
||||
"""
|
||||
try:
|
||||
graph = Graph()
|
||||
graph.parse(data=xml_content, format="xml")
|
||||
|
||||
# Extract all triples from this Description
|
||||
triples = [_convert_rdf_triple_to_dict(s, p, o) for s, p, o in graph]
|
||||
return triples
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing Description XML: {e}")
|
||||
logger.debug(f"XML content (first 500 chars): {xml_content[:500]}")
|
||||
return [] # Return empty list on error, don't crash
|
||||
|
||||
|
||||
def stream_rdfxml_chunks_from_lines(
|
||||
lines: Iterator[str],
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
It's more Pythonic to combine lines 313-314 as: especially since you don't use It's more Pythonic to combine lines 313-314 as:
```
for line_count, line in enumerate(lines)
```
especially since you don't use `line_count` outside the loop.
aditya
commented
Fixed !! Fixed !!
|
||||
chunk_size: int = DEFAULT_BATCH_SIZE,
|
||||
) -> Iterator[list[dict]]:
|
||||
"""Stream RDF/XML format from a line iterator using standard XML parser.
|
||||
|
||||
Uses xml.etree.ElementTree.iterparse for robust XML parsing that handles:
|
||||
- XML comments, CDATA, processing instructions
|
||||
- Multi-line elements and attributes
|
||||
- Nested elements with correct depth tracking
|
||||
- All XML edge cases per W3C specification
|
||||
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
I'm VERY, VERY nervous when we try to do our own parsing of something as complex as https://www.w3.org/TR/2006/REC-xml11-20060816/ . For example, lines 322 parses correctly-formed XML files... but it also allows XML files that incorrectly have multiple prologs. We should use a real XML parser. I'm VERY, VERY nervous when we try to do our own parsing of something as complex as https://www.w3.org/TR/2006/REC-xml11-20060816/ .
For example, lines 322 parses correctly-formed XML files... but it also allows XML files that incorrectly have multiple prologs.
We should use a real XML parser.
aditya
commented
Fixed !! Fixed !!
|
||||
Args:
|
||||
lines: Iterator yielding text lines of RDF/XML data
|
||||
chunk_size: Number of Description elements to accumulate before yielding
|
||||
|
||||
Yields:
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
I'm very nervous about line 327-329. This will definitely ONLY work with uniprot files. I recommend using a real XML parser and asking it to read the namespace. Especially since this code wouldn't work if the I'm very nervous about line 327-329.
This will definitely ONLY work with uniprot files.
I recommend using a real XML parser and asking it to read the namespace. Especially since this code wouldn't work if the `rdf:RDF` namespace were split over several lines.
aditya
commented
Fixed !! Fixed !!
|
||||
Lists of triple dictionaries
|
||||
"""
|
||||
console = Console()
|
||||
console.print("[yellow]Streaming RDF/XML with standard XML parser[/yellow]")
|
||||
|
||||
# Convert line iterator to file-like object for XML parser
|
||||
file_obj = io.BufferedReader(_LineIteratorReader(lines))
|
||||
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
Why the Why the `rstrip()`? Line 315 writes that it was already stripped.
aditya
commented
Fixed !! Fixed !!
|
||||
triple_batch: list[dict] = []
|
||||
batch_description_count = 0
|
||||
total_descriptions = 0
|
||||
|
||||
try:
|
||||
# Use iterparse to process XML incrementally
|
||||
# Get 'end' events when elements are complete
|
||||
for _event, elem in ET.iterparse(file_obj, events=('end',)):
|
||||
# Process complete Description elements
|
||||
if _is_description_element(elem):
|
||||
total_descriptions += 1
|
||||
|
||||
# Serialize element to string (preserves namespaces)
|
||||
description_xml = ET.tostring(elem, encoding='unicode')
|
||||
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
Lines 341-352 and lines 385-401 are very similar. Instead of duplicating, could they be turned into a method? Lines 341-352 and lines 385-401 are very similar. Instead of duplicating, could they be turned into a method?
aditya
commented
Fixed !! Fixed !!
|
||||
# Wrap for rdflib parsing
|
||||
wrapped_xml = _wrap_description_xml(description_xml)
|
||||
|
||||
# Parse and extract triples
|
||||
triples = _parse_description_xml(wrapped_xml)
|
||||
if triples:
|
||||
triple_batch.extend(triples)
|
||||
batch_description_count += 1
|
||||
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
scripts/rdf_to_hf_incremental.py:361:89: E501 Line too long (100 > 88)
|
359 | # Count opening/closing tags to track depth correctly
360 | # Opening tags: <tag> or <tag ...> (not </tag or <tag/>)
361 | # Find all <tag patterns (this excludes </tag> because regex requires [a-zA-Z_] after <)
| ^^^^^^^^^^^^ E501
362 | all_tag_starts = re.findall(r'<([a-zA-Z_][\w:.-]*)', stripped)
363 | # Closing tags: </tag>
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
# Clear element to free memory
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
Lines 362, 364, and 367 count tags that happen inside comments. XML is complex. Really. Please fix this by using a real XML parser instead of trying to parse XML directly. Lines 362, 364, and 367 count tags that happen inside comments.
XML is complex. Really. Please fix this by using a real XML parser instead of trying to parse XML directly.
|
||||
elem.clear()
|
||||
|
||||
# Yield when chunk_size reached
|
||||
if batch_description_count >= chunk_size and triple_batch:
|
||||
yield triple_batch
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
scripts/rdf_to_hf_incremental.py:367:89: E501 Line too long (89 > 88)
|
365 | # Self-closing tags: <tag ... /> (these don't change depth)
366 | # Find tags that end with /> before the next >
367 | self_closing_tags = len(re.findall(r'<([a-zA-Z_][\w:.-]*)[^>]*/>', stripped))
| ^ E501
368 |
369 | # Opening tags = all tag starts - self-closing tags
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
triple_batch = []
|
||||
batch_description_count = 0
|
||||
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
scripts/rdf_to_hf_incremental.py:370:89: E501 Line too long (94 > 88)
|
369 | # Opening tags = all tag starts - self-closing tags
370 | # (all_tag_starts already excludes closing tags, so we only subtract self-closing)
| ^^^^^^ E501
371 | opening_tags = len(all_tag_starts) - self_closing_tags
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
# Progress logging every N descriptions
|
||||
if total_descriptions % PROGRESS_LOG_INTERVAL == 0:
|
||||
console.print(
|
||||
f"[dim]Processed {total_descriptions:,} "
|
||||
f"Description elements...[/dim]"
|
||||
)
|
||||
|
||||
except ET.ParseError as e:
|
||||
logger.error(f"XML parsing error: {e}")
|
||||
logger.error("This may indicate malformed XML in the source file")
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
scripts/rdf_to_hf_incremental.py:378:13: SIM102 Use a single `if` statement instead of nested `if` statements
|
377 | # Check if Description element is closed
378 | / if "</rdf:Description>" in stripped:
379 | | # The closing tag was already accounted for in closing_tags above
380 | | if depth == 0:
| |______________________________^ SIM102
381 | # Complete Description element found
382 | in_description = False
|
= help: Combine `if` statements using `and`
```
aditya
commented
Fixed !! Fixed !!
|
||||
|
||||
# Yield final batch
|
||||
if triple_batch:
|
||||
yield triple_batch
|
||||
|
||||
console.print(
|
||||
f"[green]Completed: processed {total_descriptions:,} "
|
||||
f"Description elements[/green]"
|
||||
)
|
||||
|
||||
|
||||
def _is_description_element(elem: ET.Element) -> bool:
|
||||
"""Check if element is an rdf:Description element.
|
||||
|
||||
Args:
|
||||
elem: XML element
|
||||
|
||||
Returns:
|
||||
True if element is rdf:Description
|
||||
"""
|
||||
# Handle both namespaced and non-namespaced forms
|
||||
tag = elem.tag
|
||||
return (
|
||||
tag.endswith('Description') and
|
||||
('rdf' in tag.lower() or 'Description' in tag)
|
||||
)
|
||||
|
||||
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
scripts/rdf_to_hf_incremental.py:408:89: E501 Line too long (99 > 88)
|
406 | if line_count % PROGRESS_LOG_INTERVAL == 0:
407 | console.print(
408 | f"[dim]Processed {line_count:,} lines, {description_count:,} descriptions...[/dim]"
| ^^^^^^^^^^^ E501
409 | )
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
def stream_rdf_chunks_from_lines(
|
||||
lines: Iterator[str],
|
||||
format: str = "turtle",
|
||||
@@ -236,7 +415,8 @@ def stream_rdf_chunks_from_lines(
|
||||
|
||||
Args:
|
||||
lines: Iterator yielding text lines of RDF data
|
||||
format: RDF serialization format (supported: nt, ntriples, turtle, ttl)
|
||||
format: RDF serialization format
|
||||
(supported: nt, ntriples, turtle, ttl, xml, rdf, rdfxml)
|
||||
chunk_size: Number of triples per chunk for Turtle parsing
|
||||
|
||||
Yields:
|
||||
@@ -249,10 +429,12 @@ def stream_rdf_chunks_from_lines(
|
||||
yield from stream_ntriples_from_lines(lines)
|
||||
elif format in ("turtle", "ttl"):
|
||||
yield from stream_turtle_chunks_from_lines(lines, chunk_size=chunk_size)
|
||||
elif format in ("xml", "rdf", "rdfxml"):
|
||||
yield from stream_rdfxml_chunks_from_lines(lines, chunk_size=chunk_size)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Streaming line-iterator parser not implemented for format: {format}. "
|
||||
f"Supported formats: nt, ntriples, turtle, ttl"
|
||||
f"Supported formats: nt, ntriples, turtle, ttl, xml, rdf, rdfxml"
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
scripts/rdf_to_hf_incremental.py:437:89: E501 Line too long (97 > 88)
|
435 | Args:
436 | lines: Iterator yielding text lines of RDF data
437 | format: RDF serialization format (supported: nt, ntriples, turtle, ttl, xml, rdf, rdfxml)
| ^^^^^^^^^ E501
438 | chunk_size: Number of triples per chunk for Turtle parsing
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
)
|
||||
|
||||
|
||||
@@ -261,9 +443,11 @@ def stream_rdf_chunks_from_lines(
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _pick_zip_member(zf: zipfile.ZipFile,
|
||||
*,
|
||||
prefer_exts: tuple[str, ...] = (".ttl", ".nt")) -> str:
|
||||
def _pick_zip_member(
|
||||
zf: zipfile.ZipFile,
|
||||
*,
|
||||
prefer_exts: tuple[str, ...] = (".ttl", ".nt", ".rdf", ".xml"),
|
||||
) -> str:
|
||||
"""Select the best RDF member file from a ZIP archive."""
|
||||
candidates = []
|
||||
for info in zf.infolist():
|
||||
@@ -314,15 +498,15 @@ def open_rdf_line_stream_from_url(
|
||||
|
||||
# TAR archives
|
||||
if lower.endswith((".tar.gz", ".tgz", ".tar")):
|
||||
for member_name, member_bytes in iter_tar_member_bytes(name, byte_iter):
|
||||
if member_name.lower().endswith((".ttl", ".nt")):
|
||||
for member_name, member_bytes in iter_tar_member_bytes(byte_iter, tar_name=name):
|
||||
if member_name.lower().endswith((".ttl", ".nt", ".rdf", ".xml")):
|
||||
lines = iter_text_lines(
|
||||
member_bytes,
|
||||
encoding=extract_cfg.encoding,
|
||||
errors=extract_cfg.errors
|
||||
errors=extract_cfg.errors,
|
||||
)
|
||||
return lines, f"{name}:{member_name}"
|
||||
raise ValueError("No .ttl/.nt member found in tar archive stream.")
|
||||
raise ValueError("No .ttl/.nt/.rdf/.xml member found in tar archive stream.")
|
||||
|
||||
# ZIP archives
|
||||
if lower.endswith(".zip"):
|
||||
@@ -342,15 +526,11 @@ def open_rdf_line_stream_from_url(
|
||||
os.remove(tmp_path)
|
||||
|
||||
# Compressed files and plain text
|
||||
compression_format = detect_compression_format(name)
|
||||
|
||||
decompressed = iter_decompressed_bytes(
|
||||
byte_iter = byte_iter,
|
||||
)
|
||||
decompressed = iter_decompressed_bytes(byte_iter, compressed_name=name)
|
||||
lines = iter_text_lines(
|
||||
decompressed,
|
||||
encoding=extract_cfg.encoding,
|
||||
errors=extract_cfg.errors
|
||||
errors=extract_cfg.errors,
|
||||
)
|
||||
return lines, name
|
||||
|
||||
@@ -415,22 +595,23 @@ def create_rdf_chunk_iterator(
|
||||
if input_type == "file":
|
||||
# Local file mode: use optimized file-based streaming with multiprocessing
|
||||
console.print("[cyan]Mode: Local file streaming[/cyan]")
|
||||
console.print(f"[dim]Using parallel file-based parser ({num_workers} "
|
||||
"workers)[/dim]")
|
||||
console.print(
|
||||
f"[dim]Using parallel file-based parser "
|
||||
f"({num_workers} workers)[/dim]"
|
||||
)
|
||||
file_path = Path(input_value)
|
||||
chunk_iter = stream_rdf_chunks(
|
||||
file_path,
|
||||
rdf_format,
|
||||
DEFAULT_BATCH_SIZE,
|
||||
num_workers
|
||||
file_path, rdf_format, DEFAULT_BATCH_SIZE, num_workers
|
||||
)
|
||||
return chunk_iter, f"file:{file_path.name}"
|
||||
|
||||
elif input_type == "registry":
|
||||
# Streaming download mode: use line-based streaming
|
||||
console.print("[cyan]Mode: Streaming download from registry[/cyan]")
|
||||
console.print("[dim]Using line-based streaming parser (no "
|
||||
"multiprocessing)[/dim]")
|
||||
console.print(
|
||||
"[dim]Using line-based streaming parser "
|
||||
"(no multiprocessing)[/dim]"
|
||||
)
|
||||
|
||||
ds = DATASET_REGISTRY[input_value]
|
||||
url = ds.url
|
||||
@@ -448,9 +629,7 @@ def create_rdf_chunk_iterator(
|
||||
console.print(f"[green]✓ Streaming source ready:[/green] {source_name}")
|
||||
|
||||
chunk_iter = stream_rdf_chunks_from_lines(
|
||||
lines,
|
||||
format=rdf_format,
|
||||
chunk_size=DEFAULT_BATCH_SIZE
|
||||
lines, format=rdf_format, chunk_size=DEFAULT_BATCH_SIZE
|
||||
)
|
||||
return chunk_iter, source_name
|
||||
|
||||
@@ -555,9 +734,12 @@ def convert_and_upload_incremental(
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
# Check if it's a rate limit error (429)
|
||||
if ("429" in error_str
|
||||
or "too many requests" in error_str
|
||||
or "rate limit" in error_str):
|
||||
is_rate_limit = (
|
||||
"429" in error_str
|
||||
or "too many requests" in error_str
|
||||
or "rate limit" in error_str
|
||||
)
|
||||
if is_rate_limit:
|
||||
if attempt < max_retries - 1:
|
||||
# Exponential backoff with jitter
|
||||
delay = min(base_delay * (2 ** attempt), max_delay)
|
||||
@@ -572,8 +754,10 @@ def convert_and_upload_incremental(
|
||||
time.sleep(wait_time)
|
||||
buffer.seek(0) # Reset buffer position for retry
|
||||
else:
|
||||
console.print("[red]✗ Max retries reached for shard "
|
||||
f"{current_shard_idx}[/red]")
|
||||
console.print(
|
||||
f"[red]✗ Max retries reached for "
|
||||
f"shard {current_shard_idx}[/red]"
|
||||
)
|
||||
raise
|
||||
else:
|
||||
# Not a rate limit error, re-raise immediately
|
||||
@@ -625,8 +809,10 @@ def convert_and_upload_incremental(
|
||||
|
||||
ensure_writer()
|
||||
finalize_and_upload(shard_idx, shard_rows)
|
||||
console.print("[green]✓ Uploaded shard "
|
||||
f"{shard_idx} ({shard_rows:,} rows)[/green]")
|
||||
console.print(
|
||||
f"[green]✓ Uploaded shard {shard_idx} "
|
||||
f"({shard_rows:,} rows)[/green]"
|
||||
)
|
||||
# Add small delay to avoid overwhelming HuggingFace API
|
||||
time.sleep(1.0)
|
||||
|
||||
@@ -634,8 +820,10 @@ def convert_and_upload_incremental(
|
||||
shard_rows = 0
|
||||
|
||||
if max_shards is not None and shard_idx >= max_shards:
|
||||
console.print("[yellow]Stopping after "
|
||||
f"max_shards={max_shards}[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]Stopping after "
|
||||
f"max_shards={max_shards}[/yellow]"
|
||||
)
|
||||
break
|
||||
|
||||
if max_shards is not None and shard_idx >= max_shards:
|
||||
@@ -651,8 +839,10 @@ def convert_and_upload_incremental(
|
||||
|
||||
ensure_writer()
|
||||
finalize_and_upload(shard_idx, shard_rows)
|
||||
console.print("[green]✓ Uploaded final shard "
|
||||
f"{shard_idx} ({shard_rows:,} rows)[/green]")
|
||||
console.print(
|
||||
f"[green]✓ Uploaded final shard {shard_idx} "
|
||||
f"({shard_rows:,} rows)[/green]"
|
||||
)
|
||||
shard_idx += 1
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
@@ -703,11 +893,15 @@ def list_datasets() -> None:
|
||||
for info in DATASET_REGISTRY.values():
|
||||
by_category[info.category] = by_category.get(info.category, 0) + 1
|
||||
|
||||
console.print(f"\n[bold]Total:[/bold] {total} datasets "
|
||||
f"({available_count} available)")
|
||||
console.print(
|
||||
f"\n[bold]Total:[/bold] {total} datasets "
|
||||
f"({available_count} available)"
|
||||
)
|
||||
console.print("[bold]By category:[/bold] ", end="")
|
||||
console.print(", ".join(f"{cat}: {count}"
|
||||
for cat, count in sorted(by_category.items())))
|
||||
cat_list = ", ".join(
|
||||
f"{cat}: {count}" for cat, count in sorted(by_category.items())
|
||||
)
|
||||
console.print(cat_list)
|
||||
console.print()
|
||||
|
||||
|
||||
@@ -796,14 +990,16 @@ size_categories:
|
||||
|
||||
### Dataset Summary
|
||||
|
||||
This dataset contains RDF triples from {dataset_info.name} converted to HuggingFace
|
||||
dataset format for easy use in machine learning pipelines.
|
||||
This dataset contains RDF triples from {dataset_info.name} converted to
|
||||
HuggingFace dataset format for easy use in machine learning pipelines.
|
||||
|
||||
- **Format:** Originally {dataset_info.format}, converted to HuggingFace Dataset
|
||||
- **Format:** Originally {dataset_info.format}, converted to HuggingFace
|
||||
Dataset
|
||||
- **Size:** {dataset_info.size_gb} GB (extracted)
|
||||
- **Entities:** {dataset_info.entities if dataset_info.entities else "N/A"}
|
||||
- **Triples:** {dataset_info.triples if dataset_info.triples else "N/A"}
|
||||
- **Original License:** {dataset_info.license if dataset_info.license else "See original source"}
|
||||
- **Original License:** {dataset_info.license if dataset_info.license else
|
||||
"See original source"}
|
||||
|
||||
### Recommended Use
|
||||
|
||||
@@ -813,7 +1009,8 @@ dataset format for easy use in machine learning pipelines.
|
||||
|
||||
## RDF Format
|
||||
|
||||
This dataset uses a standard lossless format for representing RDF triples. Each triple is a row with 6 fields:
|
||||
This dataset uses a standard lossless format for representing RDF triples.
|
||||
Each triple is a row with 6 fields:
|
||||
- `subject`: Subject URI or blank node
|
||||
- `predicate`: Predicate URI
|
||||
- `object`: Object value (URI, literal, or blank node)
|
||||
@@ -859,7 +1056,10 @@ This dataset is part of the CleverThis knowledge graph collection.
|
||||
def main() -> int:
|
||||
"""Unified RDF to HuggingFace converter with automatic mode detection."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert RDF to HuggingFace with incremental Parquet writing",
|
||||
description=(
|
||||
"Convert RDF to HuggingFace dataset "
|
||||
"with incremental Parquet writing"
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Input Modes:
|
||||
@@ -879,7 +1079,8 @@ Examples:
|
||||
python rdf_to_hf_incremental.py /data/wordnet.nt username/wordnet-dataset
|
||||
|
||||
# Process local file with explicit format
|
||||
python rdf_to_hf_incremental.py /data/wordnet.nt username/wordnet-dataset --format ntriples
|
||||
python rdf_to_hf_incremental.py /data/wordnet.nt username/wordnet-dataset \\
|
||||
--format ntriples
|
||||
|
||||
# Production with checkpointing
|
||||
python rdf_to_hf_incremental.py dbpedia-core username/dbpedia \\
|
||||
@@ -963,7 +1164,10 @@ Features:
|
||||
"--num-workers",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Number of processes for local file parsing (default: CPU count - 1)",
|
||||
help=(
|
||||
"Number of worker processes for local file parsing "
|
||||
"(default: CPU count - 1)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify-after",
|
||||
@@ -1008,10 +1212,14 @@ Features:
|
||||
|
||||
# Validate required arguments if not in list mode
|
||||
if not args.input or not args.repo_id:
|
||||
console.print("[red]Error: input and repo_id are required "
|
||||
"(unless using --list)[/red]")
|
||||
console.print("[yellow]Usage: rdf_to_hf_incremental.py INPUT REPO_ID "
|
||||
"[OPTIONS][/yellow]")
|
||||
console.print(
|
||||
"[red]Error: input and repo_id are required "
|
||||
"(unless using --list)[/red]"
|
||||
)
|
||||
console.print(
|
||||
"[yellow]Usage: rdf_to_hf_incremental.py "
|
||||
"INPUT REPO_ID [OPTIONS][/yellow]"
|
||||
)
|
||||
console.print("[yellow] or: rdf_to_hf_incremental.py --list[/yellow]")
|
||||
return 1
|
||||
|
||||
@@ -1028,42 +1236,72 @@ Features:
|
||||
if not rdf_format:
|
||||
if input_type == "registry":
|
||||
ds = DATASET_REGISTRY[input_value]
|
||||
rdf_format = ds.format
|
||||
console.print("[dim]Format auto-detected from registry: "
|
||||
f"{rdf_format}[/dim]")
|
||||
# Map registry format to streaming parser format
|
||||
registry_format = ds.format.lower()
|
||||
format_mapping = {
|
||||
"rdf": "rdf",
|
||||
"rdf/xml": "rdf",
|
||||
"xml": "rdf",
|
||||
"turtle": "turtle",
|
||||
"ttl": "turtle",
|
||||
"nt": "ntriples",
|
||||
"ntriples": "ntriples",
|
||||
}
|
||||
rdf_format = format_mapping.get(registry_format, registry_format)
|
||||
console.print(
|
||||
f"[dim]Format auto-detected from registry: "
|
||||
f"{registry_format} → {rdf_format}[/dim]"
|
||||
)
|
||||
elif input_type == "file":
|
||||
# Infer from file extension, default to turtle
|
||||
ext = Path(input_value).suffix.lower()
|
||||
# Infer from file extension, handling compressed files
|
||||
file_path = Path(input_value)
|
||||
# Get stem (filename without extension) and check for double extensions
|
||||
stem = file_path.stem.lower()
|
||||
ext = file_path.suffix.lower()
|
||||
|
||||
# Handle compressed files: .rdf.xz, .ttl.gz, etc.
|
||||
if ext in (".gz", ".bz2", ".xz", ".zip"):
|
||||
# Check if stem has an RDF extension
|
||||
stem_path = Path(stem)
|
||||
if stem_path.suffix.lower() in (".rdf", ".xml", ".ttl", ".nt"):
|
||||
ext = stem_path.suffix.lower()
|
||||
|
||||
format_map = {
|
||||
".ttl": "turtle",
|
||||
".nt": "ntriples",
|
||||
".nq": "nquads",
|
||||
".rdf": "xml",
|
||||
".xml": "xml",
|
||||
".rdf": "rdf",
|
||||
".xml": "rdf",
|
||||
".jsonld": "json-ld",
|
||||
".n3": "n3",
|
||||
".trig": "trig"
|
||||
}
|
||||
rdf_format = format_map.get(ext, "turtle")
|
||||
console.print(f"[dim]Format inferred from extension: {rdf_format}[/dim]")
|
||||
console.print(
|
||||
f"[dim]Format inferred from extension: "
|
||||
f"{ext} → {rdf_format}[/dim]"
|
||||
)
|
||||
|
||||
console.print(f"[dim]Rows per shard: {args.rows_per_shard:,}, "
|
||||
f"Record batch size: {args.record_batch_size:,}[/dim]")
|
||||
console.print(
|
||||
f"[dim]Rows per shard: {args.rows_per_shard:,}, "
|
||||
f"Record batch size: {args.record_batch_size:,}[/dim]"
|
||||
)
|
||||
|
||||
# Handle --dry-run mode
|
||||
if args.dry_run:
|
||||
console.print("\n[bold yellow]═══ DRY RUN MODE ═══[/bold yellow]")
|
||||
console.print("[yellow]Showing what would be done:[/yellow]\n")
|
||||
console.print(
|
||||
"[yellow]Showing what would be done without executing:[/yellow]\n"
|
||||
)
|
||||
|
||||
console.print(f"[cyan]Input type:[/cyan] {input_type}")
|
||||
console.print(f"[cyan]Input value:[/cyan] {input_value}")
|
||||
console.print(f"[cyan]RDF format:[/cyan] {rdf_format}")
|
||||
console.print(f"[cyan]Target repo:[/cyan] {args.repo_id}")
|
||||
console.print(f"[cyan]Rows per shard:[/cyan] {args.rows_per_shard:,}")
|
||||
console.print("[cyan]Record batch size:[/cyan] "
|
||||
f"{args.record_batch_size:,}")
|
||||
console.print("[cyan]Num workers:[/cyan] "
|
||||
f"{args.num_workers or 'auto (CPU count - 1)'}")
|
||||
console.print(f"[cyan]Record batch size:[/cyan] {args.record_batch_size:,}")
|
||||
num_workers_display = args.num_workers or 'auto (CPU count - 1)'
|
||||
console.print(f"[cyan]Num workers:[/cyan] {num_workers_display}")
|
||||
console.print(f"[cyan]Checkpoint file:[/cyan] {args.checkpoint or 'None'}")
|
||||
|
||||
if input_type == "registry":
|
||||
@@ -1078,21 +1316,33 @@ Features:
|
||||
# Handle both string and numeric triple counts
|
||||
if isinstance(ds.triples, (int, float)):
|
||||
console.print(f" Estimated triples: {int(ds.triples):,}")
|
||||
estimated_shards = (int(ds.triples) // args.rows_per_shard) + 1
|
||||
console.print(f" Estimated shards: ~{estimated_shards:,}")
|
||||
estimated_shards = (
|
||||
(int(ds.triples) // args.rows_per_shard) + 1
|
||||
)
|
||||
console.print(
|
||||
f" Estimated shards: ~{estimated_shards:,}"
|
||||
)
|
||||
else:
|
||||
console.print(f" Estimated triples: {ds.triples}")
|
||||
|
||||
console.print("\n[bold]Would perform:[/bold]")
|
||||
console.print(f" 1. {'Stream from ' + DATASET_REGISTRY[input_value].url if input_type == 'registry' else 'Read from ' + input_value}") # noqa: E501
|
||||
if input_type == 'registry':
|
||||
action_1 = f"Stream from {DATASET_REGISTRY[input_value].url}"
|
||||
else:
|
||||
action_1 = f"Read from {input_value}"
|
||||
console.print(f" 1. {action_1}")
|
||||
console.print(f" 2. Parse RDF triples in {rdf_format} format")
|
||||
console.print(" 3. Convert to Parquet shards "
|
||||
f"({args.rows_per_shard:,} rows each)")
|
||||
console.print(
|
||||
f" 3. Convert to Parquet shards "
|
||||
f"({args.rows_per_shard:,} rows each)"
|
||||
)
|
||||
console.print(f" 4. Upload shards to {args.repo_id}")
|
||||
console.print(" 5. Create and upload README.md")
|
||||
|
||||
console.print("\n[bold green]✓ Dry run complete. "
|
||||
"Use without --dry-run to execute.[/bold green]")
|
||||
console.print(
|
||||
"\n[bold green]✓ Dry run complete. "
|
||||
"Use without --dry-run to execute.[/bold green]"
|
||||
)
|
||||
return 0
|
||||
|
||||
# Initialize HuggingFace API
|
||||
@@ -1100,8 +1350,9 @@ Features:
|
||||
if token:
|
||||
console.print("[dim]Using provided/environment token[/dim]")
|
||||
else:
|
||||
console.print("[dim]Using cached credentials from "
|
||||
"'huggingface-cli login'[/dim]")
|
||||
console.print(
|
||||
"[dim]Using cached credentials from 'huggingface-cli login'[/dim]"
|
||||
)
|
||||
|
||||
try:
|
||||
api = HfApi(token=token)
|
||||
@@ -1157,11 +1408,16 @@ Features:
|
||||
start_shard=start_shard,
|
||||
)
|
||||
|
||||
console.print("\n[bold green]✓ Successfully uploaded to "
|
||||
f"{args.repo_id}[/bold green]")
|
||||
console.print(
|
||||
f"\n[bold green]✓ Successfully uploaded to "
|
||||
f"{args.repo_id}[/bold green]"
|
||||
)
|
||||
console.print(f"[green] Total rows: {total_rows:,}[/green]")
|
||||
console.print(f"[green] Total shards: {total_shards}[/green]")
|
||||
console.print(f"[green] Repository: https://huggingface.co/datasets/{args.repo_id}[/green]")
|
||||
console.print(
|
||||
f"[green] Repository: "
|
||||
f"https://huggingface.co/datasets/{args.repo_id}[/green]"
|
||||
)
|
||||
|
||||
# Create and upload README.md (dataset card)
|
||||
if input_type == "registry":
|
||||
@@ -1183,35 +1439,41 @@ Features:
|
||||
console.print("[green]✓ Dataset card (README.md) "
|
||||
"uploaded[/green]")
|
||||
except Exception as e:
|
||||
console.print("[yellow]⚠ Warning: Could not upload README.md: "
|
||||
f"{e}[/yellow]")
|
||||
console.print("[dim]You can manually create a README.md file "
|
||||
"for the dataset[/dim]")
|
||||
console.print(
|
||||
f"[yellow]⚠ Warning: Could not upload README.md: "
|
||||
f"{e}[/yellow]"
|
||||
)
|
||||
console.print(
|
||||
"[dim]You can manually create a README.md file "
|
||||
"for the dataset[/dim]"
|
||||
)
|
||||
|
||||
# Verification
|
||||
if args.verify_after:
|
||||
console.print("\n[yellow]Verifying uploaded shards...[/yellow]")
|
||||
try:
|
||||
repo_files = api.list_repo_files(
|
||||
repo_id=args.repo_id,
|
||||
repo_type="dataset"
|
||||
repo_id=args.repo_id, repo_type="dataset"
|
||||
)
|
||||
data_files = [f for f in repo_files if f.startswith("data/")]
|
||||
console.print(f"[green]✓ Found {len(data_files)} shard files "
|
||||
"in repo[/green]")
|
||||
console.print(
|
||||
f"[green]✓ Found {len(data_files)} shard files "
|
||||
f"in repo[/green]"
|
||||
)
|
||||
|
||||
sample_n = min(args.verify_sample_shards, len(data_files))
|
||||
for f in data_files[:sample_n]:
|
||||
local_path = hf_hub_download(
|
||||
repo_id=args.repo_id,
|
||||
repo_type="dataset",
|
||||
filename=f
|
||||
filename=f,
|
||||
)
|
||||
pf = pq.ParquetFile(local_path)
|
||||
console.print(f"[green]✓ Verified readable:[/green] {f}")
|
||||
console.print(
|
||||
"[dim] Rows: "
|
||||
f"{pf.metadata.num_rows if pf.metadata else 'unknown'}[/dim]")
|
||||
num_rows = (
|
||||
pf.metadata.num_rows if pf.metadata else 'unknown'
|
||||
)
|
||||
console.print(f"[dim] Rows: {num_rows}[/dim]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ Verification failed: {e}[/red]")
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -18,12 +21,49 @@ class HTTPStreamConfig:
|
||||
follow_redirects: bool = True
|
||||
|
khird
commented
Method name no longer reflects functionality as this is a manager-method supporting both ftp and http now Method name no longer reflects functionality as this is a manager-method supporting both ftp and http now
|
||||
|
||||
|
||||
def _calculate_backoff_sleep(
|
||||
attempt: int, base_s: float, max_s: float
|
||||
) -> float:
|
||||
"""Calculate exponential backoff sleep time."""
|
||||
return min(base_s * (2 ** (attempt - 1)), max_s)
|
||||
|
||||
|
||||
def stream_http_bytes(
|
||||
url: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
start_byte: int | None = None,
|
||||
config: HTTPStreamConfig | None = None,
|
||||
) -> Iterator[bytes]:
|
||||
"""Stream bytes from an HTTP(S) or FTP URL with retry and optional Range start.
|
||||
|
||||
- Yields chunks of bytes.
|
||||
- If start_byte is provided, attempts to use Range requests (HTTP/HTTPS only).
|
||||
- Retries on network/timeouts and 5xx responses with exponential backoff.
|
||||
- Automatically detects protocol and routes to appropriate handler.
|
||||
"""
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
src/streaming/http_stream.py:44:89: E501 Line too long (101 > 88)
|
42 | yield from stream_ftp_bytes(url, config=config)
43 | elif protocol in ("http", "https"):
44 | yield from stream_http_bytes_impl(url, headers=headers, start_byte=start_byte, config=config)
| ^^^^^^^^^^^^^ E501
45 | else:
46 | raise ValueError(f"Unsupported protocol: {protocol}. Supported: http, https, ftp")
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
parsed = urlparse(url)
|
||||
protocol = parsed.scheme.lower()
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
src/streaming/http_stream.py:46:89: E501 Line too long (90 > 88)
|
44 | yield from stream_http_bytes_impl(url, headers=headers, start_byte=start_byte, config=config)
45 | else:
46 | raise ValueError(f"Unsupported protocol: {protocol}. Supported: http, https, ftp")
| ^^ E501
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
|
||||
if protocol == "ftp":
|
||||
yield from stream_ftp_bytes(url, config=config)
|
||||
elif protocol in ("http", "https"):
|
||||
yield from stream_http_bytes_impl(
|
||||
url, headers=headers, start_byte=start_byte, config=config
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported protocol: {protocol}. "
|
||||
f"Supported: http, https, ftp"
|
||||
)
|
||||
|
||||
|
||||
def stream_http_bytes_impl(
|
||||
url: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
start_byte: int | None = None,
|
||||
config: HTTPStreamConfig | None = None,
|
||||
) -> Iterator[bytes]:
|
||||
"""Stream bytes from an HTTP(S) URL with retry and optional Range start.
|
||||
|
||||
@@ -57,10 +97,11 @@ def stream_http_bytes(
|
||||
yield chunk
|
||||
return
|
||||
except (
|
||||
httpx.TimeoutException,
|
||||
httpx.NetworkError,
|
||||
httpx.RemoteProtocolError,
|
||||
httpx.HTTPStatusError) as e:
|
||||
httpx.TimeoutException,
|
||||
httpx.NetworkError,
|
||||
httpx.RemoteProtocolError,
|
||||
httpx.HTTPStatusError,
|
||||
) as e:
|
||||
# Retry on network/timeouts and 5xx. For 4xx (except 429), fail fast.
|
||||
if isinstance(e, httpx.HTTPStatusError):
|
||||
status = e.response.status_code
|
||||
@@ -70,10 +111,84 @@ def stream_http_bytes(
|
||||
if status < 500 and status != 429:
|
||||
raise
|
||||
|
||||
if attempt > cfg.max_retries:
|
||||
if attempt >= cfg.max_retries:
|
||||
raise
|
||||
|
||||
sleep_s = min(cfg.backoff_base_s * (2 ** (attempt - 1)), cfg.backoff_max_s)
|
||||
sleep_s = _calculate_backoff_sleep(
|
||||
attempt, cfg.backoff_base_s, cfg.backoff_max_s
|
||||
)
|
||||
time.sleep(sleep_s)
|
||||
|
||||
|
||||
def stream_ftp_bytes(
|
||||
url: str,
|
||||
*,
|
||||
config: HTTPStreamConfig | None = None,
|
||||
) -> Iterator[bytes]:
|
||||
"""Stream bytes from an FTP URL with retry logic.
|
||||
|
||||
FTP Protocol Limitations:
|
||||
- No Range header support (cannot resume interrupted downloads)
|
||||
- No Content-Length in all cases (progress may be indeterminate)
|
||||
|
||||
Args:
|
||||
url: FTP URL to stream from
|
||||
config: Streaming configuration (chunk_size, timeouts, retries)
|
||||
|
||||
Yields:
|
||||
Chunks of bytes from the FTP stream
|
||||
|
||||
Raises:
|
||||
ValueError: If FTP authentication fails or file not found
|
||||
urllib.error.URLError: On network errors after retries exhausted
|
||||
"""
|
||||
cfg = config or HTTPStreamConfig()
|
||||
attempt = 0
|
||||
|
||||
while True:
|
||||
attempt += 1
|
||||
try:
|
||||
timeout = int(cfg.connect_timeout_s + cfg.read_timeout_s)
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
I think that this line should be Do you agree? I think that this line should be
```
if attempt >= cfg.max_retries:
```
Do you agree?
|
||||
with urllib.request.urlopen(url, timeout=timeout) as response:
|
||||
while True:
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
Because the definition of In other news, Because the definition of `sleep_s` is a little complex and it's repeated three times (lines 153, 159, and 166), I would rather it be in its own function, in case we want to change how to calculate the sleep time.
---
In other news, `ruff check` reports:
```
src/streaming/http_stream.py:153:89: E501 Line too long (91 > 88)
|
151 | if attempt > cfg.max_retries:
152 | raise
153 | sleep_s = min(cfg.backoff_base_s * (2 ** (attempt - 1)), cfg.backoff_max_s)
| ^^^ E501
154 | time.sleep(sleep_s)
155 | else:
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
chunk = response.read(cfg.chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
return
|
||||
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
src/streaming/http_stream.py:159:89: E501 Line too long (91 > 88)
|
157 | if attempt > cfg.max_retries:
158 | raise
159 | sleep_s = min(cfg.backoff_base_s * (2 ** (attempt - 1)), cfg.backoff_max_s)
| ^^^ E501
160 | time.sleep(sleep_s)
|
```
aditya
commented
Fixed !! Fixed !!
|
||||
except urllib.error.URLError as e:
|
||||
reason = str(e.reason) if hasattr(e, "reason") else str(e)
|
||||
|
||||
|
brent.edwards marked this conversation as resolved
Outdated
brent.edwards
commented
`ruff check` reports:
```
src/streaming/http_stream.py:162:29: F841 [*] Local variable `e` is assigned to but never used
|
160 | time.sleep(sleep_s)
161 |
162 | except Exception as e:
| ^ F841
163 | # Unexpected errors: retry if we have attempts left
164 | if attempt > cfg.max_retries:
|
= help: Remove assignment to unused variable `e`
```
aditya
commented
Fixed !! Fixed !!
|
||||
# FTP-specific error codes
|
||||
if "530" in reason or "Login incorrect" in reason:
|
||||
raise ValueError(f"FTP authentication failed: {url}") from e
|
||||
elif "550" in reason or "No such file" in reason:
|
||||
raise ValueError(f"File not found on FTP server: {url}") from e
|
||||
elif "timed out" in reason.lower():
|
||||
# Retry on timeout
|
||||
if attempt >= cfg.max_retries:
|
||||
raise
|
||||
sleep_s = _calculate_backoff_sleep(
|
||||
attempt, cfg.backoff_base_s, cfg.backoff_max_s
|
||||
)
|
||||
time.sleep(sleep_s)
|
||||
else:
|
||||
# Other errors: retry if we have attempts left
|
||||
if attempt >= cfg.max_retries:
|
||||
raise
|
||||
sleep_s = _calculate_backoff_sleep(
|
||||
attempt, cfg.backoff_base_s, cfg.backoff_max_s
|
||||
)
|
||||
time.sleep(sleep_s)
|
||||
|
||||
except Exception:
|
||||
# Unexpected errors: retry if we have attempts left
|
||||
if attempt >= cfg.max_retries:
|
||||
raise
|
||||
sleep_s = _calculate_backoff_sleep(
|
||||
attempt, cfg.backoff_base_s, cfg.backoff_max_s
|
||||
)
|
||||
time.sleep(sleep_s)
|
||||
|
||||
|
||||
|
||||
Is it possible to get the XML content where the parse error occurred? The specific crash-provoking section may be more relevant than the first 500 of the document, which is likely to be mainly boilerplate anyway.