pywmdr 0.2.dev3__tar.gz → 0.2.dev5__tar.gz

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.
Files changed (27) hide show
  1. {pywmdr-0.2.dev3/pywmdr.egg-info → pywmdr-0.2.dev5}/PKG-INFO +11 -1
  2. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/README.md +9 -0
  3. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pyproject.toml +2 -1
  4. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/__init__.py +2 -0
  5. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/cli_options.py +7 -0
  6. pywmdr-0.2.dev5/pywmdr/migrations.py +417 -0
  7. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/pygeoapi_plugin.py +104 -0
  8. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/wmdr2/ets.py +1 -1
  9. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5/pywmdr.egg-info}/PKG-INFO +11 -1
  10. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr.egg-info/SOURCES.txt +2 -0
  11. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr.egg-info/requires.txt +1 -0
  12. pywmdr-0.2.dev5/tests/test_migrations.py +57 -0
  13. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/LICENSE.md +0 -0
  14. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/bundle.py +0 -0
  15. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/codelists.py +0 -0
  16. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/errors.py +0 -0
  17. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/ets.py +0 -0
  18. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/resources/0-20008-0-THE.json +0 -0
  19. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/resources/ets-report.json +0 -0
  20. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/util.py +0 -0
  21. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr/wmdr2/__init__.py +0 -0
  22. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr.egg-info/dependency_links.txt +0 -0
  23. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr.egg-info/entry_points.txt +0 -0
  24. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/pywmdr.egg-info/top_level.txt +0 -0
  25. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/setup.cfg +0 -0
  26. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/tests/test_codelists.py +0 -0
  27. {pywmdr-0.2.dev3 → pywmdr-0.2.dev5}/tests/test_wmdr_ets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywmdr
3
- Version: 0.2.dev3
3
+ Version: 0.2.dev5
4
4
  Summary: A Python implementation of the test suite for WIGOS Metadata Record
5
5
  Author-email: Tom Kralidis <tomkralidis@gmail.com>
6
6
  Maintainer-email: Tom Kralidis <tomkralidis@gmail.com>
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
23
23
  License-File: LICENSE.md
24
24
  Requires-Dist: click
25
25
  Requires-Dist: jsonschema
26
+ Requires-Dist: lxml
26
27
  Requires-Dist: rfc3339-validator
27
28
  Requires-Dist: rfc3987
28
29
  Requires-Dist: shapely
@@ -95,6 +96,15 @@ pywmdr ets validate https://example.org/path/to/file.json --verbosity DEBUG
95
96
 
96
97
  # write results to logfile
97
98
  pywmdr ets validate https://example.org/path/to/file.json --verbosity DEBUG --logfile /tmp/foo.txt
99
+
100
+ # migrate WMDR1 to WMDR2 (file on disk)
101
+ pywmdr migrate /path/to/file.xml --verbosity DEBUG
102
+
103
+ # migrate WMDR1 to WMDR2 (URL)
104
+ pywmdr migrate https://example.org/path/to/file.xml --verbosity DEBUG
105
+
106
+ # migrate WMDR1 to WMDR2 (URL), write results to file
107
+ pywmdr migrate https://example.org/path/to/file.xml --output file.json
98
108
  ```
99
109
 
100
110
  ## Using the API
@@ -57,6 +57,15 @@ pywmdr ets validate https://example.org/path/to/file.json --verbosity DEBUG
57
57
 
58
58
  # write results to logfile
59
59
  pywmdr ets validate https://example.org/path/to/file.json --verbosity DEBUG --logfile /tmp/foo.txt
60
+
61
+ # migrate WMDR1 to WMDR2 (file on disk)
62
+ pywmdr migrate /path/to/file.xml --verbosity DEBUG
63
+
64
+ # migrate WMDR1 to WMDR2 (URL)
65
+ pywmdr migrate https://example.org/path/to/file.xml --verbosity DEBUG
66
+
67
+ # migrate WMDR1 to WMDR2 (URL), write results to file
68
+ pywmdr migrate https://example.org/path/to/file.xml --output file.json
60
69
  ```
