8
\$\begingroup\$

I have built Python objects to simulate a one-to-many relationship and then store the object in a MongoDB instance. I am using lots of composition to achieve what I want. The code works perfectly fine but I get the feeling that the overall design can be improved a lot.

The problem statement

  1. Each Portfolio can have zero or many Models.
  2. Each Model can have one or many Scenarios.
  3. When I create a new Model, by default one Scenario should be created and that scenario is called the baseline scenario.

The approach I used is very simple and it works for now. The output expected is a dictionary with all these entities modeled properly to store the entire document in the MongoDB instance.

import datetime
from bson.objectid import ObjectId
import time

from pymongo import MongoClient

# MongoDB settings for Development stage
client = MongoClient()
db = client['test']
portfolios = db['portfolios']


class PortfolioModels:
    def __init__(self, model_name, parent_id):
        """
        This class will be used mostly as the composition object for Portfolio class.
        :param model_name: The name of the model.
        :param parent_id: The portfolio id i.e self.if from the Portfolio object to keep track of the lineages.
        :return: Initializes all the attributes for PortfolioModels.

        For :self.id:, :self.created_date: and :self.updated_date: see the constructor of :Portfolio: class.
        :self.model_scenarios: by default has the predefined attributes attached when the new PortfolioModel creates.

        This is the intended behaviour because of our requirements i.e each model should have at least a single
        scenario when a new model is created. It keeps track of the parent Id for proper graph lineages.
        """
        self.id = str(ObjectId())
        self.portfolio_id = parent_id
        self.model_name = model_name
        self.created_date = time.mktime(ObjectId().generation_time.timetuple()) * 1000
        self.updated_date = time.mktime(datetime.datetime.utcnow().timetuple()) * 1000
        self.model_scenarios = [ModelScenarios(self.id).flatten]

    def add_model_scenario(self, scenario_name):
        """
        Adds new scenario to the single model instance.
        :param scenario_name: Name of the scenario.
        :return: Appends the newly created ModelScenario object to :self.model_scenarios: and
        sets the flag :is_baseline_model: to False because when this method is called, there will be already
        one scenario present in the :self.model_scenarios: attribute and since ModelScenarios can have only one
        baseline scenario, it automatically sets the :is_baseline_scenario: to False.
        """
        self.model_scenarios.append(ModelScenarios(scenario_name=scenario_name,
                                                   portfolio_model_id=self.id,
                                                   is_baseline_scenario=False).flatten)

    @property
    def flatten(self):
        """
        This is used to flush out the results after computing as a dict.
        :return: A dictionary of attributed fetched from respective calls.
        """
        return {
            "_id": self.id,
            "PortfolioId": self.portfolio_id,
            "ModelName": self.model_name,
            "CreateDate": self.created_date,
            "LastUpdateDate": self.updated_date,
            "ModelScenarios": self.model_scenarios
        }


class Portfolios:
    def __init__(self, portfolio_name):
        """
        The main outer wrapper for the entire suite of embedded objects, something like a outer most folder.
        :param portfolio_name: The name of the portfolio.
        :return: Initializes all the attributes.

        :self.id: Converts the ObjectId() like ObjectId("557ab89188172b78c46b17a9") to string representation of
        ObjectId i.e "557ab89188172b78c46b17a9". Referenced in Issue #3 in GitHub.

        :self.last_updated_date: Converts the Python datetime to JavaScript compatible datetime.
        >>> d = datetime.datetime.utcnow()
        >>> d
        datetime.datetime(2015, 6, 12, 10, 51, 18, 891722)
        >>> time.mktime(d.timetuple()) * 1000
        1434086616000.0

        Note: Javascript's Date() takes milliseconds as an argument. Python's uses seconds.
        You have to multiply by 1000.

        In JavaScript, we can use the above Python output to convert it to the JavaScript Date object.

        E.g: new Date(1434086616000.0)
        """
        self.id = str(ObjectId())
        self.portfolio_name = portfolio_name
        self.last_updated_date = time.mktime(datetime.datetime.utcnow().timetuple()) * 1000
        self.recent_activity = list()
        self.portfolio_models = list()

    def add_portfolio_model(self, model_name):
        """
        Adds the new portfolio model to :self.portfolio_models: attribute attached to the Portfolio object.
        :param model_name: Name of the Model
        :return: The new model gets appended to the :self.portfolios_model: along with the parent id
        i.e the id of the portfolio and returns the output as dictionary i.e the new portfolio model will be appended
        like a dictionary generated by the :flatten: property.
        """
        self.portfolio_models.append(PortfolioModels(model_name, self.id).flatten)

    def add_recent_activity(self, activity):
        """
        Adds new activity to the :self.recent_activity:
        :param activity: The name of activity.
        :return: Appends new activity the Portfolio object.
        """
        self.recent_activity.append(activity)

    @property
    def expose(self):
        """
        This is used to flush out the results after computing as a dict.
        :return: A dictionary of attributed fetched from respective calls.
        """
        return {
            "_id": self.id,
            "PortfolioName": self.portfolio_name,
            "RecentActivity": self.recent_activity,
            "LastUpdateDate": self.last_updated_date,
            "PortfolioModels": self.portfolio_models
        }


