Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7521faa
wip
pgierz Dec 12, 2025
80223e2
wip for cli debugger
pgierz Dec 12, 2025
0d98352
wip
pgierz Dec 12, 2025
8ff2dee
...
pgierz Dec 12, 2025
89955b2
...
pgierz Dec 12, 2025
0513d32
feat: accessor API with lazy registration, StdLibAccessor, and .proce…
pgierz Mar 27, 2026
3d950a9
Merge remote-tracking branch 'origin/prep-release' into fix/gh250
pgierz Mar 27, 2026
0c78b9a
style: fix trailing whitespace and isort
pgierz Mar 27, 2026
492ca1c
fix: update pi_uxarray download URL (old Nextcloud share expired)
pgierz Mar 27, 2026
3b2af74
fix: add compound_name to CMIP7 test configs and setuptools to Docker
pgierz Mar 27, 2026
fa75da0
fix: use CMIP6-style compound names (Table.variable) in CMIP7 test co…
pgierz Mar 27, 2026
0ea98cf
fix: guard pyfesom2 imports in tests to avoid collection errors
pgierz Mar 27, 2026
66cf054
fix: guard pyfesom2 import in regridding.py for environments without …
pgierz Mar 27, 2026
11f5bbe
fix: skip pyfesom2-dependent tests when pyfesom2 is not importable
pgierz Mar 27, 2026
b405898
fix: CMIP7 compound_name matching when DRV has plain variable_id
pgierz Mar 27, 2026
ab87d7b
fix: propagate pipeline/flow errors instead of silently logging them
pgierz Mar 27, 2026
3345d7e
fix: remove raw convert() from DefaultPipeline steps
pgierz Mar 27, 2026
3c8d3f5
fix: ConfigurationError from dict default and missing activity_id
pgierz Mar 27, 2026
197802c
fix: dimension_mapping Mock iteration error and doctest in base_model…
pgierz Mar 27, 2026
ff766ee
fix: fall back to _pycmor_cfg for dimension_mapping when rule attr is…
pgierz Mar 27, 2026
51a00e1
fix: derive table_id from CMIP6-style compound names (Table.variable)
pgierz Mar 30, 2026
6ffc348
fix: filter None values from global attributes before applying to dat…
pgierz Mar 30, 2026
8f8f7a5
fix: pin sphinx<9 for sphinx_toolbox compatibility
pgierz Mar 30, 2026
130e897
feat: rename non-standard time dimension on load (OpenIFS support)
pgierz Mar 30, 2026
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
1 change: 1 addition & 0 deletions Dockerfile.test
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ RUN micromamba install -y -n base -c conda-forge \
h5netcdf \
h5py \
pip \
setuptools \
&& micromamba clean --all --yes

# Activate the base environment for subsequent commands
Expand Down
2 changes: 1 addition & 1 deletion doc/infer_freq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ Accessor Methods
~~~~~~~~~~~~~~~~

Time frequency functionality is available through xarray accessors. For comprehensive
documentation of all accessor methods, including both specialized (``timefreq``) and
documentation of all accessor methods, including both specialized (``timefreq``) and
unified (``pymor``) accessors, see:

.. seealso::
Expand Down
20 changes: 10 additions & 10 deletions doc/netcdf_chunking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,41 +23,41 @@ Configuration Options
Global Configuration via Inherit Block
---------------------------------------

The recommended way to configure chunking is through the ``inherit`` block in your pycmor configuration file.
The recommended way to configure chunking is through the ``inherit`` block in your pycmor configuration file.
Settings in the ``inherit`` block are automatically passed down to all rules, making them available as rule attributes:

.. code-block:: yaml

general:
cmor_version: "CMIP6"
CMIP_Tables_Dir: ./cmip6-cmor-tables/Tables/

pycmor:
warn_on_no_rule: False

# Chunking configuration that applies to all rules
inherit:
# Enable/disable chunking
netcdf_enable_chunking: yes

# Chunking algorithm: simple, even_divisor, or iterative
netcdf_chunk_algorithm: simple

# Target chunk size (can be specified as bytes or string like '100MB')
netcdf_chunk_size: 100MB

# Tolerance for chunk size matching (0.0-1.0, used by even_divisor and iterative)
netcdf_chunk_tolerance: 0.5

# Prefer chunking along time dimension
netcdf_chunk_prefer_time: yes

# Compression level (1-9, higher = better compression but slower)
netcdf_compression_level: 4

# Enable zlib compression
netcdf_enable_compression: yes

rules:
- model_variable: temp
cmor_variable: tas
Expand Down
1 change: 1 addition & 0 deletions doc/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
sphinx>=7.4.7,<9
sphinx-book-theme
sphinx-click
sphinx-copybutton
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ dev = [
]

