Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.6.2 - 20260-04-30]
## [0.6.3 - 2026-05-07]
### CHANGED
- Update gebco fetchez module to fetch new 2026 grid, and allow subsetting!
- Moved recipe validation from cli to fetchez.recipe
- Updated verbosity in fetchez.core


## [0.6.2 - 2026-04-30]
### ADDED
- ProfileRegistry - streams
- ReaderRegistry - streams
Expand All @@ -21,7 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- core.run_fetchez now returns the final list of entries for use in the api and elsewhere.
- Update run cli to allow for inherited options for modules.

## BUGFIX
### BUGFIX
- fix bug in earthdata.icesat2 for harmony fetching.

## [0.5.5 - 2026-04-24]
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ authors:

website: https://ciresdem.github.io/fetchez/
title: "Fetchez"
version: 0.6.2
date-released: 2026-04-24
version: 0.6.3
date-released: 2026-05-07
url: "https://github.com/ciresdem/fetchez"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<p align="center"><strong>Fetch geospatial data with ease.</strong></p>

<p align="center">
<a href="https://github.com/continuous-dems/fetchez"><img src="https://img.shields.io/badge/version-0.6.2-blue.svg" alt="Version"></a>
<a href="https://github.com/continuous-dems/fetchez"><img src="https://img.shields.io/badge/version-0.6.3-blue.svg" alt="Version"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.12+-yellow.svg" alt="Python"></a>
<a href="https://badge.fury.io/py/fetchez"><img src="https://badge.fury.io/py/fetchez.svg" alt="PyPI version"></a>
Expand Down
97 changes: 11 additions & 86 deletions src/fetchez/cli/recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,39 +23,6 @@
RECIPE_COMMANDS = ["copy", "dump", "info", "list", "validate", "run", "schemas"]


def validate_dependencies(recipe_obj):
"""Interrogates all modules and hooks to ensure heavy dependencies exist."""

errors = []

# Check all global hooks
for hook in getattr(recipe_obj, "global_hooks", []):
if hasattr(hook, "_validate_deps"):
passed, msg = hook._validate_deps()
if not passed:
errors.append(f"[{hook.name}] {msg}")

# Check all streaming hooks inside the modules
for mod in getattr(recipe_obj, "modules", []):
for hook in getattr(mod, "hooks", []):
if hasattr(hook, "_validate_deps"):
passed, msg = hook._validate_deps()
if not passed:
errors.append(f"[{mod.name} -> {hook.name}] {msg}")

if errors:
click.secho("\n[ DEPENDENCY VALIDATION CHECK FAILED ]", fg="red", bold=True)
click.secho(
"The following dependencies are missing for this recipe:", fg="yellow"
)
for error in errors:
click.echo(f" {error}")
click.echo(
"\nPlease install the required packages or modify the recipe and try again.\n"
)
sys.exit(1)


def _load_yaml(target):
base_config = None
if os.path.exists(target) and not os.path.isdir(target):
Expand Down Expand Up @@ -186,12 +153,7 @@ def copy_recipe(name):
@recipes_group.command("validate", cls=FetchezMainCommand)
@click.argument("name")
def recipe_validate(name):
"""Check a recipe for syntax errors and missing modules/hooks."""

from fetchez.registry import ModuleRegistry, HookRegistry

ModuleRegistry.load_all()
HookRegistry.load_all()
"""Check a recipe for syntax errors, logical issues, and missing dependencies."""

base_config = _load_yaml(name)
if not base_config:
Expand All @@ -200,57 +162,20 @@ def recipe_validate(name):
)
sys.exit(1)

errors = 0
click.secho(f"Validating {name}...", fg="blue")

validate_dependencies(base_config)

for mod in base_config.get("modules", []):
mod_name = mod.get("module")
mod_keys = mod.keys()
valid_keys = ["module", "hooks", "args"]

for key in mod_keys:
if key not in valid_keys:
click.secho(
f" Module `{mod_name}` has unexpected reference to `{key}`",
fg="red",
)
errors += 1