class ModelScenarios:
    def __init__(self, portfolio_model_id, scenario_name='Baseline Scenario', is_baseline_scenario=True):
        """
        This is the composition class for the :PortfolioModels: class.
        :param portfolio_model_id: The parent id of PortfolioModels object to keep track of objects.
        :param scenario_name: Name of scenario.
        :param is_baseline_scenario: Boolean Value to set the baseline scenario.
        :return: Initializes the object attributes.
        """
        self.id = str(ObjectId())
        self.portfolio_model_id = portfolio_model_id
        self.scenario_name = scenario_name
        self.created_date = time.mktime(ObjectId().generation_time.timetuple()) * 1000
        self.last_updated_date = time.mktime(datetime.datetime.utcnow().timetuple()) * 1000
        self.is_baseline_scenario = is_baseline_scenario

    @property
    def flatten(self):
        """
        This is used to flush out the results after computing as a dict.
        :return: A dictionary of attributed fetched from respective calls.
        """
        return {
            '_id': self.id,
            'PortfolioModelId': self.portfolio_model_id,
            'ScenarioName': self.scenario_name,
            'CreateDate': self.created_date,
            'LastUpdateDate': self.last_updated_date,
            'IsBaselineScenario': self.is_baseline_scenario
        }


if __name__ == '__main__':
    bb = Portfolios("BB Visa")

    cc = PortfolioModels("BB Visa Model", bb.id)
    cc.add_model_scenario("High Fico Scenario")

    dd = PortfolioModels("New Test Model", bb.id)

    bb.portfolio_models.append(cc.flatten)
    bb.portfolio_models.append(dd.flatten)

    ff = Portfolios("New BB Portfolio")

    portfolios.drop()
    portfolios.insert(bb.expose)

    portfolios.insert(ff.expose)

The output produced is shown below.

[
        {
            "RecentActivity": [],
            "PortfolioModels": [
                {
                    "_id": "557a882e5dddb71b9c51cf73",
                    "PortfolioId": "557a882e5dddb71b9c51cf72",
                    "LastUpdateDate": 1434073814000,
                    "CreateDate": 1434073814000,
                    "ModelName": "BB Visa Model",
                    "ModelScenarios": [
                        {
                            "ScenarioName": "Baseline Scenario",
                            "PortfolioModelId": "557a882e5dddb71b9c51cf73",
                            "_id": "557a882e5dddb71b9c51cf75",
                            "LastUpdateDate": 1434073814000,
                            "IsBaselineScenario": true,
                            "CreateDate": 1434073814000
                        },
                        {
                            "ScenarioName": "High Fico Scenario",
                            "PortfolioModelId": "557a882e5dddb71b9c51cf73",
                            "_id": "557a882e5dddb71b9c51cf77",
                            "LastUpdateDate": 1434073814000,
                            "IsBaselineScenario": false,
                            "CreateDate": 1434073814000
                        }
                    ]
                },
                {
                    "_id": "557a882e5dddb71b9c51cf79",
                    "PortfolioId": "557a882e5dddb71b9c51cf72",
                    "LastUpdateDate": 1434073814000,
                    "CreateDate": 1434073814000,
                    "ModelName": "New Test Model",
                    "ModelScenarios": [
                        {
                            "ScenarioName": "Baseline Scenario",
                            "PortfolioModelId": "557a882e5dddb71b9c51cf79",
                            "_id": "557a882e5dddb71b9c51cf7b",
                            "LastUpdateDate": 1434073814000,
                            "IsBaselineScenario": true,
                            "CreateDate": 1434073814000
                        }
                    ]
                }
            ],
            "_id": "557a882e5dddb71b9c51cf72",
            "PortfolioName": "BB Visa",
            "LastUpdateDate": 1434073814000
        },
        {
            "RecentActivity": [],
            "PortfolioModels": [],
            "_id": "557a882e5dddb71b9c51cf7d",
            "PortfolioName": "New BB Portfolio",
            "LastUpdateDate": 1434073814000
        }
    ]
\$\endgroup\$
1
  • \$\begingroup\$ It seems reasonable, without me knowing anything about MongoDB. I would suggest that you replace list() with []. \$\endgroup\$
    – Reinderien
    Aug 1, 2015 at 5:39

1 Answer 1

3
\$\begingroup\$

Don't set Portfolios._id yourself. If you insert() a dictionary without the key _id into a collection, mongodb will take care of creating an ObjectId on its own. The way you are doing it, you only store the string representation of the ObjectId and basically only have a string as your _id and lose some basic functionality.

Don't convert datetime.datetime objects to epoch timestamps to store them on fields like Portfolios.last_updated_date. pymongo automatically converts datetime.datetime objects to ISODate() and the other way around. That way you always have a human readable timestamp and easier ways to query your data.

You are composing the complete Portfolios-Object (including child-Models) first and then insert it into a collection. This seems fine and correct, but the fields PortfolioModels.id, PortfolioModels.portfolio_d, ModelScenarios.id and ModelScenarios.portfolio_model_id seem redundant, because the parent-child-relationships are already modeled by structure. This is the beauty of object-oriented databases. :)

By adding .flattened ModelScenarios to PortfolioModels and PortfolioModels to Portfolios, you mix your Models with the dict-representation of your models that get written to the mongodb. I would recommend keeping everything in your own format until just before writing to the mongo, so you are not losing the functionality of your classes.

.flatten and .expose do basicly the same thing so there is no reason to give them different names. Basically they are .to_dict(), right? It also might also be a great idea to write .from_dict() methods. So in the end you can easily transform what you get from a mongo to your models and the other way around.

Last but not least some naming advice:

  • Make your class names singular. Portfolios() creates one object which seems weird.
  • Try avoiding "code-stuttering". I guess all the code is in one module which may be in a package, so the namespace should make clear those 3 classes belong together. Ask yourself if you lose or gain readability by renaming (only a few examples, there are more of this):
    • Portfolio, PortfolioModel, ModelCenario => Portfolio, Model, Scenario
    • Portfolio.portfolio_name => Portfolio.name
    • PortfolioModel.model_name => Model.name
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.