pywcmp 0.14.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.
- pywcmp/__init__.py +47 -0
- pywcmp/bundle.py +130 -0
- pywcmp/errors.py +31 -0
- pywcmp/ets.py +91 -0
- pywcmp/kpi.py +103 -0
- pywcmp/pygeoapi_plugin.py +290 -0
- pywcmp/resources/dictionary.txt +2 -0
- pywcmp/resources/ets-report.json +83 -0
- pywcmp/resources/example-ca-eccc-msc.nwp-gdps.json +160 -0
- pywcmp/resources/kpi-report.json +106 -0
- pywcmp/util.py +266 -0
- pywcmp/wcmp2/__init__.py +24 -0
- pywcmp/wcmp2/ets.py +612 -0
- pywcmp/wcmp2/kpi.py +637 -0
- pywcmp-0.14.0.dist-info/METADATA +195 -0
- pywcmp-0.14.0.dist-info/RECORD +20 -0
- pywcmp-0.14.0.dist-info/WHEEL +5 -0
- pywcmp-0.14.0.dist-info/entry_points.txt +2 -0
- pywcmp-0.14.0.dist-info/licenses/LICENSE.md +30 -0
- pywcmp-0.14.0.dist-info/top_level.txt +1 -0
pywcmp/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
|
4
|
+
# Ján Osuský <jan.osusky@iblsoft.com>
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2026 Tom Kralidis
|
|
7
|
+
# Copyright (c) 2022 Government of Canada
|
|
8
|
+
# Copyright (c) 2020 IBL Software Engineering spol. s r. o.
|
|
9
|
+
#
|
|
10
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
11
|
+
# or more contributor license agreements. See the NOTICE file
|
|
12
|
+
# distributed with this work for additional information
|
|
13
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
14
|
+
# to you under the Apache License, Version 2.0 (the
|
|
15
|
+
# "License"); you may not use this file except in compliance
|
|
16
|
+
# with the License. You may obtain a copy of the License at
|
|
17
|
+
#
|
|
18
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
19
|
+
#
|
|
20
|
+
# Unless required by applicable law or agreed to in writing,
|
|
21
|
+
# software distributed under the License is distributed on an
|
|
22
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
23
|
+
# KIND, either express or implied. See the License for the
|
|
24
|
+
# specific language governing permissions and limitations
|
|
25
|
+
# under the License.
|
|
26
|
+
#
|
|
27
|
+
###############################################################################
|
|
28
|
+
|
|
29
|
+
import click
|
|
30
|
+
|
|
31
|
+
from pywcmp.ets import ets
|
|
32
|
+
from pywcmp.bundle import bundle
|
|
33
|
+
from pywcmp.kpi import kpi
|
|
34
|
+
from pywcmp.util import get_package_version
|
|
35
|
+
|
|
36
|
+
__version__ = get_package_version()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@click.group()
|
|
40
|
+
@click.version_option(version=__version__)
|
|
41
|
+
def cli():
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
cli.add_command(ets)
|
|
46
|
+
cli.add_command(bundle)
|
|
47
|
+
cli.add_command(kpi)
|
pywcmp/bundle.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2025 Tom Kralidis
|
|
6
|
+
#
|
|
7
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
8
|
+
# or more contributor license agreements. See the NOTICE file
|
|
9
|
+
# distributed with this work for additional information
|
|
10
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
11
|
+
# to you under the Apache License, Version 2.0 (the
|
|
12
|
+
# "License"); you may not use this file except in compliance
|
|
13
|
+
# with the License. You may obtain a copy of the License at
|
|
14
|
+
#
|
|
15
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
#
|
|
17
|
+
# Unless required by applicable law or agreed to in writing,
|
|
18
|
+
# software distributed under the License is distributed on an
|
|
19
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
20
|
+
# KIND, either express or implied. See the License for the
|
|
21
|
+
# specific language governing permissions and limitations
|
|
22
|
+
# under the License.
|
|
23
|
+
#
|
|
24
|
+
###############################################################################
|
|
25
|
+
|
|
26
|
+
import io
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
import shutil
|
|
31
|
+
import tempfile
|
|
32
|
+
import zipfile
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
|
|
36
|
+
from pywcmp.util import (get_cli_common_options, get_userdir, urlopen_,
|
|
37
|
+
setup_logger)
|
|
38
|
+
|
|
39
|
+
LOGGER = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
USERDIR = get_userdir()
|
|
42
|
+
|
|
43
|
+
TEMPDIR = tempfile.TemporaryDirectory()
|
|
44
|
+
TEMPDIR2 = Path(tempfile.TemporaryDirectory().name)
|
|
45
|
+
|
|
46
|
+
WCMP2_FILES_TEMP = TEMPDIR2 / 'wcmp-2'
|
|
47
|
+
WIS2_TOPIC_HIERARCHY_DIR_TEMP = TEMPDIR2 / 'wis2-topic-hierarchy'
|
|
48
|
+
|
|
49
|
+
WCMP2_FILES = get_userdir() / 'wcmp-2'
|
|
50
|
+
WIS2_TOPIC_HIERARCHY_DIR = get_userdir() / 'wis2-topic-hierarchy'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.group()
|
|
54
|
+
def bundle():
|
|
55
|
+
"""Configuration bundle management"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@click.command()
|
|
60
|
+
@get_cli_common_options
|
|
61
|
+
@click.pass_context
|
|
62
|
+
def sync(ctx, logfile, verbosity):
|
|
63
|
+
"""Sync configuration bundle"""
|
|
64
|
+
|
|
65
|
+
setup_logger(verbosity, logfile)
|
|
66
|
+
LOGGER.debug('Caching schemas, codelists and topic hierarchy')
|
|
67
|
+
|
|
68
|
+
LOGGER.debug('Caching WCMP2 artifacts')
|
|
69
|
+
LOGGER.debug(f'Downloading WCMP2 schema to {WCMP2_FILES_TEMP}')
|
|
70
|
+
WCMP2_FILES_TEMP.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
WCMP2_SCHEMA = 'https://raw.githubusercontent.com/wmo-im/wcmp2/main/schemas/wcmp2-bundled.json' # noqa
|
|
72
|
+
|
|
73
|
+
json_schema = WCMP2_FILES_TEMP / 'wcmp2-bundled.json'
|
|
74
|
+
with json_schema.open('wb') as fh:
|
|
75
|
+
fh.write(urlopen_(f'{WCMP2_SCHEMA}').read())
|
|
76
|
+
|
|
77
|
+
WCMP2_CODELISTS = WCMP2_FILES_TEMP / 'codelists'
|
|
78
|
+
LOGGER.debug(f'Downloading WCMP2 codelists to {WCMP2_CODELISTS}')
|
|
79
|
+
WCMP2_CODELISTS.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
CODELISTS_URL = 'https://github.com/wmo-im/wcmp2-codelists/archive/refs/heads/main.zip' # noqa
|
|
81
|
+
FH = io.BytesIO(urlopen_(CODELISTS_URL).read())
|
|
82
|
+
with zipfile.ZipFile(FH) as z:
|
|
83
|
+
LOGGER.debug(f'Processing zipfile "{z.filename}"')
|
|
84
|
+
for name in z.namelist():
|
|
85
|
+
LOGGER.debug(f'Processing entry "{name}"')
|
|
86
|
+
if '.csv' in name:
|
|
87
|
+
filename = os.path.basename(name)
|
|
88
|
+
|
|
89
|
+
if not filename:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
dest_file = WCMP2_CODELISTS / filename
|
|
93
|
+
LOGGER.debug(f'Creating "{dest_file}"')
|
|
94
|
+
with z.open(name) as src, dest_file.open('wb') as dest:
|
|
95
|
+
shutil.copyfileobj(src, dest)
|
|
96
|
+
|
|
97
|
+
LOGGER.debug('Downloading WIS2 topic hierarchy')
|
|
98
|
+
WIS2_TOPIC_HIERARCHY_DIR_TEMP.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
ZIPFILE_URL = 'https://wmo-im.github.io/wis2-topic-hierarchy/wth-bundle.zip' # noqa
|
|
101
|
+
FH = io.BytesIO(urlopen_(ZIPFILE_URL).read())
|
|
102
|
+
with zipfile.ZipFile(FH) as z:
|
|
103
|
+
LOGGER.debug(f'Processing zipfile "{z.filename}"')
|
|
104
|
+
for name in z.namelist():
|
|
105
|
+
LOGGER.debug(f'Processing entry "{name}"')
|
|
106
|
+
filename = os.path.basename(name)
|
|
107
|
+
|
|
108
|
+
dest_file = WIS2_TOPIC_HIERARCHY_DIR_TEMP / filename
|
|
109
|
+
LOGGER.debug(f'Creating "{dest_file}"')
|
|
110
|
+
with z.open(name) as src, dest_file.open('wb') as dest:
|
|
111
|
+
shutil.copyfileobj(src, dest)
|
|
112
|
+
|
|
113
|
+
LOGGER.debug('Downloading IANA link relations')
|
|
114
|
+
IANA_URL = 'https://www.iana.org/assignments/link-relations/link-relations-1.csv' # noqa
|
|
115
|
+
iana_file = WCMP2_FILES_TEMP / 'link-relations-1.csv'
|
|
116
|
+
with iana_file.open('wb') as fh:
|
|
117
|
+
fh.write(urlopen_(f'{IANA_URL}').read())
|
|
118
|
+
|
|
119
|
+
LOGGER.debug(f'Removing {USERDIR}')
|
|
120
|
+
if USERDIR.exists():
|
|
121
|
+
shutil.rmtree(USERDIR)
|
|
122
|
+
|
|
123
|
+
LOGGER.debug(f'Moving files from {TEMPDIR2} to {USERDIR}')
|
|
124
|
+
shutil.move(TEMPDIR2, USERDIR)
|
|
125
|
+
|
|
126
|
+
LOGGER.debug(f'Cleaning up {TEMPDIR}')
|
|
127
|
+
TEMPDIR.cleanup()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
bundle.add_command(sync)
|
pywcmp/errors.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2022 Tom Kralidis
|
|
6
|
+
#
|
|
7
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
8
|
+
# or more contributor license agreements. See the NOTICE file
|
|
9
|
+
# distributed with this work for additional information
|
|
10
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
11
|
+
# to you under the Apache License, Version 2.0 (the
|
|
12
|
+
# "License"); you may not use this file except in compliance
|
|
13
|
+
# with the License. You may obtain a copy of the License at
|
|
14
|
+
#
|
|
15
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
#
|
|
17
|
+
# Unless required by applicable law or agreed to in writing,
|
|
18
|
+
# software distributed under the License is distributed on an
|
|
19
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
20
|
+
# KIND, either express or implied. See the License for the
|
|
21
|
+
# specific language governing permissions and limitations
|
|
22
|
+
# under the License.
|
|
23
|
+
#
|
|
24
|
+
###############################################################################
|
|
25
|
+
|
|
26
|
+
class TestSuiteError(Exception):
|
|
27
|
+
"""custom exception handler"""
|
|
28
|
+
def __init__(self, message, errors):
|
|
29
|
+
"""set error list/stack"""
|
|
30
|
+
super(TestSuiteError, self).__init__(message)
|
|
31
|
+
self.errors = errors
|
pywcmp/ets.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
|
4
|
+
# Ján Osuský <jan.osusky@iblsoft.com>
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2025 Tom Kralidis
|
|
7
|
+
# Copyright (c) 2022 Government of Canada
|
|
8
|
+
# Copyright (c) 2020 IBL Software Engineering spol. s r. o.
|
|
9
|
+
#
|
|
10
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
11
|
+
# or more contributor license agreements. See the NOTICE file
|
|
12
|
+
# distributed with this work for additional information
|
|
13
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
14
|
+
# to you under the Apache License, Version 2.0 (the
|
|
15
|
+
# "License"); you may not use this file except in compliance
|
|
16
|
+
# with the License. You may obtain a copy of the License at
|
|
17
|
+
#
|
|
18
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
19
|
+
#
|
|
20
|
+
# Unless required by applicable law or agreed to in writing,
|
|
21
|
+
# software distributed under the License is distributed on an
|
|
22
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
23
|
+
# KIND, either express or implied. See the License for the
|
|
24
|
+
# specific language governing permissions and limitations
|
|
25
|
+
# under the License.
|
|
26
|
+
#
|
|
27
|
+
###############################################################################
|
|
28
|
+
|
|
29
|
+
# executable test for WCMP2
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
from pywcmp.wcmp2.ets import WMOCoreMetadataProfileTestSuite2
|
|
36
|
+
from pywcmp.util import (get_cli_common_options, parse_wcmp, setup_logger,
|
|
37
|
+
urlopen_)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@click.group()
|
|
41
|
+
def ets():
|
|
42
|
+
"""executable test suite"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@click.command()
|
|
47
|
+
@click.pass_context
|
|
48
|
+
@get_cli_common_options
|
|
49
|
+
@click.argument('file_or_url')
|
|
50
|
+
@click.option('--fail-on-schema-validation/--no-fail-on-schema-validation',
|
|
51
|
+
'-f', default=True,
|
|
52
|
+
help='Stop the ETS on failing schema validation')
|
|
53
|
+
@click.option('--relax-centre-id-checks', '-r', is_flag=True,
|
|
54
|
+
default=False, help='Relax centre identifier based checks')
|
|
55
|
+
def validate(ctx, file_or_url, logfile, verbosity,
|
|
56
|
+
fail_on_schema_validation=True,
|
|
57
|
+
relax_centre_id_checks=False):
|
|
58
|
+
"""validate against the abstract test suite"""
|
|
59
|
+
|
|
60
|
+
setup_logger(verbosity, logfile)
|
|
61
|
+
|
|
62
|
+
click.echo(f'Opening {file_or_url}')
|
|
63
|
+
|
|
64
|
+
if file_or_url.startswith('http'):
|
|
65
|
+
content = urlopen_(file_or_url).read()
|
|
66
|
+
else:
|
|
67
|
+
with open(file_or_url) as fh:
|
|
68
|
+
content = fh.read()
|
|
69
|
+
|
|
70
|
+
click.echo(f'Validating {file_or_url}')
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
data = parse_wcmp(content)
|
|
74
|
+
except Exception as err:
|
|
75
|
+
raise click.ClickException(err)
|
|
76
|
+
ctx.exit(1)
|
|
77
|
+
|
|
78
|
+
click.echo('Detected WCMP2 discovery metadata')
|
|
79
|
+
ts = WMOCoreMetadataProfileTestSuite2(data)
|
|
80
|
+
try:
|
|
81
|
+
results = ts.run_tests(fail_on_schema_validation,
|
|
82
|
+
relax_centre_id_checks)
|
|
83
|
+
except Exception as err:
|
|
84
|
+
raise click.ClickException(err)
|
|
85
|
+
ctx.exit(1)
|
|
86
|
+
|
|
87
|
+
click.echo(json.dumps(results, indent=4))
|
|
88
|
+
ctx.exit(results['summary']['FAILED'])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
ets.add_command(validate)
|
pywcmp/kpi.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
|
4
|
+
# Ján Osuský <jan.osusky@iblsoft.com>
|
|
5
|
+
#
|
|
6
|
+
# Copyright (c) 2023 Tom Kralidis
|
|
7
|
+
# Copyright (c) 2022 Government of Canada
|
|
8
|
+
# Copyright (c) 2020 IBL Software Engineering spol. s r. o.
|
|
9
|
+
#
|
|
10
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
11
|
+
# or more contributor license agreements. See the NOTICE file
|
|
12
|
+
# distributed with this work for additional information
|
|
13
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
14
|
+
# to you under the Apache License, Version 2.0 (the
|
|
15
|
+
# "License"); you may not use this file except in compliance
|
|
16
|
+
# with the License. You may obtain a copy of the License at
|
|
17
|
+
#
|
|
18
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
19
|
+
#
|
|
20
|
+
# Unless required by applicable law or agreed to in writing,
|
|
21
|
+
# software distributed under the License is distributed on an
|
|
22
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
23
|
+
# KIND, either express or implied. See the License for the
|
|
24
|
+
# specific language governing permissions and limitations
|
|
25
|
+
# under the License.
|
|
26
|
+
#
|
|
27
|
+
###############################################################################
|
|
28
|
+
|
|
29
|
+
# WMO Core Metadata Profile Key Performance Indicators (KPIs)
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
|
|
36
|
+
from pywcmp.ets import WMOCoreMetadataProfileTestSuite2
|
|
37
|
+
from pywcmp.wcmp2.kpi import (
|
|
38
|
+
WMOCoreMetadataProfileKeyPerformanceIndicators as wcmp_kpis2
|
|
39
|
+
)
|
|
40
|
+
from pywcmp.util import (get_cli_common_options, parse_wcmp, setup_logger,
|
|
41
|
+
urlopen_)
|
|
42
|
+
|
|
43
|
+
LOGGER = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@click.group()
|
|
47
|
+
def kpi():
|
|
48
|
+
"""key performance indicators"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@click.command()
|
|
53
|
+
@click.pass_context
|
|
54
|
+
@get_cli_common_options
|
|
55
|
+
@click.argument('file_or_url')
|
|
56
|
+
@click.option('--fail-on-ets/--no-fail-on-ets',
|
|
57
|
+
'-f', default=True, help='Stop the KPI on failing ETS')
|
|
58
|
+
@click.option('--summary', '-s', is_flag=True, default=False,
|
|
59
|
+
help='Provide summary of KPI test results')
|
|
60
|
+
@click.option('--kpi', '-k', help='KPI to run, default is all')
|
|
61
|
+
def validate(ctx, file_or_url, summary, kpi, logfile, verbosity,
|
|
62
|
+
fail_on_ets=True):
|
|
63
|
+
"""run key performance indicators"""
|
|
64
|
+
|
|
65
|
+
setup_logger(verbosity, logfile)
|
|
66
|
+
|
|
67
|
+
if file_or_url.startswith('http'):
|
|
68
|
+
content = urlopen_(file_or_url).read()
|
|
69
|
+
else:
|
|
70
|
+
with open(file_or_url) as fh:
|
|
71
|
+
content = fh.read()
|
|
72
|
+
|
|
73
|
+
click.echo(f'Validating {file_or_url}')
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
data = parse_wcmp(content)
|
|
77
|
+
except Exception as err:
|
|
78
|
+
raise click.ClickException(err)
|
|
79
|
+
ctx.exit(1)
|
|
80
|
+
|
|
81
|
+
if fail_on_ets:
|
|
82
|
+
ts = WMOCoreMetadataProfileTestSuite2(data)
|
|
83
|
+
try:
|
|
84
|
+
_ = ts.run_tests(fail_on_schema_validation=True)
|
|
85
|
+
except Exception as err:
|
|
86
|
+
raise click.ClickException(err)
|
|
87
|
+
ctx.exit(1)
|
|
88
|
+
|
|
89
|
+
kpis = wcmp_kpis2(data)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
kpis_results = kpis.evaluate(kpi)
|
|
93
|
+
except ValueError as err:
|
|
94
|
+
raise click.UsageError(f'Invalid KPI {kpi}: {err}')
|
|
95
|
+
ctx.exit(1)
|
|
96
|
+
|
|
97
|
+
if not summary or kpi is not None:
|
|
98
|
+
click.echo(json.dumps(kpis_results, indent=4))
|
|
99
|
+
else:
|
|
100
|
+
click.echo(json.dumps(kpis_results['summary'], indent=4))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
kpi.add_command(validate)
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2026 Tom Kralidis
|
|
6
|
+
#
|
|
7
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
8
|
+
# or more contributor license agreements. See the NOTICE file
|
|
9
|
+
# distributed with this work for additional information
|
|
10
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
11
|
+
# to you under the Apache License, Version 2.0 (the
|
|
12
|
+
# "License"); you may not use this file except in compliance
|
|
13
|
+
# with the License. You may obtain a copy of the License at
|
|
14
|
+
#
|
|
15
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
#
|
|
17
|
+
# Unless required by applicable law or agreed to in writing,
|
|
18
|
+
# software distributed under the License is distributed on an
|
|
19
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
20
|
+
# KIND, either express or implied. See the License for the
|
|
21
|
+
# specific language governing permissions and limitations
|
|
22
|
+
# under the License.
|
|
23
|
+
#
|
|
24
|
+
###############################################################################
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# pywcmp as a service
|
|
28
|
+
# -------------------
|
|
29
|
+
#
|
|
30
|
+
# This file is intended to be used as a pygeoapi process plugin which will
|
|
31
|
+
# provide pywcmp functionality via OGC API - Processes.
|
|
32
|
+
#
|
|
33
|
+
# To integrate this plugin in pygeoapi:
|
|
34
|
+
#
|
|
35
|
+
# 1. ensure pywcmp is installed into the pygeoapi deployment environment
|
|
36
|
+
#
|
|
37
|
+
# 2. add the processes to the pygeoapi configuration as follows:
|
|
38
|
+
#
|
|
39
|
+
# pywcmp-wis2-wcmp2-ets:
|
|
40
|
+
# type: process
|
|
41
|
+
# processor:
|
|
42
|
+
# name: pywcmp.pygeoapi_plugin.WCMP2ETSProcessor
|
|
43
|
+
#
|
|
44
|
+
# pywcmp-wis2-wcmp2-kpi:
|
|
45
|
+
# type: process
|
|
46
|
+
# processor:
|
|
47
|
+
# name: pywcmp.pygeoapi_plugin.WCMP2KPIProcessor
|
|
48
|
+
#
|
|
49
|
+
# 3. (re)start pygeoapi
|
|
50
|
+
#
|
|
51
|
+
# The resulting processes will be available at the following endpoints:
|
|
52
|
+
#
|
|
53
|
+
# /processes/pywcmp-wis2-wcmp2-ets
|
|
54
|
+
#
|
|
55
|
+
# /processes/pywcmp-wis2-wcmp2-kpi
|
|
56
|
+
#
|
|
57
|
+
# Note that pygeoapi's OpenAPI/Swagger interface (at /openapi) will also
|
|
58
|
+
# provide a developer-friendly interface to test and run requests
|
|
59
|
+
#
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
import json
|
|
63
|
+
import logging
|
|
64
|
+
|
|
65
|
+
from pygeoapi.process.base import BaseProcessor, ProcessorExecuteError
|
|
66
|
+
|
|
67
|
+
from pywcmp.wcmp2.ets import WMOCoreMetadataProfileTestSuite2
|
|
68
|
+
from pywcmp.wcmp2.kpi import WMOCoreMetadataProfileKeyPerformanceIndicators
|
|
69
|
+
from pywcmp.util import get_package_version, THISDIR, urlopen_
|
|
70
|
+
|
|
71
|
+
LOGGER = logging.getLogger(__name__)
|
|
72
|
+
|
|
73
|
+
with (THISDIR / 'resources' / 'ets-report.json').open() as fh:
|
|
74
|
+
ETS_REPORT_SCHEMA = json.load(fh)
|
|
75
|
+
|
|
76
|
+
with (THISDIR / 'resources' / 'kpi-report.json').open() as fh:
|
|
77
|
+
KPI_REPORT_SCHEMA = json.load(fh)
|
|
78
|
+
|
|
79
|
+
with (THISDIR / 'resources' / 'example-ca-eccc-msc.nwp-gdps.json').open() as fh: # noqa
|
|
80
|
+
EXAMPLE_WCMP2 = json.load(fh)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
PROCESS_WCMP2_ETS = {
|
|
84
|
+
'version': get_package_version(),
|
|
85
|
+
'id': 'pywcmp-wis2-wcmp2-ets',
|
|
86
|
+
'title': {
|
|
87
|
+
'en': 'WCMP2 ETS validator'
|
|
88
|
+
},
|
|
89
|
+
'description': {
|
|
90
|
+
'en': 'Validate a WCMP2 document against the ETS'
|
|
91
|
+
},
|
|
92
|
+
'keywords': ['wis2', 'wcmp2', 'ets', 'test suite', 'metadata'],
|
|
93
|
+
'links': [{
|
|
94
|
+
'type': 'text/html',
|
|
95
|
+
'rel': 'about',
|
|
96
|
+
'title': 'information',
|
|
97
|
+
'href': 'https://wmo-im.github.io/wcmp2',
|
|
98
|
+
'hreflang': 'en-US'
|
|
99
|
+
}],
|
|
100
|
+
'jobControlOptions': ['sync-execute', 'async-execute'],
|
|
101
|
+
'inputs': {
|
|
102
|
+
'record': {
|
|
103
|
+
'title': 'WCMP2 record',
|
|
104
|
+
'description': 'WCMP2 record (can be inline or remote link)',
|
|
105
|
+
'schema': {
|
|
106
|
+
'type': ['object', 'string']
|
|
107
|
+
},
|
|
108
|
+
'minOccurs': 1,
|
|
109
|
+
'maxOccurs': 1,
|
|
110
|
+
'metadata': None,
|
|
111
|
+
'keywords': ['wcmp2']
|
|
112
|
+
},
|
|
113
|
+
'fail_on_schema_validation': {
|
|
114
|
+
'title': 'Fail on schema validation',
|
|
115
|
+
'description': 'Stop the ETS on failing schema validation',
|
|
116
|
+
'schema': {
|
|
117
|
+
'type': 'boolean',
|
|
118
|
+
'default': True
|
|
119
|
+
},
|
|
120
|
+
'minOccurs': 0,
|
|
121
|
+
'maxOccurs': 1,
|
|
122
|
+
'metadata': None,
|
|
123
|
+
'keywords': ['schema', 'validation']
|
|
124
|
+
},
|
|
125
|
+
'relax_centre_id_checks': {
|
|
126
|
+
'title': 'Skip centre identifier checks',
|
|
127
|
+
'description': 'Skip centre identifier checks from failing',
|
|
128
|
+
'schema': {
|
|
129
|
+
'type': 'boolean',
|
|
130
|
+
'default': False
|
|
131
|
+
},
|
|
132
|
+
'minOccurs': 0,
|
|
133
|
+
'maxOccurs': 1,
|
|
134
|
+
'metadata': None,
|
|
135
|
+
'keywords': ['centre identifier']
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
'outputs': {
|
|
139
|
+
'result': {
|
|
140
|
+
'title': 'Report of ETS results',
|
|
141
|
+
'description': 'Report of ETS results',
|
|
142
|
+
'schema': {
|
|
143
|
+
'contentMediaType': 'application/json',
|
|
144
|
+
**ETS_REPORT_SCHEMA
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
'example': {
|
|
149
|
+
'inputs': {
|
|
150
|
+
'record': EXAMPLE_WCMP2,
|
|
151
|
+
'fail_on_schema_validation': True,
|
|
152
|
+
'relax_centre_id_checks': False
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
PROCESS_WCMP2_KPI = {
|
|
159
|
+
'version': get_package_version(),
|
|
160
|
+
'id': 'pywcmp-wis2-wcmp2-kpi',
|
|
161
|
+
'title': {
|
|
162
|
+
'en': 'WCMP2 KPI evaluator'
|
|
163
|
+
},
|
|
164
|
+
'description': {
|
|
165
|
+
'en': 'Validate a WCMP2 document against the KPI suite'
|
|
166
|
+
},
|
|
167
|
+
'keywords': ['wis2', 'wcmp2', 'kpi', 'test suite', 'metadata'],
|
|
168
|
+
'links': [{
|
|
169
|
+
'type': 'text/html',
|
|
170
|
+
'rel': 'about',
|
|
171
|
+
'title': 'information',
|
|
172
|
+
'href': 'https://wmo-im.github.io/wcmp2',
|
|
173
|
+
'hreflang': 'en-US'
|
|
174
|
+
}],
|
|
175
|
+
'jobControlOptions': ['sync-execute', 'async-execute'],
|
|
176
|
+
'inputs': {
|
|
177
|
+
'record': {
|
|
178
|
+
'title': 'WCMP2 record',
|
|
179
|
+
'description': 'WCMP2 record (can be inline or remote link)',
|
|
180
|
+
'schema': {
|
|
181
|
+
'type': ['object', 'string']
|
|
182
|
+
},
|
|
183
|
+
'minOccurs': 1,
|
|
184
|
+
'maxOccurs': 1,
|
|
185
|
+
'metadata': None,
|
|
186
|
+
'keywords': ['wcmp2']
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
'outputs': {
|
|
190
|
+
'result': {
|
|
191
|
+
'title': 'Report of KPI results',
|
|
192
|
+
'description': 'Report of KPI results',
|
|
193
|
+
'schema': {
|
|
194
|
+
'contentMediaType': 'application/json',
|
|
195
|
+
**KPI_REPORT_SCHEMA
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
'example': {
|
|
200
|
+
'inputs': {
|
|
201
|
+
'record': EXAMPLE_WCMP2
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class WCMP2ETSProcessor(BaseProcessor):
|
|
208
|
+
"""WCMP2 ETS"""
|
|
209
|
+
|
|
210
|
+
def __init__(self, processor_def):
|
|
211
|
+
"""
|
|
212
|
+
Initialize object
|
|
213
|
+
|
|
214
|
+
:param processor_def: provider definition
|
|
215
|
+
|
|
216
|
+
:returns: pywcmp.pygeoapi_plugin.WCMP2ETSProcessor
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
super().__init__(processor_def, PROCESS_WCMP2_ETS)
|
|
220
|
+
|
|
221
|
+
def execute(self, data, outputs=None):
|
|
222
|
+
|
|
223
|
+
response = None
|
|
224
|
+
mimetype = 'application/json'
|
|
225
|
+
record = data.get('record')
|
|
226
|
+
fail_on_schema_validation = data.get('fail_on_schema_validation', True)
|
|
227
|
+
relax_centre_id_checks = data.get('relax_centre_id_checks', False)
|
|
228
|
+
|
|
229
|
+
if record is None:
|
|
230
|
+
msg = 'Missing record'
|
|
231
|
+
LOGGER.error(msg)
|
|
232
|
+
raise ProcessorExecuteError(msg)
|
|
233
|
+
|
|
234
|
+
if isinstance(record, str) and record.startswith('http'):
|
|
235
|
+
LOGGER.debug('Record is a link')
|
|
236
|
+
record = json.loads(urlopen_(record).read())
|
|
237
|
+
else:
|
|
238
|
+
LOGGER.debug('Record is inline')
|
|
239
|
+
|
|
240
|
+
LOGGER.debug('Running ETS against record')
|
|
241
|
+
response = WMOCoreMetadataProfileTestSuite2(record).run_tests(
|
|
242
|
+
fail_on_schema_validation=fail_on_schema_validation,
|
|
243
|
+
relax_centre_id_checks=relax_centre_id_checks)
|
|
244
|
+
|
|
245
|
+
return mimetype, response
|
|
246
|
+
|
|
247
|
+
def __repr__(self):
|
|
248
|
+
return '<WCMP2ETSProcessor>'
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class WCMP2KPIProcessor(BaseProcessor):
|
|
252
|
+
"""WCMP2 KPI"""
|
|
253
|
+
|
|
254
|
+
def __init__(self, processor_def):
|
|
255
|
+
"""
|
|
256
|
+
Initialize object
|
|
257
|
+
|
|
258
|
+
:param processor_def: provider definition
|
|
259
|
+
|
|
260
|
+
:returns: pywcmp.pygeoapi_plugin.WCMP2KPIProcessor
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
super().__init__(processor_def, PROCESS_WCMP2_KPI)
|
|
264
|
+
|
|
265
|
+
def execute(self, data, outputs=None):
|
|
266
|
+
|
|
267
|
+
response = None
|
|
268
|
+
mimetype = 'application/json'
|
|
269
|
+
|
|
270
|
+
record = data.get('record')
|
|
271
|
+
|
|
272
|
+
if record is None:
|
|
273
|
+
msg = 'Missing record'
|
|
274
|
+
LOGGER.error(msg)
|
|
275
|
+
raise ProcessorExecuteError(msg)
|
|
276
|
+
|
|
277
|
+
if isinstance(record, str) and record.startswith('http'):
|
|
278
|
+
LOGGER.debug('Record is a link')
|
|
279
|
+
record = json.loads(urlopen_(record).read())
|
|
280
|
+
else:
|
|
281
|
+
LOGGER.debug('Record is inline')
|
|
282
|
+
|
|
283
|
+
LOGGER.debug('Running KPIs against record')
|
|
284
|
+
kpis = WMOCoreMetadataProfileKeyPerformanceIndicators(record)
|
|
285
|
+
response = kpis.evaluate()
|
|
286
|
+
|
|
287
|
+
return mimetype, response
|
|
288
|
+
|
|
289
|
+
def __repr__(self):
|
|
290
|
+
return '<WCMP2KPIProcessor>'
|