From 9d405e7b5cc5649e55cffae230b9e9ad9f195b09 Mon Sep 17 00:00:00 2001 From: Mark Oberfield Date: Fri, 15 Apr 2022 18:20:05 +0000 Subject: [PATCH] Fixes for Issue #34 * Updated README.md to fix URL to MDL main page and line number to bulletin write() method * bulletin write() method changed to accept a file or a os.PathLike object * The XML declaration line is now always written to a file * Updated test_bulletin.py due to change in write() method. --- README.md | 4 +-- gifts/common/bulletin.py | 57 +++++++++++++++++++++------------------- tests/test_bulletin.py | 8 +++--- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 96eec7b..fe173db 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This repository is a scientific product and is not official communication of the ------------------------------------------------------------------------------- # Generate IWXXM From TAC -This repository hosts software provided by the United States National Weather Service's [Meteorological Development Laboratory](https://vlab.ncep.noaa.gov/web/mdl) (MDL) that transforms Annex 3 Traditional Alphanumeric Code (TAC) forms into IWXXM format. +This repository hosts software provided by the United States National Weather Service's [Meteorological Development Laboratory](https://vlab.noaa.gov/web/mdl) (MDL) that transforms Annex 3 Traditional Alphanumeric Code (TAC) forms into IWXXM format. The ICAO Meteorological Information Exchange Model (IWXXM) is a format for reporting weather information in eXtensible Markup Language (XML). The IWXXM XML schemas, developed and hosted by the WMO in association with ICAO, are used to encode aviation products described in the Meteorological Service for International Air Navigation, Annex 3 to the Convention on International Civil Aviation. @@ -57,7 +57,7 @@ To illustrate the use of the software, the demo subdirectory contains two simple ## Bulletins Every GIFTs encoder, after processing a TAC message successfully, returns an object of the class [Bulletin](https://github.com/NOAA-MDL/GIFTs/blob/master/gifts/common/bulletin.py). The Bulletin object has similarities to a python list object: it has a "length" (the number of IWXXM XML reports); can be indexed; can be iterated; and [ElementTree](https://docs.python.org/3/library/xml.etree.elementtree.html) reports added and removed with the usual python list operations. In addition to the built-in list operations, python's [print()](https://docs.python.org/3/library/functions.html#print) function will nicely format (for human eyes) the bulletin object and write out the complete XML document to a file (default is sys.stdout). -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.2 The Bulletin class provides a convenient [write()](https://github.com/NOAA-MDL/GIFTs/blob/master/gifts/common/bulletin.py#L177) method to generate the ``3 XML document for transmission over the AMHS. +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.2 The Bulletin class provides a convenient [write()](https://github.com/NOAA-MDL/GIFTs/blob/master/gifts/common/bulletin.py#L189) method to generate the ``3 XML document for transmission over the AMHS. Because of the character length of the ``, 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 `` file for transmission over the AMHS is to set the `compress` flag to True in the Bulletin object's write() method, like so: diff --git a/gifts/common/bulletin.py b/gifts/common/bulletin.py index 0da40bd..6033bdc 100644 --- a/gifts/common/bulletin.py +++ b/gifts/common/bulletin.py @@ -21,11 +21,6 @@ class Bulletin(object): def __init__(self): self._children = [] - if sys.version_info[0] == 3: - self.encoding = 'unicode' - 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): @@ -44,9 +39,13 @@ def __str__(self): # # Pad it with spaces and newlines self._addwhitespace() - xmlstring = ET.tostring(self.bulletin, encoding=self.encoding, method='xml') + if sys.version_info[0] == 3: + xmlstring = ET.tostring(self.bulletin, encoding='unicode', method='xml') + else: + xmlstring = ET.tostring(self.bulletin, encoding='UTF-8', method='xml') + self.bulletin = None - return xmlstring.replace(' />', '/>') + return xmlstring def _addwhitespace(self): tab = " " @@ -169,22 +168,30 @@ def export(self): def _write(self, obj, header, compress): + tree = ET.ElementTree(element=self.bulletin) 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') + ahl_line = '{}\n'.format(self._wmoAHL) + obj.write(ahl_line.encode('UTF-8')) + try: + tree.write(obj, encoding='UTF-8', xml_declaration=True, method='xml', short_empty_elements=True) + except TypeError: + tree.write(obj, encoding='UTF-8', xml_declaration=True, method='xml') - if compress: - obj(result.encode('utf-8')) - else: - obj(result) + def _iswriteable(self, obj): + try: + return obj.writable() and obj.mode == 'wb' + except AttributeError: + try: + return isinstance(obj, file) and obj.mode == 'wb' + except NameError: + return False def write(self, obj=None, header=False, compress=False): """Writes ElementTree to a file or stream. obj - if none provided, XML is written to current working directory, or character string as directory, or - a write() method + os.PathLike object such as a file object in 'wb' mode 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. @@ -208,17 +215,13 @@ def write(self, obj=None, header=False, compress=False): except AttributeError: self._export(canBeCompressed) # - # If the object name is 'write'; Assume it's configured properly for writing - try: - if obj.__name__ == 'write': - self._write(obj, header, canBeCompressed) - return None - - except AttributeError: - pass + # If the object name is writable and mode is correct + if self._iswriteable(obj): + self._write(obj, header, canBeCompressed) + return 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 or (isinstance(obj, str) and os.path.isdir(obj)): if obj is None: obj = os.getcwd() @@ -231,12 +234,12 @@ def write(self, obj=None, header=False, compress=False): if canBeCompressed: _fh = gzip.open(fullpath, 'wb') else: - _fh = open(fullpath, 'w') + _fh = open(fullpath, 'wb') - self._write(_fh.write, header, canBeCompressed) + self._write(_fh, header, canBeCompressed) _fh.close() return fullpath else: - raise IOError('First argument is an unsupported type: %s' % str(type(obj))) + raise IOError('First argument is an unsupported type: %s' % (str(type(obj)))) diff --git a/tests/test_bulletin.py b/tests/test_bulletin.py index 393a499..f93385d 100644 --- a/tests/test_bulletin.py +++ b/tests/test_bulletin.py @@ -151,8 +151,8 @@ def test_writes(): os.unlink(fn) # # Pass write method -- doesn't matter what the name is. - _fh = open(os.devnull, 'w') - fn = collective1.write(_fh.write) + _fh = open(os.devnull, 'wb') + fn = collective1.write(_fh) assert fn is None # # Indicate bulletin, when written out, is to be compressed @@ -216,8 +216,8 @@ def test_header_option(): # # Insert WMO AHL line filename = os.path.join(tempfile.gettempdir(), 'asdfas.txt') - _fh = open(filename, 'w') - collective.write(_fh.write, header=True) + _fh = open(filename, 'wb') + collective.write(_fh, header=True) _fh.close() # Verify first line is the WMO AHL