Source code for fetchez.api

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
fetchez.api
~~~~~~~~~~~
High-level Python Interface for Fetchez.

Usage::

    import fetchez

    # Search
    fetchez.search("bathymetry")

    # Get Data (Returns list of local file paths)
    files = fetchez.get("nos_hydro", region=[-120, -118, 33, 34], year=2020)

    # Advanced (With Hooks)
    files = fetchez.get("charts", region=[-120, -118, 33, 34], hooks=['unzip', 'filename_filter:match=.000'])
"""

import os
import logging
from typing import List, Optional, Dict, Any

from .utils import parse_hook_string
from .core import run_fetchez
from .spatial import parse_region
from .registry import (
    ModuleRegistry,
    HookRegistry,
    RecipeRegistry,
    SchemaRegistry,
    PresetRegistry,
    ProfileRegistry,
)

logger = logging.getLogger(__name__)


def _search_registry(registry_cls, term: Optional[str] = None) -> Dict[str, Any]:
    """Helper to load and search a specific registry."""

    registry_cls.load_all()
    full_reg = registry_cls.get_registry()

    if not term:
        return full_reg

    found = {}
    term_lower = term.lower()
    for name, meta in full_reg.items():
        desc = meta.get("desc", getattr(meta, "meta_desc", ""))
        tags = [t.lower() for t in getattr(meta, "meta_tags", [])]
        aliases = [a.lower() for a in getattr(meta, "meta_aliases", [])]
        category = meta.get("category", getattr(meta, "meta_category", ""))

        if (
            term_lower in name.lower()
            or term_lower in desc.lower()
            or term_lower in tags
            or term_lower in aliases
            or term_lower in category.lower()
        ):
            found[name] = meta

    return found


[docs] def list_modules() -> Dict[str, Any]: return _search_registry(ModuleRegistry)
[docs] def list_hooks() -> Dict[str, Any]: return _search_registry(HookRegistry)
[docs] def search_hooks(term) -> Dict[str, Any]: return _search_registry(HookRegistry, term)
[docs] def list_recipes() -> Dict[str, Any]: return _search_registry(RecipeRegistry)
[docs] def list_schemas() -> Dict[str, Any]: return _search_registry(SchemaRegistry)
[docs] def list_presets() -> Dict[str, Any]: return _search_registry(PresetRegistry)
[docs] def list_profiles() -> Dict[str, Any]: return _search_registry(ProfileRegistry)
[docs] def search_profiles(term) -> Dict[str, Any]: return _search_registry(ProfileRegistry, term)
[docs] def get( module: str, region: Optional[List[float] | str] = None, outdir: Optional[str] = None, threads: int = 4, hooks: Optional[List[str]] = None, **kwargs, ) -> List[str]: """Fetch data from a module in one line. Args: module: Module name (e.g., 'nos_hydro', 'tnm'). region: [W, E, S, N] or 'loc:Boulder'. outdir: Where to save files (default: ./<module>). threads: Parallel download threads. hooks: List of hook strings (e.g. ['unzip', 'audit']). **kwargs: Arguments passed directly to the module (year=..., datatype=...). Returns: A list of absolute paths to the downloaded files. """ ModuleRegistry.load_all() HookRegistry.load_all() ModCls = ModuleRegistry.get_class(module) if not ModCls: raise ValueError(f"Unknown module: {module}") src_region = parse_region(region)[0] if region else None active_hooks = [] if hooks: for h_str in hooks: hook_config = parse_hook_string(h_str) HookCls = HookRegistry.get_class(hook_config.get("name")) if HookCls: active_hooks.append(HookCls(**hook_config.get("args", {}))) else: logger.warning(f"Hook {hook_config.get('name')} not found. Skipping.") try: mod_instance = ModCls( src_region=src_region, hook=active_hooks, outdir=outdir, **kwargs ) except Exception as e: logger.error(f"Failed to initialize {module}: {e}") return [] logger.debug(f"Querying {module}...") try: mod_instance.run() except Exception as e: logger.error(f"Query failed: {e}") return [] if not mod_instance.results: logger.debug(f"No results found for {module} with given parameters.") return [] # Grab the final results from the fetchez pipeline final_results = run_fetchez([mod_instance], threads=threads) downloaded_files = [] for mod, entry in final_results: # mod_instance.results: if entry.get("status") == 0: fn = entry.get("dst_fn") if fn and os.path.exists(fn): downloaded_files.append(os.path.abspath(fn)) return downloaded_files
[docs] def run_recipe(target: str, region: Optional[str] = None) -> bool: """Execute a YAML recipe. 'target' can be a local file path or the name of a registered recipe. """ import yaml from .recipe import Recipe RecipeRegistry.load_all() base_config = None if os.path.exists(target): with open(target, "r", encoding="utf-8") as f: base_config = yaml.safe_load(f) else: recipe_meta = RecipeRegistry.get_recipe(target) if recipe_meta: base_config = recipe_meta["config"] logger.info(f"Loaded registered recipe: {target}") if not base_config: logger.error(f"Recipe '{target}' not found locally or in the registry.") return False if region: base_config["region"] = region try: Recipe.from_file(base_config).run() return True except Exception as e: logger.error(f"Failed to run recipe '{target}': {e}") return False