Metadata-Version: 2.4
Name: pymetkit
Version: 1.21.0.30
Summary: Python interface to metkit
Author-email: European Centre for Medium-Range Weather Forecasts (ECMWF) <software.support@ecmwf.int>
License-Expression: Apache-2.0
Project-URL: Documentation, https://github.com/ecmwf/metkit
Project-URL: Homepage, https://github.com/ecmwf/metkit
Project-URL: Issues, https://github.com/ecmwf/metkit/issues
Project-URL: Repository, https://github.com/ecmwf/metkit
Keywords: python,metkit,mars,tools
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: findlibs>=0.1.2
Requires-Dist: pydantic>=2
Requires-Dist: PyYAML>=5.1
Requires-Dist: requests>=2.34
Requires-Dist: metkitlib==1.21.0.30
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pytest-flakes; extra == "test"
Requires-Dist: Sybil[pytest]; extra == "test"
Provides-Extra: docs
Requires-Dist: Sphinx; extra == "docs"
Requires-Dist: breathe; extra == "docs"
Requires-Dist: sphinx-book-theme; extra == "docs"
Requires-Dist: requests; extra == "docs"
Requires-Dist: sphinxcontrib-mermaid; extra == "docs"
Requires-Dist: autoapi; extra == "docs"
Requires-Dist: sphinx-autoapi; extra == "docs"
Provides-Extra: dev
Requires-Dist: isort; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: flake8; extra == "dev"
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: license
Dynamic: license-file
Dynamic: license-expression
Dynamic: requires-dist
Dynamic: requires-python

# pymetkit

[![Static Badge](https://github.com/ecmwf/codex/raw/refs/heads/main/Project%20Maturity/emerging_badge.svg)](https://github.com/ecmwf/codex/raw/refs/heads/main/Project%20Maturity#emerging)

> \[!IMPORTANT\]
> This software is **Emerging** and subject to ECMWF's guidelines on [Software Maturity](https://github.com/ecmwf/codex/raw/refs/heads/main/Project%20Maturity).

`pymetkit` is a Python interface to [metkit](https://github.com/ecmwf/metkit), ECMWF's
meteorological toolkit. It exposes the MARS request model in a Pythonic way. 

The native `libmetkit` shared library and its dependencies are located at runtime via
[findlibs](https://github.com/ecmwf/findlibs).

## Usage

```python
from pymetkit import MarsRequest, parse_mars_request

# Build a request from a verb and a selection
request = MarsRequest(
    "retrieve",
    {
        "class": "od",
        "domain": "g",
        "date": "-1",
        "expver": "0001",
        "step": range(0, 13, 6),
    },
)

# Expand against the MARS language definition
expanded = request.expand()
print(expanded.verb(), dict(expanded))

# Parse requests from a string or a file
requests = parse_mars_request("retrieve,class=od,date=-1,param=129,step=12")
```

## ParamDB — parameter database

`ParamDB` maps between ECMWF short names, long names and numeric parameter IDs, backed by a
bundled `parameter_metadata.json` or, in `mode="online"`, the ECMWF parameter API.

```python
from pymetkit import ParamDB, AmbiguousParamError

db = ParamDB()                       # mode="offline" by default; data loads lazily

db.shortname_to_param_id("msl")      # 151  — unambiguous
db.param_id_to_shortname(151)        # "msl"
db.shortname_to_longname("2t")       # "2 metre temperature"
db.get_units(167)                    # "K"
```

### Ambiguous short names

Some short names map to more than one parameter ID (e.g. `tp` → `228` and `228228`). ParamDB
never guesses — an ambiguous lookup **raises** by default:

```python
try:
    db.shortname_to_param_id("tp")
except AmbiguousParamError as exc:
    print(exc.shortname)             # "tp"
    for cand in exc.candidates:      # every ParamIDCandidate, sorted
        print(cand.param_id, cand.table)
```

You can resolve the ambiguity in three ways:

```python
# 1. Narrow with a MARS context (resolved via the C++ expand engine)
db.shortname_to_param_id("tp", context={"class": "od"})   # 228

# 2. Narrow with hard metadata filters (no MARS request constructed)
db.shortname_to_param_id("tp", table=128)                 # 228

# 3. Accept the canonical (first-sorted, lowest-table/id) candidate
db.shortname_to_param_id("tp", default=True)              # 228
```

To inspect the options programmatically instead of catching the error, use
`shortname_to_param_id_candidates`, which returns a list of `ParamIDCandidate`
(`param_id`, `table`, `origin`, `access`, `mars_request_context`):

```python
for cand in db.shortname_to_param_id_candidates("tp"):
    if cand.hard_filter_selector is not None:
        # A hard-filter selector proven to select exactly this candidate.
        print(cand.param_id, cand.hard_filter_selector)
    else:
        # No hard filter uniquely identifies this candidate (e.g. two ids
        # share the same table, origin and access); use context= instead.
        print(cand.param_id, "no unique hard-filter selector")
```

> **Note:** `hard_filter_selector` is either a dict of `table`/`origin`/`access`
> kwargs **proven to select exactly one** candidate, or `None` when no
> combination of the available hard filters disambiguates it. The API never
> advertises a selector that would remain ambiguous.

> **Note:** Per-candidate MARS context computation is temporarily deferred, so every returned
> or raised `ParamIDCandidate` currently carries `mars_request_context=None`. Passing
> `context=` to *narrow* a lookup still works; only the *advertised* selecting context is
> unavailable for now. The `context=` path is resolved by the compiled MetKit `expand`
> engine, which is always available (`pymetkit` imports the native `pymetkit._internal`
> extension unconditionally).

## Command line

```bash
python -m pymetkit --print-home        # metkit library home
python -m pymetkit --print-home-deps   # all dependency homes and versions
```

## Technical details

### Regenerating bundled parameter metadata

The bundled `share/metkit/parameter_metadata.json` (and `.yaml`) are generated by
fetching from the [ECMWF parameter database API](https://codes.ecmwf.int/parameter-database/).
Run the generator script when the upstream database changes:

```bash
python -m pymetkit.paramdb.generate_metadata
```

This requires network access and the `requests` and `pyyaml` packages. It writes the
following files relative to the repository root:

| File | Description |
|------|-------------|
| `share/metkit/parameter_metadata.json` | Compact JSON — preferred at runtime (~10-50× faster to load than YAML) |
| `share/metkit/parameter_metadata.yaml` | Human-readable YAML — fallback if JSON is absent |
| `share/metkit/unit_metadata.yaml` | Unit definitions |
| `share/metkit/parameter_entry_schema.json` | JSON Schema for `ParameterEntry` validation |
| `share/metkit/mars_context_schema.json` | JSON Schema for `MarsRequestContext` validation |

Commit the updated files to keep the bundled metadata in sync with the upstream database.

## Documentation

For implementation details and tooling, see the [Metkit project pages](https://sites.ecmwf.int/docs/metkit).

To build the latest documentation locally, follow the guide at [Metkit](https://github.com/ecmwf/metkit).


## License

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ecmwf/metkit/blob/develop/LICENSE)
