Skip to content

Commit 58475ac

Browse files
authored
update pygeoapi process, add raise_for_status (#133)
* allow for inline or remote link for pygeoapi process * add raise_for_status() * fix ref * improve data policy failure text
1 parent ba68f50 commit 58475ac

File tree

5 files changed

+222
-15
lines changed

5 files changed

+222
-15
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ pywcmp kpi validate --kpi title /path/to/file.json -v INFO
8484
... data = json.load(fh)
8585
>>> # test ETS
8686
>>> ts = WMOCoreMetadataProfileTestSuite2(datal)
87-
>>> ts.run_tests() # raises ValueError error stack on exception
87+
>>> ts.run_tests()
88+
>>> ts.raise_for_status() # raises pywcmp.errors.TestSuiteError on exception with list of errors captured in .errors property
8889
>>> # test a URL
8990
>>> from urllib2 import urlopen
9091
>>> from StringIO import StringIO

pywcmp/pygeoapi_plugin.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666

6767
from pywcmp.wcmp2.ets import WMOCoreMetadataProfileTestSuite2
6868
from pywcmp.wcmp2.kpi import WMOCoreMetadataProfileKeyPerformanceIndicators
69-
from pywcmp.util import THISDIR
69+
from pywcmp.util import THISDIR, urlopen_
7070

7171
LOGGER = logging.getLogger(__name__)
7272

@@ -76,6 +76,9 @@
7676
with (THISDIR / 'resources' / 'kpi-report.json').open() as fh:
7777
KPI_REPORT_SCHEMA = json.load(fh)
7878

79+
with (THISDIR / 'resources' / 'example-ca-eccc-msc.nwp-gdps.json').open() as fh: # noqa
80+
EXAMPLE_WCMP2 = json.load(fh)
81+
7982

8083
PROCESS_WCMP2_ETS = {
8184
'version': '0.1.0',
@@ -97,9 +100,9 @@
97100
'inputs': {
98101
'record': {
99102
'title': 'WCMP2 record',
100-
'description': 'WCMP2 record',
103+
'description': 'WCMP2 record (can be inline or remote link)',
101104
'schema': {
102-
'type': 'string'
105+
'type': ['object', 'string']
103106
},
104107
'minOccurs': 1,
105108
'maxOccurs': 1,
@@ -131,9 +134,7 @@
131134
},
132135
'example': {
133136
'inputs': {
134-
'record': {
135-
'$ref': 'https://raw.githubusercontent.com/World-Meteorological-Organization/pywcmp/master/tests/data/wcmp2-passing.json' # noqa
136-
},
137+
'record': EXAMPLE_WCMP2,
137138
'fail_on_schema_validation': True
138139
}
139140
}
@@ -160,9 +161,9 @@
160161
'inputs': {
161162
'record': {
162163
'title': 'WCMP2 record',
163-
'description': 'WCMP2 record',
164+
'description': 'WCMP2 record (can be inline or remote link)',
164165
'schema': {
165-
'type': 'string'
166+
'type': ['object', 'string']
166167
},
167168
'minOccurs': 1,
168169
'maxOccurs': 1,
@@ -182,9 +183,7 @@
182183
},
183184
'example': {
184185
'inputs': {
185-
'record': {
186-
'$ref': 'https://raw.githubusercontent.com/World-Meteorological-Organization/pywcmp/master/tests/data/wcmp2-passing.json' # noqa
187-
}
186+
'record': EXAMPLE_WCMP2
188187
}
189188
}
190189
}
@@ -216,6 +215,12 @@ def execute(self, data, outputs=None):
216215
LOGGER.error(msg)
217216
raise ProcessorExecuteError(msg)
218217

218+
if isinstance(record, str) and record.startswith('http'):
219+
LOGGER.debug('Record is a link')
220+
record = json.loads(urlopen_(record).read())
221+
else:
222+
LOGGER.debug('Record is inline')
223+
219224
LOGGER.debug('Running ETS against record')
220225
response = WMOCoreMetadataProfileTestSuite2(record).run_tests(
221226
fail_on_schema_validation=fail_on_schema_validation)
@@ -244,13 +249,20 @@ def execute(self, data, outputs=None):
244249

245250
response = None
246251
mimetype = 'application/json'
252+
247253
record = data.get('record')
248254

249255
if record is None:
250256
msg = 'Missing record'
251257
LOGGER.error(msg)
252258
raise ProcessorExecuteError(msg)
253259

260+
if isinstance(record, str) and record.startswith('http'):
261+
LOGGER.debug('Record is a link')
262+
record = json.loads(urlopen_(record).read())
263+
else:
264+
LOGGER.debug('Record is inline')
265+
254266
LOGGER.debug('Running KPIs against record')
255267
kpis = WMOCoreMetadataProfileKeyPerformanceIndicators(record)
256268
response = kpis.evaluate()
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
{
2+
"id": "urn:wmo:md:ca-eccc-msc:nwp.msc_nwp_gdps",
3+
"conformsTo": [
4+
"http://wis.wmo.int/spec/wcmp/2/conf/core"
5+
],
6+
"type": "Feature",
7+
"geometry": {
8+
"type": "Polygon",
9+
"coordinates": [
10+
[
11+
[
12+
-180,
13+
-90
14+
],
15+
[
16+
-180,
17+
90
18+
],
19+
[
20+
180,
21+
90
22+
],
23+
[
24+
180,
25+
-90
26+
],
27+
[
28+
-180,
29+
-90
30+
]
31+
]
32+
]
33+
},
34+
"time": {
35+
"interval": [
36+
"1963-10-01",
37+
".."
38+
]
39+
},
40+
"properties": {
41+
"title": "Global Deterministic Prediction System",
42+
"description": "The Global Deterministic Prediction System (GDPS) carries out physics calculations to arrive at deterministic predictions of atmospheric elements from the current day out to 10 days into the future. Atmospheric elements include temperature, precipitation, cloud cover, wind speed and direction, humidity and others. This product contains raw numerical results of these calculations. Geographical coverage is global. Data is available at horizontal resolution of about 15 km up to 33 vertical levels. Predictions are performed twice a day.",
43+
"themes": [
44+
{
45+
"concepts": [
46+
{
47+
"id": "Prediction"
48+
},
49+
{
50+
"id": "Global"
51+
},
52+
{
53+
"id": "Deterministic"
54+
},
55+
{
56+
"id": "Weather forecasts"
57+
},
58+
{
59+
"id": "Air temperature"
60+
},
61+
{
62+
"id": "Meteorological data"
63+
}
64+
],
65+
"scheme": "https://canada.multites.net/cst"
66+
},
67+
{
68+
"concepts": [
69+
{
70+
"id": "weather",
71+
"title": "Weather",
72+
"url": "https://codes.wmo.int/wis/topic-hierarchy/earth-system-discipline/weather"
73+
}
74+
],
75+
"scheme": "https://codes.wmo.int/wis/topic-hierarchy/earth-system-discipline"
76+
}
77+
],
78+
"contacts": [
79+
{
80+
"name": "National Inquiry Response Team",
81+
"organization": "Government of Canada; Environment and Climate Change Canada; Meteorological Service of Canada",
82+
"phones": [
83+
{
84+
"value": "+18199972800"
85+
}
86+
],
87+
"emails": [
88+
{
89+
"value": "enviroinfo@ec.gc.ca"
90+
}
91+
],
92+
"addresses": [
93+
{
94+
"deliveryPoint": [
95+
"77 Westmorland Street, suite 260"
96+
],
97+
"city": "Fredericton",
98+
"administrativeArea": "NB",
99+
"postalCode": "E3B 6Z4",
100+
"country": "Canada"
101+
}
102+
],
103+
"contactInstructions": "email",
104+
"links": [
105+
{
106+
"rel": "canonical",
107+
"type": "text/html",
108+
"href": "https://www.canada.ca/en/environment-climate-change.html"
109+
}
110+
],
111+
"roles": [
112+
"host",
113+
"producer"
114+
]
115+
}
116+
],
117+
"wmo:dataPolicy": "core",
118+
"language": {
119+
"code": "en"
120+
},
121+
"type": "dataset",
122+
"created": "2018-01-01T11:11:23Z",
123+
"updated": "2022-06-17T08:22:24Z"
124+
},
125+
"links": [
126+
{
127+
"rel": "data",
128+
"href": "https://dd.weather.gc.ca/model_gem_global",
129+
"type": "text/html",
130+
"title": "MSC Datamart",
131+
"distribution": {
132+
"availableFormats": [
133+
{
134+
"name": "GRIB2"
135+
}
136+
]
137+
}
138+
},
139+
{
140+
"rel": "license",
141+
"href": "https://open.canada.ca/en/open-government-licence-canada",
142+
"type": "text/html",
143+
"title": "Open Government Licence - Canada"
144+
},
145+
{
146+
"rel": "service",
147+
"href": "https://geo.weather.gc.ca/geomet?lang=en&service=WMS&request=GetCapabilities&layers=GDPS.ETA_TT",
148+
"type": "application/xml",
149+
"title": "Air temperature [degrees]"
150+
},
151+
{
152+
"rel": "items",
153+
"channel": "origin/a/wis2/ca-eccc-msc/data/core/weather/prediction/forecast/medium-range/deterministic/global",
154+
"href": "mqtts://everyone:everyone@globalbroker.meteo.fr:8883",
155+
"type": "application/geo+json",
156+
"title": "Data notifications"
157+
}
158+
]
159+
}

pywcmp/wcmp2/ets.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@
3636
from jsonschema.validators import Draft202012Validator
3737
from shapely.geometry import shape
3838

39+
from pywis_topics.topics import TopicHierarchy
40+
3941
import pywcmp
42+
from pywcmp.errors import TestSuiteError
4043
from pywcmp.bundle import WCMP2_FILES
41-
from pywis_topics.topics import TopicHierarchy
4244
from pywcmp.util import (get_current_datetime_rfc3339, get_userdir,
4345
is_valid_created_datetime)
4446

@@ -71,6 +73,7 @@ def __init__(self, data: dict):
7173

7274
self.test_id = None
7375
self.record = data
76+
self.errors = []
7477

7578
self.th = TopicHierarchy(tables=get_userdir())
7679

@@ -79,6 +82,7 @@ def run_tests(self, fail_on_schema_validation=False):
7982

8083
results = []
8184
tests = []
85+
8286
ets_report = {
8387
'id': str(uuid.uuid4()),
8488
'report_type': 'ets',
@@ -103,7 +107,10 @@ def run_tests(self, fail_on_schema_validation=False):
103107
raise ValueError(msg)
104108

105109
for t in tests:
106-
results.append(getattr(self, t)())
110+
result = getattr(self, t)()
111+
results.append(result)
112+
if result['code'] == 'FAILED':
113+
self.errors.append(result)
107114

108115
for code in ['PASSED', 'FAILED', 'SKIPPED']:
109116
r = len([t for t in results if t['code'] == code])
@@ -115,6 +122,16 @@ def run_tests(self, fail_on_schema_validation=False):
115122

116123
return ets_report
117124

125+
def raise_for_status(self):
126+
"""
127+
Raise error if one or more failures were found during validation.
128+
129+
:returns: `pywcmp.errors.TestSuiteError` or `None`
130+
"""
131+
132+
if len(self.errors) > 0:
133+
raise TestSuiteError('Invalid WCMP2 record', self.errors)
134+
118135
def test_requirement_validation(self):
119136
"""
120137
Validate that a WCMP record is valid to the authoritative WCMP schema.
@@ -450,7 +467,7 @@ def test_requirement_data_policy(self):
450467
if link['rel'] == 'license']
451468
if not conditions_links:
452469
status['code'] = 'FAILED'
453-
status['message'] = 'missing recommended conditions'
470+
status['message'] = 'recommended data requires a link with rel=license' # noqa
454471
return status
455472

456473
return status

tests/run_tests.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import os
3131
import unittest
3232

33+
from pywcmp.errors import TestSuiteError
3334
from pywcmp.ets import WMOCoreMetadataProfileTestSuite2
3435
from pywcmp.wcmp2.kpi import (
3536
calculate_grade, WMOCoreMetadataProfileKeyPerformanceIndicators)
@@ -120,6 +121,23 @@ def test_fail(self):
120121
with self.assertRaises(ValueError):
121122
ts.run_tests(fail_on_schema_validation=True)
122123

124+
def test_raise_for_status(self):
125+
"""Simple test for raise_for_status"""
126+
127+
with open(get_test_file_path('data/wcmp2-passing.json')) as fh:
128+
ts = WMOCoreMetadataProfileTestSuite2(json.load(fh))
129+
_ = ts.run_tests(fail_on_schema_validation=True)
130+
131+
assert ts.raise_for_status() is None
132+
133+
with open(get_test_file_path('data/wcmp2-failing-invalid-time-resolution.json')) as fh: # noqa
134+
record = json.load(fh)
135+
ts = WMOCoreMetadataProfileTestSuite2(record)
136+
_ = ts.run_tests(fail_on_schema_validation=True)
137+
138+
with self.assertRaises(TestSuiteError):
139+
ts.raise_for_status()
140+
123141
def test_fail_invalid_time_resolution(self):
124142
"""Simple test for a failing record with an invalid time resolution"""
125143

0 commit comments

Comments
 (0)