Source code for geopathfinder.file_naming

# Copyright (c) 2025, TU Wien
# of Geodesy and Geoinformation (GEO).
# All rights reserved.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL VIENNA UNIVERSITY OF TECHNOLOGY,
# DEPARTMENT OF GEODESY AND GEOINFORMATION BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import os
import copy
from collections import OrderedDict


[docs] class SmartFilenamePart: """ Represents a part of filename. """ def __init__(self, arg, start=0, length=None, delimiter="_", pad="-", decoder=None, encoder=None, compact=False): """ Constructor of SmartFilenamePart class. Parameters ---------- arg: object Input argument, which can be a string or the decoded part of the filename. start: int, optional Start index of filename part (default is 0). length: int, optional Length of filename part. delimiter : str, optional Delimiter (default: '_') pad : str, optional Padding symbol (default: '-'). decoder: function, optional Decodes a certain value (str -> object). encoder: function, optional Encodes a certain value (object -> str). """ self.arg = arg self.start = start self.delimiter = delimiter self.pad = pad self.compact = compact self.decoder = (lambda x: x) if decoder is None else decoder self.encoder = (lambda x: x) if encoder is None else encoder length = 0 if compact else length self.length = length if length is not None and length != 0 else len( self.encoded) # check validity if not self.has_valid_len(): err_msg = "Length does not comply with definition: {:} > {:}".format( len(self), self.length) raise ValueError(err_msg)
[docs] def has_valid_len(self): """ Checks if a SmartFilenamePart instance has a valid len. It is valid if the specified length is equal to the length of the SmartFilenamePart, or if len == 0, i.e accepting any length. Returns ------- bool True if SmartFilenamePart instance is valid, else False. """ # 0 for accepting any length if self.length == 0 or self.compact: check = True else: check = self.length == len(self) return check
@property def encoded(self): """ Converts filename part to an encoded (string) representation. Returns ------- str Encoded (string) representation of a filename part. """ return self.encoder(self.arg) @property def decoded(self): """ Converts filename part to a decoded (object) representation. Returns ------- object Decoded (object) representation of a filename part. """ enc_wo_pad = self.encoded.strip(self.pad) if enc_wo_pad != '': return self.decoder(enc_wo_pad) else: return None def __str__(self): """ Returns the string representation of the field. Returns ------- str String representation of the class. """ if self.compact and self.arg == "": return '' return self.encoded.ljust(self.length, self.pad) def __repr__(self): """ Returns the class representation, which is the field + delimiter. Returns ------- str String representation of the class. """ return str(self) + self.delimiter def __len__(self): """ Returns length of the filename part. Returns ------- int Length of the filename part. """ return len(str(self)) def __add__(self, other): """ Defines summation rule for two SmartFilenamePart/str instances. Parameters ---------- other: SmartFilenamePart, str Second summand. Returns ------- str Concatenated strings separated by a delimiter. """ return repr(self) + str(other)
[docs] class SmartFilename(object): """ SmartFilename class handles file names with pre-defined field names and field length. """ def __init__(self, fields, fields_def, ext=None, pad='-', delimiter='_', convert=False, compact=False): """ Define name of fields, length, pad and delimiter symbol. Parameters ---------- fields : dict Name of fields (keys) and (values). fields_def : OrderedDict Name of fields (keys) in right order and length (values). It must contain: - "len": int Length of filename part (must be given). "0" to allow any length. - "start": int, optional Start index of filename part (default is 0). - "delim": str, optional Delimiter between this and the following filename part (default is the one from the parent class). - "pad": str, Padding for filename part (default is the one from the parent class). ext : str, optional File name extension (default: None). pad : str, optional Padding symbol (default: '-'). delimiter : str, optional Delimiter (default: '_') convert: bool, optional If true, decoding is applied to parts of the filename, where such an operation is available (default is False). compact: bool, optional If true, empty fields are replaced by a single pad character instead of the whole length. """ self.ext = ext self.delimiter = delimiter self.pad = pad self.convert = convert self.compact = compact self._fn_map = self.__build_map(fields, fields_def) self.obj = self.__init_filename_obj()
[docs] @classmethod def from_filename(cls, filename_str, fields_def, pad="-", delimiter="_", convert=False, compact=False): """ Converts a filename given as a string into a SmartFilename class object. Parameters ---------- filename_str : str Filename without any paths (e.g., "M20170725_test.tif"). fields_def : OrderedDict Name of fields (keys) in right order and length (values). It must contain: - "len": int Length (positive number) of filename part (must be given). 0 to allow any length. - "start": int, optional Start index of filename part (default is 0). - "delim": str, optional Delimiter between this and the following filename part (default is the one from the parent class). - "pad": str, Padding for filename part (default is the one from the parent class). pad : str, optional Padding symbol (default: '-'). delimiter : str, optional Delimiter (default: '_') convert: bool, optional If true, decoding is applied to parts of the filename, where such an operation is available (default is False). Returns ------- SmartFilename Class representing a filename. """ # get extensions from filename ext = os.path.splitext(filename_str)[1] fields = dict() pos = 0 for name, value in fields_def.items(): start = value.get('start') length = value.get('len') delim = value.get('delim') if length is not None and length < 0: err_msg = "The length of the attribute '{}' must be a positive number".format( name) raise ValueError(err_msg) # parse part of filename via start and end position if start is not None and length is not None: fields[name] = filename_str[start:(start + length)] elif length is not None: start = pos if compact: length = 0 if delim is None and delimiter is None: raise Exception( 'The compact filename design requires a delimiter for each field!' ) if length == 0: # handle variable length end = filename_str.find(delimiter, start) if delimiter in filename_str[start:] \ else filename_str.find('.', start) length = end - start # if length is not 0, i.e. an entry is available, then add the field. Otherwise skip it. if length > 0: fields[name] = filename_str[start:(start + length)] else: length = 0 pos += length if delim is not None: pos += len(delim) else: pos += len(delimiter) if cls.__name__ == "SmartFilename": return cls(fields, fields_def, ext=ext, convert=convert, pad=pad, delimiter=delimiter) else: return cls(fields, ext=ext, convert=convert)
def __init_filename_obj(self): """ Initialises the class 'FilenameObj' to set all filename attributes as class variables. This enables an easier access to filename properties. Returns ------- FilenameObj """ class FilenameObj(object): def __init__(self): pass filename_obj = FilenameObj() for name, fn_part in self._fn_map.items(): if self.convert: setattr(filename_obj, name, fn_part.decoded) else: setattr(filename_obj, name, fn_part.encoded) return filename_obj def __build_map(self, fields, fields_def): """ Creates a dictionary/map between filename part names and SmartFilenamePart instances. Paramaters ---------- fields : dict Name of fields (keys) and (values). fields_def : OrderedDict Name of fields (keys) in right order and length (values). It must contain: - "len": int Length (positive number) of filename part (must be given). 0 to allow any length. - "start": int, optional Start index of filename part (default is 0). - "delim": str, optional Delimiter between this and the following filename part (default is the one from the parent class). - "pad": str, Padding for filename part (default is the one from the parent class). Returns ------- dict Contains SmartFilenamePart instances representing each part of the filename. """ # check fields consistency for key in fields.keys(): if key not in fields_def.keys(): raise KeyError("Field name undefined: {:}".format(key)) fn_map = OrderedDict() last_key_name = list(fields_def.keys())[-1] for name, keys in fields_def.items(): if name not in fields: elem = "" else: elem = fields[name] fn_part_kwargs = dict() if 'delim' not in keys: fn_part_kwargs['delimiter'] = self.delimiter else: fn_part_kwargs['delimiter'] = keys['delim'] if 'len' in keys: # check delimiter in case of zero length if keys['len'] == 0 and fn_part_kwargs['delimiter'] == '': err_msg = 'A variable field length (length = 0) requires a delimiter!' raise ValueError(err_msg) if keys['len'] < 0: err_msg = "The length of the attribute '{}' must be a positive number".format( name) raise ValueError(err_msg) fn_part_kwargs['length'] = keys['len'] if 'decoder' in keys: fn_part_kwargs['decoder'] = keys['decoder'] if 'encoder' in keys: fn_part_kwargs['encoder'] = keys['encoder'] fn_part_kwargs['compact'] = self.compact if name == last_key_name: # set empty delimiter for last field fn_part_kwargs['delimiter'] = '' # reset delimiter of last element to be empty smart_fn_part = SmartFilenamePart(elem, **fn_part_kwargs) fn_map[name] = smart_fn_part return fn_map def _build_fn(self): """ Build file name based on fields, padding and length. Returns ------- filename : str Filled file name. """ fn_parts = list(self._fn_map.values()) filename = ''.join([repr(fn_part) for fn_part in fn_parts]) # handle case if last field is not defined (skip the last delimeter then) if filename.endswith(self.delimiter): filename = filename[:-1] if self.ext is not None: filename += self.ext return filename def _get_field(self, name): """ Returns the value of the field with a given key. Parameters ---------- name : str Name of the field. Returns ------- str, object Part of the filename associated with given key. Depending on the chosen flag 'convert', it is either a str (convert=False) or an object. """ # check and reset the attribute of the object variable field_from_obj = self._fn_map[name].encoder(getattr(self.obj, name)) if field_from_obj and (field_from_obj != self._fn_map[name].encoded): fn_part = copy.deepcopy(self._fn_map[name]) fn_part.arg = field_from_obj if fn_part.has_valid_len(): self._fn_map[name] = fn_part if self.convert: return self._fn_map[name].decoded else: return self._fn_map[name].encoded.strip(self._fn_map[name].pad) def __getitem__(self, name): """ Returns the value of the field with a given key. Parameters ---------- name : str Name of the field. Returns ------- str, object Part of the filename associated with given key. Depending on the chosen flag 'convert', it is either a str (convert=False) or an object. If the key can't be found in the fields definition, the method tries to return a property of an inherited class. """ if name in self._fn_map: return self._get_field(name) elif hasattr(self, name): return getattr(self, name) else: raise KeyError( '"{}" is neither a class variable nor a file attribute.'. format(name)) def __setitem__(self, name, value): """ Sets the value of a filename field corresponding to the given key. Parameters ---------- name : str Name of the field. value: object Value of the field. """ if name in self._fn_map: fn_part = copy.deepcopy(self._fn_map[name]) fn_part.arg = value if self.compact: fn_part.length = 0 if not fn_part.has_valid_len(): err_msg = "Length does not comply with definition: {:} > {:}".format( len(fn_part.encoded), fn_part.length) raise ValueError(err_msg) else: self._fn_map[name] = fn_part value = fn_part.encoded.replace(self.pad, '') if self.convert: setattr(self.obj, name, fn_part.decoded) else: setattr(self.obj, name, value) else: raise KeyError("Field name undefined: {:}".format(name)) def __repr__(self): """ Returns the string representation of the class. Returns ------- str String representation of the class. """ return self._build_fn()