Files
dataset-uploader/scripts/upload_all_datasets.py
brent.edwards c1e8615e8d style: reformat long lines to fit within 88 characters
Co-authored-by: aider (openrouter/google/gemini-2.5-pro) <aider@aider.chat>
2026-02-05 15:13:04 -08:00

1526 lines
52 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Download, process, and upload all RDF datasets to HuggingFace Hub.
WORKFLOW:
For each dataset, the script executes three phases:
Step 1: Download
- Downloads raw RDF from public source URL
- Extracts to usable RDF format
- Progress bars with download speed
Step 2: Decompress
- Decompresses RDF files (if necessary)
Step 3: Convert
- Parses RDF file (Turtle, N-Triples, RDF/XML, etc.)
- Converts to HuggingFace dataset format
- Preserves all semantic information (lossless)
- Creates dataset card (README.md) with metadata
Step 4: Upload
- Uploads to HuggingFace Hub under CleverThis organization
- Creates/updates dataset repository
- Uploads dataset card with comprehensive documentation
- Makes dataset publicly accessible
DATASET CATEGORIES:
small < 1 GB (WordNet, Schema.org, Gene Ontology)
medium 1-10 GB (YAGO 4.5, DisGeNET, GeoNames)
large 10-100 GB (DBpedia, UniProt, DBLP)
xlarge > 100 GB (Wikidata, BabelNet, Bio2RDF)
DIRECTORY STRUCTURE:
dataset_processing/
├── downloads/ # Raw RDF downloads
│ ├── wordnet/
│ │ ├── english-wordnet-2024.ttl.gz
│ │ └── english-wordnet-2024.ttl
│ └── yago-4.5/
│ └── yago-4.5.0.2-tiny.ttl
└── hf_datasets/ # Converted HuggingFace datasets
├── wordnet/
│ ├── data/
│ ├── dataset_info.json
│ └── README.md
└── yago-4.5/
├── data/
├── dataset_info.json
└── README.md
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
from bloom_filter2 import BloomFilter
from dotenv import load_dotenv
from scripts.download_and_decompress import (
decompress,
download,
get_dataset_info,
get_most_recent_file,
)
env_path = Path(__file__).resolve().parent.parent / ".env"
if env_path.exists():
load_dotenv(dotenv_path=env_path, override=False)
from dataset_registry import ( # noqa: E402
DATASET_REGISTRY,
DatasetInfo,
get_dataset_config,
)
from dataset_validator import ( # noqa: E402
_new_string_store,
collect_parquet_strings,
collect_source_strings,
dataset_can_download_from_hf,
dataset_exists_on_hf,
dataset_has_required_columns,
dataset_is_parquet_file,
strings_in_parquet_not_source,
strings_in_source_not_parquet,
)
from google_sheets_tracker import create_tracker # noqa: E402
from huggingface_hub import HfApi # noqa: E402
from rich.console import Console # noqa: E402
from rich.table import Table # noqa: E402
console = Console()
def parse_args() -> tuple[argparse.Namespace, argparse.ArgumentParser]:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Download, process, and upload all RDF datasets to HuggingFace Hub."
)
parser.add_argument(
"--dataset",
"-d",
type=str,
action="append",
help="Specific dataset(s) to process (can be specified multiple times)",
)
parser.add_argument(
"--category",
"-c",
choices=["small", "medium", "large", "xlarge", "all"],
default=None,
help="Process datasets by category",
)
parser.add_argument(
"--base-dir",
type=Path,
default=Path("./dataset_processing"),
help="Base directory for work",
)
parser.add_argument(
"--skip-download", action="store_true", help="Skip download step"
)
parser.add_argument(
"--skip-convert", action="store_true", help="Skip conversion step"
)
parser.add_argument("--skip-upload", action="store_true", help="Skip upload step")
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without doing it",
)
parser.add_argument(
"--list", action="store_true", help="List datasets that would be processed"
)
parser.add_argument(
"--validate",
action="store_true",
help="Validate datasets on HuggingFace without processing",
)
parser.add_argument(
"--rm",
action="store_true",
dest="remove_downloaded",
help="Remove mode: wipe all downloads at start, "
"delete each dataset after processing",
)
parser.add_argument(
"--parallel",
"-p",
type=int,
default=1,
help="Number of datasets to process in parallel (default: 1, sequential)",
)
parser.add_argument(
"--repeat",
action="store_true",
default=3,
help="Repeat download/upload attempts",
)
parser.add_argument(
"--sheet",
action="store_true",
help="Enable Google Sheets tracking for dataset status",
)
return parser.parse_args(), parser
def get_datasets_to_process(args: argparse.Namespace) -> list[str]:
"""
Determine which datasets to process.
Args:
args: A list of arguments parsed by argparse
Returns:
Which datasets to process.
"""
if args.dataset:
datasets_to_process = args.dataset
elif args.category == "all":
datasets_to_process = list(DATASET_REGISTRY.keys())
elif args.category:
datasets_to_process = [
d_id
for d_id, d_info in DATASET_REGISTRY.items()
if d_info.category == args.category
]
else:
datasets_to_process = []
return datasets_to_process
def list_datasets(datasets_to_process: list[str]) -> None:
"""Print a list of datasets that would be processed."""
console.print("\n[bold cyan]Datasets to Process:[/bold cyan]\n")
table = Table(show_header=True)
table.add_column("ID", style="cyan")
table.add_column("Name", style="green")
table.add_column("Category", style="yellow")
table.add_column("Size", style="blue")
table.add_column("Available", style="magenta")
if len(datasets_to_process) == 0:
list_of_datasets = list(DATASET_REGISTRY.keys())
else:
list_of_datasets = datasets_to_process
for dataset_id in list_of_datasets:
info = DATASET_REGISTRY[dataset_id]
available = "[green]✓[/green]" if info.available else "[red]✗[/red]"
table.add_row(
dataset_id, info.name, info.category, f"{info.size_gb} GB", available
)
console.print(table)
available_count = sum(
1 for d_id in datasets_to_process if DATASET_REGISTRY[d_id].available
)
console.print(
f"\n[bold]Total: {len(datasets_to_process)} datasets "
f"({available_count} available)[/bold]"
)
def check_dataset_exists_on_hf(dataset_id: str) -> tuple[bool, str | None]:
"""
Check if dataset already exists on HuggingFace Hub.
Checks both the dataset name and dataset name with '-v1' suffix.
Args:
dataset_id: Dataset identifier
Returns:
Tuple of (exists, existing_repo_id)
- exists: True if dataset exists, False otherwise
- existing_repo_id: Full repo ID if exists, None otherwise
"""
try:
config = get_dataset_config()
organization = config.get("organization")
if not organization:
console.print(
"[yellow]Warning: Organization not found in config file. "
"Cannot check for existing datasets on HuggingFace.[/yellow]"
)
return (False, None)
except Exception as e:
console.print(
f"[yellow]Warning: Could not read config file: {e}. "
"Cannot check for existing datasets on HuggingFace.[/yellow]"
)
return (False, None)
try:
hf_token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
api = HfApi(token=hf_token) if hf_token else HfApi()
datasets = list(api.list_datasets(author=organization))
repo_id_base = f"{organization}/{dataset_id}"
repo_id_v1 = f"{organization}/{dataset_id}-v1"
for dataset in datasets:
if dataset.id == repo_id_base:
return (True, repo_id_base)
if dataset.id == repo_id_v1:
return (True, repo_id_v1)
return (False, None)
except Exception as e:
console.print(
f"[yellow]Warning: Could not check HuggingFace for "
f"existing dataset: {e}[/yellow]"
)
console.print("[yellow]Continuing with processing...[/yellow]")
return (False, None)
def get_valid_hf_license(license_string: str | None) -> str:
"""Map dataset license to valid HuggingFace license identifier.
Args:
license_string: License string from dataset registry
Returns:
Valid HuggingFace license identifier
"""
if not license_string:
return "other"
# Get license mappings from config
config = get_dataset_config()
license_map = config.get("license_mappings", {})
# Convert keys to lowercase for comparison
license_map_lower = {k.lower(): v for k, v in license_map.items()}
normalized = license_string.lower().strip()
return license_map_lower.get(normalized, "other")
def get_size_category(dataset_info: DatasetInfo) -> str:
"""Get HuggingFace size category.
Args:
dataset_info: Dataset information
Returns:
Size category string
"""
if dataset_info.size_gb < 0.01:
return "n<1K"
elif dataset_info.size_gb < 1:
return "1K<n<10K"
elif dataset_info.size_gb < 10:
return "10K<n<100K"
elif dataset_info.size_gb < 100:
return "100K<n<1M"
elif dataset_info.size_gb < 1000:
return "1M<n<10M"
elif dataset_info.size_gb < 10000:
return "10M<n<100M"
else:
return "100M<n"
def get_rdf_format_documentation(dataset_info: DatasetInfo) -> str:
"""Get comprehensive RDF format documentation for README.
Args:
dataset_info: Dataset information
Returns:
Markdown documentation for RDF format
"""
return f"""
## Dataset Format: Lossless RDF Representation
This dataset uses a **standard lossless format** for representing RDF (Resource Description Framework)
data in HuggingFace Datasets. All semantic information from the original RDF knowledge graph is preserved,
enabling perfect round-trip conversion between RDF and HuggingFace formats.
### Schema
Each RDF triple is represented as a row with **6 fields**:
| Field | Type | Description | Example |
|-------|------|-------------|---------|
| `subject` | string | Subject of the triple (URI or blank node) | `"http://schema.org/Person"` |
| `predicate` | string | Predicate URI | `"http://www.w3.org/1999/02/22-rdf-syntax-ns#type"` |
| `object` | string | Object of the triple | `"John Doe"` or `"http://schema.org/Thing"` |
| `object_type` | string | Type of object: `"uri"`, `"literal"`, or `"blank_node"` | `"literal"` |
| `object_datatype` | string | XSD datatype URI (for typed literals) | `"http://www.w3.org/2001/XMLSchema#integer"` |
| `object_language` | string | Language tag (for language-tagged literals) | `"en"` |
### Example: RDF Triple Representation
**Original RDF (Turtle)**:
```turtle
<http://example.org/John> <http://schema.org/name> "John Doe"@en .
```
**HuggingFace Dataset Row**:
```python
{{
"subject": "http://example.org/John",
"predicate": "http://schema.org/name",
"object": "John Doe",
"object_type": "literal",
"object_datatype": None,
"object_language": "en"
}}
```
### Loading the Dataset
```python
from datasets import load_dataset
# Load the dataset
dataset = load_dataset("CleverThis/{dataset_info.id}")
# Access the data
data = dataset["data"]
# Iterate over triples
for row in data:
subject = row["subject"]
predicate = row["predicate"]
obj = row["object"]
obj_type = row["object_type"]
print(f"Triple: ({{subject}}, {{predicate}}, {{obj}})")
print(f" Object type: {{obj_type}}")
if row["object_language"]:
print(f" Language: {{row['object_language']}}")
if row["object_datatype"]:
print(f" Datatype: {{row['object_datatype']}}")
```
### Converting Back to RDF
The dataset can be converted back to any RDF format (Turtle, N-Triples, RDF/XML,
etc.) with **zero information loss**:
```python
from datasets import load_dataset
from rdflib import Graph, URIRef, Literal, BNode
def convert_to_rdf(dataset_name, output_file="output.ttl", split="data"):
\"\"\"Convert HuggingFace dataset back to RDF Turtle format.\"\"\"
# Load dataset
dataset = load_dataset(dataset_name)
# Create RDF graph
graph = Graph()
# Convert each row to RDF triple
for row in dataset[split]:
# Subject
if row["subject"].startswith("_:"):
subject = BNode(row["subject"][2:])
else:
subject = URIRef(row["subject"])
# Predicate (always URI)
predicate = URIRef(row["predicate"])
# Object (depends on object_type)
if row["object_type"] == "uri":
obj = URIRef(row["object"])
elif row["object_type"] == "blank_node":
obj = BNode(row["object"][2:])
elif row["object_type"] == "literal":
if row["object_datatype"]:
obj = Literal(row["object"], datatype=URIRef(row["object_datatype"]))
elif row["object_language"]:
obj = Literal(row["object"], lang=row["object_language"])
else:
obj = Literal(row["object"])
graph.add((subject, predicate, obj))
# Serialize to Turtle (or any RDF format)
graph.serialize(output_file, format="turtle")
print(f"Exported {{len(graph)}} triples to {{output_file}}")
return graph
# Usage
graph = convert_to_rdf("CleverThis/{dataset_info.id}", "reconstructed.ttl")
```
### Information Preservation Guarantee
This format preserves **100% of RDF information**:
- ✅ **URIs**: Exact string representation preserved
- ✅ **Literals**: Full text content preserved
- ✅ **Datatypes**: XSD and custom datatypes preserved
(e.g., `xsd:integer`, `xsd:dateTime`)
- ✅ **Language Tags**: BCP 47 language tags preserved (e.g., `@en`, `@fr`, `@ja`)
- ✅ **Blank Nodes**: Node structure preserved (identifiers may change but
graph isomorphism maintained)
**Round-trip guarantee**: Original RDF → HuggingFace → Reconstructed RDF
produces **semantically identical** graphs.
### Querying the Dataset
You can filter and query the dataset like any HuggingFace dataset:
```python
from datasets import load_dataset
dataset = load_dataset("CleverThis/{dataset_info.id}")
# Find all triples with English literals
english_literals = dataset["data"].filter(
lambda x: x["object_type"] == "literal" and x["object_language"] == "en"
)
print(f"Found {{len(english_literals)}} English literals")
# Find all rdf:type statements
type_statements = dataset["data"].filter(
lambda x: "rdf-syntax-ns#type" in x["predicate"]
)
print(f"Found {{len(type_statements)}} type statements")
# Convert to Pandas for analysis
import pandas as pd
df = dataset["data"].to_pandas()
# Analyze predicate distribution
print(df["predicate"].value_counts())
```
### Dataset Format
The dataset contains all triples in a single **data** split, suitable for
machine learning tasks such as:
- Knowledge graph completion
- Link prediction
- Entity embedding
- Relation extraction
- Graph neural networks
### Format Specification
For complete technical documentation of the RDF-to-HuggingFace format, see:
📖 [RDF to HuggingFace Format Specification](https://github.com/CleverThis/cleverernie/blob/master/docs/rdf_huggingface_format_specification.md)
The specification includes:
- Detailed schema definition
- All RDF node type mappings
- Performance benchmarks
- Edge cases and limitations
- Complete code examples
### Conversion Metadata
- **Source Format**: {dataset_info.format}
- **Original Size**: {dataset_info.size_gb} GB
- **Conversion Tool**: [CleverErnie RDF Pipeline](https://github.com/CleverThis/cleverernie)
- **Format Version**: 1.0
- **Conversion Date**: {datetime.now().strftime("%Y-%m-%d")}
""" # noqa: E501
def get_rdf_format(dataset_info: DatasetInfo) -> str:
"""Get RDF format string for rdflib.
Args:
dataset_info: Dataset information
Returns:
Format string for rdflib
"""
# Get format mappings from config
config = get_dataset_config()
format_map = config.get("format_mappings", {})
# Default mappings for common formats
default_mappings = {
"rdf": "xml",
"rdf/xml": "xml",
"xml": "xml",
"turtle": "turtle",
"ttl": "turtle",
"nt": "nt",
"ntriples": "nt",
"n3": "n3",
"jsonld": "json-ld",
"json-ld": "json-ld",
}
format_lower = dataset_info.format.lower()
# First try config mappings, then default mappings, then fallback
mapped_format = format_map.get(format_lower)
if mapped_format:
return mapped_format
mapped_format = default_mappings.get(format_lower)
if mapped_format:
return mapped_format
# Fallback to turtle for unknown formats
return "turtle"
def create_dataset_card(dataset_info: DatasetInfo) -> str:
"""Create README.md content for the dataset card.
Args:
dataset_info: Dataset information
Returns:
Markdown content for dataset card
"""
return f"""---
license: {get_valid_hf_license(dataset_info.license)}
task_categories:
- text-generation
- feature-extraction
language:
- en
tags:
- rdf
- knowledge-graph
- semantic-web
- triples
size_categories:
- {get_size_category(dataset_info)}
---
# {dataset_info.name}
## Dataset Description
{dataset_info.description}
**Original Source:** {dataset_info.url}
### Dataset Summary
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
- **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"}
### Recommended Use
{dataset_info.recommended_for}
{f"### Notes: {dataset_info.notes}" if dataset_info.notes else ""}
{get_rdf_format_documentation(dataset_info)}
## Citation
If you use this dataset, please cite the original source:
**Original Dataset:** {dataset_info.name}
**URL:** {dataset_info.url}
**License:** {dataset_info.license if dataset_info.license else "See original source"}
## Dataset Preparation
This dataset was prepared using the CleverErnie GISM framework:
```bash
# Download original dataset
python scripts/rdf_dataset_downloader.py {dataset_info.id} -o datasets/
# Convert to HuggingFace format
python scripts/convert_rdf_to_hf_dataset_unified.py \\
datasets/{dataset_info.id}/[file] \\
hf_datasets/{dataset_info.id} \\
--format {get_rdf_format(dataset_info)} \\
--strategy auto
# Upload to HuggingFace Hub
python scripts/upload_all_datasets.py --dataset {dataset_info.id}
```
## Additional Information
### Original Source
{dataset_info.url}
### Conversion Details
- Converted using: [CleverErnie GISM](https://github.com/cleverthis/cleverernie)
- Conversion script: `scripts/convert_rdf_to_hf_dataset_unified.py`
- Dataset format: Single 'data' split with all triples
- Strategy: Auto-selected based on dataset size and format
### Maintenance
This dataset is maintained by the CleverThis organization.
"""
def get_dataset_dir(args: argparse.Namespace, dataset_id: str) -> Path | None:
hf_dataset_dir = args.base_dir / "hf_datasets" / dataset_id
try:
hf_dataset_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
console.print(f"[red]Error creating directory: {e}[/red]")
console.print(f"[yellow]HF dataset dir: {hf_dataset_dir}[/yellow]")
console.print("[yellow]Check permissions and disk space[/yellow]")
return None
return hf_dataset_dir
def convert(args: argparse.Namespace, rdf_file: Path, dataset_id: str) -> None:
"""
This function converts the dataset to the HuggingFace dataset format.
At the end of the function, the dataset should be available in the
hf_datasets directory.
If the conversion was unsuccessful, the function will throw an exception.
"""
download_dir = rdf_file.parent
dataset_info = get_dataset_info(dataset_id)
if not dataset_info:
raise ValueError(f"Dataset {dataset_id} not found in registry")
hf_dataset_dir = get_dataset_dir(args, dataset_id)
if hf_dataset_dir is None:
raise ValueError("Could not retrieve dataset directory")
if not args.skip_convert:
# Special handling for FB15k-237
if dataset_id == "fb15k-237":
# Use specialized FB15k-237 converter that handles all splits
fb15k_script = Path(__file__).parent / "convert_fb15k237_to_hf.py"
fb15k_dir = download_dir / "fb15k-237" / "FB15K-237.2"
if args.dry_run:
console.print(
"[dim]Would run: python scripts/convert_fb15k237_to_hf.py "
"{fb15k_dir} {hf_dataset_dir}[/dim]"
)
else:
result = subprocess.run(
[
sys.executable,
str(fb15k_script),
str(fb15k_dir),
str(hf_dataset_dir),
],
capture_output=False,
)
if result.returncode != 0:
console.print(
f"[red]Conversion failed for {dataset_id} "
f"(exit code: {result.returncode})[/red]"
)
console.print(f"[yellow]Input file: {rdf_file}[/yellow]")
console.print(f"[yellow]Output dir: {hf_dataset_dir}[/yellow]")
console.print(
"[yellow]Try running conversion manually to see "
"detailed error messages[/yellow]"
)
raise ValueError("Conversion failed")
# Create dataset card
readme_path = hf_dataset_dir / "README.md"
readme_path.write_text(create_dataset_card(dataset_info))
console.print(f"[green]Created dataset card: {readme_path}[/green]")
# Special handling for NELL-995
elif dataset_id == "nell-995":
# Use specialized NELL-995 converter that handles raw.kb format
nell_script = Path(__file__).parent / "convert_nell995_to_hf.py"
if args.dry_run:
console.print(
"[dim]Would run: python scripts/convert_nell995_to_hf.py "
"{download_dir} {hf_dataset_dir}[/dim]"
)
else:
result = subprocess.run(
[
sys.executable,
str(nell_script),
str(download_dir),
str(hf_dataset_dir),
],
capture_output=False,
)
if result.returncode != 0:
console.print(
f"[red]Conversion failed for {dataset_id}. "
f"Return code: {result.returncode}[/red]"
)
raise ValueError("Conversion failed")
# Create dataset card
readme_path = hf_dataset_dir / "README.md"
readme_path.write_text(create_dataset_card(dataset_info))
console.print(f"[green]Created dataset card: {readme_path}[/green]")
# Special handling for ConceptNet
elif dataset_id == "conceptnet":
# Use specialized ConceptNet converter that handles CSV format
conceptnet_script = Path(__file__).parent / "convert_conceptnet_to_hf.py"
# Find the downloaded CSV file
csv_files = list(download_dir.rglob("*.csv")) + list(
download_dir.rglob("*.csv.gz")
)
if not csv_files:
console.print(
f"[red]No CSV files found for ConceptNet in {download_dir}[/red]"
)
raise ValueError("No CSV files found")
csv_file = csv_files[0]
console.print(f"[green]Found ConceptNet CSV file: {csv_file}[/green]")
if args.dry_run:
console.print(
"[dim]Would run: python scripts/convert_conceptnet_to_hf.py "
"{csv_file} {hf_dataset_dir}[/dim]"
)
else:
result = subprocess.run(
[
sys.executable,
str(conceptnet_script),
str(csv_file),
str(hf_dataset_dir),
],
capture_output=False,
)
if result.returncode != 0:
console.print(
f"[red]Conversion failed for {dataset_id}. "
f"Return code: {result.returncode}[/red]"
)
raise ValueError("Conversion failed")
# Create dataset card
readme_path = hf_dataset_dir / "README.md"
readme_path.write_text(create_dataset_card(dataset_info))
console.print(f"[green]Created dataset card: {readme_path}[/green]")
else:
# Original conversion logic for other datasets
# Use unified converter which automatically selects best strategy
unified_script = (
Path(__file__).parent / "convert_rdf_to_hf_dataset_unified.py"
)
# Unified script will auto-select best strategy based on file size
# and format
console.print(
"[dim]Using unified converter (auto-selects best strategy)[/dim]"
)
if args.dry_run:
console.print(
"[dim]Would run: python "
"scripts/convert_rdf_to_hf_dataset_unified.py "
"{rdf_file} {hf_dataset_dir} --strategy auto[/dim]"
)
else:
# Use unified converter with auto-strategy selection
# Adjust chunk size based on dataset size
chunk_size = 50000 # Default
if dataset_info.size_gb > 10:
chunk_size = 100000 # Larger chunks for very large datasets
elif dataset_info.size_gb < 1:
chunk_size = 10000 # Smaller chunks for small datasets
result = subprocess.run(
[
sys.executable,
str(unified_script),
str(rdf_file),
str(hf_dataset_dir),
"--format",
get_rdf_format(dataset_info),
"--strategy",
"auto", # Let the unified script auto-select best strategy
"--chunk-size",
str(chunk_size),
"--description",
dataset_info.description,
"--homepage",
dataset_info.url,
"--license",
dataset_info.license if dataset_info.license else "Unknown",
],
capture_output=False,
)
if result.returncode != 0:
console.print(
f"[red]Conversion failed for {dataset_id}. "
f"Return code: {result.returncode}[/red]"
)
def upload(args: argparse.Namespace, dataset_id: str) -> None:
"""
This function uploads the dataset to the HuggingFace Hub.
This function throws a ValueError if the upload fails.
"""
dataset_info = get_dataset_info(dataset_id)
if not dataset_info:
raise ValueError(f"Dataset {dataset_id} not found in registry")
hf_dataset_dir = get_dataset_dir(args, dataset_id)
if hf_dataset_dir is None:
raise ValueError("Could not retrieve dataset directory")
if not args.skip_upload:
# Get repository ID from dataset info or config file
if hasattr(dataset_info, "huggingface_repo") and dataset_info.huggingface_repo:
repo_id = dataset_info.huggingface_repo
else:
try:
config = get_dataset_config()
organization = config.get("organization")
if not organization:
raise ValueError(
"Organization not found in config file. "
"Please set 'organization' field in dataset_config.json"
)
repo_id = f"{organization}/{dataset_id}"
except Exception as e:
raise ValueError(
f"Could not read organization from config file: {e}. "
"Please ensure dataset_config.json exists and contains "
"'organization' field."
) from e
if args.dry_run:
console.print(f"[dim]Would upload to: {repo_id}[/dim]")
else:
try:
from datasets import load_from_disk
from huggingface_hub import HfApi
# Load dataset
dataset = load_from_disk(str(hf_dataset_dir))
# Upload
console.print(f"[cyan]Uploading to {repo_id}...[/cyan]")
dataset.push_to_hub(repo_id, private=False)
# Upload README separately if it exists
readme_path = hf_dataset_dir / "README.md"
if readme_path.exists():
api = HfApi()
api.upload_file(
path_or_fileobj=str(readme_path),
path_in_repo="README.md",
repo_id=repo_id,
repo_type="dataset",
)
else:
# Create README if it doesn't exist (e.g., when skipping conversion)
console.print(
"[yellow]README.md not found, creating it now...[/yellow]"
)
readme_path.write_text(create_dataset_card(dataset_info))
api = HfApi()
api.upload_file(
path_or_fileobj=str(readme_path),
path_in_repo="README.md",
repo_id=repo_id,
repo_type="dataset",
)
console.print(
f"[bold green]✓ Successfully uploaded to {repo_id}[/bold green]"
)
except ImportError as e:
console.print(f"[red]Missing required packages for upload: {e}[/red]")
console.print(
"[yellow]Install with: "
"pip install datasets huggingface-hub[/yellow]"
)
raise ValueError("Missing required packages for upload") from e
except FileNotFoundError as e:
console.print(
f"[red]Dataset directory not found: {hf_dataset_dir}[/red]"
)
console.print(
"[yellow]Make sure conversion step completed successfully[/yellow]"
)
raise ValueError("Dataset directory not found") from e
except PermissionError as e:
console.print(
"[red]Permission denied uploading to HuggingFace Hub[/red]"
)
console.print(
"[yellow]Check your HuggingFace token: "
"huggingface-cli login[/yellow]"
)
console.print("[yellow]Or set HF_TOKEN environment variable[/yellow]")
raise ValueError("Permission denied uploading to HuggingFace") from e
except Exception as e:
error_msg = str(e).lower()
if "authentication" in error_msg or "token" in error_msg:
console.print(f"[red]Authentication failed: {e}[/red]")
console.print("[yellow]Run: huggingface-cli login[/yellow]")
console.print(
"[yellow]Or set HF_TOKEN environment variable[/yellow]"
)
raise ValueError("Authentication failed") from e
if "network" in error_msg or "connection" in error_msg:
console.print(f"[red]Network error during upload: {e}[/red]")
console.print(
"[yellow]Check internet connection and try again[/yellow]"
)
raise ValueError("Network error during upload") from e
if "disk" in error_msg or "space" in error_msg:
console.print(f"[red]Disk space error: {e}[/red]")
console.print("[yellow]Check available disk space[/yellow]")
raise ValueError("Disk space error") from e
console.print(f"[red]Upload failed: {e}[/red]")
console.print(f"[yellow]Repository: {repo_id}[/yellow]")
console.print(f"[yellow]Dataset path: {hf_dataset_dir}[/yellow]")
raise ValueError("Upload failed") from e
else:
console.print("[dim]Skipping upload[/dim]")
def is_sub_bloom_filter(bf1: BloomFilter, bf2: BloomFilter) -> bool:
"""
Returns True if bf1 is a subset of bf2.
"""
if not bf1._match_template(bf2):
return False
assert bf1.backend is not None
assert bf2.backend is not None
for i in range(0, bf1.num_bits_m):
if bf1.backend.is_set(i) and not bf2.backend.is_set(i):
return False
return True
def validate_string_parity(
dataset_id: str,
parquet_files: list[Path],
base_dir: Path,
) -> bool:
"""Validate that source strings and parquet strings match (Level 2/3)."""
source_strings = collect_source_strings(dataset_id, base_dir)
if not source_strings:
console.print(
f"[yellow]Warning: No source strings found for '{dataset_id}'. "
"Check downloads directory and file formats.[/yellow]"
)
return False
if len(parquet_files) == 1:
parquet_strings = collect_parquet_strings(parquet_files[0])
else:
parquet_strings = _new_string_store()
for parquet_file in parquet_files:
parquet_strings.union(collect_parquet_strings(parquet_file))
missing_in_parquet = not is_sub_bloom_filter(parquet_strings, source_strings)
if missing_in_parquet:
console.print("[red]There were strings in parquet not in the source.[/red]")
missing_strings = strings_in_parquet_not_source(
parquet_paths=parquet_files,
source_strings=source_strings,
)
console.print(f"[yellow]Missing strings: {missing_strings}[/yellow]")
return False
missing_in_source = not is_sub_bloom_filter(source_strings, parquet_strings)
if missing_in_source:
console.print("[red]There were strings in the source not in parquet.[/red] ")
missing_strings = strings_in_source_not_parquet(
dataset_id=dataset_id,
base_dir=base_dir,
parquet_strings=parquet_strings,
)
console.print(f"[yellow]Missing strings: {missing_strings}[/yellow]")
return False
if not missing_in_parquet and not missing_in_source:
console.print("[green]Validation: Strings match.[/green]")
return not missing_in_parquet and not missing_in_source
def validate(datasets_to_process: list[str], base_dir: Path) -> bool:
"""Run Level 1 validation checks for datasets on HuggingFace."""
validation_root = base_dir / "validation_cache"
validation_root.mkdir(parents=True, exist_ok=True)
all_valid = True
for dataset_id in datasets_to_process:
console.print(f"[bold cyan]Validating {dataset_id}...[/bold cyan]")
exists = dataset_exists_on_hf(dataset_id)
if not exists:
console.print(
f"[red]Validation failed: dataset '{dataset_id}' "
"does not exist on HuggingFace.[/red]"
)
all_valid = False
continue
cache_dir = validation_root / dataset_id
download_path = dataset_can_download_from_hf(
dataset_id,
cache_dir=cache_dir,
)
if not download_path:
console.print(
f"[red]Validation failed: dataset '{dataset_id}' "
"could not be downloaded from HuggingFace.[/red]"
)
all_valid = False
continue
parquet_files = sorted(Path(download_path).rglob("*.parquet"))
if not parquet_files:
console.print(
f"[red]Validation failed: no parquet files found for "
f"'{dataset_id}'.[/red]"
)
all_valid = False
continue
for parquet_file in parquet_files:
if not dataset_is_parquet_file(parquet_file):
console.print(
f"[red]Validation failed: '{parquet_file}' is not "
"readable as parquet.[/red]"
)
all_valid = False
continue
if not dataset_has_required_columns(parquet_file):
console.print(
f"[red]Validation failed: '{parquet_file}' is missing "
"required columns.[/red]"
)
all_valid = False
continue
if not validate_string_parity(dataset_id, parquet_files, base_dir):
all_valid = False
for parquet_file in parquet_files:
try:
parquet_file.unlink()
except OSError as exc:
console.print(f"[red]Failed to delete '{parquet_file}': {exc}[/red]")
all_valid = False
return all_valid
def remove_dir(download_dir: Path) -> None:
"""
Removes a specified directory and its contents. Provides feedback on success or
failure during the operation and warns the user if manual removal may be
required.
Parameters:
download_dir (Path): A pathlib.Path object representing the directory to be removed.
Raises:
OSError: Raised when an error occurs during the directory removal process.
Returns:
None
"""
if download_dir.exists():
try:
shutil.rmtree(download_dir)
console.print(f"[green]✓ Removed {download_dir}[/green]")
except OSError as e:
console.print(
f"[yellow]Warning: Could not remove {download_dir}: {e}[/yellow]"
)
console.print("[yellow]You may need to remove it manually[/yellow]")
def main() -> int:
"""
The main function calls everything else.
Returns:
A Unix-style exit code. 0 == success.
"""
args, parser = parse_args()
# Fix for "AF_UNIX path too long" error in multiprocessing
# This forces the temporary directory to be /tmp (short path) instead of
# a potentially deep workspace path
os.environ["TMPDIR"] = "/tmp"
start_time = time.monotonic()
# List datasets to process
datasets_to_process = get_datasets_to_process(args)
datasets_time = time.monotonic()
datasets_duration = datasets_time - start_time
print(f"`get_datasets_to_process` took {datasets_duration:.2f} seconds:")
# List mode:
if args.list:
list_datasets(datasets_to_process)
return 0
if not datasets_to_process:
parser.print_help()
console.print(
"[red]Error: No datasets specified. Use --dataset or --category.[/red]"
)
return 1
if args.validate:
validation_ok = validate(datasets_to_process, args.base_dir)
return 0 if validation_ok else 1
sheet_tracker = None
if args.sheet:
try:
sheet_tracker = create_tracker(
spreadsheet_id=None,
worksheet_name=None,
credentials_path=None,
)
if sheet_tracker:
console.print("[green]✓ Google Sheets tracking enabled[/green]")
else:
console.print(
"[yellow]Warning: Could not initialize Google Sheets "
"tracker.[/yellow]"
)
console.print(
"[yellow]Check GOOGLE_SHEETS_ID and "
"GOOGLE_SHEETS_CREDENTIALS_PATH environment "
"variables.[/yellow]"
)
console.print("[yellow]Continuing without sheet tracking...[/yellow]")
except Exception as e:
console.print(
f"[yellow]Warning: Error initializing Google Sheets "
f"tracker: {e}[/yellow]"
)
console.print("[yellow]Continuing without sheet tracking...[/yellow]")
downloaded = False
rdf_file = None
current_dataset_id = None
try:
for dataset_id in datasets_to_process:
current_dataset_id = dataset_id
one_dataset_start_time = time.monotonic()
# Check if dataset already exists on HuggingFace (before processing)
dataset_exists, existing_repo_id = check_dataset_exists_on_hf(dataset_id)
if dataset_exists:
console.print(
f"[yellow]⊘ Skipping '{dataset_id}': Already exists on "
f"HuggingFace as '{existing_repo_id}'[/yellow]"
)
if sheet_tracker:
try:
current_status = sheet_tracker.get_dataset_status(dataset_id)
current_status_value = (
current_status.get("status", "").upper().strip()
)
if current_status_value == "DONE":
console.print(
f"[dim]Sheet status already DONE for {dataset_id}[/dim]"
)
except Exception:
pass
# Skip all processing steps and continue to next dataset
continue
if sheet_tracker:
should_process, reason = sheet_tracker.check_should_process(
dataset_id, force=False
)
if not should_process:
console.print(
f"[yellow]⊘ Skipping '{dataset_id}': {reason}[/yellow]"
)
continue
try:
sheet_tracker.mark_in_progress(dataset_id)
console.print(
f"[dim]Updated sheet: {dataset_id} -> IN PROGRESS[/dim]"
)
except Exception:
pass
# Download
attempt_count = 0
while attempt_count < args.repeat:
attempt_count += 1
console.print(
f"[bold cyan]Downloading {dataset_id}... "
f"Attempt {attempt_count}[/bold cyan]"
)
(downloaded, rerun, rdf_file) = download(
dataset_id=dataset_id,
base_dir=args.base_dir,
skip_download=args.skip_download,
dry_run=args.dry_run,
)
if not rerun:
break
if (not downloaded or not rdf_file) and not args.dry_run:
console.print("[red]Download failed. Aborting.[/red]")
if sheet_tracker:
try:
error_msg = f"Download failed for {dataset_id}"
sheet_tracker.reset_to_not_started(dataset_id, error_msg)
console.print(
f"[dim]Updated sheet: {dataset_id} -> "
f"NOT STARTED (with error)[/dim]"
)
except Exception:
pass
return 1
one_dataset_download_time = time.monotonic()
one_dataset_download_duration = (
one_dataset_download_time - one_dataset_start_time
)
print(f"`download` took {one_dataset_download_duration:.2f} seconds:")
# Decompress
if args.dry_run:
console.print(f"[dim][DRY RUN] Would decompress {dataset_id}...[/dim]")
else:
console.print(f"[bold cyan]Decompressing {dataset_id}...[/bold cyan]")
assert rdf_file is not None
result = decompress(rdf_file)
if not result:
console.print("[red]Decompression failed. Aborting.[/red]")
if sheet_tracker:
try:
error_msg = f"Decompression failed for {dataset_id}"
sheet_tracker.reset_to_not_started(dataset_id, error_msg)
console.print(
f"[dim]Updated sheet: {dataset_id} -> "
f"NOT STARTED (with error)[/dim]"
)
except Exception:
pass
return 1
one_dataset_decompress_time = time.monotonic()
one_dataset_decompress_duration = (
one_dataset_decompress_time - one_dataset_download_time
)
print(f"`decompress` took {one_dataset_decompress_duration:.2f} seconds:")
# Convert
assert rdf_file is not None
if not args.dry_run:
rdf_file = get_most_recent_file(rdf_file.parent)
if rdf_file is None:
console.print("[red]Could not find RDF file. Aborting.[/red]")
if sheet_tracker:
try:
error_msg = f"Could not find RDF file for {dataset_id}"
sheet_tracker.reset_to_not_started(dataset_id, error_msg)
console.print(
f"[dim]Updated sheet: {dataset_id} -> "
f"NOT STARTED (with error)[/dim]"
)
except Exception:
pass
return 1
console.print(
"[bold cyan]Converting "
f"{rdf_file if rdf_file else 'DRY RUN'}...[/bold cyan]"
)
# Pass a dummy path for dry run if rdf_file is None
if rdf_file:
convert_file = rdf_file
else:
convert_file = Path(
f"dataset_processing/downloads/{dataset_id}/dummy.ttl"
)
try:
convert(args, convert_file, dataset_id)
except ValueError as e:
console.print(f"[red]Conversion failed: {e} Aborting.[/red]")
if sheet_tracker:
try:
error_msg = f"Conversion failed for {dataset_id}: {e!r}"
sheet_tracker.reset_to_not_started(dataset_id, error_msg)
console.print(
f"[dim]Updated sheet: {dataset_id} -> "
f"NOT STARTED (with error)[/dim]"
)
except Exception:
pass
return 1
except Exception as e:
if sheet_tracker:
try:
error_msg = f"Conversion error for {dataset_id}: {e!r}"
sheet_tracker.reset_to_not_started(dataset_id, error_msg)
console.print(
f"[dim]Updated sheet: {dataset_id} -> "
f"NOT STARTED (with error)[/dim]"
)
except Exception:
pass
raise
one_dataset_convert_time = time.monotonic()
one_dataset_convert_duration = (
one_dataset_convert_time - one_dataset_decompress_time
)
print(f"`convert` took {one_dataset_convert_duration:.2f} seconds:")
# Upload
upload_error = None
attempt_count = 0
while attempt_count < args.repeat:
attempt_count += 1
console.print(
f"[bold cyan]Uploading {dataset_id}... "
f"Attempt {attempt_count}[/bold cyan]"
)
try:
upload(args, dataset_id)
break
except ValueError as e:
upload_error = str(e)
if attempt_count < args.repeat:
continue
else:
if sheet_tracker:
try:
error_msg = (
f"Upload failed for {dataset_id}: {upload_error}"
)
sheet_tracker.reset_to_not_started(
dataset_id, error_msg
)
console.print(
f"[dim]Updated sheet: {dataset_id} -> "
f"NOT STARTED (with error)[/dim]"
)
except Exception:
pass
console.print("[red]Upload failed. Aborting.[/red]")
return 1
one_dataset_upload_time = time.monotonic()
one_dataset_upload_duration = (
one_dataset_upload_time - one_dataset_convert_time
)
print(f"`upload` took {one_dataset_upload_duration:.2f} seconds:")
if sheet_tracker:
try:
sheet_tracker.mark_completed(dataset_id)
console.print(f"[dim]Updated sheet: {dataset_id} -> DONE[/dim]")
except Exception:
pass
# Remove if needed.
if args.remove_downloaded and not args.dry_run and rdf_file is not None:
console.print(f"[bold cyan]Removing {rdf_file}[/bold cyan]")
remove_dir(rdf_file.parent)
validation_ok = validate(list(datasets_to_process), args.base_dir)
if not validation_ok:
console.print("[red]Validation failed for one or more datasets.[/red]")
return 1
# Success!
console.print("[green]All done![/green]")
total_duration = time.monotonic() - start_time
print(f"`main` took {total_duration:.2f} seconds:")
return 0
except KeyboardInterrupt:
console.print("\n[yellow]Script interrupted by user (Ctrl+C)[/yellow]")
if sheet_tracker and current_dataset_id:
try:
error_msg = f"Script interrupted for {current_dataset_id}"
sheet_tracker.reset_to_not_started(current_dataset_id, error_msg)
console.print(
f"[dim]Updated sheet: {current_dataset_id} -> "
f"NOT STARTED (interrupted)[/dim]"
)
except Exception:
pass
return 130
except Exception as e:
console.print(f"\n[red]Unexpected error: {e}[/red]")
if sheet_tracker and current_dataset_id:
try:
error_msg = f"Unexpected error for {current_dataset_id}: {e!r}"
sheet_tracker.reset_to_not_started(current_dataset_id, error_msg)
console.print(
f"[dim]Updated sheet: {current_dataset_id} -> "
f"NOT STARTED (error)[/dim]"
)
except Exception:
pass
raise
if __name__ == "__main__":
exit(main())