pywmdr 0.2.dev0__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.
- pywmdr/__init__.py +42 -0
- pywmdr/bundle.py +75 -0
- pywmdr/cli_options.py +45 -0
- pywmdr/errors.py +27 -0
- pywmdr/pygeoapi_plugin.py +152 -0
- pywmdr/record.py +72 -0
- pywmdr/resources/20250504_0-20008-0-NRB.json +94 -0
- pywmdr/resources/ets-report.json +85 -0
- pywmdr/util.py +103 -0
- pywmdr/wmdr2/__init__.py +24 -0
- pywmdr/wmdr2/ets.py +155 -0
- pywmdr-0.2.dev0.dist-info/METADATA +172 -0
- pywmdr-0.2.dev0.dist-info/RECORD +17 -0
- pywmdr-0.2.dev0.dist-info/WHEEL +5 -0
- pywmdr-0.2.dev0.dist-info/entry_points.txt +2 -0
- pywmdr-0.2.dev0.dist-info/licenses/LICENSE.md +24 -0
- pywmdr-0.2.dev0.dist-info/top_level.txt +1 -0
pywmdr/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
import click
|
|
27
|
+
|
|
28
|
+
from pywmdr.record import record
|
|
29
|
+
from pywmdr.bundle import bundle
|
|
30
|
+
from pywmdr.util import get_package_version
|
|
31
|
+
|
|
32
|
+
__version__ = get_package_version()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.group()
|
|
36
|
+
@click.version_option(version=__version__)
|
|
37
|
+
def cli():
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
cli.add_command(bundle)
|
|
42
|
+
cli.add_command(record)
|
pywmdr/bundle.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
#
|
|
20
|
+
###############################################################################
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
import shutil
|
|
25
|
+
import tempfile
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
|
|
29
|
+
from pywmdr import cli_options
|
|
30
|
+
from pywmdr.util import get_userdir, urlopen_
|
|
31
|
+
|
|
32
|
+
LOGGER = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
USERDIR = get_userdir()
|
|
35
|
+
|
|
36
|
+
TEMPDIR = tempfile.TemporaryDirectory()
|
|
37
|
+
TEMPDIR2 = Path(tempfile.TemporaryDirectory().name)
|
|
38
|
+
|
|
39
|
+
WMDR2_FILES = get_userdir() / 'wmdr2'
|
|
40
|
+
WMDR2_FILES_TEMP = TEMPDIR2 / 'wmdr2'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.group()
|
|
44
|
+
def bundle():
|
|
45
|
+
"""Configuration bundle management"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.command()
|
|
50
|
+
@click.pass_context
|
|
51
|
+
@cli_options.OPTION_VERBOSITY
|
|
52
|
+
def sync(ctx, verbosity):
|
|
53
|
+
"""Sync configuration bundle"""
|
|
54
|
+
|
|
55
|
+
LOGGER.debug('Caching schema')
|
|
56
|
+
LOGGER.debug(f'Downloading WMDR2 schema to {WMDR2_FILES_TEMP}')
|
|
57
|
+
WMDR2_FILES_TEMP.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
WMDR2_SCHEMA = 'https://raw.githubusercontent.com/wmo-im/wmdr2/refs/heads/main/schemas/wmdr2-bundled.json' # noqa
|
|
59
|
+
|
|
60
|
+
json_schema = WMDR2_FILES_TEMP / 'wmdr2-bundled.json'
|
|
61
|
+
with json_schema.open('wb') as fh:
|
|
62
|
+
fh.write(urlopen_(f'{WMDR2_SCHEMA}').read())
|
|
63
|
+
|
|
64
|
+
LOGGER.debug(f'Removing {USERDIR}')
|
|
65
|
+
if USERDIR.exists():
|
|
66
|
+
shutil.rmtree(USERDIR)
|
|
67
|
+
|
|
68
|
+
LOGGER.debug(f'Moving files from {TEMPDIR2} to {USERDIR}')
|
|
69
|
+
shutil.move(TEMPDIR2, USERDIR)
|
|
70
|
+
|
|
71
|
+
LOGGER.debug(f'Cleaning up {TEMPDIR}')
|
|
72
|
+
TEMPDIR.cleanup()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
bundle.add_command(sync)
|
pywmdr/cli_options.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
#
|
|
20
|
+
###############################################################################
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def OPTION_VERBOSITY(f):
|
|
29
|
+
logging_options = ['ERROR', 'WARNING', 'INFO', 'DEBUG']
|
|
30
|
+
|
|
31
|
+
def callback(ctx, param, value):
|
|
32
|
+
value2 = value or 'INFO'
|
|
33
|
+
logging.basicConfig(stream=sys.stdout,
|
|
34
|
+
level=getattr(logging, value2))
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
return click.option('--verbosity', '-v',
|
|
38
|
+
type=click.Choice(logging_options),
|
|
39
|
+
help='Verbosity',
|
|
40
|
+
callback=callback)(f)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cli_callbacks(f):
|
|
44
|
+
f = OPTION_VERBOSITY(f)
|
|
45
|
+
return f
|
pywmdr/errors.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
#
|
|
20
|
+
###############################################################################
|
|
21
|
+
|
|
22
|
+
class TestSuiteError(Exception):
|
|
23
|
+
"""custom exception handler"""
|
|
24
|
+
def __init__(self, message, errors):
|
|
25
|
+
"""set error list/stack"""
|
|
26
|
+
super(TestSuiteError, self).__init__(message)
|
|
27
|
+
self.errors = errors
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
#
|
|
20
|
+
###############################################################################
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# pywmdr as a service
|
|
24
|
+
# -------------------
|
|
25
|
+
#
|
|
26
|
+
# This file is intended to be used as a pygeoapi process plugin which will
|
|
27
|
+
# provide pywmdr functionality via OGC API - Processes.
|
|
28
|
+
#
|
|
29
|
+
# To integrate this plugin in pygeoapi:
|
|
30
|
+
#
|
|
31
|
+
# 1. ensure pywmdr is installed into the pygeoapi deployment environment
|
|
32
|
+
#
|
|
33
|
+
# 2. add the processes to the pygeoapi configuration as follows:
|
|
34
|
+
#
|
|
35
|
+
# pywmdr-record-validate:
|
|
36
|
+
# type: process
|
|
37
|
+
# processor:
|
|
38
|
+
# name: pywmdr.pygeoapi_plugin.WMDR2ETSProcessor
|
|
39
|
+
#
|
|
40
|
+
# 3. (re)start pygeoapi
|
|
41
|
+
#
|
|
42
|
+
# The resulting processes will be available at the following endpoints:
|
|
43
|
+
#
|
|
44
|
+
# /processes/pywmdr-record-validate
|
|
45
|
+
#
|
|
46
|
+
# Note that pygeoapi's OpenAPI/Swagger interface (at /openapi) will also
|
|
47
|
+
# provide a developer-friendly interface to test and run requests
|
|
48
|
+
#
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
import logging
|
|
52
|
+
|
|
53
|
+
from pygeoapi.process.base import BaseProcessor, ProcessorExecuteError
|
|
54
|
+
|
|
55
|
+
from pywmdr.wmdr2.ets import WMDR2TestSuite
|
|
56
|
+
from pywmdr.util import get_package_version, THISDIR, urlopen_
|
|
57
|
+
|
|
58
|
+
LOGGER = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
with (THISDIR / 'resources' / 'ets-report.json').open() as fh:
|
|
61
|
+
ETS_REPORT_SCHEMA = json.load(fh)
|
|
62
|
+
|
|
63
|
+
with (THISDIR / 'resources' / '20250504_0-20008-0-NRB.json').open() as fh:
|
|
64
|
+
EXAMPLE_WMDR2 = json.load(fh)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
PROCESS_WMDR2_ETS = {
|
|
68
|
+
'version': get_package_version(),
|
|
69
|
+
'id': 'pywmdr-record-validate',
|
|
70
|
+
'title': {
|
|
71
|
+
'en': 'WMDR2 record validator'
|
|
72
|
+
},
|
|
73
|
+
'description': {
|
|
74
|
+
'en': 'Validate a WMDR2 record against the ETS'
|
|
75
|
+
},
|
|
76
|
+
'keywords': ['wigos', 'wmdr2', 'ets', 'test suite', 'metadata'],
|
|
77
|
+
'links': [{
|
|
78
|
+
'type': 'text/html',
|
|
79
|
+
'rel': 'about',
|
|
80
|
+
'title': 'information',
|
|
81
|
+
'href': 'https://github.com/wmo-im/wmdr2',
|
|
82
|
+
'hreflang': 'en-US'
|
|
83
|
+
}],
|
|
84
|
+
'inputs': {
|
|
85
|
+
'record': {
|
|
86
|
+
'title': 'WMDR2 record',
|
|
87
|
+
'description': 'WMDR2 record (can be inline or remote link)',
|
|
88
|
+
'schema': {
|
|
89
|
+
'type': ['object', 'string']
|
|
90
|
+
},
|
|
91
|
+
'minOccurs': 1,
|
|
92
|
+
'maxOccurs': 1,
|
|
93
|
+
'metadata': None,
|
|
94
|
+
'keywords': ['wmdr2']
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
'outputs': {
|
|
98
|
+
'result': {
|
|
99
|
+
'title': 'Report of ETS results',
|
|
100
|
+
'description': 'Report of ETS results',
|
|
101
|
+
'schema': {
|
|
102
|
+
'contentMediaType': 'application/json',
|
|
103
|
+
**ETS_REPORT_SCHEMA
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
'example': {
|
|
108
|
+
'inputs': {
|
|
109
|
+
'record': EXAMPLE_WMDR2
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class WMDR2ETSProcessor(BaseProcessor):
|
|
116
|
+
"""WMDR2 ETS"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, processor_def):
|
|
119
|
+
"""
|
|
120
|
+
Initialize object
|
|
121
|
+
|
|
122
|
+
:param processor_def: provider definition
|
|
123
|
+
|
|
124
|
+
:returns: pywmdr.pygeoapi_plugin.WMDR2ETSProcessor
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
super().__init__(processor_def, PROCESS_WMDR2_ETS)
|
|
128
|
+
|
|
129
|
+
def execute(self, data, outputs=None):
|
|
130
|
+
|
|
131
|
+
response = None
|
|
132
|
+
mimetype = 'application/json'
|
|
133
|
+
record = data.get('record')
|
|
134
|
+
|
|
135
|
+
if record is None:
|
|
136
|
+
msg = 'Missing record'
|
|
137
|
+
LOGGER.error(msg)
|
|
138
|
+
raise ProcessorExecuteError(msg)
|
|
139
|
+
|
|
140
|
+
if isinstance(record, str) and record.startswith('http'):
|
|
141
|
+
LOGGER.debug('Record is a link')
|
|
142
|
+
record = json.loads(urlopen_(record).read())
|
|
143
|
+
else:
|
|
144
|
+
LOGGER.debug('Record is inline')
|
|
145
|
+
|
|
146
|
+
LOGGER.debug('Running ETS against record')
|
|
147
|
+
response = WMDR2TestSuite(record).run_tests()
|
|
148
|
+
|
|
149
|
+
return mimetype, response
|
|
150
|
+
|
|
151
|
+
def __repr__(self):
|
|
152
|
+
return '<WMDR2ETSProcessor>'
|
pywmdr/record.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
#
|
|
20
|
+
###############################################################################
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
|
|
26
|
+
from pywmdr.wmdr2.ets import WMDR2TestSuite
|
|
27
|
+
from pywmdr import cli_options
|
|
28
|
+
from pywmdr.util import parse_wmdr2, urlopen_
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
def record():
|
|
33
|
+
"""WMDR2 record utilities"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.command()
|
|
38
|
+
@click.pass_context
|
|
39
|
+
@click.argument('file_or_url')
|
|
40
|
+
@cli_options.OPTION_VERBOSITY
|
|
41
|
+
def validate(ctx, file_or_url, verbosity):
|
|
42
|
+
"""validate WMDR2 record against the specification"""
|
|
43
|
+
|
|
44
|
+
click.echo(f'Opening {file_or_url}')
|
|
45
|
+
|
|
46
|
+
if file_or_url.startswith('http'):
|
|
47
|
+
content = urlopen_(file_or_url).read()
|
|
48
|
+
else:
|
|
49
|
+
with open(file_or_url) as fh:
|
|
50
|
+
content = fh.read()
|
|
51
|
+
|
|
52
|
+
click.echo(f'Validating {file_or_url}')
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
data = parse_wmdr2(content)
|
|
56
|
+
except Exception as err:
|
|
57
|
+
raise click.ClickException(err)
|
|
58
|
+
ctx.exit(1)
|
|
59
|
+
|
|
60
|
+
click.echo('Detected WMDR2 record')
|
|
61
|
+
ts = WMDR2TestSuite(data)
|
|
62
|
+
try:
|
|
63
|
+
results = ts.run_tests()
|
|
64
|
+
except Exception as err:
|
|
65
|
+
raise click.ClickException(err)
|
|
66
|
+
ctx.exit(1)
|
|
67
|
+
|
|
68
|
+
click.echo(json.dumps(results, indent=4))
|
|
69
|
+
ctx.exit(results['summary']['FAILED'])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
record.add_command(validate)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "urn:wmo:md:ke-meteo:0-20008-0-NRB",
|
|
3
|
+
"type": "Feature",
|
|
4
|
+
"conformsTo": [
|
|
5
|
+
"http://wis.wmo.int/spec/wmdr/2/conf/core"
|
|
6
|
+
],
|
|
7
|
+
"geometry": {
|
|
8
|
+
"type": "Point",
|
|
9
|
+
"coordinates": [
|
|
10
|
+
36.75919,
|
|
11
|
+
-1.30169,
|
|
12
|
+
1795.0
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"properties": {
|
|
16
|
+
"created": "1996-01-01T00:00:00Z",
|
|
17
|
+
"wmo:dataPolicy": "core",
|
|
18
|
+
"contacts": [
|
|
19
|
+
{
|
|
20
|
+
"name": "World Meteorological Organization WMO and Federal Office for Meteorology and Climatology MeteoSwiss",
|
|
21
|
+
"organization": "World Meteorological Organization WMO and Federal Office for Meteorology and Climatology MeteoSwiss",
|
|
22
|
+
"emails": [
|
|
23
|
+
{
|
|
24
|
+
"value": "oscar@wmo.int"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"addresses": [
|
|
28
|
+
{
|
|
29
|
+
"deliveryPoint": [
|
|
30
|
+
"7bis, avenue de la Paix"
|
|
31
|
+
],
|
|
32
|
+
"city": "Geneva",
|
|
33
|
+
"postalCode": "CH-1211",
|
|
34
|
+
"country": "Switzerland"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"contactInstructions": "email",
|
|
38
|
+
"links": [
|
|
39
|
+
{
|
|
40
|
+
"rel": "canonical",
|
|
41
|
+
"type": "text/html",
|
|
42
|
+
"href": "https://oscar.wmo.int/surface"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"roles": [
|
|
46
|
+
"host"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "Waweru,Amos,Mr",
|
|
51
|
+
"organization": "Kenyan Meteorological Department",
|
|
52
|
+
"phones": [
|
|
53
|
+
{
|
|
54
|
+
"value": "+254723521586"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"emails": [
|
|
58
|
+
{
|
|
59
|
+
"value": "waweru_k@yahoo.com"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"value": "amoskamau1969@gmail.com"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"addresses": [
|
|
66
|
+
{
|
|
67
|
+
"deliveryPoint": [
|
|
68
|
+
"Kenya Meteorological Department 30259\nNairobi \nKenya"
|
|
69
|
+
],
|
|
70
|
+
"city": "Nairobi",
|
|
71
|
+
"administrativeArea": "NB",
|
|
72
|
+
"postalCode": "00100",
|
|
73
|
+
"country": "Kenya"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"contactInstructions": "africa",
|
|
77
|
+
"roles": [
|
|
78
|
+
"processor",
|
|
79
|
+
"producer"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"links": [
|
|
85
|
+
{
|
|
86
|
+
"rel": "license",
|
|
87
|
+
"href": "https://creativecommons.org/licenses/by/4.0/"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"rel": "stations",
|
|
91
|
+
"href": "http://www.meteo.go.ke/obsv/ozone.html"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema: https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/wmo-im/pywmdr/refs/heads/main/pywmdr/resources/ets-report.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"title": "WMDR2 Executable Test Suite report",
|
|
6
|
+
"description": "WMDR2 Executable Test Suite report",
|
|
7
|
+
"properties": {
|
|
8
|
+
"id": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"format": "uuid"
|
|
11
|
+
},
|
|
12
|
+
"report_type": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"enum": [
|
|
15
|
+
"ets"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"summary": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"description": "summary information",
|
|
21
|
+
"properties": {
|
|
22
|
+
"PASSED": {
|
|
23
|
+
"type": "integer"
|
|
24
|
+
},
|
|
25
|
+
"FAILED": {
|
|
26
|
+
"type": "integer"
|
|
27
|
+
},
|
|
28
|
+
"SKIPPED": {
|
|
29
|
+
"type": "integer"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": [
|
|
33
|
+
"PASSED",
|
|
34
|
+
"FAILED",
|
|
35
|
+
"SKIPPED"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"tests": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"id": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"format": "uri"
|
|
46
|
+
},
|
|
47
|
+
"code": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": [
|
|
50
|
+
"PASSED",
|
|
51
|
+
"FAILED",
|
|
52
|
+
"SKIPPED"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"message": {
|
|
56
|
+
"type": "string"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"required": [
|
|
60
|
+
"id",
|
|
61
|
+
"code"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"generated_by": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
},
|
|
68
|
+
"datetime": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"format": "date-time"
|
|
71
|
+
},
|
|
72
|
+
"metadata_id": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"required": [
|
|
77
|
+
"id",
|
|
78
|
+
"report_type",
|
|
79
|
+
"summary",
|
|
80
|
+
"generated_by",
|
|
81
|
+
"datetime",
|
|
82
|
+
"metadata_id",
|
|
83
|
+
"tests"
|
|
84
|
+
]
|
|
85
|
+
}
|
pywmdr/util.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
#
|
|
20
|
+
###############################################################################
|
|
21
|
+
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
import importlib.metadata
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
import ssl
|
|
28
|
+
from urllib.error import URLError
|
|
29
|
+
from urllib.request import urlopen
|
|
30
|
+
|
|
31
|
+
LOGGER = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
THISDIR = Path(__file__).parent.resolve()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_current_datetime_rfc3339() -> str:
|
|
37
|
+
"""
|
|
38
|
+
Gets the current datetime in RFC3339 format
|
|
39
|
+
|
|
40
|
+
:returns: `str` of RFC3339
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_userdir() -> str:
|
|
47
|
+
"""
|
|
48
|
+
Helper function to get userdir
|
|
49
|
+
|
|
50
|
+
:returns: user's home directory
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
return Path.home() / '.pywmdr'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_package_version() -> str:
|
|
57
|
+
"""
|
|
58
|
+
Helper function to get package version
|
|
59
|
+
|
|
60
|
+
:returns: `str` of version of package
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
return importlib.metadata.version('pywmdr')
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_wmdr2(content: str) -> dict:
|
|
67
|
+
"""
|
|
68
|
+
Parse a string of WMDR2 JSON into a dict
|
|
69
|
+
|
|
70
|
+
:param content: `str` of JSON
|
|
71
|
+
|
|
72
|
+
:returns: `dict` object of WMDR2
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
LOGGER.debug('Attempting to parse as JSON')
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(content)
|
|
78
|
+
except json.decoder.JSONDecodeError as err:
|
|
79
|
+
LOGGER.error(err)
|
|
80
|
+
raise RuntimeError(f'Encoding error: {err}')
|
|
81
|
+
|
|
82
|
+
return data
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def urlopen_(url: str):
|
|
86
|
+
"""
|
|
87
|
+
Helper function for downloading a URL
|
|
88
|
+
|
|
89
|
+
:param url: URL to download
|
|
90
|
+
|
|
91
|
+
:returns: `http.client.HTTPResponse`
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
response = urlopen(url)
|
|
96
|
+
except (ssl.SSLError, URLError) as err:
|
|
97
|
+
LOGGER.warning(err)
|
|
98
|
+
LOGGER.warning('Creating unverified context')
|
|
99
|
+
context = ssl._create_unverified_context()
|
|
100
|
+
|
|
101
|
+
response = urlopen(url, context=context)
|
|
102
|
+
|
|
103
|
+
return response
|
pywmdr/wmdr2/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
###############################################################################
|
pywmdr/wmdr2/ets.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
#
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
#
|
|
20
|
+
###############################################################################
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import uuid
|
|
25
|
+
|
|
26
|
+
from jsonschema import FormatChecker
|
|
27
|
+
from jsonschema.validators import Draft202012Validator
|
|
28
|
+
|
|
29
|
+
from pywmdr.bundle import WMDR2_FILES
|
|
30
|
+
from pywmdr.errors import TestSuiteError
|
|
31
|
+
from pywmdr.util import get_current_datetime_rfc3339, get_package_version
|
|
32
|
+
|
|
33
|
+
LOGGER = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def gen_test_id(test_id: str) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Convenience function to print test identifier as URI
|
|
39
|
+
|
|
40
|
+
:param test_id: test suite identifier
|
|
41
|
+
|
|
42
|
+
:returns: test identifier as URI
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
return f'http://wis.wmo.int/spec/wmdr/2/conf/core/{test_id}'
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class WMDR2TestSuite:
|
|
49
|
+
def __init__(self, record):
|
|
50
|
+
"""
|
|
51
|
+
initializer
|
|
52
|
+
|
|
53
|
+
:param data: dict of WMDR2 JSON
|
|
54
|
+
|
|
55
|
+
:returns: `pywmdr.wmdr2.ets.WMDR2TestSuite`
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
self.version = get_package_version()
|
|
59
|
+
self.errors = []
|
|
60
|
+
self.record = record
|
|
61
|
+
|
|
62
|
+
def run_tests(self):
|
|
63
|
+
|
|
64
|
+
results = []
|
|
65
|
+
tests = []
|
|
66
|
+
|
|
67
|
+
ets_report = {
|
|
68
|
+
'id': str(uuid.uuid4()),
|
|
69
|
+
'report_type': 'ets',
|
|
70
|
+
'summary': {
|
|
71
|
+
'PASSED': 0,
|
|
72
|
+
'FAILED': 0,
|
|
73
|
+
'SKIPPED': 0,
|
|
74
|
+
'WARNINGS': 0
|
|
75
|
+
},
|
|
76
|
+
'generated_by': f'pywmdr {self.version} (https://github.com/wmo-im/pywmdr)' # noqa
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for f in dir(WMDR2TestSuite):
|
|
80
|
+
if all([callable(getattr(WMDR2TestSuite, f)),
|
|
81
|
+
f.startswith('test_requirement'),
|
|
82
|
+
not f.endswith('validation')]):
|
|
83
|
+
tests.append(f)
|
|
84
|
+
|
|
85
|
+
LOGGER.debug('Running schema validation')
|
|
86
|
+
result = self.test_requirement_validation()
|
|
87
|
+
results.append(result)
|
|
88
|
+
if result['code'] == 'FAILED':
|
|
89
|
+
self.errors.append(result)
|
|
90
|
+
|
|
91
|
+
for t in tests:
|
|
92
|
+
result = getattr(self, t)()
|
|
93
|
+
results.append(result)
|
|
94
|
+
if result['code'] == 'FAILED':
|
|
95
|
+
self.errors.append(result)
|
|
96
|
+
|
|
97
|
+
for code in ['PASSED', 'FAILED', 'SKIPPED', 'WARNINGS']:
|
|
98
|
+
r = len([t for t in results if t['code'] == code])
|
|
99
|
+
ets_report['summary'][code] = r
|
|
100
|
+
|
|
101
|
+
ets_report['tests'] = results
|
|
102
|
+
ets_report['datetime'] = get_current_datetime_rfc3339()
|
|
103
|
+
ets_report['metadata_id'] = self.record['id']
|
|
104
|
+
|
|
105
|
+
return ets_report
|
|
106
|
+
|
|
107
|
+
def test_requirement_validation(self):
|
|
108
|
+
"""
|
|
109
|
+
Validate that an WMDR2 record is valid to the authoritative schema.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
validation_errors = []
|
|
113
|
+
|
|
114
|
+
format_checkers = ['date-time', 'email', 'regex',
|
|
115
|
+
'uri', 'uri-reference']
|
|
116
|
+
|
|
117
|
+
status = {
|
|
118
|
+
'id': gen_test_id('validation'),
|
|
119
|
+
'code': 'PASSED'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
schema = WMDR2_FILES / 'eomp-bundled.json'
|
|
123
|
+
|
|
124
|
+
if not schema.exists():
|
|
125
|
+
msg = "WMDR2 schema missing. Run 'pywmdr bundle sync' to cache"
|
|
126
|
+
LOGGER.error(msg)
|
|
127
|
+
raise RuntimeError(msg)
|
|
128
|
+
|
|
129
|
+
with schema.open() as fh:
|
|
130
|
+
LOGGER.debug(f'Validating {self.record} against {schema}')
|
|
131
|
+
validator = Draft202012Validator(
|
|
132
|
+
json.load(fh),
|
|
133
|
+
format_checker=FormatChecker(formats=format_checkers)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
for error in validator.iter_errors(self.record):
|
|
137
|
+
LOGGER.debug(f'{error.json_path}: {error.message}')
|
|
138
|
+
validation_errors.append(f'{error.json_path}: {error.message}')
|
|
139
|
+
|
|
140
|
+
if validation_errors:
|
|
141
|
+
status['code'] = 'FAILED'
|
|
142
|
+
status['message'] = f'{len(validation_errors)} error(s)'
|
|
143
|
+
status['errors'] = validation_errors
|
|
144
|
+
|
|
145
|
+
return status
|
|
146
|
+
|
|
147
|
+
def raise_for_status(self):
|
|
148
|
+
"""
|
|
149
|
+
Raise error if one or more failures were found during validation.
|
|
150
|
+
|
|
151
|
+
:returns: `pywmdr.errors.TestSuiteError` or `None`
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
if len(self.errors) > 0:
|
|
155
|
+
raise TestSuiteError('Invalid WMDR2 record', self.errors)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pywmdr
|
|
3
|
+
Version: 0.2.dev0
|
|
4
|
+
Summary: A Python implementation of the test suite for WIGOS Metadata Record
|
|
5
|
+
Author-email: Tom Kralidis <tomkralidis@gmail.com>
|
|
6
|
+
Maintainer-email: Tom Kralidis <tomkralidis@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Project-URL: homepage, https://github.com/wmo-im/pywmdr
|
|
9
|
+
Project-URL: source, https://github.com/wmo-im/pywmdr
|
|
10
|
+
Project-URL: documentation, https://github.com/wmo-im/pywmdr
|
|
11
|
+
Project-URL: issues, https://github.com/wmo-im/pywmdr/issues
|
|
12
|
+
Keywords: wmo,wigos,station metadata,test suite
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE.md
|
|
24
|
+
Requires-Dist: click
|
|
25
|
+
Requires-Dist: jsonschema
|
|
26
|
+
Requires-Dist: rfc3339-validator
|
|
27
|
+
Requires-Dist: rfc3987
|
|
28
|
+
Requires-Dist: shapely
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: flake8; extra == "dev"
|
|
31
|
+
Provides-Extra: release
|
|
32
|
+
Requires-Dist: build; extra == "release"
|
|
33
|
+
Requires-Dist: twine; extra == "release"
|
|
34
|
+
Requires-Dist: wheel; extra == "release"
|
|
35
|
+
Provides-Extra: test
|
|
36
|
+
Requires-Dist: pytest; extra == "test"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# pywmdr
|
|
40
|
+
|
|
41
|
+
[](https://github.com/wmo-im/pywmdr/actions)
|
|
42
|
+
|
|
43
|
+
# WIGOS Metadata Record Test Suite
|
|
44
|
+
|
|
45
|
+
pywmdr provides validation and quality assessment capabilities for the [WIGOS Metadata Record](https://wmo-im.github.io/wmdr2) (WMDR2) standard.
|
|
46
|
+
|
|
47
|
+
- validation against [WMDR2](https://wmo-im.github.io/wmdr2/standard/wmdr2-DRAFT.html), specifically [Annex A: Conformance Class Abstract Test Suite](https://wmo-im.github.io/wmdr2/standard/wmdr2-DRAFT.html#_conformance_class_abstract_test_suite_normative)), implementing an executable test suite against the ATS
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
### pip
|
|
52
|
+
|
|
53
|
+
Install latest stable version from [PyPI](https://pypi.org/project/pywmdr).
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip3 install pywmdr
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### From source
|
|
60
|
+
|
|
61
|
+
Install latest development version.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python3 -m venv pywmdr
|
|
65
|
+
cd pywmdr
|
|
66
|
+
. bin/activate
|
|
67
|
+
git clone https://github.com/wmo-im/pywmdr.git
|
|
68
|
+
cd pywmdr
|
|
69
|
+
pip3 install .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Running
|
|
73
|
+
|
|
74
|
+
From command line:
|
|
75
|
+
```bash
|
|
76
|
+
# fetch version
|
|
77
|
+
pywmdr --version
|
|
78
|
+
|
|
79
|
+
# sync supporting configuration bundle (schemas, topics, etc.)
|
|
80
|
+
pywmdr bundle sync
|
|
81
|
+
|
|
82
|
+
# abstract test suite
|
|
83
|
+
|
|
84
|
+
# validate WMDR2 metadata against abstract test suite (file on disk)
|
|
85
|
+
pywmdr ets validate /path/to/file.json
|
|
86
|
+
|
|
87
|
+
# validate WMDR2 metadata against abstract test suite (URL)
|
|
88
|
+
pywmdr ets validate https://example.org/path/to/file.json
|
|
89
|
+
|
|
90
|
+
# validate WMDR2 metadata against abstract test suite (URL), but turn JSON Schema validation off
|
|
91
|
+
pywmdr ets validate https://example.org/path/to/file.json --no-fail-on-schema-validation
|
|
92
|
+
|
|
93
|
+
# adjust debugging messages (CRITICAL, ERROR, WARNING, INFO, DEBUG) to stdout
|
|
94
|
+
pywmdr ets validate https://example.org/path/to/file.json --verbosity DEBUG
|
|
95
|
+
|
|
96
|
+
# write results to logfile
|
|
97
|
+
pywmdr ets validate https://example.org/path/to/file.json --verbosity DEBUG --logfile /tmp/foo.txt
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Using the API
|
|
101
|
+
```pycon
|
|
102
|
+
>>> # test a file on disk
|
|
103
|
+
>>> import json
|
|
104
|
+
>>> from pywmdr.wmdr2.ets import WIGOSMetadataRepresentationTestSuite2
|
|
105
|
+
>>> from pywmdr.errors import TestSuiteError
|
|
106
|
+
>>> with open('/path/to/file.json') as fh:
|
|
107
|
+
... data = json.load(fh)
|
|
108
|
+
>>> # test ETS
|
|
109
|
+
>>> ts = WMOCoreMetadataProfileTestSuite2(datal)
|
|
110
|
+
>>> ts.run_tests()
|
|
111
|
+
>>> ts.raise_for_status() # raises pywmdr.errors.TestSuiteError on exception with list of errors captured in .errors property
|
|
112
|
+
>>> # test a URL
|
|
113
|
+
>>> from urllib2 import urlopen
|
|
114
|
+
>>> from StringIO import StringIO
|
|
115
|
+
>>> content = StringIO(urlopen('https://....').read())
|
|
116
|
+
>>> data = json.loads(content)
|
|
117
|
+
>>> ts = WMOCoreMetadataProfileTestSuite2(data)
|
|
118
|
+
>>> ts.run_tests()
|
|
119
|
+
>>> ts.raise_for_status() # raises pywmdr.errors.TestSuiteError on exception with list of errors captured in .errors property
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python3 -m venv pywmdr
|
|
126
|
+
cd pywmdr
|
|
127
|
+
source bin/activate
|
|
128
|
+
git clone https://github.com/World-Meteorological-Organization/pywmdr.git
|
|
129
|
+
pip3 install .
|
|
130
|
+
pip3 install ".[dev]"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Running tests
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pytest tests
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Releasing
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# create release (x.y.z is the release version)
|
|
143
|
+
vi pyproject.toml # update [project]/version
|
|
144
|
+
git commit -am 'update release version x.y.z'
|
|
145
|
+
git push origin master
|
|
146
|
+
git tag -a x.y.z -m 'tagging release version x.y.z'
|
|
147
|
+
git push --tags
|
|
148
|
+
|
|
149
|
+
# upload to PyPI
|
|
150
|
+
rm -fr build dist *.egg-info
|
|
151
|
+
python3 -m build
|
|
152
|
+
twine upload dist/*
|
|
153
|
+
|
|
154
|
+
# publish release on GitHub (https://github.com/wmo-im/pywmdr/releases/new)
|
|
155
|
+
|
|
156
|
+
# bump version back to dev
|
|
157
|
+
vi pyproject.toml # update [project]/version
|
|
158
|
+
git commit -am 'back to dev'
|
|
159
|
+
git push origin master
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Code Conventions
|
|
163
|
+
|
|
164
|
+
[PEP8](https://www.python.org/dev/peps/pep-0008)
|
|
165
|
+
|
|
166
|
+
## Issues
|
|
167
|
+
|
|
168
|
+
Issues are managed at https://github.com/wmo-im/pywmdr/issues
|
|
169
|
+
|
|
170
|
+
## Contact
|
|
171
|
+
|
|
172
|
+
* [Tom Kralidis](https://github.com/tomkralidis)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pywmdr/__init__.py,sha256=3JVAiKN_rLYTMR5kOl52qEGRtO7hki7PpxdKS0MhanE,1327
|
|
2
|
+
pywmdr/bundle.py,sha256=bwqO-pVVeF2shVBKMj2lwgKuVeXbmHPEsU4o-jutCfk,2276
|
|
3
|
+
pywmdr/cli_options.py,sha256=N70OlGeonJvkVNdDoh-X606J1JCUWnKwpa5gduoJ_aM,1533
|
|
4
|
+
pywmdr/errors.py,sha256=Uu2J6vmTjaWuNJaW-mtdZ_KCztc9x4wjaLRVDZP6Lk0,1176
|
|
5
|
+
pywmdr/pygeoapi_plugin.py,sha256=Ib8RH3fh62p4ajAOhk1f73etqXkBsnDtYUqVgjzvh_k,4449
|
|
6
|
+
pywmdr/record.py,sha256=L4wDXuWtCaqK4yO6rcrndJ8X-xN8fJSHB9CQJbTclM4,2083
|
|
7
|
+
pywmdr/util.py,sha256=LE_Ds6OuG7vNQxjFQcogWiPD5CNQyHhqIXeW6TNXJZE,2639
|
|
8
|
+
pywmdr/resources/20250504_0-20008-0-NRB.json,sha256=gvSgRDp4U_Oh5Wyu54a3IhcOvaxg-cpjrNNALyOGhR4,2958
|
|
9
|
+
pywmdr/resources/ets-report.json,sha256=nHIjU-9Hw8C72N0HntkGJweAYWnqZkonIFuwgDSW_NQ,2241
|
|
10
|
+
pywmdr/wmdr2/__init__.py,sha256=VK88WG3wpt5RqksGgutMnr02OhUlksWcoAnoeSRRWic,1035
|
|
11
|
+
pywmdr/wmdr2/ets.py,sha256=gQLzaM7AEPgCAfMWlXSQGi7CL5h8t1FibyvjmUpzfb0,4858
|
|
12
|
+
pywmdr-0.2.dev0.dist-info/licenses/LICENSE.md,sha256=Q8E0TXZpH75ydPcip7DXr21Y3uwJNgQUFv3ipMXwPkw,839
|
|
13
|
+
pywmdr-0.2.dev0.dist-info/METADATA,sha256=C5G9J09OS00I_BE4q3Qww0ugxHg_0_1GPyd16iKrrPI,5104
|
|
14
|
+
pywmdr-0.2.dev0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
15
|
+
pywmdr-0.2.dev0.dist-info/entry_points.txt,sha256=Ga7GlkDlThWwprQDc5OawyWtV_RT9FXilgXjwKacynU,38
|
|
16
|
+
pywmdr-0.2.dev0.dist-info/top_level.txt,sha256=_Zdw0CS7tVYk-CJZmu9xEqIzOuY98RsI6CUMh8Kh818,7
|
|
17
|
+
pywmdr-0.2.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Authors:
|
|
2
|
+
|
|
3
|
+
Tom Kralidis <tomkralidis@gmail.com>
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2025 Tom Kralidis
|
|
6
|
+
|
|
7
|
+
***
|
|
8
|
+
|
|
9
|
+
Licensed to the Apache Software Foundation (ASF) under one
|
|
10
|
+
or more contributor license agreements. See the NOTICE file
|
|
11
|
+
distributed with this work for additional information
|
|
12
|
+
regarding copyright ownership. The ASF licenses this file
|
|
13
|
+
to you under the Apache License, Version 2.0 (the
|
|
14
|
+
"License"); you may not use this file except in compliance
|
|
15
|
+
with the License. You may obtain a copy of the License at
|
|
16
|
+
|
|
17
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
18
|
+
|
|
19
|
+
Unless required by applicable law or agreed to in writing,
|
|
20
|
+
software distributed under the License is distributed on an
|
|
21
|
+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
22
|
+
KIND, either express or implied. See the License for the
|
|
23
|
+
specific language governing permissions and limitations
|
|
24
|
+
under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pywmdr
|