Source code for pybankreader.reports

import inspect
import six

from .records import Record
from . import exceptions


[docs]class CompoundRecord(Record): """ A wrapper for a record field that represents a list of records and possibly of multiple types of records. This is mainly required to control the position of individual record attributes in the given report. """ _records = None _record_iter = 0 _data = None def __init__(self, *args): """ Same the records as a list :param list args: individual record types """ super(CompoundRecord, self).__init__() self._records = args self._data = []
[docs] def advance(self): try: self._records[self._record_iter+1] except IndexError: return False else: self._record_iter += 1 return True
[docs] def reset(self): self._record_iter = 0
[docs] def get_record(self): return self._records[self._record_iter]()
[docs]class ReportBase(type): """ The metaclass responsible for creating `hint_<record>` and `process_<record>` fields, as well as instantiating those in correct order and mapping them onto a Report instance. """ def __new__(mcs, klazz, bases, attrs): # Do this only on subclasses of Report parents = [b for b in bases if isinstance(b, ReportBase)] if not parents: # It's something else, so go ahead return super(ReportBase, mcs).__new__(mcs, klazz, bases, attrs) # The filter for weeding out things that don't interest us def filter_records(x): """ Filter out things that are not record fields. This applies to litst, that have instances of something else than Record """ if isinstance(x[1], Record): return True if isinstance(x[1], list): for x in x[1]: if not inspect.isclass(x) or not issubclass(x, Record): return False return True return False real_attrs = filter(filter_records, six.iteritems(attrs)) # Prepare the stub for `process_<field>_` methods def _process_stub(self, record): """ Default process method just returning the record back :return Record: the record """ return record # Add hint methods, so they can indicate that a field should advance def _field_hint(self, line): """ Default hint method always returning True :return bool: Always True """ return True methods = {} record_list = [] records = {} for name, record_klazz in sorted(real_attrs, key=lambda x: x[1]): attrs.pop(name) methods["process_{}".format(name)] = _process_stub methods["hint_{}".format(name)] = _field_hint records[name] = record_klazz attrs[name] = None record_list.append(name) # Check that we have at least one record if not len(record_list): msg = "Your report '{}' must have at least one record". \ format(klazz) raise exceptions.ConfigurationError(msg) # Create the class isntance klazz_inst = super(ReportBase, mcs).__new__(mcs, klazz, bases, attrs) # Add `process` and `hint` methods, if they're not defined for name, pointer in six.iteritems(methods): if not hasattr(klazz_inst, name): setattr(klazz_inst, name, pointer) # Add the assembled records setattr(klazz_inst, '_record_map', records) setattr(klazz_inst, '_record_list', record_list) return klazz_inst
[docs]class Report(six.with_metaclass(ReportBase, object)): _last_exception = None """ This stores history of exceptions, so we can trace the error more accurately """ data = None """ The actual data field. All reports will have at least this one defined. """ def __init__(self, file_like=None): """ The constructor handles initialization of any list fields, that may be defined. Optionally, it can take the file-like to read from directly :param file_like: the file from which to read data """ for key, record_klazz in six.iteritems(self._record_map): if isinstance(record_klazz, CompoundRecord): # Do not forget to reset bailed imports! record_klazz.reset() # If no data field is defined, make it a list anyway self.data = self.data or [] if file_like: self.load(file_like)
[docs] def load(self, file_like): """ Read individual records and assign them to proper instance fields, as they go. When the system cannot parse a record, we advance to the next record type first, before we raise an exception indicating that the report is invalid. """ curr_record_idx = 0 while True: line = file_like.readline() if not line: break # We need to handle the iteration of the record classes while True: curr_record = self._record_list[curr_record_idx] record_obj = self._record_map[curr_record] is_list = isinstance(record_obj, CompoundRecord) if is_list: compound_record = record_obj record_obj = record_obj.get_record() try: # First, check the hint method okay = getattr(self, 'hint_{}'.format(curr_record))(line) # The hint tells us, that we need to advance, so let's do # that by raising ValidationError directly if not okay: msg = "{} hint says that I should advance".\ format(curr_record) raise exceptions.ValidationError( '__hint__', msg ) record_obj.load(line.strip()) except exceptions.ValidationError as val_error: # Save the exception... if self._last_exception: val_error.parent = self._last_exception self._last_exception = val_error # And continue about our business try: if is_list and compound_record.advance(): # Okay, we may just need to switch to different # record type in the compound record continue else: self._record_list[curr_record_idx+1] except IndexError: # Nope, this is the end and we're out of here raise val_error else: # Okay, there is hope, since there is another record # in the record list curr_record_idx += 1 else: break # Clear exception stack self._last_exception = None # Process hook process_method = getattr(self, 'process_{}'.format(curr_record)) processed = process_method(record_obj) if processed is None: if is_list: compound_record.reset() continue # If the datum is supposed to be in a list, we need to put it there # as such. Otherwise, just set the attribute if is_list: data_list = getattr(self, curr_record) data_list.append(processed) # The order of records in compound record is non-linear compound_record.reset() else: setattr(self, curr_record, processed)