61
70
 
62
71
  ## Using the API
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pywmdr"
7
- version = "0.2.dev3"
7
+ version = "0.2.dev5"
8
8
  description = "A Python implementation of the test suite for WIGOS Metadata Record"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -30,6 +30,7 @@ classifiers = [
30
30
  dependencies = [
31
31
  "click",
32
32
  "jsonschema",
33
+ "lxml",
33
34
  "rfc3339-validator",
34
35
  "rfc3987",
35
36
  "shapely"
@@ -28,6 +28,7 @@ import click
28
28
  from pywmdr.ets import ets
29
29
  from pywmdr.bundle import bundle
30
30
  from pywmdr.util import get_package_version
31
+ from pywmdr.migrations import migrate
31
32
 
32
33
  __version__ = get_package_version()
33
34
 
@@ -40,3 +41,4 @@ def cli():
40
41
 
41
42
  cli.add_command(bundle)
42
43
  cli.add_command(ets)
44
+ cli.add_command(migrate)
@@ -25,6 +25,13 @@ import sys
25
25
  import click
26
26
 
27
27
 
28
+ OPTION_OUTPUT = click.option(
29
+ '--output',
30
+ '-o',
31
+ type=click.File('w', encoding='utf-8'),
32
+ help='Name of output file')
33
+
34
+
28
35
  def OPTION_VERBOSITY(f):
29
36
  logging_options = ['ERROR', 'WARNING', 'INFO', 'DEBUG']
30
37
 
@@ -0,0 +1,417 @@
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 datetime
23
+ import json
24
+ import logging
25
+ import re
26
+
27
+ from lxml import etree
28
+
29
+ import click
30
+
31
+ from pywmdr import cli_options
32
+ from pywmdr.util import urlopen_
33
+
34
+ LOGGER = logging.getLogger(__name__)
35
+
36
+ NAMESPACES = {
37
+ 'gmd': 'http://www.isotc211.org/2005/gmd',
38
+ 'gco': 'http://www.isotc211.org/2005/gco',
39
+ 'gml': 'http://www.opengis.net/gml/3.2',
40
+ 'om': 'http://www.opengis.net/om/2.0',
41
+ 'wmdr': 'http://def.wmo.int/wmdr/2017',
42
+ 'xlink': 'http://www.w3.org/1999/xlink'
43
+ }
44
+
45
+
46
+ def xpath_(node: etree.Element, xpath: str) -> str | None:
47
+ """
48
+ Helper function to derive a value from an XPath
49
+
50
+ :param node: `etree.Element`
51
+ :param xpath: `str` of XPath expression
52
+
53
+ :returns: `str` of matching XPath value or `None`
54
+ """
55
+
56
+ try:
57
+ val = node.xpath(xpath, namespaces=NAMESPACES)[0]
58
+ if hasattr(val, 'text'): # node
59
+ return val.text
60
+ else: # attribute
61
+ return val
62
+ except IndexError:
63
+ return None
64
+
65
+
66
+ class Migration:
67
+ def __init__(self, record: str):
68
+ """
69
+ Initializer
70
+
71
+ :param record: `str` of WMDR1 XML record
72
+
73
+ :returns: `pywmdr.migrations.Migration`
74
+ """
75
+
76
+ self.record = record
77
+ self.warnings = []
78
+
79
+ def migrate(self) -> dict:
80
+ """
81
+ Migrate a WMDR1 XML to WMDR2
82
+
83
+ :returns: `dict` of WMDR2
84
+ """
85
+
86
+ created = datetime.datetime.now(datetime.UTC).strftime(
87
+ '%Y-%m-%dT%H:%M:%SZ').replace('+00:00', 'Z')
88
+
89
+ wmdr2 = {
90
+ 'id': None,
91
+ 'type': 'Feature',
92
+ 'conformsTo': ['http://wigos.wmo.int/spec/wmdr/2/conf/core'],
93
+ 'time': None,
94
+ 'geometry': None,
95
+ 'properties': {
96
+ 'type': 'facility',
97
+ 'created': created,
98
+ 'contacts': []
99
+ },
100
+ 'links': []
101
+ }
102
+
103
+ if isinstance(self.record, str):
104
+ record_ = self.record.encode('utf-8')
105
+ elif isinstance(self.record, bytes):
106
+ record_ = self.record
107
+
108
+ tree = etree.fromstring(record_)
109
+
110
+ facility = tree.xpath('//wmdr:facility', namespaces=NAMESPACES)[0]
111
+
112
+ val = xpath_(facility, './/wmdr:ObservingFacility/gml:identifier')
113
+ wmdr2['id'] = val
114
+
115
+ val = xpath_(facility, './/wmdr:geospatialLocation//gml:Point/gml:pos')
116
+
117
+ if val is not None:
118
+ coordinates = [float(x) for x in val.split()]
119
+ wmdr2['geometry'] = {
120
+ 'type': 'Point',
121
+ 'coordinates': [coordinates[1], coordinates[0]]
122
+ }
123
+ if len(coordinates) == 3:
124
+ wmdr2['geometry']['coordinates'].append(coordinates[2])
125
+
126
+ val = xpath_(facility, './/wmdr:ObservingFacility/gml:name')
127
+ wmdr2['properties']['title'] = val
128
+
129
+ val = xpath_(facility, './/wmdr:ObservingFacility/wmdr:description//wmdr:description') # noqa
130
+ if val is not None:
131
+ wmdr2['properties']['description'] = val
132
+
133
+ val = xpath_(facility, './/wmdr:facilityType/@xlink:href')
134
+ wmdr2['properties']['facilityType'] = {
135
+ 'id': val.split('/')[-1],
136
+ 'url': val
137
+ }
138
+
139
+ val = xpath_(facility, './/wmdr:wmoRegion/@xlink:href')
140
+ if val is not None:
141
+ wmdr2['properties']['wmoRegion'] = {
142
+ 'id': val.split('/')[-1],
143
+ 'url': val
144
+ }
145
+ else:
146
+ wmdr2['properties']['wmoRegion'] = {
147
+ 'id': 'unknown',
148
+ 'url': 'http://codes.wmo.int/wmdr/WMORegion/unknown'
149
+ }
150
+
151
+ val = xpath_(facility, './/wmdr:dateEstablished')
152
+ wmdr2['time'] = {
153
+ 'interval': [val]
154
+ }
155
+
156
+ val = xpath_(facility, './/wmdr:dateClosed')
157
+ if val is not None:
158
+ wmdr2['time']['interval'].append(val)
159
+ else:
160
+ wmdr2['time']['interval'].append('..')
161
+
162
+ for ci_rp in tree.xpath('//gmd:CI_ResponsibleParty', namespaces=NAMESPACES): # noqa
163
+ contact2 = self.ci_responsibleparty_to_contact(ci_rp)
164
+ if contact2 is not None:
165
+ wmdr2['properties']['contacts'].append(contact2)
166
+
167
+ if not wmdr2['properties']['contacts']:
168
+ wmdr2['properties'].pop('contacts')
169
+ if contact2 is not None:
170
+ wmdr2['properties']['contacts'].append(contact2)
171
+
172
+ for link in facility.xpath('.//wmdr:onlineResource//gmd:URL', namespaces=NAMESPACES): # noqa
173
+ wmdr2['links'].append({
174
+ 'rel': 'related',
175
+ 'href': link.text
176
+ })
177
+
178
+ territories = tree.xpath('//wmdr:territory', namespaces=NAMESPACES)
179
+ if territories:
180
+ wmdr2['properties']['territories'] = []
181
+ for territory in territories:
182
+ territory_ = {}
183
+ territory_name = xpath_(
184
+ territory, './/wmdr:territoryName/@xlink:href')
185
+
186
+ if territory_name is not None:
187
+ territory_['territory'] = {
188
+ 'id': territory_name.split('/')[-1],
189
+ 'url': territory_name
190
+ }
191
+ begin = xpath_(
192
+ territory, './/wmdr:validPeriod//gml:beginPosition')
193
+
194
+ territory_['dates'] = [begin]
195
+
196
+ end = xpath_(territory, './/wmdr:validPeriod//gml:endPosition') # noqa
197
+
198
+ if end is None:
199
+ territory_['dates'].append('..')
200
+ else:
201
+ territory_['dates'].append(end)
202
+ wmdr2['properties']['territories'].append(territory_)
203
+
204
+ observations = tree.xpath('//wmdr:observation/wmdr:ObservingCapability', namespaces=NAMESPACES) # noqa
205
+
206
+ if observations:
207
+ wmdr2['properties']['observations'] = []
208
+
209
+ for observation in observations:
210
+ observation_ = {
211
+ 'programAffiliations': [],
212
+ 'configurations': []
213
+ }
214
+ observed_geometry = xpath_(
215
+ observation, './/om:type/@xlink:href')
216
+
217
+ if observed_geometry is not None:
218
+ observation_['observedGeometry'] = {
219
+ 'id': observed_geometry.split('/')[-1],
220
+ 'url': observed_geometry
221
+ }
222
+
223
+ observed_property = xpath_(
224
+ observation, './/om:observedProperty/@xlink:href')
225
+
226
+ observation_['observedProperty'] = {
227
+ 'id': observed_property.split('/')[-1],
228
+ 'url': observed_property
229
+ }
230
+
231
+ program_affiliations = observation.xpath(
232
+ './wmdr:programAffiliation/@xlink:href',
233
+ namespaces=NAMESPACES)
234
+ if program_affiliations:
235
+ for program_affiliation in program_affiliations:
236
+ observation_['programAffiliations'].append({
237
+ 'programAffiliation': {
238
+ 'id': program_affiliation.split('/')[-1],
239
+ 'url': program_affiliation
240
+ }})
241
+
242
+ deployments = observation.xpath(
243
+ './/wmdr:Deployment', namespaces=NAMESPACES)
244
+
245
+ for deployment in deployments:
246
+ configuration_ = {
247
+ 'id': None,
248
+ 'time': {
249
+ 'interval': []
250
+ }
251
+ }
252
+ configuration_['id'] = xpath_(deployment, './/@gml:id')
253
+
254
+ begin = xpath_(
255
+ deployment, './/wmdr:validPeriod//gml:beginPosition')
256
+
257
+ configuration_['time']['interval'].append(begin)
258
+
259
+ end = xpath_(
260
+ deployment, './/wmdr:validPeriod//gml:endPosition')
261
+
262
+ if end is None:
263
+ configuration_['time']['interval'].append('..')
264
+ else:
265
+ configuration_['time']['interval'].append(end)
266
+
267
+ observing_method = xpath_(
268
+ deployment, './/wmdr:observingMethod/@xlink:href')
269
+ if observing_method is not None:
270
+ configuration_['observingMethod'] = {
271
+ 'id': observing_method.split('/')[-1],
272
+ 'url': observing_method
273
+ }
274
+
275
+ observation_['configurations'].append(configuration_)
276
+
277
+ if not observation_['configurations']:
278
+ msg = f"No configurations found for observation ({observation_['observedProperty']['url']})" # noqa
279
+ self.warnings.append(msg)
280
+ observation_.pop('configurations')
281
+
282
+ wmdr2['properties']['observations'].append(observation_)
283
+
284
+ wmdr2['links'].append({
285
+ 'rel': 'original',
286
+ 'type': 'application/xml',
287
+ 'title': f"{wmdr2['properties']['title']} as WMDR1",
288
+ 'href': f"https://oscar.wmo.int/oai/provider?verb=GetRecord&metadataPrefix=wmdr&identifier={wmdr2['id']}" # noqa
289
+ })
290
+
291
+ return wmdr2
292
+
293
+
294
+ def ci_responsibleparty_to_contact(self, node: etree.Element) -> tuple[dict, list[str]]: # noqa
295
+ """
296
+ Helper function to transform ISO gmd:CI_ResponsibleParty to a
297
+ WMDR2 contact
298
+
299
+ :param node: `etree.Element` of gmd:CI_ResponsibleParty
300
+
301
+ :returns: `dict` of WMDR2 contact object, or `None`
302
+ """
303
+
304
+ val = None
305
+ contact = {}
306
+
307
+ val = xpath_(node, './/gmd:role/gmd:CI_RoleCode/@codeListValue')
308
+ if val is None:
309
+ LOGGER.debug('No role found; skipping')
310
+ return
311
+ else:
312
+ contact['roles'] = [val]
313
+
314
+ val = xpath_(node, './/gmd:individualName/gco:CharacterString')
315
+ if val is not None:
316
+ contact['name'] = val
317
+
318
+ val = xpath_(node, './/gmd:organisationName/gco:CharacterString')
319
+ if val is not None:
320
+ contact['organization'] = val
321
+
322
+ phones = node.xpath('.//gmd:voice/gco:CharacterString',
323
+ namespaces=NAMESPACES)
324
+ if phones:
325
+ contact['phones'] = []
326
+ for phone in phones:
327
+ contact['phones'].append({'value': phone.text})
328
+
329
+ emails = node.xpath('.//gmd:electronicMailAddress/gco:CharacterString',
330
+ namespaces=NAMESPACES)
331
+ if emails:
332
+ contact['emails'] = []
333
+ for email in emails:
334
+ # validate we have email address
335
+ # (something at something dot something, no white space)
336
+ email_valid = re.fullmatch(r'[^@\s]+@[^@\s]+\.[^@\s]+', email.text) is not None # noqa
337
+ email_address = email.text.strip()
338
+ if not email_valid:
339
+ msg = f'Invalid email: {email_address}'
340
+ self.warnings.append(msg)
341
+ contact['emails'].append({'value': email_address})
342
+
343
+ val = xpath_(node, './/gmd:contactInstructions/gco:CharacterString')
344
+ if val is not None:
345
+ contact['contactInstructions'] = val
346
+
347
+ try:
348
+ val = node.xpath('.//gmd:address', namespaces=NAMESPACES)[0]
349
+ except IndexError:
350
+ val = None
351
+
352
+ if val is not None:
353
+ address = {}
354
+
355
+ val2 = xpath_(val, './/gmd:deliveryPoint/gco:CharacterString')
356
+ if val2 is not None:
357
+ address['deliveryPoint'] = [val2]
358
+
359
+ val2 = xpath_(val, './/gmd:city/gco:CharacterString')
360
+ if val2 is not None:
361
+ address['city'] = val2
362
+
363
+ val2 = xpath_(val, './/gmd:postalCode/gco:CharacterString')
364
+ if val2 is not None:
365
+ address['postalCode'] = val2
366
+
367
+ val2 = xpath_(val, './/gmd:country/gco:CharacterString')
368
+ if val2 is not None:
369
+ address['country'] = val2
370
+
371
+ val2 = xpath_(val, './/gmd:onlineResource//gmd:URL')
372
+ if val2 is not None:
373
+ address['links'] = [{
374
+ 'href': val2
375
+ }]
376
+
377
+ if address:
378
+ contact['addresses'] = [address]
379
+
380
+ return contact
381
+
382
+
383
+ @click.command()
384
+ @click.pass_context
385
+ @click.argument('file_or_url')
386
+ @cli_options.OPTION_OUTPUT
387
+ @cli_options.OPTION_VERBOSITY
388
+ def migrate(ctx, file_or_url, output, verbosity):
389
+ """migrate a WMDR1 record to WMDR2 core"""
390
+
391
+ click.echo(f'Opening {file_or_url}')
392
+
393
+ if file_or_url.startswith('http'):
394
+ content = urlopen_(file_or_url).read()
395
+ else:
396
+ with open(file_or_url) as fh:
397
+ content = fh.read()
398
+
399
+ click.echo(f'Migrating {file_or_url} from WMDR1 to WMDR2')
400
+
401
+ try:
402
+ m = Migration(content)
403
+ content = m.migrate()
404
+ if output is not None:
405
+ json.dump(content, output, indent=4)
406
+ else:
407
+ click.echo(json.dumps(content, indent=4))
408
+
409
+ if m.warnings:
410
+ click.echo('\nWARNINGS:')
411
+ for w in m.warnings:
412
+ click.echo(f'- {w}')
413
+ except Exception as err:
414
+ import traceback
415
+ print(traceback.format_exc())
416
+ raise click.ClickException(err)
417
+ ctx.exit(1)
@@ -37,11 +37,18 @@
37
37
  # processor:
38
38
  # name: pywmdr.pygeoapi_plugin.WMDR2ETSProcessor
39
39
  #
40
+ # pywmdr-wmdr2-migrate:
41
+ # type: process
42
+ # processor:
43
+ # name: pywmdr.pygeoapi_plugin.WMDR1toWMDR2Processor
44
+ #
45
+ #
40
46
  # 3. (re)start pygeoapi
41
47
  #
42
48
  # The resulting processes will be available at the following endpoints:
43
49
  #
44
50
  # /processes/pywmdr-record-validate
51
+ # /processes/pywmdr-wmdr2-migrate
45
52
  #
46
53
  # Note that pygeoapi's OpenAPI/Swagger interface (at /openapi) will also
47
54
  # provide a developer-friendly interface to test and run requests
@@ -52,6 +59,8 @@ import logging
52
59
 
53
60
  from pygeoapi.process.base import BaseProcessor, ProcessorExecuteError
54
61
 
62
+ from pywmdr.bundle import WMDR2_FILES
63
+ from pywmdr.migrations import Migration
55
64
  from pywmdr.wmdr2.ets import WMDR2TestSuite
56
65
  from pywmdr.util import get_package_version, THISDIR, urlopen_
57
66
 
@@ -63,6 +72,11 @@ with (THISDIR / 'resources' / 'ets-report.json').open() as fh:
63
72
  with (THISDIR / 'resources' / '0-20008-0-THE.json').open() as fh:
64
73
  EXAMPLE_WMDR2 = json.load(fh)
65
74
 
75
+ with (THISDIR / 'resources' / '0-20000-0-87582.xml').open() as fh:
76
+ EXAMPLE_WMDR1 = fh.read()
77
+
78
+ with (WMDR2_FILES / 'wmdr2-bundled.json').open() as fh:
79
+ WMDR2_SCHEMA = json.load(fh)
66
80
 
67
81
  PROCESS_WMDR2_ETS = {
68
82
  'version': get_package_version(),
@@ -81,6 +95,7 @@ PROCESS_WMDR2_ETS = {
81
95
  'href': 'https://github.com/wmo-im/wmdr2',
82
96
  'hreflang': 'en-US'
83
97
  }],
98
+ 'jobControlOptions': ['sync-execute', 'async-execute'],
84
99
  'inputs': {
85
100
  'record': {
86
101
  'title': 'WMDR2 record',
@@ -112,6 +127,55 @@ PROCESS_WMDR2_ETS = {
112
127
  }
113
128
 
114
129
 
130
+ PROCESS_MIGRATE = {
131
+ 'version': get_package_version(),
132
+ 'id': 'pywmdr-wmdr2-migrate',
133
+ 'title': {
134
+ 'en': 'WMDR1 to WMDR2 migration utility'
135
+ },
136
+ 'description': {
137
+ 'en': 'Migrate a WMDR1 XML into WMDR2'
138
+ },
139
+ 'keywords': ['wigos', 'wmdr2', 'wmdr1', 'migrate', 'metadata'],
140
+ 'links': [{
141
+ 'type': 'text/html',
142
+ 'rel': 'about',
143
+ 'title': 'information',
144
+ 'href': 'https://github.com/wmo-im/wmdr2',
145
+ 'hreflang': 'en-US'
146
+ }],
147
+ 'jobControlOptions': ['sync-execute', 'async-execute'],
148
+ 'inputs': {
149
+ 'record': {
150
+ 'title': 'WMD1 record',
151
+ 'description': 'WMDR1 record (can be inline or remote link)',
152
+ 'schema': {
153
+ 'type': ['object', 'string']
154
+ },
155
+ 'minOccurs': 1,
156
+ 'maxOccurs': 1,
157
+ 'metadata': None,
158
+ 'keywords': ['wmdr1']
159
+ }
160
+ },
161
+ 'outputs': {
162
+ 'result': {
163
+ 'title': 'WMDR1 to WMDR2 migration result',
164
+ 'description': 'WMDR1 to WMDR2 migration result',
165
+ 'schema': {
166
+ 'contentMediaType': 'application/geo+json',
167
+ **WMDR2_SCHEMA
168
+ }
169
+ }
170
+ },
171
+ 'example': {
172
+ 'inputs': {
173
+ 'record': EXAMPLE_WMDR2
174
+ }
175
+ }
176
+ }
177
+
178
+
115
179
  class WMDR2ETSProcessor(BaseProcessor):
116
180
  """WMDR2 ETS"""
117
181
 
@@ -150,3 +214,43 @@ class WMDR2ETSProcessor(BaseProcessor):
150
214
 
151
215
  def __repr__(self):
152
216
  return '<WMDR2ETSProcessor>'
217
+
218
+
219
+ class WMDR2MigrateProcessor(BaseProcessor):
220
+ """Migrate processor"""
221
+
222
+ def __init__(self, processor_def):
223
+ """
224
+ Initialize object
225
+
226
+ :param processor_def: provider definition
227
+
228
+ :returns: pywmdr.pygeoapi_plugin.WMDR2MigrateProcessor
229
+ """
230
+
231
+ super().__init__(processor_def, PROCESS_MIGRATE)
232
+
233
+ def execute(self, data, outputs=None):
234
+
235
+ response = None
236
+ mimetype = 'application/json'
237
+ record = data.get('record')
238
+
239
+ if record is None:
240
+ msg = 'Missing record'
241
+ LOGGER.error(msg)
242
+ raise ProcessorExecuteError(msg)
243
+
244
+ if isinstance(record, str) and record.startswith('http'):
245
+ LOGGER.debug('Record is a link')
246
+ record = json.loads(urlopen_(record).read())
247
+ else:
248
+ LOGGER.debug('Record is inline')
249
+
250
+ LOGGER.debug('Running ETS against record')
251
+ response = Migration(record).migrate()
252
+
253
+ return mimetype, response
254
+
255
+ def __repr__(self):
256
+ return '<WMDR2MigrateProcessor>'
@@ -154,7 +154,7 @@ class WMDR2TestSuite:
154
154
  validation_errors = []
155
155
 
156
156
  status = {
157
- 'id': gen_test_id('codelisted_values'),
157
+ 'id': gen_test_id('codelisted-values'),
158
158
  'code': 'PASSED'
159
159
  }
160
160
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywmdr
3
- Version: 0.2.dev3
3
+ Version: 0.2.dev5
4
4
  Summary: A Python implementation of the test suite for WIGOS Metadata Record
5
5
  Author-email: Tom Kralidis <tomkralidis@gmail.com>
6
6
  Maintainer-email: Tom Kralidis <tomkralidis@gmail.com>
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
23
23
  License-File: LICENSE.md
24
24
  Requires-Dist: click
25
25
  Requires-Dist: jsonschema
26
+ Requires-Dist: lxml
26
27
  Requires-Dist: rfc3339-validator
27
28
  Requires-Dist: rfc3987
28
29
  Requires-Dist: shapely
@@ -95,6 +96,15 @@ pywmdr ets validate https://example.org/path/to/file.json --verbosity DEBUG
95
96
 
96
97
  # write results to logfile
97
98
  pywmdr ets validate https://example.org/path/to/file.json --verbosity DEBUG --logfile /tmp/foo.txt
99
+
100
+ # migrate WMDR1 to WMDR2 (file on disk)
101
+ pywmdr migrate /path/to/file.xml --verbosity DEBUG
102
+
103
+ # migrate WMDR1 to WMDR2 (URL)
104
+ pywmdr migrate https://example.org/path/to/file.xml --verbosity DEBUG
105
+
106
+ # migrate WMDR1 to WMDR2 (URL), write results to file
107
+ pywmdr migrate https://example.org/path/to/file.xml --output file.json
98
108
  ```
99
109
 
100
110
  ## Using the API
@@ -7,6 +7,7 @@ pywmdr/cli_options.py
7
7
  pywmdr/codelists.py
8
8
  pywmdr/errors.py
9
9
  pywmdr/ets.py
10
+ pywmdr/migrations.py
10
11
  pywmdr/pygeoapi_plugin.py
11
12
  pywmdr/util.py
12
13
  pywmdr.egg-info/PKG-INFO
@@ -20,4 +21,5 @@ pywmdr/resources/ets-report.json
20
21
  pywmdr/wmdr2/__init__.py
21
22
  pywmdr/wmdr2/ets.py
22
23
  tests/test_codelists.py
24
+ tests/test_migrations.py
23
25
  tests/test_wmdr_ets.py
@@ -1,5 +1,6 @@
1
1
  click
2
2
  jsonschema
3
+ lxml
3
4
  rfc3339-validator
4
5
  rfc3987
5
6
  shapely
@@ -0,0 +1,57 @@
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 lxml import etree
23
+ import pytest
24
+
25
+ from pywmdr.migrations import Migration, xpath_
26
+ from .util import get_test_file_path
27
+
28
+
29
+ @pytest.mark.parametrize("xml, xpath, expected", [
30
+ ('<root><foo>test</foo></root>', './/foo', 'test'),
31
+ ('<root><foo/></root>', './/foo', None),
32
+ ('<root><foo bar="attr">test</foo></root>', './/foo/@bar', 'attr'),
33
+ ])
34
+ def test_xpath_(xml, xpath, expected):
35
+ """Simple tests for testing XPath helper function"""
36
+
37
+ node = etree.fromstring(xml)
38
+
39
+ assert xpath_(node, xpath) == expected
40
+
41
+
42
+ @pytest.mark.parametrize("filename, wsi, facility_type, nobs", [
43
+ ('0-12-0-10BRACN60417.xml', '0-12-0-10BRACN60417', 'landFixed', 23)
44
+ ])
45
+ def test_wmdr1_to_wmdr2(filename, wsi, facility_type, nobs):
46
+ """Simple tests for WMDR1 XML to WMDR2 migration"""
47
+
48
+ with get_test_file_path(filename).open() as fh:
49
+ data = fh.read()
50
+
51
+ wmdr2 = Migration(data).migrate()
52
+
53
+ assert wmdr2['id'] == wsi
54
+ assert wmdr2['type'] == 'Feature'
55
+ assert wmdr2['properties']['type'] == 'facility'
56
+ assert wmdr2['properties']['facilityType']['id'] == facility_type
57
+ assert len(wmdr2['properties']['observations']) == nobs
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes