Source code for pymbs.config

"""
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
"""

import os
from pathlib import Path
import sys

import yaml

from pymbs.enums import URL
from pymbs.exceptions import handle_gracefully
from pymbs.log import get_logger

logger = get_logger(__name__)

# Python Decimal Module
DEFAULT_PRECISION = 18
DEFAULT_EMAX = 12
DEFAULT_EMIN = -10
DEFAULT_ROUND_PRECISION = 10

# Pandas
DEFAULT_MAX_ROWS = 400


[docs]class Config(object): """The Config object holds all configuration values for PyMBS, except for those related to Logging, which are stored in the ``pymbs.log`` module. See the documentation for that module in regards to the Logging settings. The user is able to customize settings via the ``config.yaml`` file, which is located in a subdirectory of the user's HOME directory, and is described further in the :ref:`setup_modeling` section of this documentation. Many of the settings in the config object *may* be specifed in environment variables. If a value for a setting is specified simultaneously in the ``config.yaml`` file *and* as an environment variable, the value from the environment variable will be used. As noted below, some of the properties set in the ``config`` object are actually attributes of the ``Context`` object in Python's ``Decimal`` module. For more information on the Decimal Context object, see: https://docs.python.org/3/library/decimal.html#decimal.Context Some of the properties set in the ``config`` object are actually options in the ``Pandas`` library. For more information, see: https://pandas.pydata.org/pandas-docs/stable/user_guide/options.html#available-options """ _platform = sys.platform _config_dir = 'pymbs' _config_file = 'config.yaml' _rel_config_path = Path(_config_dir, _config_file) _user_home_dir = Path.home() _config_file_mode = 0o774 if _platform != 'win32': _user_app_config_dir = Path(_user_home_dir, '.config') else: _user_app_config_dir = Path(_user_home_dir, 'AppData', 'Local') _pymbs_config_path = Path( _user_app_config_dir, _rel_config_path ) _ipython_active = False def __init__(self): Config._detect_ipython() _config = Config._load_config() # PyMBS Config self._project_dir = None self._terms_sheet = None self._model = {} self._cache = {} # Pandas Config self._precision = None self._emax = None self._emin = None self._round_precision = None self._max_rows = None self._configure(_config) ######################## ##### PyMBS Config ##### ######################## @property def project_dir(self): """The project_dir *must* be set, as it can not be known or discovered in advance. The project directory is a dicrecotry in the file system that contains one subdirectory for each deal modeled with PyMBS. By convention, each subdirectory is named using the name of the deal being modeled. For more information about the directory structure, see the :ref:`setup` section of this documentation. This may set in the ``config.yaml`` file, or via the ``PYMBS_PROJECT_DIR`` environment variable. """ return self._project_dir @project_dir.setter def project_dir(self, value): logger.info(f"Setting project_dir in config: {value}") self._project_dir = value @property def terms_sheet(self): """The terms_sheet value should *never* be set directly. This value is set by calling the ``api.load_deal(series_name)`` function, passing the name of the directory that holds the files for the deal. The ``load_deal`` function deserializes the ``series_ts.json`` file and converts all ``Float`` values into ``Decimal`` values, using Python's Decimal module. The Terms Sheet structure may be viewed as a Python dictionary by referencing the ``config.terms_sheet`` property. """ return self._terms_sheet @terms_sheet.setter def terms_sheet(self, value): if value: logger.info( f"Setting terms_sheet in config: " f"Series: {value['deal']['series_id']} | " f"TS Version: {value['deal']['ts_version']} | " f"Underwriter: {value['deal']['lead_underwriter']}" ) else: logger.info(f"Setting terms_sheet in config to None") self._terms_sheet = value @property def model(self): """The model value should *never* be set directly. This value is set by calling the ``api.load_model(model_json)`` function, passing the name of the JSON file that holds the model for the deal. The ``load_model`` function deserializes the JSON data into Python data types, for use in all subsequent function calls to compute the outputs of the model. The model structure may be viewed as a Python dictionary by referencing the ``config.model`` property. """ return self._model @property def cache(self): """The cache value should *never* be set directly. This Python dictionary is referenced and modified by private functions throughout the PyMBS code. The values in this dictionary change quicky when running the model, so viewing it by referencing the ``config.cache`` property may not make sense, except in a debugging context. """ return self._cache @property def round_precision(self): """round_precision is the number of decimals to round to, when rounding a decimal value. It is *not* part of the specification described in the Python Decimal Module, but is a custom value created and used by PyMBS. This value is used in the custom ``_round_dec()`` function, located in the ``pymbs.utils`` module. This may set in the ``config.yaml`` file, or via the ``PYMBS_ROUND_PRECISION`` environment variable. """ return self._round_precision @round_precision.setter def round_precision(self, value): logger.info(f"Setting round_precision in config: {value}") self._round_precision = value ################################# ##### Python Decimal Config ##### ################################# @property def precision(self): """prec[ision] is an attribute of the ``Context`` object in Python's Decimal module. The precision is an integer in the range [1, MAX_PREC] that sets the precision for arithmetic operations in the context. This may set in the ``config.yaml`` file, or via the ``PYMBS_PRECISION`` environment variable. """ return self._precision @precision.setter def precision(self, value): logger.info(f"Setting precision in config: {value}") self._precision = value @property def emax(self): """Emax is an attribute of the ``Context`` object in Python's Decimal module. The Emax field is an integer specifying the outer limit allowable for the max exponent. Emax must be in the range [0, MAX_EMAX]. This may set in the ``config.yaml`` file, or via the ``PYMBS_EMAX`` environment variable. """ return self._emax @emax.setter def emax(self, value): logger.info(f"Setting emax in config: {value}") self._emax = value @property def emin(self): """Emin is an attribute of the ``Context`` object in Python's Decimal module. The Emin field is an integer specifying the outer limit allowable for the min exponent. Emin must be in the range [MIN_EMIN, 0]. This may set in the ``config.yaml`` file, or via the ``PYMBS_EMIN`` environment variable. """ return self._emin @emin.setter def emin(self, value): logger.info(f"Setting emin in config: {value}") self._emin = value ######################### ##### Pandas Config ##### ######################### @property def max_rows(self): """This sets the maximum number of rows Pandas should output when printing out various output. For example, this value determines whether the repr() for a dataframe prints out fully or just a truncated or summary repr. ‘None’ value means unlimited. The Pandas default is 60. The PyMBS default for this value is 400, which was determined by rounding up from 360, which is the number of monthly periods in a 30-year cash flow. This may set in the ``config.yaml`` file, or via the ``PYMBS_MAX_ROWS`` environment variable. """ return self._max_rows @max_rows.setter def max_rows(self, value): logger.info(f"Setting max_rows in config: {value}") self._max_rows = value def _configure(self, _config): _env_project_dir = os.getenv('PYMBS_PROJECT_DIR') _env_precision = os.getenv('PYMBS_PRECISION') _env_emax = os.getenv('PYMBS_EMAX') _env_emin = os.getenv('PYMBS_EMIN') _env_round_precision = os.getenv('PYMBS_ROUND_PRECISION') _env_max_rows = os.getenv('PYMBS_MAX_ROWS') _file_project_dir = None _file_precision = None _file_emax = None _file_emin = None _file_round_precision = None _file_max_rows = None pymbs_config = _config.get('pymbs') if pymbs_config: _file_project_dir = pymbs_config.get('project directory') pandas_config = _config.get('pandas') if pandas_config: _file_precision = pandas_config.get('precision') _file_emax = pandas_config.get('emax') _file_emin = pandas_config.get('emin') _file_round_precision = pandas_config.get('round precision') _file_max_rows = pandas_config.get('max rows') if _env_project_dir and _file_project_dir: self.project_dir = _env_project_dir elif _env_project_dir: self.project_dir = _env_project_dir elif _file_project_dir: self.project_dir = _file_project_dir if _env_precision and _file_precision: self.precision = _env_precision elif _env_precision: self.precision = _env_precision elif _file_precision: self.precision = _file_precision else: self.precision = DEFAULT_PRECISION if _env_emax and _file_emax: self.emax = _env_emax elif _env_emax: self.emax = _env_emax elif _file_emax: self.emax = _file_emax else: self.emax = DEFAULT_EMAX if _env_emin and _file_emin: self.emin = _env_emin elif _env_emin: self.emin = _env_emin elif _file_emin: self.emin = _file_emin else: self.emin = DEFAULT_EMIN if _env_round_precision and _file_round_precision: self.round_precision = _env_round_precision elif _env_round_precision: self.round_precision = _env_round_precision elif _file_round_precision: self.round_precision = _file_round_precision else: self.round_precision = DEFAULT_ROUND_PRECISION if _env_max_rows and _file_max_rows: self.max_rows = _env_max_rows elif _env_max_rows: self.max_rows = _env_max_rows elif _file_max_rows: self.max_rows = _file_max_rows else: self.max_rows = DEFAULT_MAX_ROWS @classmethod def _detect_ipython(cls): """Detect if we are running inside IPython or not. """ try: cfg = get_ipython().config # noqa cls._ipython_active = True except NameError: cls._ipython_active = False @staticmethod def _load_config(): alt_config_path = os.getenv('PYMBS_CONFIG_PATH') if alt_config_path: _pymbs_config_path = alt_config_path else: _pymbs_config_path = Config._pymbs_config_path try: with open(_pymbs_config_path, 'r') as stream: data = yaml.full_load(stream) except FileNotFoundError: pymbs_config_dir = Path( Config._user_app_config_dir, Config._config_dir ) if not pymbs_config_dir.exists(): pymbs_config_dir.mkdir( mode=Config._config_file_mode, parents=True ) _pymbs_config_path.touch() handle_gracefully( Config._ipython_active, logger, 'no_config', pymbs_config_path=_pymbs_config_path, help_url=URL.SETUP_MODELING.value ) else: if data: return data else: handle_gracefully( Config._ipython_active, logger, 'no_config_data', pymbs_config_path=_pymbs_config_path, help_url=URL.SETUP_MODELING.value )
config = Config()