Add dependency download and CLI functionality
- Implement dependency download and extraction for Zig packages - Create new CLI commands for downloading and converting ZON files - Add support for downloading dependencies from ZON files - Update project dependencies to include httpx and tqdm - Add WTFPL license file - Enhance README with more detailed usage instructions and project motivation
This commit is contained in:
121
zig_fetch_py/__main__.py
Normal file
121
zig_fetch_py/__main__.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""
|
||||
Command-line interface for zig-fetch-py.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from loguru import logger
|
||||
|
||||
from zig_fetch_py.downloader import process_dependencies
|
||||
from zig_fetch_py.parser import zon_to_json
|
||||
|
||||
|
||||
def setup_logger(verbose: bool = False):
|
||||
"""
|
||||
Set up the logger.
|
||||
|
||||
Args:
|
||||
verbose: Whether to enable verbose logging
|
||||
"""
|
||||
logger.remove()
|
||||
log_level = "DEBUG" if verbose else "INFO"
|
||||
logger.add(sys.stderr, level=log_level)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging")
|
||||
@click.pass_context
|
||||
def cli(ctx: click.Context, verbose: bool):
|
||||
"""Zig package manager utilities."""
|
||||
# Set up logging
|
||||
setup_logger(verbose)
|
||||
|
||||
# Ensure we have a context object
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["VERBOSE"] = verbose
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("zon_file", type=click.Path(exists=True, readable=True, path_type=Path))
|
||||
@click.pass_context
|
||||
def download(ctx: click.Context, zon_file: Path):
|
||||
"""
|
||||
Download dependencies from a ZON file.
|
||||
|
||||
ZON_FILE: Path to the ZON file
|
||||
"""
|
||||
logger.info(f"Processing dependencies from {zon_file}")
|
||||
dependencies = process_dependencies(str(zon_file))
|
||||
|
||||
if dependencies:
|
||||
logger.info(f"Successfully processed {len(dependencies)} dependencies:")
|
||||
for name, path in dependencies.items():
|
||||
logger.info(f" - {name}: {path}")
|
||||
else:
|
||||
logger.warning("No dependencies were processed")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("zon_file", type=click.Path(exists=True, readable=True, path_type=Path))
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output",
|
||||
type=click.Path(writable=True, path_type=Path),
|
||||
help="Output file (default: stdout)",
|
||||
)
|
||||
@click.option(
|
||||
"-i", "--indent", type=int, default=2, help="Indentation for the JSON output"
|
||||
)
|
||||
@click.option(
|
||||
"--empty-tuple-as-dict",
|
||||
is_flag=True,
|
||||
help="Parse empty tuples as empty dictionaries",
|
||||
)
|
||||
@click.pass_context
|
||||
def convert(
|
||||
ctx: click.Context,
|
||||
zon_file: Path,
|
||||
output: Path,
|
||||
indent: int,
|
||||
empty_tuple_as_dict: bool,
|
||||
):
|
||||
"""
|
||||
Convert a ZON file to JSON.
|
||||
|
||||
ZON_FILE: Path to the ZON file to convert
|
||||
"""
|
||||
try:
|
||||
# Read the ZON file
|
||||
with open(zon_file, "r") as f:
|
||||
zon_content = f.read()
|
||||
|
||||
# Convert to JSON
|
||||
json_content = zon_to_json(
|
||||
zon_content, indent=indent, empty_tuple_as_dict=empty_tuple_as_dict
|
||||
)
|
||||
|
||||
# Output the JSON
|
||||
if output:
|
||||
with open(output, "w") as f:
|
||||
f.write(json_content)
|
||||
logger.info(f"JSON written to {output}")
|
||||
else:
|
||||
click.echo(json_content)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.error(f"File not found: {zon_file}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the CLI."""
|
||||
cli() # pylint: disable=no-value-for-parameter
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
193
zig_fetch_py/downloader.py
Normal file
193
zig_fetch_py/downloader.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""
|
||||
Dependency downloader for Zig packages.
|
||||
|
||||
This module handles downloading and extracting dependencies specified in ZON files.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from zig_fetch_py.parser import parse_zon_file
|
||||
|
||||
|
||||
def get_cache_dir() -> Path:
|
||||
"""
|
||||
Get the Zig cache directory for packages.
|
||||
|
||||
Returns:
|
||||
Path to the Zig cache directory (~/.cache/zig/p)
|
||||
"""
|
||||
cache_dir = Path.home() / ".cache" / "zig" / "p"
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
return cache_dir
|
||||
|
||||
|
||||
def download_file(url: str, target_path: Path) -> None:
|
||||
"""
|
||||
Download a file from a URL to a target path.
|
||||
|
||||
Args:
|
||||
url: URL to download from
|
||||
target_path: Path to save the downloaded file
|
||||
"""
|
||||
logger.info(f"Downloading {url} to {target_path}")
|
||||
|
||||
# Create client with environment proxies
|
||||
with httpx.Client(follow_redirects=True) as client:
|
||||
with client.stream("GET", url) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
# Create parent directories if they don't exist
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write the file
|
||||
with open(target_path, "wb") as f:
|
||||
for chunk in response.iter_bytes():
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def extract_tarball(tarball_path: Path, extract_dir: Path) -> Path:
|
||||
"""
|
||||
Extract a tarball to a directory.
|
||||
|
||||
Args:
|
||||
tarball_path: Path to the tarball
|
||||
extract_dir: Directory to extract to
|
||||
|
||||
Returns:
|
||||
Path to the extracted directory
|
||||
"""
|
||||
logger.info(f"Extracting {tarball_path} to {extract_dir}")
|
||||
|
||||
# Create extraction directory if it doesn't exist
|
||||
extract_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tarfile.open(tarball_path, "r:*") as tar:
|
||||
# Get the common prefix of all files in the tarball
|
||||
members = tar.getmembers()
|
||||
common_prefix = Path(Path(members[0].name).parts[0]) if members else None
|
||||
|
||||
# Extract all files
|
||||
tar.extractall(path=extract_dir)
|
||||
|
||||
# Return the path to the extracted directory
|
||||
return extract_dir / common_prefix if common_prefix else extract_dir
|
||||
|
||||
|
||||
def process_dependency(
|
||||
name: str, dep_info: Dict[str, Any], cache_dir: Path
|
||||
) -> Optional[Path]:
|
||||
"""
|
||||
Process a single dependency from a ZON file.
|
||||
|
||||
Args:
|
||||
name: Name of the dependency
|
||||
dep_info: Dependency information from the ZON file
|
||||
cache_dir: Cache directory to store the dependency
|
||||
|
||||
Returns:
|
||||
Path to the extracted dependency directory, or None if the dependency is already cached
|
||||
"""
|
||||
url = dep_info.get("url")
|
||||
hash_value = dep_info.get("hash")
|
||||
|
||||
if not url or not hash_value:
|
||||
logger.warning(f"Dependency {name} is missing url or hash, skipping")
|
||||
return None
|
||||
|
||||
# Check if the dependency is already cached
|
||||
target_dir = cache_dir / hash_value
|
||||
if target_dir.exists():
|
||||
logger.info(
|
||||
f"Dependency {name} ({hash_value}) is already cached at {target_dir}"
|
||||
)
|
||||
return target_dir
|
||||
|
||||
# Create a temporary directory for downloading and extracting
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_dir_path = Path(temp_dir)
|
||||
|
||||
# Download the tarball
|
||||
tarball_path = temp_dir_path / f"{name}.tar.gz"
|
||||
download_file(url, tarball_path)
|
||||
|
||||
# Extract the tarball to a temporary directory
|
||||
extract_path = extract_tarball(tarball_path, temp_dir_path / "extract")
|
||||
|
||||
# Move the extracted directory to the cache directory with the hash as the name
|
||||
if extract_path and extract_path.exists():
|
||||
if not target_dir.parent.exists():
|
||||
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(extract_path), str(target_dir))
|
||||
|
||||
logger.info(f"Dependency {name} ({hash_value}) cached at {target_dir}")
|
||||
return target_dir
|
||||
else:
|
||||
logger.error(f"Failed to extract {name} from {tarball_path}")
|
||||
return None
|
||||
|
||||
|
||||
def process_dependencies(zon_file_path: str) -> Dict[str, Path]:
|
||||
"""
|
||||
Process all dependencies from a ZON file.
|
||||
|
||||
Args:
|
||||
zon_file_path: Path to the ZON file
|
||||
|
||||
Returns:
|
||||
Dictionary mapping dependency names to their extracted paths
|
||||
"""
|
||||
# Parse the ZON file
|
||||
zon_data = parse_zon_file(zon_file_path)
|
||||
|
||||
# Get the dependencies section
|
||||
dependencies = zon_data.get("dependencies", {})
|
||||
if not dependencies:
|
||||
logger.warning(f"No dependencies found in {zon_file_path}")
|
||||
return {}
|
||||
|
||||
# Get the cache directory
|
||||
cache_dir = get_cache_dir()
|
||||
|
||||
# Process each dependency
|
||||
result = {}
|
||||
for name, dep_info in dependencies.items():
|
||||
path = process_dependency(name, dep_info, cache_dir)
|
||||
if path:
|
||||
result[name] = path
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main(zon_file_path: str) -> None:
|
||||
"""
|
||||
Main entry point for the dependency downloader.
|
||||
|
||||
Args:
|
||||
zon_file_path: Path to the ZON file
|
||||
"""
|
||||
logger.info(f"Processing dependencies from {zon_file_path}")
|
||||
dependencies = process_dependencies(zon_file_path)
|
||||
|
||||
if dependencies:
|
||||
logger.info(f"Successfully processed {len(dependencies)} dependencies:")
|
||||
for name, path in dependencies.items():
|
||||
logger.info(f" - {name}: {path}")
|
||||
else:
|
||||
logger.warning("No dependencies were processed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: {sys.argv[0]} <zon_file_path>")
|
||||
sys.exit(1)
|
||||
|
||||
main(sys.argv[1])
|
||||
@ -1,60 +1,79 @@
|
||||
"""
|
||||
Command-line interface for the ZON parser.
|
||||
Command-line interface for zon2json.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from loguru import logger
|
||||
|
||||
from zig_fetch_py.parser import parse_zon_file
|
||||
from zig_fetch_py.parser import zon_to_json
|
||||
|
||||
|
||||
def setup_logger(verbose: bool = False):
|
||||
"""
|
||||
Set up the logger.
|
||||
|
||||
Args:
|
||||
verbose: Whether to enable verbose logging
|
||||
"""
|
||||
logger.remove()
|
||||
log_level = "DEBUG" if verbose else "INFO"
|
||||
logger.add(sys.stderr, level=log_level)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("file", type=click.Path(exists=True, readable=True))
|
||||
@click.argument("zon_file", type=click.Path(exists=True, readable=True, path_type=Path))
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output",
|
||||
type=click.Path(writable=True),
|
||||
help="Output JSON file path (default: stdout)",
|
||||
type=click.Path(writable=True, path_type=Path),
|
||||
help="Output file (default: stdout)",
|
||||
)
|
||||
@click.option(
|
||||
"-i", "--indent", type=int, default=2, help="Indentation for the JSON output"
|
||||
)
|
||||
@click.option(
|
||||
"--empty-tuple-as-dict",
|
||||
is_flag=True,
|
||||
help="Parse empty tuples as empty dictionaries",
|
||||
)
|
||||
@click.option("-p", "--pretty", is_flag=True, help="Pretty print JSON output")
|
||||
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging")
|
||||
def main(file, output, pretty, verbose):
|
||||
"""Parse ZON files and convert to JSON.
|
||||
|
||||
This tool parses Zig Object Notation (ZON) files and converts them to JSON format.
|
||||
def main(zon_file, output, indent, empty_tuple_as_dict, verbose):
|
||||
"""
|
||||
# Configure logging
|
||||
log_level = "DEBUG" if verbose else "INFO"
|
||||
logger.remove() # Remove default handler
|
||||
logger.add(sys.stderr, level=log_level)
|
||||
Convert a ZON file to JSON.
|
||||
|
||||
logger.info(f"Processing file: {file}")
|
||||
ZON_FILE: Path to the ZON file to convert
|
||||
"""
|
||||
# Set up logging
|
||||
setup_logger(verbose)
|
||||
|
||||
try:
|
||||
result = parse_zon_file(file)
|
||||
# Read the ZON file
|
||||
with open(zon_file, "r") as f:
|
||||
zon_content = f.read()
|
||||
|
||||
indent = 4 if pretty else None
|
||||
json_str = json.dumps(result, indent=indent)
|
||||
# Convert to JSON
|
||||
json_content = zon_to_json(
|
||||
zon_content, indent=indent, empty_tuple_as_dict=empty_tuple_as_dict
|
||||
)
|
||||
|
||||
# Output the JSON
|
||||
if output:
|
||||
logger.info(f"Writing output to: {output}")
|
||||
with open(output, "w") as f:
|
||||
f.write(json_str)
|
||||
f.write(json_content)
|
||||
logger.info(f"JSON written to {output}")
|
||||
else:
|
||||
logger.debug("Writing output to stdout")
|
||||
click.echo(json_str)
|
||||
click.echo(json_content)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.error(f"File not found: {zon_file}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# This is only executed when the module is run directly
|
||||
if __name__ == "__main__":
|
||||
# When imported as a module, click will handle the function call
|
||||
# When run directly, we need to call it explicitly
|
||||
main()
|
||||
|
||||
@ -3,8 +3,7 @@ ZON parser module - Parses Zig Object Notation (ZON) files.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union, Tuple, Optional
|
||||
from typing import Any, Dict, List, Union, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
Reference in New Issue
Block a user