doc = [
"sphinx>=7.4.7,<9",
"sphinx-book-theme>=1.1.4",
"sphinx-click>=6.0.0",
"sphinx-copybutton>=0.5.2",
Expand Down
37 changes: 32 additions & 5 deletions src/pycmor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,38 @@

from . import _version

# Import unified accessor to trigger xarray registration
# This makes ds.pycmor.coords, ds.pycmor.dims, and time frequency methods available
from .xarray import accessor # noqa: F401

__author__ = "Paul Gierz <pgierz@awi.de>"
__all__ = []
__all__ = ["enable_xarray_accessor"]

__version__ = _version.get_versions()["version"]

_accessor_registered = False


def enable_xarray_accessor(log_level="INFO"):
"""Enable the pycmor xarray accessor (ds.pycmor).

This function lazily registers the pycmor accessor on xarray Dataset
and DataArray objects. It is idempotent -- calling it multiple times
has no additional effect.

Parameters
----------
log_level : str, optional
Logging level for loguru (default: "INFO").
"""
global _accessor_registered
if _accessor_registered:
return

from .xarray.accessor import PycmorAccessor, PycmorDataArrayAccessor # noqa: F401

# Configure loguru with rich formatting
try:
from loguru import logger

logger.enable("pycmor")
except ImportError:
pass

_accessor_registered = True
154 changes: 154 additions & 0 deletions src/pycmor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,160 @@ def populate_cache(files: List):
fc.save()


################################################################################
################################################################################
################################################################################

################################################################################
# CMIP7 Testing Commands
################################################################################


@cli.command()
@click_loguru.init_logger()
@click.argument("compound_name", type=click.STRING)
@click.option(
"--version",
"-v",
default="v1.2.2.2",
help="CMIP7 data request version to test against",
show_default=True,
)
@click.option(
"--metadata-file",
"-m",
type=click.Path(exists=True),
help="Path to local metadata JSON file (optional)",
)
@click.option(
"--show-all-variants",
"-a",
is_flag=True,
help="Show all variants of the variable if found",
)
def cmip7_name_test(compound_name, version, metadata_file, show_all_variants):
"""
Test a CMIP7 compound name against the data request.

Checks if the given compound name exists in the CMIP7 data request
and displays metadata information.

Example compound name format: realm.variable.branding.frequency.region
Example: atmos.tas.tavg-h2m-hxy-u.mon.GLB
"""
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from .data_request.cmip7_interface import CMIP7Interface

console = Console()

try:
# Initialize interface
console.print("[bold]Loading CMIP7 Data Request...[/bold]")
interface = CMIP7Interface()
interface.load_metadata(version=version, metadata_file=metadata_file)
console.print(f"[green]✓[/green] Loaded metadata for version: {version}\n")

# Try to find the compound name
console.print(f"[bold]Testing compound name:[/bold] {compound_name}\n")
metadata = interface.get_variable_metadata(compound_name)

if metadata:
# Found it!
console.print(Panel("[bold green]✓ Compound name FOUND in data request[/bold green]", border_style="green"))

# Display metadata in a table
table = Table(title="Variable Metadata", show_header=True, header_style="bold magenta")
table.add_column("Property", style="cyan", no_wrap=True)
table.add_column("Value", style="white")

# Key properties to display
display_props = [
"variable_id",
"standard_name",
"long_name",
"units",
"frequency",
"modeling_realm",
"cmip6_compound_name",
"cell_methods",
"cell_measures",
]

for prop in display_props:
if prop in metadata:
value = str(metadata[prop])
# Truncate very long values
if len(value) > 80:
value = value[:77] + "..."
table.add_row(prop, value)

console.print(table)

# Show all variants if requested
if show_all_variants:
parts = compound_name.split(".")
if len(parts) == 5:
realm, variable, branding, frequency, region = parts
console.print(f"\n[bold]Finding all variants of variable '{variable}' in realm '{realm}'...[/bold]")
variants = interface.find_variable_variants(variable, realm=realm)

if len(variants) > 1:
console.print(f"Found {len(variants)} total variants:\n")
for var in variants:
console.print(f" • {var['cmip7_compound_name']}")
else:
console.print("No other variants found.")

else:
# Not found
console.print(Panel("[bold red]✗ Compound name NOT FOUND in data request[/bold red]", border_style="red"))

# Try to provide helpful information
parts = compound_name.split(".")
if len(parts) != 5:
console.print(
f"\n[yellow]Warning:[/yellow] Compound name should have 5 parts "
f"(realm.variable.branding.frequency.region), but got {len(parts)} parts."
)
else:
realm, variable, branding, frequency, region = parts
console.print("\n[bold]Searching for similar variables...[/bold]")

# Try to find variants of this variable
variants = interface.find_variable_variants(variable, realm=realm)
if variants:
console.print(f"\nFound {len(variants)} variant(s) of '{variable}' in realm '{realm}':")
for var in variants:
console.print(f" • {var['cmip7_compound_name']}")
console.print("\n[yellow]Hint:[/yellow] Check if one of these matches what you're looking for.")
else:
console.print(f"\n[yellow]No variants found for variable '{variable}' in realm '{realm}'.[/yellow]")
console.print("\n[yellow]Suggestions:[/yellow]")
console.print(" 1. Check spelling of variable name")
console.print(" 2. Verify the realm is correct")
console.print(" 3. Use 'pycmor table-explorer' to browse available variables")

return 0

except ImportError as e:
console.print(
Panel(
"[bold red]Error: CMIP7 Data Request API not installed[/bold red]\n\n"
f"{str(e)}\n\n"
"Install with: pip install CMIP7-data-request-api",
border_style="red",
)
)
return 1
except Exception as e:
console.print(Panel(f"[bold red]Error:[/bold red] {str(e)}", border_style="red"))
logger.exception("Failed to test compound name")
return 1


################################################################################
################################################################################
################################################################################
Expand Down
Loading
Loading