Set, read, and write program-wide properties in one location. Includes logging.
import logging
import sys
import platform
import networkx as nx
import json
import os
from django.core.exceptions import ObjectDoesNotExist

from IndrasNet.models import ABMModel

SWITCH = '-'
PERIODS = 'periods'
BASE_DIR = 'base_dir'
DATAFILE = 'datafile'

OS = "OS"

UTYPE = "user_type"
# user types (DUMMY type is only for automated test)
TERMINAL = "terminal"
IPYTHON = "iPython"
IPYTHON_NB = "iPython Notebook"
WEB = "Web browser"
DUMMY = "dummy"

VALUE = "val"
QUESTION = "question"
DEFAULT_VAL = "default_val"
ATYPE = "atype"
HIVAL = "hival"
LOWVAL = "lowval"

global user_type
user_type = TERMINAL

type_dict = {INT: int, FLT: float, BOOL: bool, STR: str}

def get_prop_from_env():
    global user_type
        user_type = os.environ[UTYPE]
    except KeyError:
# this can't be done before logging is set up!
#        logging.info("Environment variable user type not found")
        user_type = TERMINAL
    return user_type

def read_props(model_nm, file_nm):
    Create a new PropArgs object from a json file
    props = json.load(open(file_nm))
    return PropArgs.create_props(model_nm, props)

class Prop():
    Container for prop attributes.

    Attributes include:
        val - the value to be used in the model run
        question - a question prompt for the user's input for the prop value
        atype - the user's answer type (INT, DBL, BOOL, STR, etc.)
        default_val - the property's default value
        lowval - the lowest value val can take on
        hival - the highest value val can take on

    def __init__(self, val=None, question=None, atype=None, default_val=None,
                        lowval=None, hival=None):
        self.val = val
        self.question = question
        self.atype = atype
        self.default_val = default_val
        self.lowval = lowval
        self.hival = hival

    def to_json(self):
        return {"val": self.val,
                "question": self.question,
                "atype": self.atype,
                "default_val": self.default_val,
                "lowval": self.lowval,
                "hival": self.hival}

    def __str__(self):
        return str(self.val)

