"""
PyMBS is a Python library for use in modeling Mortgage-Backed Securities.
Copyright (C) 2019 Brian Farrell
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Contact: brian.farrell@me.com
"""
from io import StringIO
from logging import Logger
import re
import sys
from typing import NoReturn
from IPython.display import display
from jinja2 import Environment, PackageLoader, select_autoescape
from pymbs.enums import ExitCode
_jinja_env = Environment(
loader=PackageLoader('pymbs', 'templates'),
autoescape=select_autoescape(['html', 'xml'])
)
[docs]def handle_gracefully(
ipython_status: bool,
logger: Logger,
message: str,
**kwargs: str) -> NoReturn:
"""Handle an error condition in a graceful manner.
Args:
ipython_status: The status as to whether or not PyMBS is running
inside an IPython kernel. This status can be passed with the
``config._ipython_active`` attribute.
logger: The logger object, passed in from the caller. This keeps
references in the log to the actual module where the excpeption
occured, rather than logging them as occuring inside the
exceptions module.
message: A string with the same name as one of the messages available
in the GracefulMessage object, described below, in this module.
kwargs: Keyword arguments, supplying any values interpolated in the
message templates of the GracefulMessage object.
"""
if message == 'no_deal':
message = 'no_data'
kwargs['data'] = 'deal'
elif message == 'no_model':
message = 'no_data'
kwargs['data'] = 'model'
elif message == 'no_model_file':
message = 'no_file'
kwargs['file_type'] = 'Model'
elif message == 'no_terms':
message = 'no_file'
kwargs['file_type'] = 'Terms Sheet'
elif message == 'no_pps':
message = 'no_file'
kwargs['file_type'] = 'Prepayment Scenario'
kwargs['ipython_status'] = ipython_status
msg = GracefulMessage(message, **kwargs)
no_exit = kwargs.get('no_exit')
if ipython_status:
display(msg)
msg = re.sub(r'\n', r'', str(msg))
if no_exit:
logger.warn(msg)
return
else:
logger.error(msg)
raise IpyExit
else:
print(msg)
msg = re.sub(r'\n\n', r' ', str(msg))
msg = re.sub(r'\n', r'', str(msg))
if no_exit:
logger.warning(msg)
return
else:
exit_code = kwargs.get('exit_code')
if not exit_code:
exit_code = ExitCode.EX_GENERAL
logger.error(msg)
sys.exit(exit_code)
[docs]class GracefulMessage(object):
"""The GracefulMessage object allows various messages to be displayed in
the most appropriate format, depending on how PyMBS is being used at the
time.
If PyMBS is being used in something like a Jupyter Notebook, where HTML
can be displayed, it will display HTML. If the front-end is only
capable of displaying text, it will display plain text.
TODO: Implement a _repr_json_ method to output the error message as a
JSON string, for when PyMBS is integrated with a RESTful/GraphQL API.
"""
def __init__(self, message, **kwargs):
self.message = message
self.kwargs = kwargs
self.msg_template = _jinja_env.get_template(f"{self.message}.j2")
def __repr__(self):
"""IPython actually misuses the conept of __repr__.
It expects __repr__ to be a string, rather than an unambiguous
representation of the object. The functionality the IPython expects
from __repr__ is actually that delivered by __str__, but if I
implement this method as __str__, rather than __repr__, IPython
actually displays a __repr__ of the object, which is not what we
want here!
When in Rome...
"""
msg = self.msg_template.render(**self.kwargs)
return msg
def _repr_html_(self):
msg = self.msg_template.render(**self.kwargs)
return msg
[docs]class IpyExit(SystemExit):
"""Exit Exception for IPython.
Exception temporarily redirects stderr to buffer.
"""
def __init__(self):
# print("exiting") # optionally print some message to stdout, too
# ... or do other stuff before exit
sys.stderr = StringIO()
def __del__(self):
sys.stderr.close()
sys.stderr = sys.__stderr__ # restore from backup
[docs]class AssumedCollatError(Exception):
"""docstring for AssumedCollatError"""
def __init__(self, group_id, repline_num=1):
self.group_id = group_id
self.repline_num = repline_num
self.message = (
f"Could not locate Repline {repline_num} for "
f"Group {repr(group_id)}. Please check Assumed Collateral."
)
def __str__(self):
return self.message
[docs]class CollatError(Exception):
"""docstring for AssumedCollatError"""
def __init__(self, group_id):
self.group_id = group_id
self.message = (
f"Could not locate collateral for "
f"Group {repr(group_id)}. Please check the Terms Sheet."
)
def __str__(self):
return self.message
[docs]class PrepaymentBenchmarkError(Exception):
"""Exception raised for errors in the input.
Attributes:
expression -- input expression in which the error occurred
message -- explanation of the error
"""
def __init__(self, prepayment_benchmark, message):
self.prepayment_benchmark = prepayment_benchmark
self.message = message
[docs]class DateError(Exception):
"""Exception raised for errors in the input.
Attributes:
expression -- input expression in which the error occurred
message -- explanation of the error
"""
def __init__(self, date, message):
self.date = date
self.message = message
[docs]class PayRuleError(Exception):
"""Exception raised for errors in the input.
Attributes:
expression -- input expression in which the error occurred
message -- explanation of the error
"""
def __init__(self, pay_rule, error):
self.pay_rule = pay_rule
self.error = error
if self.error == 'invalid':
self.message = f"The pay rule {pay_rule} is not a valid pay rule."
elif self.error == 'no_parse':
self.message = (
f"The pay rule | {pay_rule} | was not parsed successfully. "
f"\n\t\tPlease check the syntax."
)
def __str__(self):
return self.message