{ "cells": [ { "cell_type": "markdown", "metadata": { "collapsed": true, "pycharm": { "name": "#%% md\n" } }, "source": [ "Mapped Types\n", "============\n", "\n", "Mapped types are Python types that don't inherit from mincePy classes and can therefore exist entirely independently.\n", "This is extremely useful if, for example, you are using objects from someone else's that you can't change or you choose not to change because it is also used independently of a database.\n", "\n", "Using type helpers\n", "------------------\n", "\n", "Let's demonstrate with a Car object" ] }, { "cell_type": "code", "execution_count": 1, "outputs": [], "source": [ "class Car:\n", " def __init__(self, make='ferrari', colour='red'):\n", " self.make = make\n", " self.colour = colour\n", "\n", " def __str__(self):\n", " return \"{} {}\".format(self.colour, self.make)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "So far, mincePy can't do anything with this:" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 2, "outputs": [ { "ename": "TypeError", "evalue": "Type is incompatible with the historian: Car", "output_type": "error", "traceback": [ "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", "\u001B[0;31mTypeError\u001B[0m Traceback (most recent call last)", "\u001B[0;32m\u001B[0m in \u001B[0;36m\u001B[0;34m\u001B[0m\n\u001B[1;32m 3\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 4\u001B[0m \u001B[0mferrari\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mCar\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m----> 5\u001B[0;31m \u001B[0mhistorian\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0msave\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mferrari\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 6\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", "\u001B[0;32m~/src/mincepy/mincepy/historians.py\u001B[0m in \u001B[0;36msave\u001B[0;34m(self, *objs)\u001B[0m\n\u001B[1;32m 187\u001B[0m \u001B[0;32mwith\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0min_transaction\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 188\u001B[0m \u001B[0;32mfor\u001B[0m \u001B[0mentry\u001B[0m \u001B[0;32min\u001B[0m \u001B[0mto_save\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 189\u001B[0;31m \u001B[0mids\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mappend\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0msave_one\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m*\u001B[0m\u001B[0mentry\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 190\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 191\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mlen\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mobjs\u001B[0m\u001B[0;34m)\u001B[0m \u001B[0;34m==\u001B[0m \u001B[0;36m1\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", "\u001B[0;32m~/src/mincepy/mincepy/historians.py\u001B[0m in \u001B[0;36msave_one\u001B[0;34m(self, obj, meta)\u001B[0m\n\u001B[1;32m 211\u001B[0m \u001B[0;31m# Save the object and metadata\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 212\u001B[0m \u001B[0;32mwith\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0min_transaction\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 213\u001B[0;31m \u001B[0mrecord\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_save_object\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mobj\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_live_depositor\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 214\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mmeta\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 215\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mmeta\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mupdate\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mrecord\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mobj_id\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmeta\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", "\u001B[0;32m~/src/mincepy/mincepy/historians.py\u001B[0m in \u001B[0;36m_save_object\u001B[0;34m(self, obj, depositor)\u001B[0m\n\u001B[1;32m 783\u001B[0m \u001B[0mhelper\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_ensure_compatible\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mtype\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mobj\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 784\u001B[0m \u001B[0;32mexcept\u001B[0m \u001B[0mTypeError\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 785\u001B[0;31m raise TypeError(\n\u001B[0m\u001B[1;32m 786\u001B[0m \"Type is incompatible with the historian: {}\".format(type(obj).__name__)) from None\n\u001B[1;32m 787\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", "\u001B[0;31mTypeError\u001B[0m: Type is incompatible with the historian: Car" ] } ], "source": [ "import mincepy\n", "historian = mincepy.connect('mongodb://127.0.0.1/mince-mapped-types', use_globally=True)\n", "\n", "ferrari = Car()\n", "historian.save(ferrari)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "To tell mincePy about `Car`s we need to define subclass of [TypeHelper](../apidoc.rst#mincepy.TypeHelper) which helps mincePy to understand your type...understandably..." ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 3, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "5f75cf4dc5e3bf28a7a85d9c\n" ] } ], "source": [ "import uuid\n", "\n", "class CarHelper(mincepy.TypeHelper):\n", " TYPE = Car\n", " TYPE_ID = uuid.UUID('21605412-30e5-4f48-9f56-f0fa8014e746')\n", " make = mincepy.field()\n", " colour = mincepy.field()\n", "\n", "historian.register_type(CarHelper)\n", "ferrari_id = historian.save(ferrari)\n", "print(ferrari_id)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "...and that's it! MincePy can now work with `Car`s.\n", "You'll notice that, unlike many ORMs, we haven't specified the types of `make` and `colour`, nor any validation options like the maximum length of the strings or whether they can be missing or not.\n", "This is deliberate.\n", "MincePy leaves validation up to your code (so you do whatever you would have done if there was no database involved) and concerns itself with getting your object in and out of the database.\n", "Speaking of which, let's see some of that in action." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 4, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "red ferrari\n" ] } ], "source": [ "del ferrari\n", "loaded_ferrari = historian.load(ferrari_id)\n", "print(loaded_ferrari)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "Cool, so how does that work?\n", "Well mincePy has created a [DataRecord](../apidoc.rst#mincepy.DataRecord) of our `Car` in the database that stores a bunch of things, including the state which can be used to recreate it.\n", "Let's have a look:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 5, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "obj_id 5f75cf4dc5e3bf28a7a85d9c\n", "type_id 21605412-30e5-4f48-9f56-f0fa8014e746\n", "creation_time 2020-10-01 14:45:01.673000\n", "version 0\n", "state {'make': 'ferrari', 'colour': 'red'}\n", "state_types [[[], UUID('21605412-30e5-4f48-9f56-f0fa8014e746')]]\n", "snapshot_hash 17480f325c8a48d9a5ea1163fcda3ff3cf0940deff21e7df6c7a72b5b626bf69\n", "snapshot_time 2020-10-01 14:45:01.673000\n", "extras {'_user': 'martin', '_hostname': 'deca'}\n" ] } ], "source": [ "print(historian.records.get(ferrari_id))\n" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "In addition to the state we see the creation and snapshots times, the version number and other information mincePy needs to store and track the object.\n", "\n", "Let's create some more `Car`s and perform some queries." ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 6, "outputs": [ { "data": { "text/plain": "10" }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "for make in 'skoda', 'honda', 'bmw':\n", " for colour in 'red', 'green', 'violet':\n", " historian.save(Car(make=make, colour=colour))\n", "\n", "historian.find().count()" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "We can, for example, find all the red ones using:" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 7, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "red ferrari\n", "red skoda\n", "red honda\n", "red bmw\n" ] } ], "source": [ "results = historian.find(CarHelper.colour == 'red')\n", "for entry in results:\n", " print(entry)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "References\n", "----------\n", "\n", "The next thing we may want to introduce is references.\n", "What if we have an object like this:" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 8, "outputs": [], "source": [ "class Person:\n", " def __init__(self, name, car=None):\n", " self.name = name\n", " self.car = car\n", "\n", " def __str__(self):\n", " return self.name if self.car is None else self.name + \"({})\".format(self.car)\n", "\n", "matt = Person('matt', loaded_ferrari)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "Here we want `Person` objects to be able to store a reference (a foreign key in ORM language) to the `Car` that they own.\n", "No problem, let's define a new helper:" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "source": [ "class PersonHelper(mincepy.TypeHelper):\n", " TYPE = Person\n", " TYPE_ID = uuid.UUID('80c7bedb-9e51-48cd-afa9-04ec97b20569')\n", " name = mincepy.field()\n", " car = mincepy.field(ref=True)\n", "\n", "historian.register_type(PersonHelper)\n", "matt_id = historian.save(matt)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } }, "execution_count": 9, "outputs": [] }, { "cell_type": "markdown", "source": [ "By using setting `ref=True` we tell mincePy that we want to the `car` field to be stored by reference rather than keeping a copy of the car in the record.\n", "Let's have a look:" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 10, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "obj_id 5f75cf51c5e3bf28a7a85da6\n", "type_id 80c7bedb-9e51-48cd-afa9-04ec97b20569\n", "creation_time 2020-10-01 14:45:05.364000\n", "version 0\n", "state {'name': 'matt', 'car': {'obj_id': ObjectId('5f75cf4dc5e3bf28a7a85d9c'), 'version': 0}}\n", "state_types [[[], UUID('80c7bedb-9e51-48cd-afa9-04ec97b20569')], [['car'], UUID('633c7035-64fe-4d87-a91e-3b7abd8a6a28')]]\n", "snapshot_hash 963c248f43a2cc8ff187c18e23b815f1f40df5a89ca2858346150cb6d0226a0a\n", "snapshot_time 2020-10-01 14:45:05.364000\n", "extras {'_user': 'martin', '_hostname': 'deca'}\n" ] } ], "source": [ "print(historian.records.get(matt_id))" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "We see that the `car` field in the `state` dictionary is in fact a reference pointing to the object id of the Ferrari.\n", "What does this all mean in practice?\n", "Well let's see what happens when we load the `matt` object from the database:" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 11, "outputs": [ { "data": { "text/plain": "True" }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "del matt\n", "loaded_matt = historian.load(matt_id)\n", "\n", "loaded_matt.car is loaded_ferrari" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "If we add another `Person` referring to the Ferrari we see that they share a reference to the same instance, as expected." ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 12, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n", "yellow ferrari\n" ] } ], "source": [ "rob = Person('rob', loaded_ferrari)\n", "rob_id = historian.save(rob)\n", "\n", "del rob, loaded_matt\n", "loaded_ferrari.colour = 'yellow'\n", "historian.save(loaded_ferrari)\n", "del loaded_ferrari\n", "\n", "matt = historian.get(matt_id)\n", "rob = historian.get(rob_id)\n", "\n", "print(matt.car is rob.car)\n", "print(matt.car)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "So, that gets you up to speed on the basics of using mapped types in mincePy.\n", "Have a look at the [API reference](../apidoc.rst) and post an issue [here](https://github.com/muhrin/mincepy/issues>) if there is anything else you would like to see documented.\n", "\n" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 0 }