envlib 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- envlib/__init__.py +9 -0
- envlib/catalogue.py +970 -0
- envlib/metadata.py +572 -0
- envlib/vocabularies/__init__.py +336 -0
- envlib/vocabularies/aggregation_statistic.json +20 -0
- envlib/vocabularies/feature.json +19 -0
- envlib/vocabularies/frequency_interval.json +21 -0
- envlib/vocabularies/license.json +63 -0
- envlib/vocabularies/method.json +37 -0
- envlib/vocabularies/processing_level.json +21 -0
- envlib/vocabularies/standard_name.json +5683 -0
- envlib/vocabularies/variable.json +4391 -0
- envlib-0.1.0.dist-info/METADATA +75 -0
- envlib-0.1.0.dist-info/RECORD +16 -0
- envlib-0.1.0.dist-info/WHEEL +4 -0
- envlib-0.1.0.dist-info/licenses/LICENSE +16 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Controlled vocabularies for envlib metadata fields.
|
|
2
|
+
|
|
3
|
+
Each vocabulary is a bundled JSON file in this package directory. A user-level
|
|
4
|
+
overlay directory (``~/.envlib/vocabularies/``) takes precedence per-file when
|
|
5
|
+
present — :func:`refresh` writes there, never into the installed package.
|
|
6
|
+
|
|
7
|
+
Vocabulary semantics:
|
|
8
|
+
|
|
9
|
+
- Upstream-sourced lists (``variable`` membership, ``standard_name``) are
|
|
10
|
+
refreshable from their APIs via :func:`refresh`.
|
|
11
|
+
- The envlib-curated ``(variable, feature) -> CF standard_name`` mapping inside
|
|
12
|
+
``variable.json`` is hand-maintained and NEVER regenerated by refresh —
|
|
13
|
+
refresh only reports new/removed upstream terms for manual curation.
|
|
14
|
+
- envlib-defined vocabularies (``feature``, ``method``, ``processing_level``,
|
|
15
|
+
``aggregation_statistic``, ``frequency_interval``, ``license``) have no
|
|
16
|
+
upstream source; refresh does not touch them.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import builtins
|
|
22
|
+
import datetime
|
|
23
|
+
import json
|
|
24
|
+
import pathlib
|
|
25
|
+
import re
|
|
26
|
+
import xml.etree.ElementTree as ET
|
|
27
|
+
|
|
28
|
+
import urllib3
|
|
29
|
+
|
|
30
|
+
BUNDLE_DIR = pathlib.Path(__file__).parent
|
|
31
|
+
USER_DIR = pathlib.Path('~/.envlib/vocabularies').expanduser()
|
|
32
|
+
|
|
33
|
+
FIELDS = (
|
|
34
|
+
'feature',
|
|
35
|
+
'variable',
|
|
36
|
+
'method',
|
|
37
|
+
'processing_level',
|
|
38
|
+
'aggregation_statistic',
|
|
39
|
+
'frequency_interval',
|
|
40
|
+
'license',
|
|
41
|
+
'standard_name',
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
ODM2_VARIABLENAME_URL = 'http://vocabulary.odm2.org/api/v1/variablename/?format=json'
|
|
45
|
+
CF_STANDARD_NAME_XML_URL = 'https://cfconventions.org/Data/cf-standard-names/current/src/cf-standard-name-table.xml'
|
|
46
|
+
|
|
47
|
+
_HTTP_OK = 200
|
|
48
|
+
|
|
49
|
+
# Per-field parsed-JSON cache and derived lookup cache (lowercased input -> canonical value).
|
|
50
|
+
_cache: dict = {}
|
|
51
|
+
_lookup_cache: dict = {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def clear_cache():
|
|
55
|
+
"""Clear the in-memory vocabulary caches (called automatically by :func:`refresh`)."""
|
|
56
|
+
_cache.clear()
|
|
57
|
+
_lookup_cache.clear()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _vocab_path(field: str) -> pathlib.Path:
|
|
61
|
+
user_path = USER_DIR / f'{field}.json'
|
|
62
|
+
if user_path.exists():
|
|
63
|
+
return user_path
|
|
64
|
+
return BUNDLE_DIR / f'{field}.json'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _check_field(field: str):
|
|
68
|
+
if field not in FIELDS:
|
|
69
|
+
msg = f'Unknown vocabulary field {field!r}; valid fields are {FIELDS}.'
|
|
70
|
+
raise ValueError(msg)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _load(field: str) -> dict:
|
|
74
|
+
_check_field(field)
|
|
75
|
+
if field not in _cache:
|
|
76
|
+
with open(_vocab_path(field), encoding='utf-8') as f:
|
|
77
|
+
_cache[field] = json.load(f)
|
|
78
|
+
return _cache[field]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _names(field: str) -> builtins.list[str]:
|
|
82
|
+
data = _load(field)
|
|
83
|
+
if field == 'standard_name':
|
|
84
|
+
return builtins.list(data['names'].keys())
|
|
85
|
+
if field == 'variable':
|
|
86
|
+
return builtins.list(data['entries'].keys())
|
|
87
|
+
return [entry['name'] for entry in data['entries']]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _lookup(field: str) -> dict:
|
|
91
|
+
"""Map of lowercased accepted input -> canonical value (includes input aliases)."""
|
|
92
|
+
if field not in _lookup_cache:
|
|
93
|
+
table = {name.lower(): name for name in _names(field)}
|
|
94
|
+
if field == 'frequency_interval':
|
|
95
|
+
for entry in _load(field)['entries']:
|
|
96
|
+
for alias in entry.get('aliases', []):
|
|
97
|
+
table[alias.lower()] = entry['name']
|
|
98
|
+
_lookup_cache[field] = table
|
|
99
|
+
return _lookup_cache[field]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def list(field: str) -> builtins.list[str]: # noqa: A001 - public API name set by the design plan
|
|
103
|
+
"""Return the valid canonical values for a vocabulary field.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
field: One of the envlib vocabulary fields (e.g. ``'variable'``, ``'feature'``).
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of canonical values. For ``frequency_interval`` this is the canonical
|
|
110
|
+
codes only — input aliases (``'24h'``, ``'60min'``) are not included.
|
|
111
|
+
"""
|
|
112
|
+
return _names(field)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def canonical(field: str, value: str) -> str:
|
|
116
|
+
"""Resolve user input to the canonical vocabulary value.
|
|
117
|
+
|
|
118
|
+
Matching is case-insensitive after whitespace stripping; ``frequency_interval``
|
|
119
|
+
additionally resolves the closed input-alias table (``'24h'`` -> ``'day'``,
|
|
120
|
+
``'60min'`` -> ``'1h'``). Raises ``ValueError`` when the value is not in the
|
|
121
|
+
vocabulary.
|
|
122
|
+
"""
|
|
123
|
+
_check_field(field)
|
|
124
|
+
if not isinstance(value, str):
|
|
125
|
+
msg = f'{field} value must be a str, not {type(value).__name__}.'
|
|
126
|
+
raise TypeError(msg)
|
|
127
|
+
key = value.strip().lower()
|
|
128
|
+
table = _lookup(field)
|
|
129
|
+
if key not in table:
|
|
130
|
+
msg = f'{value!r} is not a valid {field}; see envlib.vocabularies.list({field!r}).'
|
|
131
|
+
raise ValueError(msg)
|
|
132
|
+
return table[key]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def is_valid(field: str, value: str) -> bool:
|
|
136
|
+
"""Whether ``value`` is acceptable input for the vocabulary field (aliases included)."""
|
|
137
|
+
try:
|
|
138
|
+
canonical(field, value)
|
|
139
|
+
except (ValueError, TypeError):
|
|
140
|
+
return False
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def frequency_entry(code: str) -> dict:
|
|
145
|
+
"""Return the frequency_interval table entry for a canonical or alias code.
|
|
146
|
+
|
|
147
|
+
The entry dict has keys ``name`` (canonical code), ``kind`` (``'fixed'`` |
|
|
148
|
+
``'calendar'``), ``seconds`` (int, or None for calendar codes), ``aliases``.
|
|
149
|
+
"""
|
|
150
|
+
canon = canonical('frequency_interval', code)
|
|
151
|
+
for entry in _load('frequency_interval')['entries']:
|
|
152
|
+
if entry['name'] == canon:
|
|
153
|
+
return entry
|
|
154
|
+
msg = f'frequency_interval table entry missing for {canon!r} (corrupt vocabulary file?).'
|
|
155
|
+
raise RuntimeError(msg)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_cf_standard_names(variable: str, feature: str) -> builtins.list[str] | None:
|
|
159
|
+
"""CF standard_name candidates envlib considers for a (variable, feature) pair.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
variable: envlib variable (CV member; raises ``ValueError`` if unknown).
|
|
163
|
+
feature: envlib feature (CV member; raises ``ValueError`` if unknown).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Ordered candidate list — the FIRST entry is the curated default that
|
|
167
|
+
envlib auto-populates at validate/register time; an empty list means
|
|
168
|
+
"curated: no applicable standard name" (common for freshwater
|
|
169
|
+
water-quality variables); ``None`` means the pair is not yet curated
|
|
170
|
+
(envlib warns but does not block).
|
|
171
|
+
"""
|
|
172
|
+
var = canonical('variable', variable)
|
|
173
|
+
feat = canonical('feature', feature)
|
|
174
|
+
entry = _load('variable')['entries'][var]
|
|
175
|
+
cf = entry.get('cf')
|
|
176
|
+
if cf is None:
|
|
177
|
+
return None
|
|
178
|
+
candidates = cf.get(feat)
|
|
179
|
+
if candidates is None:
|
|
180
|
+
return None
|
|
181
|
+
return [c['standard_name'] for c in candidates]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
###################################################
|
|
185
|
+
# Refresh from upstream sources
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _fetch(url: str) -> bytes:
|
|
189
|
+
resp = urllib3.request('GET', url, timeout=urllib3.Timeout(total=120))
|
|
190
|
+
if resp.status != _HTTP_OK:
|
|
191
|
+
msg = f'GET {url} returned HTTP {resp.status}.'
|
|
192
|
+
raise ConnectionError(msg)
|
|
193
|
+
return resp.data
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _utc_today() -> str:
|
|
197
|
+
return datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d')
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _write_vocab(target_dir: pathlib.Path, field: str, data: dict):
|
|
201
|
+
path = target_dir / f'{field}.json'
|
|
202
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
203
|
+
json.dump(data, f, indent=1, ensure_ascii=False)
|
|
204
|
+
f.write('\n')
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _snake_case(term: str) -> str:
|
|
208
|
+
s = re.sub(r'(?<=[a-z0-9])([A-Z])', r'_\1', term)
|
|
209
|
+
s = re.sub(r'_+', '_', s)
|
|
210
|
+
return s.lower()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _refresh_standard_name(target_dir: pathlib.Path) -> dict:
|
|
214
|
+
# The CF table ships as XML from cfconventions.org over HTTPS; content is not
|
|
215
|
+
# attacker-controlled in the threat model here, so stdlib parsing is acceptable.
|
|
216
|
+
root = ET.fromstring(_fetch(CF_STANDARD_NAME_XML_URL)) # noqa: S314
|
|
217
|
+
version = root.findtext('version_number')
|
|
218
|
+
last_modified = root.findtext('last_modified')
|
|
219
|
+
names = {}
|
|
220
|
+
for entry in root.findall('entry'):
|
|
221
|
+
names[entry.get('id')] = entry.findtext('canonical_units') or ''
|
|
222
|
+
aliases = {}
|
|
223
|
+
for alias in root.findall('alias'):
|
|
224
|
+
aliases[alias.get('id')] = alias.findtext('entry_id')
|
|
225
|
+
data = {
|
|
226
|
+
'_meta': {
|
|
227
|
+
'vocabulary': 'standard_name',
|
|
228
|
+
'source': f'CF standard name table v{version} ({CF_STANDARD_NAME_XML_URL})',
|
|
229
|
+
'cf_version': version,
|
|
230
|
+
'cf_last_modified': last_modified,
|
|
231
|
+
'updated': _utc_today(),
|
|
232
|
+
},
|
|
233
|
+
'names': dict(sorted(names.items())),
|
|
234
|
+
'aliases': dict(sorted(aliases.items())),
|
|
235
|
+
}
|
|
236
|
+
_write_vocab(target_dir, 'standard_name', data)
|
|
237
|
+
return {'cf_version': version, 'names': len(names), 'aliases': len(aliases)}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _refresh_variable(target_dir: pathlib.Path) -> dict:
|
|
241
|
+
current = json.loads(json.dumps(_load('variable'))) # deep copy of the effective file
|
|
242
|
+
entries = current['entries']
|
|
243
|
+
overrides = current.get('odm2_spelling_overrides', {})
|
|
244
|
+
|
|
245
|
+
payload = json.loads(_fetch(ODM2_VARIABLENAME_URL))
|
|
246
|
+
# The ODM2 API wraps results as {'meta': {...}, 'objects': [...]}.
|
|
247
|
+
objects = payload['objects'] if isinstance(payload, dict) else payload
|
|
248
|
+
if isinstance(payload, dict):
|
|
249
|
+
total = payload.get('meta', {}).get('total_count')
|
|
250
|
+
if total is not None and total > len(objects):
|
|
251
|
+
msg = f'ODM2 API returned {len(objects)} of {total} terms (pagination?); refusing a partial refresh.'
|
|
252
|
+
raise RuntimeError(msg)
|
|
253
|
+
fetched_terms = [item['term'] for item in objects]
|
|
254
|
+
|
|
255
|
+
added = []
|
|
256
|
+
mismatched = []
|
|
257
|
+
conflicts_with_extension = []
|
|
258
|
+
name_sources: dict = {}
|
|
259
|
+
for term in fetched_terms:
|
|
260
|
+
name = overrides.get(term, _snake_case(term))
|
|
261
|
+
name_sources.setdefault(name, []).append(term)
|
|
262
|
+
entry = entries.get(name)
|
|
263
|
+
if entry is None:
|
|
264
|
+
entries[name] = {'source': 'odm2', 'odm2_term': term}
|
|
265
|
+
added.append(name)
|
|
266
|
+
elif entry.get('source') == 'envlib':
|
|
267
|
+
# ODM2 grew a term colliding with an envlib extension — manual curation
|
|
268
|
+
# decides whether to promote the extension; refresh never edits it.
|
|
269
|
+
conflicts_with_extension.append({'name': name, 'odm2_term': term})
|
|
270
|
+
elif entry.get('odm2_term') is None:
|
|
271
|
+
entry['odm2_term'] = term
|
|
272
|
+
elif entry['odm2_term'] != term:
|
|
273
|
+
mismatched.append({'name': name, 'existing': entry['odm2_term'], 'fetched': term})
|
|
274
|
+
|
|
275
|
+
collisions = {name: terms for name, terms in name_sources.items() if len(terms) > 1}
|
|
276
|
+
fetched_set = set(fetched_terms)
|
|
277
|
+
removed = [
|
|
278
|
+
name
|
|
279
|
+
for name, entry in entries.items()
|
|
280
|
+
if entry.get('source') == 'odm2' and entry.get('odm2_term') not in fetched_set
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
current['entries'] = dict(sorted(entries.items()))
|
|
284
|
+
current['_meta']['updated'] = _utc_today()
|
|
285
|
+
_write_vocab(target_dir, 'variable', current)
|
|
286
|
+
return {
|
|
287
|
+
'total': len(entries),
|
|
288
|
+
'added': sorted(added),
|
|
289
|
+
'removed': sorted(removed), # reported only — refresh never deletes entries
|
|
290
|
+
'collisions': collisions,
|
|
291
|
+
'mismatched_odm2_terms': mismatched,
|
|
292
|
+
'conflicts_with_extension': conflicts_with_extension,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def refresh(field: str | None = None, _target_dir=None) -> dict:
|
|
297
|
+
"""Refresh upstream-sourced vocabularies from their APIs.
|
|
298
|
+
|
|
299
|
+
Fetches the ODM2 variablename list and/or the current CF standard-name table
|
|
300
|
+
and writes updated vocabulary files to the user overlay directory
|
|
301
|
+
(``~/.envlib/vocabularies/``) — never into the installed package. For
|
|
302
|
+
``variable``, only ODM2-sourced membership is updated (additively — upstream
|
|
303
|
+
removals are reported, never deleted); curated ``cf`` mappings and envlib
|
|
304
|
+
extensions are never touched.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
field: ``'variable'``, ``'standard_name'``, or None for both.
|
|
308
|
+
_target_dir: Internal/dev hook — write somewhere other than the user
|
|
309
|
+
overlay dir (used to generate the bundled files).
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Report dict keyed by refreshed field (counts, added/removed terms,
|
|
313
|
+
snake-case collisions, extension conflicts).
|
|
314
|
+
"""
|
|
315
|
+
refreshable = ('standard_name', 'variable')
|
|
316
|
+
fields: tuple[str, ...]
|
|
317
|
+
if field is None:
|
|
318
|
+
fields = refreshable
|
|
319
|
+
elif field in refreshable:
|
|
320
|
+
fields = (field,)
|
|
321
|
+
else:
|
|
322
|
+
_check_field(field)
|
|
323
|
+
msg = f'{field!r} has no upstream source to refresh from.'
|
|
324
|
+
raise ValueError(msg)
|
|
325
|
+
|
|
326
|
+
target_dir = pathlib.Path(_target_dir).expanduser() if _target_dir is not None else USER_DIR
|
|
327
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
328
|
+
|
|
329
|
+
report = {}
|
|
330
|
+
for f in fields:
|
|
331
|
+
if f == 'standard_name':
|
|
332
|
+
report[f] = _refresh_standard_name(target_dir)
|
|
333
|
+
else:
|
|
334
|
+
report[f] = _refresh_variable(target_dir)
|
|
335
|
+
clear_cache()
|
|
336
|
+
return report
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"vocabulary": "aggregation_statistic",
|
|
4
|
+
"source": "statistical subset of the CF Conventions cell_methods vocabulary; no upstream refresh",
|
|
5
|
+
"updated": "2026-07-07"
|
|
6
|
+
},
|
|
7
|
+
"entries": [
|
|
8
|
+
{"name": "point", "description": "Instantaneous sampling; no aggregation over the interval."},
|
|
9
|
+
{"name": "mean", "description": "Arithmetic mean over the aggregation interval."},
|
|
10
|
+
{"name": "sum", "description": "Sum (accumulation) over the aggregation interval."},
|
|
11
|
+
{"name": "maximum", "description": "Maximum value within the aggregation interval."},
|
|
12
|
+
{"name": "minimum", "description": "Minimum value within the aggregation interval."},
|
|
13
|
+
{"name": "median", "description": "Median value within the aggregation interval."},
|
|
14
|
+
{"name": "mode", "description": "Most frequent value within the aggregation interval."},
|
|
15
|
+
{"name": "mid_range", "description": "Average of the maximum and minimum within the aggregation interval."},
|
|
16
|
+
{"name": "variance", "description": "Variance of values within the aggregation interval."},
|
|
17
|
+
{"name": "standard_deviation", "description": "Standard deviation of values within the aggregation interval."},
|
|
18
|
+
{"name": "range", "description": "Maximum minus minimum within the aggregation interval."}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"vocabulary": "feature",
|
|
4
|
+
"source": "envlib-defined, mapped to ENVO URIs (looked up via EBI OLS4, 2026-07-07; envo_label records the ENVO term when it differs from the envlib name)",
|
|
5
|
+
"updated": "2026-07-07"
|
|
6
|
+
},
|
|
7
|
+
"entries": [
|
|
8
|
+
{"name": "atmosphere", "description": "Ambient air, weather, and climate.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_01000267"},
|
|
9
|
+
{"name": "waterway", "description": "Rivers, streams, and canals.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_00000029", "envo_label": "watercourse"},
|
|
10
|
+
{"name": "still_water", "description": "Lakes, reservoirs, and ponds.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_01000617", "envo_label": "lentic water body"},
|
|
11
|
+
{"name": "ocean", "description": "Open ocean and seas.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_00000015"},
|
|
12
|
+
{"name": "groundwater", "description": "Aquifers and subsurface water.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_01001004"},
|
|
13
|
+
{"name": "glacier", "description": "Glaciers and ice sheets.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_00000133"},
|
|
14
|
+
{"name": "wetland", "description": "Swamps, marshes, and bogs.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_01001209", "envo_label": "wetland ecosystem"},
|
|
15
|
+
{"name": "soil", "description": "Soil and subsurface earth.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_00001998"},
|
|
16
|
+
{"name": "coastline", "description": "Coastal zones and estuaries.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_00000303", "envo_label": "sea coast"},
|
|
17
|
+
{"name": "land", "description": "General land surface and terrain.", "envo_uri": "http://purl.obolibrary.org/obo/ENVO_01001785"}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"vocabulary": "frequency_interval",
|
|
4
|
+
"source": "envlib-defined frequency codes (finalized 2026-07-05); no upstream refresh. Only canonical codes are stored, hashed, displayed, and query-matched; aliases are accepted input synonyms only. Admission rule for future fixed codes: the duration must divide 24 h evenly. None (not an entry) is the value for irregular cadences.",
|
|
5
|
+
"updated": "2026-07-07"
|
|
6
|
+
},
|
|
7
|
+
"entries": [
|
|
8
|
+
{"name": "1min", "kind": "fixed", "seconds": 60, "aliases": []},
|
|
9
|
+
{"name": "5min", "kind": "fixed", "seconds": 300, "aliases": []},
|
|
10
|
+
{"name": "10min", "kind": "fixed", "seconds": 600, "aliases": []},
|
|
11
|
+
{"name": "15min", "kind": "fixed", "seconds": 900, "aliases": []},
|
|
12
|
+
{"name": "30min", "kind": "fixed", "seconds": 1800, "aliases": []},
|
|
13
|
+
{"name": "1h", "kind": "fixed", "seconds": 3600, "aliases": ["60min"]},
|
|
14
|
+
{"name": "3h", "kind": "fixed", "seconds": 10800, "aliases": []},
|
|
15
|
+
{"name": "6h", "kind": "fixed", "seconds": 21600, "aliases": []},
|
|
16
|
+
{"name": "12h", "kind": "fixed", "seconds": 43200, "aliases": []},
|
|
17
|
+
{"name": "day", "kind": "fixed", "seconds": 86400, "aliases": ["24h"]},
|
|
18
|
+
{"name": "month", "kind": "calendar", "seconds": null, "aliases": []},
|
|
19
|
+
{"name": "year", "kind": "calendar", "seconds": null, "aliases": []}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"vocabulary": "license",
|
|
4
|
+
"source": "curated SPDX subset + envlib extensions for common non-SPDX open-access data licenses; open-access licenses only; no upstream refresh",
|
|
5
|
+
"updated": "2026-07-07"
|
|
6
|
+
},
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"name": "CC-BY-4.0",
|
|
10
|
+
"title": "Creative Commons Attribution 4.0 International",
|
|
11
|
+
"source": "spdx",
|
|
12
|
+
"url": "https://spdx.org/licenses/CC-BY-4.0.html"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "CC-BY-SA-4.0",
|
|
16
|
+
"title": "Creative Commons Attribution Share Alike 4.0 International",
|
|
17
|
+
"source": "spdx",
|
|
18
|
+
"url": "https://spdx.org/licenses/CC-BY-SA-4.0.html"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "CC-BY-NC-4.0",
|
|
22
|
+
"title": "Creative Commons Attribution Non Commercial 4.0 International",
|
|
23
|
+
"source": "spdx",
|
|
24
|
+
"url": "https://spdx.org/licenses/CC-BY-NC-4.0.html"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "CC-BY-NC-SA-4.0",
|
|
28
|
+
"title": "Creative Commons Attribution Non Commercial Share Alike 4.0 International",
|
|
29
|
+
"source": "spdx",
|
|
30
|
+
"url": "https://spdx.org/licenses/CC-BY-NC-SA-4.0.html"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "CC0-1.0",
|
|
34
|
+
"title": "Creative Commons Zero v1.0 Universal (public domain dedication)",
|
|
35
|
+
"source": "spdx",
|
|
36
|
+
"url": "https://spdx.org/licenses/CC0-1.0.html"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "ODbL-1.0",
|
|
40
|
+
"title": "Open Data Commons Open Database License v1.0",
|
|
41
|
+
"source": "spdx",
|
|
42
|
+
"url": "https://spdx.org/licenses/ODbL-1.0.html"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "CC-BY-3.0",
|
|
46
|
+
"title": "Creative Commons Attribution 3.0 Unported",
|
|
47
|
+
"source": "spdx",
|
|
48
|
+
"url": "https://spdx.org/licenses/CC-BY-3.0.html"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "CC-BY-SA-3.0",
|
|
52
|
+
"title": "Creative Commons Attribution Share Alike 3.0 Unported",
|
|
53
|
+
"source": "spdx",
|
|
54
|
+
"url": "https://spdx.org/licenses/CC-BY-SA-3.0.html"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "Copernicus-1.0",
|
|
58
|
+
"title": "Licence to Use Copernicus Products (ECMWF/Copernicus, e.g. ERA5, C3S/CAMS products)",
|
|
59
|
+
"source": "envlib",
|
|
60
|
+
"url": "https://apps.ecmwf.int/datasets/licences/copernicus/"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"vocabulary": "method",
|
|
4
|
+
"source": "envlib-defined (ported from tethys for migration continuity); no upstream refresh",
|
|
5
|
+
"updated": "2026-07-07"
|
|
6
|
+
},
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"name": "derivation",
|
|
10
|
+
"description": "A method for creating results by simple calculations from field activities, sample analyses, or sensor recordings."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "estimation",
|
|
14
|
+
"description": "A method for creating results by rough approximation or professional judgement. Does not use an analytical or numerical model."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "field_activity",
|
|
18
|
+
"description": "A method for creating results by performing an activity in the field at or on a sampling feature. Includes manually-operated instruments (e.g., handheld probes, manual gauging, field surveys) - anything requiring a human at the measurement location."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "simulation",
|
|
22
|
+
"description": "A method for creating results by running an analytical or numerical model. Generally more complex and/or more uncertain than derivations. Used for climate model runs, reanalyses (e.g., ERA5), and other model outputs."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "sample_analysis",
|
|
26
|
+
"description": "A method for ex situ analysis of a sample using an instrument, typically in a laboratory, for the purpose of measuring properties of a sample."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "sensor_recording",
|
|
30
|
+
"description": "A method for creating results by independent, automated sensor measurements without direct human interaction at the sensor during the measurement. Includes in-situ data loggers and remote sensing."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "forecast",
|
|
34
|
+
"description": "A type of simulation to predict the future. Kept distinct from simulation so consumers can filter forecasts from historical model runs."
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"vocabulary": "processing_level",
|
|
4
|
+
"source": "envlib-defined; no upstream refresh",
|
|
5
|
+
"updated": "2026-07-07"
|
|
6
|
+
},
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"name": "raw",
|
|
10
|
+
"description": "As collected or generated; no quality control applied."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "preliminary",
|
|
14
|
+
"description": "Automatically screened or near-real-time; subject to revision (e.g., ERA5T, USGS provisional, telemetered data with automated checks)."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "quality_controlled",
|
|
18
|
+
"description": "Vetted and considered settled (e.g., consolidated ERA5, USGS approved, a council's validated archive)."
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|