# Source code for obspy.io.xseed.fields

    # -*- coding: utf-8 -*-
    """
    Helper module containing xseed fields.

    :copyright:
        The ObsPy Development Team (devs@obspy.org)
    :license:
        GNU Lesser General Public License, Version 3
        (https://www.gnu.org/copyleft/lesser.html)
    """
    from __future__ import (absolute_import, division, print_function,
                            unicode_literals)
    from future.builtins import *  # NOQA @UnusedWildImport

    import re
    import warnings

    from lxml.etree import Element, SubElement

    from obspy import UTCDateTime
    from .utils import (datetime_2_string, SEEDParserException, get_xpath,
                        set_xpath, to_tag)

    class SEEDTypeException(Exception):
        pass

    class Field(object):
        """
        General SEED field.
        """
        def __init__(self, id, name, *args, **kwargs):  # @UnusedVariable
            # default
            self.id = id
            self.flag = ''
            self.name = name
            self.xseed_version = kwargs.get('xseed_version', None)
            self.seed_version = kwargs.get('version', None)
            self.mask = kwargs.get('mask', None)
            self.xpath = kwargs.get('xpath', None)
            if self.id:
                self.field_id = "F%02d" % self.id
            else:
                self.field_id = None
            self.field_name = kwargs.get('xml_tag', to_tag(self.name))
            self.attribute_name = to_tag(self.name)
            # options
            self.optional = kwargs.get('optional', False)
            self.ignore = kwargs.get('ignore', False)
            self.strict = kwargs.get('strict', False)
            self.compact = kwargs.get('compact', False)
            self.default_value = kwargs.get('default_value', self.default)

        def __str__(self):
            if self.id:
                return "F%02d" % self.id

        def _repr_pretty_(self, p, cycle):  # @UnusedVariable
            p.text(str(self))

        def convert(self, value):
            return value

        def _format_string(self, s):
            """
            Using SEED specific flags to format strings.

            This method is partly adopted from fseed.py, the SEED builder for
            SeisComP written by Andres Heinloo, GFZ Potsdam in 2005.
            """
            if isinstance(s, bytes):
                sn = s.decode('utf-8').strip()
            else:
                sn = str(s).strip()
            if self.flags and 'T' in self.flags:
                if not sn and self.default_value:
                    return self.default_value
                return datetime_2_string(sn, self.compact)
            if not self.flags:
                return sn
            rx_list = []
            if 'U' in self.flags:
                # upper case A—Z
                rx_list.append(r"[A-Z]")
            if 'L' in self.flags:
                # lower case a—z
                rx_list.append(r"[a-z]")
            if 'N' in self.flags:
                # digits 0—9
                rx_list.append(r"[0-9]")
            if 'P' in self.flags:
                # any punctuation characters (including “_”)
                rx_list.append(r"[^A-Za-z0-9 ]")
            if 'S' in self.flags:
                # spaces between words
                rx_list.append(r" ")
            if '_' in self.flags:
                # underline symbol
                rx_list.append(r"_")
            # auto-format
            if 'U' in self.flags and 'L' not in self.flags:
                sn = sn.upper()
            elif 'L' in self.flags and 'U' not in self.flags:
                sn = sn.lower()
            rx = "|".join(rx_list)
            sn = "".join(re.findall(rx, sn))
            if re.match("(" + rx + ")*$", sn) is None:
                msg = "Can't convert string %s with flags %s" % (s, self.flags)
                raise SEEDTypeException(msg)
            return sn

        def parse_seed(self, blockette, data):
            """
            """
            try:
                text = self.read(data, strict=blockette.strict)
            except Exception as e:
                if blockette.strict:
                    raise e
                # default value if not set
                text = self.default_value
            # check if already exists
            if hasattr(blockette, self.attribute_name):
                temp = getattr(blockette, self.attribute_name)
                if not isinstance(temp, list):
                    temp = [temp]
                temp.append(text)
                text = temp
            setattr(blockette, self.attribute_name, text)
            self.data = text
            # debug
            if blockette.debug:
                print('  %s: %s' % (self, text))

        def get_seed(self, blockette, pos=0):
            """
            """
            self.compact = blockette.compact
            try:
                result = getattr(blockette, self.attribute_name)
            except Exception:
                if blockette.strict:
                    msg = "Missing attribute %s in Blockette %s"
                    raise Exception(msg % (self.name, blockette))
                result = self.default_value
            # watch for multiple entries
            if isinstance(result, list):
                result = result[pos]
            # debug
            if blockette.debug:
                print('  %s: %s' % (self, result))
            return self.write(result, strict=blockette.strict)

        def get_xml(self, blockette, pos=0):
            """
            """
            if self.ignore:
                # debug
                if blockette.debug:
                    print('  %s: ignored')
                return []
            try:
                result = getattr(blockette, self.attribute_name)
            except Exception:
                if blockette.strict:
                    msg = "Missing attribute %s in Blockette %s"
                    raise Exception(msg % (self.name, blockette))
                result = self.default
            # watch for multiple entries
            if isinstance(result, list):
                result = result[pos]
            # optional if empty
            if self.optional:
                try:
                    result = result.strip()
                except Exception:
                    pass
                if not result:
                    # debug
                    if blockette.debug:
                        print('  %s: skipped because optional')
                    return []
            # reformat float
            if isinstance(self, Float):
                result = self.write(result)
            # Converts to XPath if necessary.
            if self.xpath:
                result = set_xpath(self.xpath, result)
            # create XML element
            node = Element(self.field_name)
            if isinstance(result, bytes):
                node.text = result.decode().strip()
            else:
                node.text = str(result).strip()
            # debug
            if blockette.debug:
                print('  %s: %s' % (self, [node]))
            return [node]

        def parse_xml(self, blockette, xml_doc, pos=0):
            """
            """
            try:
                text = xml_doc.xpath(self.field_name + "/text()")[pos]
            except Exception:
                setattr(blockette, self.attribute_name, self.default_value)
                # debug
                if blockette.debug:
                    print('  %s: set to default value %s' % (self,
                                                             self.default_value))
                return
            # Parse X-Path if necessary. The isdigit test assures legacy support
            # for XSEED without XPaths.
            if self.xpath and not text.isdigit():
                text = get_xpath(text)
            # check if already exists
            if hasattr(blockette, self.attribute_name):
                temp = getattr(blockette, self.attribute_name)
                if not isinstance(temp, list):
                    temp = [temp]
                temp.append(text)
                text = temp
            setattr(blockette, self.attribute_name, self.convert(text))
            # debug
            if blockette.debug:
                print('  %s: %s' % (self, text))

    class Integer(Field):
        """
        An integer field.
        """
        def __init__(self, id, name, length, **kwargs):
            self.default = 0
            Field.__init__(self, id, name, **kwargs)
            self.length = length

        def convert(self, value):
            try:
                if isinstance(value, list):
                    return [int(_i) for _i in value]
                else:
                    return int(value)
            except Exception:
                if not self.strict:
                    return self.default_value
                msg = "No integer value found for %s." % self.attribute_name
                raise SEEDTypeException(msg)

        def read(self, data, strict=False):  # @UnusedVariable
            temp = data.read(self.length)
            return self.convert(temp)

        def write(self, data, strict=False):  # @UnusedVariable
            format_str = "%%0%dd" % self.length
            try:
                temp = int(data)
            except Exception:
                msg = "No integer value found for %s." % self.attribute_name
                raise SEEDTypeException(msg)
            result = format_str % temp
            if len(result) != self.length:
                msg = "Invalid field length %d of %d in %s." % \
                      (len(result), self.length, self.attribute_name)
                raise SEEDTypeException(msg)
            return result.encode()

    class Float(Field):
        """
        A float number with a fixed length and output mask.
        """
        def __init__(self, id, name, length, **kwargs):
            self.default = 0.0
            Field.__init__(self, id, name, **kwargs)
            self.length = length
            if not self.mask:
                msg = "Float field %s requires a data mask." % self.attribute_name
                raise SEEDTypeException(msg)

        def convert(self, value):
            try:
                if isinstance(value, list):
                    return [float(_i) for _i in value]
                else:
                    return float(value)
            except Exception:
                if not self.strict:
                    return self.default_value
                msg = "No float value found for %s." % self.attribute_name
                raise SEEDTypeException(msg)

        def read(self, data, strict=False):  # @UnusedVariable
            temp = data.read(self.length)
            return self.convert(temp)

        def write(self, data, strict=False):  # @UnusedVariable
            format_str = "%%0%ds" % self.length
            try:
                temp = float(data)
            except Exception:
                msg = "No float value found for %s." % self.attribute_name
                raise SEEDTypeException(msg)
            # special format for exponential output
            result = format_str % (self.mask % temp)
            if 'E' in self.mask or 'e' in self.mask:
                result = result.upper()
            if len(result) != self.length:
                msg = "Invalid field length %d of %d in %s." % \
                      (len(result), self.length, self.attribute_name)
                raise SEEDTypeException(msg)
            return result.encode()

    class FixedString(Field):
        """
        A string field with a fixed width.
        """
        def __init__(self, id, name, length, flags='', **kwargs):
            self.default = ' ' * length
            Field.__init__(self, id, name, **kwargs)
            self.length = length
            self.flags = flags

        def read(self, data, strict=False):  # @UnusedVariable
            return self._format_string(data.read(self.length).strip())

        def write(self, data, strict=False):  # @UnusedVariable
            # Leave fixed length alphanumeric fields left justified (no leading
            # spaces), and pad them with spaces (after the field’s contents).
            format_str = "%%-%ds" % self.length
            result = format_str % self._format_string(data)
            if len(result) != self.length:
                msg = "Invalid field length %d of %d in %s." % \
                      (len(result), self.length, self.attribute_name)
                raise SEEDTypeException(msg)
            return result.encode()

    class VariableString(Field):
        """
        Variable length ASCII string, ending with a tilde: ~ (ASCII 126).

        Variable length fields cannot have leading or trailing spaces. Character
        counts for variable length fields do not include the tilde terminator.
        """
        def __init__(self, id, name, min_length=0, max_length=None, flags='',
                     **kwargs):
            self.default = ''
            Field.__init__(self, id, name, **kwargs)
            self.min_length = min_length
            self.max_length = max_length
            self.flags = flags

        def convert(self, value):
            # check for datetime
            if 'T' in self.flags:
                return UTCDateTime(value)
            return value

        def read(self, data, strict=False):
            data = self._read(data)
            # check for datetime
            if 'T' in self.flags:
                # default value
                if data:
                    # create a full SEED date string
                    temp = b"0000,000,00:00:00.0000"
                    data += temp[len(data):]
                    return UTCDateTime(data.decode())
                if self.default_value:
                    return self.default_value
                if self.min_length:
                    if strict:
                        raise SEEDParserException
                    warnings.warn('Date is required.', UserWarning)
                return ""
            else:
                if self.flags:
                    return self._format_string(data)
                else:
                    return data

        def _read(self, data):
            buffer = b''
            if self.min_length:
                buffer = data.read(self.min_length)
                if b'~' in buffer:
                    return buffer.split(b'~')[0]
            temp = b''
            i = self.min_length
            while temp != b'~':
                temp = data.read(1)
                if temp == b'~':
                    return buffer
                elif temp == b'':
                    # raise if EOF is reached
                    raise SEEDTypeException('Variable string has no terminator')
                buffer += temp
                i = i + 1
            return buffer

        def write(self, data, strict=False):  # @UnusedVariable
            result = self._format_string(data).encode('utf-8')
            if self.max_length and len(result) > self.max_length + 1:
                msg = "Invalid field length %d of %d in %s." % \
                      (len(result), self.max_length, self.attribute_name)
                if strict:
                    raise SEEDTypeException(msg)
                msg += ' Reducing to %d chars.' % (self.max_length)
                warnings.warn(msg, UserWarning)
                result = result[:self.max_length]
            # MSEED manual p. 30: Character counts for variable length fields do
            # not include the tilde terminator - however this is not valid for
            # minimum sizes - e.g. optional date fields in Blockette 10
            # so we add here the terminator string, and check minimum size below
            result += b'~'
            if len(result) < self.min_length:
                msg = "Invalid field length %d of %d in %s." % \
                      (len(result), self.min_length, self.attribute_name)
                if strict:
                    raise SEEDTypeException(msg)
                delta = self.min_length - len(result)
                msg += ' Adding %d space(s).' % (delta)
                warnings.warn(msg, UserWarning)
                result = b' ' * delta + result
            return result

    class Loop(Field):
        """
        A loop over multiple elements.
        """
        def __init__(self, name, index_field, data_fields, **kwargs):
            self.default = False
            Field.__init__(self, None, name, **kwargs)
            # initialize + default values
            if not isinstance(data_fields, list):
                data_fields = [data_fields]
            self.data_fields = data_fields
            self.index_field = to_tag(index_field)
            self.length = 0
            # loop types
            self.repeat_title = kwargs.get('repeat_title', False)
            self.omit_tag = kwargs.get('omit_tag', False)
            self.flat = kwargs.get('flat', False)

        def parse_seed(self, blockette, data):
            """
            """
            try:
                self.length = int(getattr(blockette, self.index_field))
            except Exception:
                msg = "Missing attribute %s in Blockette %s"
                raise Exception(msg % (self.index_field, blockette))
            # loop over number of entries
            debug = blockette.debug
            blockette.debug = False
            temp = []
            for _i in range(0, self.length):
                # loop over data fields within one entry
                for field in self.data_fields:
                    field.parse_seed(blockette, data)
                    if debug:
                        temp.append(field.data)
            # debug
            if debug:
                if len(temp) > 3:
                    print('  LOOP: ... (%d elements) ' % (len(temp)))
                else:
                    print('  LOOP: %s' % (temp))
                blockette.debug = debug

        def get_seed(self, blockette):
            """
            """
            try:
                self.length = int(getattr(blockette, self.index_field))
            except Exception:
                msg = "Missing attribute %s in Blockette %s"
                raise Exception(msg % (self.index_field, blockette))
            # loop over number of entries
            data = b''
            for i in range(0, self.length):
                # loop over data fields within one entry
                for field in self.data_fields:
                    data += field.get_seed(blockette, i)
            return data

        def get_xml(self, blockette, pos=0):  # @UnusedVariable
            """
            """
            if self.ignore:
                return []
            try:
                self.length = int(getattr(blockette, self.index_field))
            except Exception:
                msg = "Missing attribute %s in Blockette %s"
                raise Exception(msg % (self.index_field, blockette))
            if self.length == 0 and self.optional:
                return []
            if self.repeat_title:
                # parent tag is repeated over every child tag
                # e.g. <parent><i1/><i2/></parent><parent><i1/><i2/></parent>
                root = Element(self.field_name)
                for _i in range(0, self.length):
                    se = SubElement(root, self.field_name)
                    # loop over data fields within one entry
                    for field in self.data_fields:
                        node = field.get_xml(blockette, _i)
                        se.extend(node)
                return root.getchildren()
            # loop over number of entries
            root = Element(self.field_name)
            for _i in range(0, self.length):
                # loop over data fields within one entry
                for field in self.data_fields:
                    node = field.get_xml(blockette, _i)
                    root.extend(node)
            # format output for requested loop type
            if self.flat:
                # flat loop: one or multiple fields are within one parent tag
                # e.g. <root>item1 item2 item1 item2</root>
                root.text = ' '.join([i.text for i in root.getchildren()])
                root[:] = []
                return [root]
            elif self.omit_tag:
                # loop omitting the parent tag: fields are at the same level
                # e.g. <item1/><item2/><item1/><item2/>
                return root.getchildren()
            else:
                # standard loop
                return [root]

        def parse_xml(self, blockette, xml_doc, pos=0):
            """
            """
            try:
                self.length = int(getattr(blockette, self.index_field))
            except Exception:
                msg = "Missing attribute %s in Blockette %s"
                raise Exception(msg % (self.index_field, blockette))
            if self.length == 0:
                return
            # loop type
            if self.flat:
                # flat loop: one or multiple fields are within one parent tag
                # e.g. <root>item1 item2 item1 item2</root>
                text = xml_doc.xpath(self.field_name + '/text()')[0].split()
                if not text:
                    return
                # loop over number of entries
                for _i in range(0, self.length):
                    # loop over data fields within one entry
                    for field in self.data_fields:
                        temp = getattr(blockette, field.attribute_name, [])
                        temp.append(field.convert(text.pop(0)))
                        setattr(blockette, field.attribute_name, temp)
                return
            elif self.omit_tag:
                # loop omitting the parent tag: fields are at the same level
                # e.g. <item1/><item2/><item1/><item2/>
                root = Element(self.field_name)
                root.extend(xml_doc)
            elif self.repeat_title:
                # parent tag is repeated over every child tag
                # e.g. <parent><i1/><i2/></parent><parent><i1/><i2/></parent>
                root = Element(self.field_name)
                root.extend(xml_doc.xpath(self.field_name + '/*'))
            else:
                # standard loop
                root = xml_doc.xpath(self.field_name)[pos]
            # loop over number of entries
            for i in range(0, self.length):
                # loop over data fields within one entry
                for field in self.data_fields:
                    field.parse_xml(blockette, root, i)