class PropArgs():
    This class holds named properties for program-wide values.
    It enables getting properties from a file, a database,
    or from the user, either via the command line or a prompt.

    def create_props(model_nm, prop_dict=None):
        Create a property object with values in 'props'.
        if prop_dict is None:
            prop_dict = {}
        return PropArgs(model_nm, prop_dict=prop_dict)

    def __init__(self, model_nm, logfile=None, prop_dict=None,
        Loads and sets properties in the following order:
        1. The Database
        2. The User's Environment (operating system, dev/prod settings, etc.)
        3. Property File
        4. Command Line
        5. Questions Prompts During Run-Time
        self.logfile = logfile
        self.model_nm = model_nm
        self.graph = nx.Graph()
        self.props = {}

        # 1. The Database

        # 2. The Environment

        # 3. Property File

        if self.props[UTYPE].val in (TERMINAL, IPYTHON, IPYTHON_NB):
            # 4. process command line args and set them as properties:

            # 5. Ask the user questions.

        elif self.props[UTYPE].val == WEB:
            self.props[PERIODS] = Prop(val=1)
            self.props[BASE_DIR] = Prop(val=os.environ[BASE_DIR])

        self.logger = Logger(self, model_name=model_nm, logfile=logfile)
        self.graph.add_edge(self, self.logger)

    def set_props_from_db(self):
            params = ABMModel.objects.get(name=self.model_nm).params.all()
            for param in params:
                atype = param.atype
                typed_default_val = self._type_val_if_possible(param.default_val,
                self.props[param.prop_name] = Prop(val=typed_default_val,
        except ObjectDoesNotExist:
            print("ABMModel not found in db: " + self.model_nm)

    def overwrite_props_from_env(self):
        global user_type
        user_type = get_prop_from_env()
        self.props[UTYPE] = Prop(val=user_type)
        self.props[OS] = Prop(val=platform.system())

    def overwrite_props_from_dict(self, prop_dict):
        General Dict:

                        val: <something>,
                        question: <something>,
                        atype: <something>,
                        val: <something>,
                        hival: <something>,
                        lowval: <something>,

        Simple Dict:

                prop_name: val,
                prop_name: val

        for prop_nm in prop_dict:
            if type(prop_dict[prop_nm]) is dict:
                atype = prop_dict[prop_nm].get(ATYPE, None)
                val = self._type_val_if_possible(prop_dict[prop_nm].get(VALUE,
                question = prop_dict[prop_nm].get(QUESTION, None)
                default_val = prop_dict[prop_nm].get(DEFAULT_VAL, None)
                hival = prop_dict[prop_nm].get(HIVAL, None)
                lowval = prop_dict[prop_nm].get(LOWVAL, None)
                self.props[prop_nm] = Prop(val=val, question=question, atype=atype, default_val=default_val,
                                           hival=hival, lowval=lowval)
                self[prop_nm] = prop_dict[prop_nm]

    def overwrite_props_from_cl(self):
        prop_nm = None
        for arg in sys.argv:
            # the first arg (-prop) names the property
            if arg.startswith(SWITCH):
                prop_nm = arg.lstrip(SWITCH)
            # the second arg is the property value
            if prop_nm is not None:
                self.props[prop_nm].val = arg
                prop_nm = None

    def overwrite_props_from_user(self):
        for prop_nm in self:
            if (hasattr(self.props[prop_nm], QUESTION)
                and self.props[prop_nm].question):
                self.props[prop_nm].val = self._keep_asking_until_correct(prop_nm)
    def _type_val_if_possible(val, atype):
        if atype in type_dict:
            type_cast = type_dict[atype]
            return type_cast(val)
            return val

    def _keep_asking_until_correct(self, prop_nm):
        atype = None
        if hasattr(self.props[prop_nm], ATYPE):
            atype = self.props[prop_nm].atype

        while True:
            answer = input(self.get_question(prop_nm))
            if not answer:
                return self.props[prop_nm].val

                typed_answer = self._type_val_if_possible(answer, atype)
            except ValueError:
                print("Input of invalid type. Should be {atype}"

            if not self._answer_within_bounds(prop_nm, typed_answer):
                print("Input must be between {lowval} and {hival} inclusive."

            return typed_answer

    def _answer_within_bounds(self, prop_nm, typed_answer):
        if (self.props[prop_nm].atype is None 
            or self.props[prop_nm].atype in (STR, BOOL)):
            return True

        if (self.props[prop_nm].lowval is not None 
            and self.props[prop_nm].lowval > typed_answer):
            return False

        if (self.props[prop_nm].hival is not None 
            and self.props[prop_nm].hival < typed_answer):
            return False

        return True

    def display(self):
        How to represent the properties on screen.
        ret = "Properties for " + self.model_nm + "\n"
        for prop_nm in self:
            ret += "\t" + prop_nm + ": " + str(self.props[prop_nm].val) + "\n"

        return ret

    def __iter__(self):
        return iter(self.props)

    def __str__(self):
        return self.display()

    def __len__(self):
        return len(self.props)

    def __contains__(self, key):
        return key in self.props

    def __setitem__(self, key, v):
        Set a property value.
        if key in self:
            self.props[key].val = v
            self.props[key] = Prop(val=v)

    def __getitem__(self, key):
        return self.props[key].val

    def __delitem__(self, key):
        del self.props[key]

    def items(self):
        return self.props.items()

    def get_logfile(self):
        Special get function for logfile name
        return self.props["log_fname"].val

    def write(self, file_nm):
        Write properties to json file.
        Useful for storing interesting parameter sets.
        dict_for_json = {}
        for prop_name in self.props:
            dict_for_json[prop_name] = self.props[prop_name].to_json()
        f = open(file_nm, 'w')
        json.dump(dict_for_json, f, indent=4)

    def to_json(self):
        return { prop_nm: self.props[prop_nm].to_json() for prop_nm in self.props }

    def get(self, key, default=None):
        if key in self.props and self.props[key].val:
            return self.props[key].val
        return default

    def get_question(self, prop_nm):
            return "{question} [{lowval}-{hival}] ({default}) "\

class Logger():
    A class to track how we are logging.

    DEF_FORMAT = '%(asctime)s:%(levelname)s:%(message)s'
    DEF_LEVEL = logging.INFO
    DEF_FILEMODE = 'w'
    # DEF_FILENAME = 'log.txt'

    def __init__(self, props, model_name, logfile=None,
        if logfile is None:
            logfile = model_name + ".log"
        fmt = props["log_format"] if "log_format" in props else Logger.DEF_FORMAT
        lvl = props["log_level"] if "log_level" in props else Logger.DEF_LEVEL
        fmd = props["log_fmode"] if "log_fmode" in props else Logger.DEF_FILEMODE
        props["log_fname"] = logfile
# we put the following back in once the model names are fixed
#  fnm = props.get("log_fname", logfile)
        logging.info("Logging initialized.")