Skip to content

Commit

Permalink
Removed mention of a confusing option to the encode() method to compr…
Browse files Browse the repository at this point in the history
…ess the

resulting XML document. Compression is invoked with the bulletin.write() method
only. Overhauled bulletin.write() method, removing filename and file handle
options as unworkable and likely not used. Revised test procedures for
bulletin object to use TCA and SWA product as examples.
  • Loading branch information
mgoberfield committed Jan 18, 2022
1 parent 4a1c811 commit 211970e
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 198 deletions.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ IWXXM became a WMO standard on 5 November 2020. Met Offices shall disseminate ME

As XML, and creating XML documents, may be unfamiliar technology to those without an IT background, MDL is providing software to assist those in creating the new XML documents based on IWXXM schemas.

It should be understood that the software provided here is a short-term solution as TAC forms of these products will cease to be a standard and no longer disseminated by 2029.
It should be understood that the software provided here is a short-term solution as TAC forms of these products will cease to be a ICAO/WMO standard by 2029.

## Prequisites
This software is written entirely in the Python language. Python interpreter v3.7 or better is required.
Expand Down Expand Up @@ -110,13 +110,10 @@ Every encoder, after processing a TAC message successfully, returns an object of

For international distribution, IWXXM reports, due to their increased character length and expanded character set, shall be sent over the Extended ATS Message Handling System (AMHS) as a File Transfer Body Part.<sup>2</sup> The Bulletin class provides a convenient [write()](https://github.com/NOAA-MDL/GIFTs/blob/master/gifts/common/bulletin.py#L177) method to generate the `<MeterologicalBulletin>`<sup>3</sup> XML document for transmission over the AMHS.

Because of the character length of the `<MeteorologicalBulletin>`, the File Transfer Body Part shall be a compressed file using the gzip protocol. By default, the `.encode()` method of the [Encoder](https://github.com/NOAA-MDL/GIFTs/blob/master/gifts/common/Encoder.py#L15) class is to generate an uncompressed file when the bulletin.write() method is invoked. To generate a compressed `<MeteorologicalBulletin>` file for transmission over the AMHS, supply an additional argument to the `.encode()` method, like so:

Encoder.encode(tacString, xml='xml.gz')
An alternative method is to set the `compress` flag to True in the Bulletin object's write() method, like so:
Because of the character length of the `<MeteorologicalBulletin>`, the File Transfer Body Part shall be a compressed file using the gzip protocol. By default, the `.encode()` method of the [Encoder](https://github.com/NOAA-MDL/GIFTs/blob/master/gifts/common/Encoder.py#L15) class is to generate an uncompressed file when the bulletin.write() method is invoked. To generate a compressed `<MeteorologicalBulletin>` file for transmission over the AMHS is to set the `compress` flag to True in the Bulletin object's write() method, like so:

bulletin.write(compress=True)
Either method will generate a gzip file containing the `<MeteorologicalBulletin>` suitable for transmission over the AMHS.
This will generate a gzip file containing the `<MeteorologicalBulletin>` suitable for transmission over the AMHS.

## Caveats
The decoders were written to follow Annex 3 specifications for the TAC forms. If your observations or forecast products deviate significantly from Annex 3, then this software will likely refuse to encode the data into IWXXM. Fortunately, solutions can be readily found, ranging from trivial to challenging (see United States METAR/SPECI [reports](https://nws.weather.gov/schemas/iwxxm-us/3.0/examples/metars)).
Expand Down
92 changes: 48 additions & 44 deletions gifts/common/bulletin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
pass

import os
import re
import sys
import time
import uuid
Expand All @@ -25,6 +26,8 @@ def __init__(self):
else:
self.encoding = 'UTF-8'

self.xmlFileNamePartA = re.compile(r'A_L[A-Z]{3}\d\d[A-Z]{4}\d{6}([ACR]{2}[A-Z])?_C_[A-Z]{4}')

def __len__(self):

return len(self._children)
Expand Down Expand Up @@ -90,14 +93,16 @@ def __add__(self, other):

return newBulletin

def _export(self):
def _export(self, compress=False):
"""Construct a <MeteorologicalBulletin> ElementTree"""

if len(self) == 0:
raise XMLError("At least one meteorologicalInformation child must be present in a bulletin.")

try:
self._bulletinID
if self.xmlFileNamePartA.match(self._bulletinId) is None:
raise XMLError('bulletinIdentifier does not conform to WMO. 386')

except AttributeError:
raise XMLError("bulletinIdentifier needs to be set")

Expand All @@ -113,8 +118,12 @@ def _export(self):
metInfo = ET.SubElement(self.bulletin, 'meteorologicalInformation')
metInfo.append(child)

fn = '{}_{}.xml'.format(self._bulletinId, time.strftime('%Y%m%d%H%M%S'))
if compress:
fn = '{}.gz'.format(fn)

bulletinId = ET.SubElement(self.bulletin, 'bulletinIdentifier')
bulletinId.text = self._bulletinID
bulletinId.text = self._internalBulletinId = fn

def what_kind(self):
"""Returns what type or 'kind' of <meteorologicalInformation> children are kept in this bulletin"""
Expand Down Expand Up @@ -147,92 +156,87 @@ def pop(self, pos=0):

def set_bulletinIdentifier(self, **kwargs):

keys = ['A_', 'tt', 'aaii', 'cccc', 'yygg', 'bbb', '_C_', 'cccc', time.strftime('_%Y%m%d%H%M%S.'), 'xml']
self._bulletinID = ''.join([kwargs.get(key, key) for key in keys])
keys = ['A_', 'tt', 'aaii', 'cccc', 'yygg', 'bbb', '_C_', 'cccc']
self._bulletinId = ''.join([kwargs.get(key, key) for key in keys])
self._wmoAHL = '{}{} {} {} {}'.format(kwargs['tt'], kwargs['aaii'], kwargs['cccc'], kwargs['yygg'],
kwargs['bbb']).rstrip()

def get_bulletinIdentifier(self):

return self._bulletinID

def export(self):
"""Construct and return a <MeteorologicalBulletin> ElementTree"""

self._export()
return self.bulletin

def _write(self, obj, header):
def _write(self, obj, header, compress):

if header:
result = '{}\n{}'.format(self._wmoAHL, ET.tostring(self.bulletin, encoding=self.encoding, method='xml'))
else:
result = ET.tostring(self.bulletin, encoding=self.encoding, method='xml')

if self._canBeCompressed:
if compress:
obj(result.encode('utf-8'))
else:
obj(result)

def write(self, obj=None, header=False, compress=False):
"""ElementTree to a file or stream.
"""Writes ElementTree to a file or stream.
obj - if none provided, XML is written to current working directory, or
write() method, or
file object, or
character string as directory, or
character string as a filename.
a write() method
header - boolean as to whether the WMO AHL line should be included as first line in file. If true,
the file is no longer valid XML.
File extension indicated on <bulletinIdentifer> element's value determines
whether compression is done OR the compress flag is set to True. (Only gzip is permitted at this time)"""
If applicable, returns fullpath to the XML bulletin"""

self._canBeCompressed = False
if compress or self._bulletinID[-2:] == 'gz':
canBeCompressed = False
if compress:
if 'gzip' in globals().keys():
self._canBeCompressed = True
canBeCompressed = True
else:
raise SystemError('No capability to compress files using gzip()')
#
# Do not include WMO AHL line in compressed files
if self._canBeCompressed:

if canBeCompressed:
header = False
if self._bulletinID[-2:] != 'gz':
self._bulletinID = '{}.gz'.format(self._bulletinID)
#
# Generate the bulletin for export to file or stream
self._export()
# Generate the Meteorological Bulletin for writing
try:
self._internalBulletinId
except AttributeError:
self._export(canBeCompressed)
#
# If the object name is 'write'; Assume it's configured properly for writing
try:
if obj.__name__ == 'write':
return self._write(obj, header)
self._write(obj, header, canBeCompressed)
return None

except AttributeError:
pass

if type(obj) == str or obj is None:

#
# Write to current directory if None, or to the directory path provided.
if obj is None or os.path.isdir(obj):
if obj is None:
obj = self._bulletinID
elif os.path.isdir(obj):
obj = os.path.join(obj, self._bulletinID)
else:
if os.path.basename(obj) != self._bulletinID:
raise XMLError('Internal ID and external file names do not agree.')
obj = os.getcwd()

if self._canBeCompressed:
_fh = gzip.open(obj, 'wb')
if header:
fullpath = os.path.join(obj, self._internalBulletinId.replace('xml', 'txt'))
else:
fullpath = os.path.join(obj, self._internalBulletinId)
#
# Write it out.
if canBeCompressed:
_fh = gzip.open(fullpath, 'wb')
else:
_fh = open(obj, 'w')
_fh = open(fullpath, 'w')

self._write(_fh.write, header)
self._write(_fh.write, header, canBeCompressed)
_fh.close()

else:
if os.path.basename(obj.name) != self._bulletinID:
raise XMLError('Internal and external file names do not agree.')
return fullpath

self._write(obj.write, header)
else:
raise IOError('First argument is an unsupported type: %s' % str(type(obj)))
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description = IWXXM encoders for Annex3 TAC products
author = Mark Oberfield - NOAA/NWS/OSTI/MDL/WIAD
author_email = Mark.Oberfield@noaa.gov
maintainer = Mark Oberfield
version = 1.3.1
version = 1.3.2
classifiers = Programming Language :: Python :: 3
Operating System :: OS Independent
Topic :: Text Processing :: Markup :: XML
Expand Down
Loading

0 comments on commit 211970e

Please sign in to comment.