### Thank you!

We would like to thank our contributors, whose efforts make this software what it is. These people have helped by writing code and documentation, and by testing. They have created and maintained this product, its associated libraries and applications, our build tools and our web sites.

#### Contributors

- Ackerley, Nick
- Allgeyer, Sebastien
- Ammon, Charles J.
- Antunes, Emanuel
- Arnarsson, Ólafur St.
- Bank, Markus
- Barsch, Robert
- Behr, Yannik
- Bernardi, Fabrizio
- Bernauer, Felix
- Bes de Berc, Maxime
- Beyreuther, Moritz
- Boltz, Shawn
- Bonaimé, Sébastien
- Carothers, Lloyd
- Chamberlain, Calum
- Chambers, Derrick
- Chen, Zhao
- Clark, Adam
- Danecek, Peter
- Egdorf, Sven
- Engels, Fabian
- Ermert, Laura
- Eulenfeld, Tom

- Fabbri, Tommaso
- Falco, Nicholas
- Fee, Jeremy
- Grellier, Clément
- Grunberg, Marc
- Hammer, Conny
- Heimann, Sebastian
- Heiniger, Lukas
- Hope, Gaute
- Igel, Heiner
- Inza, Adolfo
- Isken, Marius
- Ketchum, David
- Koymans, Mathijs
- Kremers, Simon
- Kress, Victor
- Krieger, Lars
- Krischer, Lion
- Kula, Damian
- Käufl, Paul
- Köhler, Andreas
- Lecocq, Thomas
- Leeman, John
- Legovini, Paride

