# 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()