Skip to content

Commit 659f222

Browse files
committed
METK-171: Add API for mars key parsing
1 parent 97063d8 commit 659f222

9 files changed

Lines changed: 221 additions & 1 deletion

File tree

python/pymetkit/src/pymetkit/metkit_c.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const char* metkit_git_sha1();
2727
metkit_error_t metkit_initialise();
2828

2929
metkit_error_t metkit_parse_marsrequests(const char* str, metkit_requestiterator_t** requests, bool strict);
30+
metkit_error_t metkit_parse_key(const char* verb, const char* keyword, const char* value,
31+
const metkit_marsrequest_t* context, bool strict, metkit_paramiterator_t** values);
3032
metkit_error_t metkit_marsrequest_new(metkit_marsrequest_t** request);
3133
metkit_error_t metkit_marsrequest_delete(const metkit_marsrequest_t* request);
3234
metkit_error_t metkit_marsrequest_set(metkit_marsrequest_t* request, const char* param, const char* values[],

python/pymetkit/src/pymetkit/pymetkit.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,75 @@ def parse_mars_request(file_or_str: IO | str, strict: bool = False) -> list[Mars
219219
return requests
220220

221221

222+
def parse_key(
223+
keyword: str,
224+
value: str | int | list,
225+
verb: str = "retrieve",
226+
context: "MarsRequest | dict | None" = None,
227+
strict: bool = False,
228+
) -> list[str]:
229+
"""Parse/normalise the values of a single MARS key, without building a full request.
230+
231+
Applies the MARS language rules for ``keyword``: range syntax such as
232+
``"1/to/10/by/1"`` is expanded, and per-key normalisation is performed (e.g.
233+
``date="-1"`` resolves to a ``yyyymmdd`` date, ``time="6"`` to ``"0600"``).
234+
235+
Context-sensitive keys (e.g. those whose interpretation depends on other
236+
keys) consult ``context``. Supply the relevant neighbouring keys there, e.g.
237+
``parse_key("levelist", "1/to/10", context={"levtype": "ml"})``.
238+
239+
Note: this performs single-pass expansion only. Second-pass, rule-based
240+
resolution (e.g. ``param``) and default inheritance are not applied; use
241+
:meth:`MarsRequest.expand` on a (scoped) request for those.
242+
243+
Params
244+
------
245+
keyword: name of the MARS key to parse (canonical, alias or unambiguous prefix)
246+
value: values to expand, as a ``"a/b/c"`` string or a list of tokens
247+
verb: MARS verb whose language defines the key (defaults to "retrieve")
248+
context: optional MarsRequest or dict of neighbouring keys for context-sensitive keys
249+
strict: if True, raise an error on invalid values
250+
251+
Returns
252+
-------
253+
list of expanded/normalised string values
254+
255+
Examples
256+
--------
257+
>>> parse_key("step", "1/to/10/by/1")
258+
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
259+
"""
260+
if isinstance(value, (list, tuple)):
261+
value = "/".join(str(v) for v in value)
262+
else:
263+
value = str(value)
264+
265+
context_c = ffi.NULL
266+
if context is not None:
267+
if isinstance(context, dict):
268+
context = MarsRequest(**context)
269+
context_c = context.ctype()
270+
271+
it_c = ffi.new("metkit_paramiterator_t **")
272+
lib.metkit_parse_key(
273+
ffi_encode(verb),
274+
ffi_encode(keyword),
275+
ffi_encode(value),
276+
context_c,
277+
strict,
278+
it_c,
279+
)
280+
it = ffi.gc(it_c[0], lib.metkit_paramiterator_delete)
281+
282+
values = []
283+
while lib.metkit_paramiterator_next(it) == lib.METKIT_ITERATOR_SUCCESS:
284+
cvalue = ffi.new("const char **")
285+
lib.metkit_paramiterator_current(it, cvalue)
286+
values.append(ffi_decode(cvalue[0]))
287+
288+
return values
289+
290+
222291
class MetKitException(RuntimeError):
223292
"""Raised when MetKit library throws exception"""
224293

python/pymetkit/tests/test_marsrequest.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from contextlib import nullcontext as does_not_raise
33
import pytest
44

5-
from pymetkit import parse_mars_request, MarsRequest, MetKitException
5+
from pymetkit import parse_mars_request, parse_key, MarsRequest, MetKitException
66

77
request = """
88
retrieve,
@@ -81,6 +81,42 @@ def test_empty_request(tmpdir):
8181
assert len(requests) == 0
8282

8383

84+
@pytest.mark.parametrize(
85+
"keyword, value, kwargs, expected",
86+
[
87+
["step", "1/to/10/by/1", {}, [str(i) for i in range(1, 11)]],
88+
["step", [0, 6, 12], {}, ["0", "6", "12"]],
89+
["time", "6/to/18/by/6", {}, ["0600", "1200", "1800"]],
90+
["date", "-1", {}, [yesterday]],
91+
# alias resolves to canonical keyword
92+
["parameter", "130", {}, ["130"]],
93+
# context-sensitive keyword: levelist depends on levtype
94+
[
95+
"levelist",
96+
"1000/to/850/by/50",
97+
{"context": {"levtype": "pl"}},
98+
["1000", "950", "900", "850"],
99+
],
100+
],
101+
)
102+
def test_parse_key(keyword, value, kwargs, expected):
103+
assert parse_key(keyword, value, **kwargs) == expected
104+
105+
106+
def test_parse_key_context_marsrequest():
107+
context = MarsRequest("retrieve", levtype="pl")
108+
assert parse_key("levelist", "500/to/300/by/100", context=context) == [
109+
"500",
110+
"400",
111+
"300",
112+
]
113+
114+
115+
def test_parse_key_strict_raises():
116+
with pytest.raises(MetKitException):
117+
parse_key("time", "notatime", strict=True)
118+
119+
84120
def test_new_request():
85121
req = MarsRequest("retrieve")
86122
assert req.verb() == "retrieve"

src/metkit/api/metkit_c.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "metkit_c.h"
22
#include <functional>
33
#include "eckit/runtime/Main.h"
4+
#include "eckit/utils/StringTools.h"
45
#include "metkit/mars/MarsExpansion.h"
56
#include "metkit/mars/MarsRequest.h"
67
#include "metkit/metkit_version.h"
@@ -183,6 +184,30 @@ metkit_error_t metkit_parse_marsrequest(const char* str, metkit_marsrequest_t* r
183184
});
184185
}
185186

187+
metkit_error_t metkit_parse_key(const char* verb, const char* keyword, const char* value,
188+
const metkit_marsrequest_t* context, bool strict, metkit_paramiterator_t** values) {
189+
return tryCatch([verb, keyword, value, context, strict, values] {
190+
ASSERT(keyword);
191+
ASSERT(value);
192+
ASSERT(values);
193+
194+
std::vector<std::string> tokens = eckit::StringTools::split("/", value);
195+
196+
metkit::mars::MarsExpansion expansion(false, strict);
197+
198+
std::vector<std::string> expanded;
199+
if (context) {
200+
expanded = expansion.parseKey(verb ? verb : "retrieve", keyword, std::move(tokens), *context);
201+
}
202+
else {
203+
expanded =
204+
expansion.parseKey(verb ? verb : "retrieve", keyword, std::move(tokens), metkit::mars::MarsRequest{});
205+
}
206+
207+
*values = new metkit_paramiterator_t(std::move(expanded));
208+
});
209+
}
210+
186211
// -----------------------------------------------------------------------------
187212
// REQUEST
188213
// -----------------------------------------------------------------------------

src/metkit/api/metkit_c.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,34 @@ metkit_error_t metkit_parse_marsrequests(const char* str, metkit_requestiterator
9292
* @return metkit_error_t Error code
9393
*/
9494
metkit_error_t metkit_parse_marsrequest(const char* str, metkit_marsrequest_t* request, bool strict);
95+
/* ---------------------------------------------------------------------------------------------------------------------
96+
* KEY PARSING
97+
* --- */
98+
99+
/**
100+
* Parse/normalise the values of a single MARS keyword in isolation, without building a full request.
101+
*
102+
* Applies the MARS language rules for the keyword: range syntax such as "1/to/10/by/1" is expanded and
103+
* per-key normalisation is performed (e.g. date "-1" resolves to a yyyymmdd date, time "6" to "0600").
104+
* The value string is split on '/' before expansion.
105+
*
106+
* Context-sensitive keywords (e.g. those whose interpretation depends on other keys) consult @p context:
107+
* pass a Request populated with the relevant neighbouring keys, or NULL/empty when not needed.
108+
*
109+
* @note This performs single-pass expansion only. Second-pass, rule-based resolution (e.g. 'param') and
110+
* default inheritance are NOT applied; use metkit_marsrequest_expand on a (scoped) request for those.
111+
*
112+
* @param verb MARS verb whose language defines the keyword (NULL defaults to "retrieve")
113+
* @param keyword keyword to parse (canonical, alias or unambiguous prefix)
114+
* @param value values to expand, as a '/'-separated string (e.g. "1/to/10/by/1")
115+
* @param context Request providing context for context-sensitive keywords, or NULL
116+
* @param strict if true, validate expanded values and raise an error on invalid values
117+
* @param[out] values ParamIterator over the expanded values. Must be deallocated with
118+
* metkit_paramiterator_delete
119+
* @return metkit_error_t Error code
120+
*/
121+
metkit_error_t metkit_parse_key(const char* verb, const char* keyword, const char* value,
122+
const metkit_marsrequest_t* context, bool strict, metkit_paramiterator_t** values);
95123
/* ---------------------------------------------------------------------------------------------------------------------
96124
* REQUEST
97125
* --- */

src/metkit/mars/MarsExpansion.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ MarsRequest MarsExpansion::expand(const MarsRequest& request) {
6060
return language(request.verb()).expand(request, inherit_, strict_);
6161
}
6262

63+
std::vector<std::string> MarsExpansion::parseKey(const std::string& verb, const std::string& keyword,
64+
std::vector<std::string> values, const MarsRequest& context) {
65+
return language(verb).parseKey(keyword, std::move(values), context, strict_);
66+
}
67+
6368
void MarsExpansion::expand(const MarsRequest& request, ExpandCallback& callback) {
6469
callback(expand(request));
6570
}

src/metkit/mars/MarsExpansion.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ class MarsExpansion : public eckit::NonCopyable {
6565
MarsRequest expand(const MarsRequest&);
6666
std::vector<MarsRequest> expand(const std::vector<MarsParsedRequest>&);
6767

68+
/// @brief Parse/normalise the values of a single keyword in isolation, using (and caching) the
69+
/// language for @p verb. See MarsLanguage::parseKey for semantics and limitations.
70+
/// @param verb MARS verb whose language defines the keyword (e.g. "retrieve")
71+
/// @param keyword keyword to parse (canonical, alias or unambiguous prefix)
72+
/// @param values values to expand (already split on '/')
73+
/// @param context other keys providing context for context-sensitive types
74+
/// @return expanded/normalised values
75+
std::vector<std::string> parseKey(const std::string& verb, const std::string& keyword,
76+
std::vector<std::string> values, const MarsRequest& context = {});
77+
6878
void expand(const MarsRequest&, ExpandCallback&);
6979
void flatten(const MarsRequest&, FlattenCallback&);
7080

src/metkit/mars/MarsLanguage.cc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,35 @@ Type* MarsLanguage::type(const std::string& name) const {
439439
}
440440

441441

442+
std::vector<std::string> MarsLanguage::parseKey(const std::string& keyword, std::vector<std::string> values,
443+
const MarsRequest& context, bool strict) {
444+
std::string p = eckit::StringTools::lower(keyword);
445+
446+
std::string canonical;
447+
if (auto c = cache_.find(p); c != cache_.end()) {
448+
canonical = c->second;
449+
}
450+
else {
451+
canonical = cache_[p] = bestMatch(p, keywords_, true, false, true, aliases_);
452+
}
453+
454+
Type* t = type(canonical);
455+
456+
if (values.size() == 1) {
457+
const std::string& s = eckit::StringTools::lower(values[0]);
458+
if (s == "all" && t->multiple()) {
459+
return {"all"};
460+
}
461+
}
462+
463+
t->expand(values, context);
464+
if (strict) {
465+
t->check(values);
466+
}
467+
return values;
468+
}
469+
470+
442471
MarsRequest MarsLanguage::expand(const MarsRequest& r, bool inherit, bool strict) {
443472
MarsRequest result(verb_);
444473

src/metkit/mars/MarsLanguage.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ class MarsLanguage : private eckit::NonCopyable {
6363

6464
Type* type(const std::string& name) const;
6565

66+
/// @brief Parse/normalise the values of a single keyword, in isolation from a full request.
67+
/// Resolves @p keyword against this verb's language (aliases and partial matches allowed),
68+
/// then expands @p values using the keyword's Type. Range syntax (e.g. "1/to/10/by/1") and
69+
/// per-key normalisation (e.g. date/time) are applied. Context-sensitive keys (e.g. those
70+
/// backed by TypeMixed) consult @p context; supply the relevant neighbouring keys there.
71+
/// @note This performs the single-pass expansion only. Second-pass resolution (pass2/finalise,
72+
/// e.g. rule-based 'param' expansion) and default inheritance are not applied here; use a
73+
/// (scoped) MarsRequest expansion for those.
74+
/// @param keyword keyword to parse (canonical, alias or unambiguous prefix)
75+
/// @param values values to expand (already split on '/')
76+
/// @param context other keys providing context for context-sensitive types
77+
/// @param strict if true, validate expanded values and throw on invalid values
78+
/// @return expanded/normalised values
79+
std::vector<std::string> parseKey(const std::string& keyword, std::vector<std::string> values,
80+
const MarsRequest& context = {}, bool strict = false);
81+
6682
bool isData(const std::string& keyword) const;
6783

6884
bool isPostProc(const std::string& keyword) const;

0 commit comments

Comments
 (0)