- Lesage, Philippe
- Li, Yulin
- Lomax, Anthony
- Lopes, Rui L.
- MacCarthy, Jonathan
- Maggi, Alessia
- Martin, Henri
- Medlin, Andrew
- Megies, Tobias
- Meschede, Matthias
- Michelini, Alberto
- Miller, Nathaniel C.
- Morgenstern, Bernhard
- Nof, Ran Novitsky
- Panning, Mark P.
- Parker, Tom
- Pestourie, Romain
- Rapagnani, Giovanni
- Reyes, Celso
- Ringler, Adam
- Rothenhäusler, Nicolas
- Russo, Emiliano
- Sales de Andrade, Elliott

- Satriano, Claudio
- Saul, Joachim
- Scheingraber, Chris
- Schneider, Simon
- Sippl, Christian
- Snoke, Arthur
- Stange, Stefan
- Stähler, Simon C.
- Sullivan, Benjamin
- Tian, Dongdong
- Trabant, Chad
- Trani, Luca
- Uieda, Leonardo
- Walker, Andrew
- Walther, Marcus
- Wassermann, Joachim
- Williams, Mark C.
- Winkelman, Andrew
- Zaccarelli, Riccardo
- Zad, Seyed Kasra Hosseini
- Zhu, Lijun
- van Driel, Martin
- van Wijk, Kasper

#### Funds

ObsPy was partially funded by the

- German Science Foundation (DFG) via grant DFG IG 16/9-1
- German Ministry for Education and Research (BMBF), GEOTECHNOLOGIEN grant 03G0646H.
- NERA project (Network of European Research Infrastructures for Earthquake Risk Assessment and Mitigation) under the European Community's Seventh Framework Programme (FP7/2007-2013) grant agreement n° 262330
- Leibniz Institute for Applied Geophysics (LIAG)
- VERCE EU-FP7 project (no. 283543)