if not ModuleRegistry.get_class(mod_name) and mod_name not in [
"file",
"local_fs",
]:
click.secho(f" Missing Module: '{mod_name}'", fg="red")
errors += 1
else:
click.secho(f" Valid Module: '{mod_name}'", fg="green")

for hook in mod.get("hooks", []):
if not HookRegistry.get_class(hook.get("name")):
click.secho(
f" Missing Hook: '{hook.get('name')}' (in module {mod_name})",
fg="red",
)
errors += 1
else:
click.secho(
f" Valid Hook: '{hook.get('name')}' (in module {mod_name})",
fg="green",
)

for hook in base_config.get("global_hooks", []):
if not HookRegistry.get_class(hook.get("name")):
click.secho(f" Missing Global Hook: '{hook.get('name')}'", fg="red")
errors += 1
else:
click.secho(f" Valid Hook: '{hook.get('name')}'", fg="green")

if errors == 0:
recipe_obj = Recipe.from_dict(base_config)
is_valid, errors = recipe_obj.validate()

if is_valid:
click.secho("Recipe appears valid!", fg="green", bold=True)
else:
click.secho(f"Failed validation with {errors} errors.", fg="red", bold=True)
click.secho(
f"\n[ VALIDATION FAILED WITH {len(errors)} ERRORS ]", fg="red", bold=True
)
for error in errors:
click.echo(f" {error}")
click.echo("\nPlease modify the recipe and try again.\n")
sys.exit(1)


Expand Down
10 changes: 5 additions & 5 deletions src/fetchez/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ def fetch_req(
return req

except Exception as e:
logger.warning(f"Attempt {attempt + 1}/{tries} failed: {e}")
logger.debug(f"Attempt {attempt + 1}/{tries} failed: {e}")
if current_timeout:
current_timeout *= 2
if current_read_timeout:
Expand Down Expand Up @@ -610,7 +610,7 @@ def fetch_file(
if req.status_code == 416:
# Range No Good: Local file is likely corrupt.
# Delete .part and retry from scratch (next loop iteration)
logger.warning(
logger.debug(
f"Invalid Range for {os.path.basename(dst_fn)}. Restarting..."
)
if os.path.exists(part_fn):
Expand Down Expand Up @@ -704,7 +704,7 @@ def fetch_file(
) as e:
if attempt < tries - 1:
wait_time = (attempt + 1) * 2
logger.warning(f"Download failed: {e}. Retrying in {wait_time}s...")
logger.debug(f"Download failed: {e}. Retrying in {wait_time}s...")
time.sleep(wait_time)
else:
logger.warning(f"Failed to download {self.url}: {e}")
Expand Down Expand Up @@ -888,7 +888,7 @@ def run_fetchez(modules: List["FetchModule"], threads: int = 3, global_hooks=Non
original_entry.update({"status": status})
if status != 0:
logger.error(
f"Failed: {os.path.basename(original_entry['dst_fn'])}"
f"Failed to fetch: {os.path.basename(original_entry['dst_fn'])}"
)
except Exception as e:
logger.error(f"Worker exception: {e}")
Expand Down Expand Up @@ -932,7 +932,7 @@ def run_fetchez(modules: List["FetchModule"], threads: int = 3, global_hooks=Non
)
collections.deque(stream, maxlen=0)
except Exception as e:
logger.error(
logger.exception(
f"Stream processing error in {os.path.basename(item.get('dst_fn', ''))}: {e}"
)

Expand Down
3 changes: 3 additions & 0 deletions src/fetchez/hooks/sidecar.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ def run(self, entries):
try:
with open(meta_fn, "w") as f:
json.dump(meta_data, f, indent=2)

# Add sidecar as an artifact to the entry
entry.setdefault("artifacts", {})[self.name] = meta_fn
except Exception as e:
logger.warning(f"Failed to write sidecar for {filepath}: {e}")

Expand Down
Loading