{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Python for STEM Teachers<br/>[Oregon Curriculum Network](http://4dsolutions.net/ocn/)\n",
    "<div align=\"center\"><h3>Context Managers in Python</h3></div>\n",
    "\n",
    "Lets talk about \"context managers\" in Python.  \n",
    "\n",
    "The \"with context( ) as ctx:\" construct provides a way of doing setup and teardown behind the scenes.  \n",
    "\n",
    "The context.\\_\\_enter\\_\\_ method creates the context, whereas context.\\_\\_exit\\_\\_ cleans up the environment as we exit the scope.  \n",
    "\n",
    "A context is typically used to establish a connection with an ecosystem resource, such as a database.  The file type object returned by open( ) is also natively a context manager, self-closing upon \\_\\_exit\\_\\_.\n",
    "\n",
    "Lets start with a SQL_DB class that establishes a connection to a database and stores the resulting connection and cursor objects at the class level.  The Python DB API gives us the wherewithall to talk to any SQL engine, provided an appropriate driver has been imported.\n",
    "\n",
    "The Python standard library hase SQLite natively, giving us an ideal tool with which to prototype a generic SQL engine.  It may actually be enough for your needs in some cases."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "import time\n",
    "import sqlite3 as sql\n",
    "import os\n",
    "\n",
    "import sys\n",
    "sys.path.append(\"/Users/kurner/Documents/classroom_labs\")\n",
    "\n",
    "class NoConnection(Exception):\n",
    "    pass\n",
    "\n",
    "class SQL_DB:  # a database\n",
    "    \n",
    "    # class level parameters\n",
    "    backend  = 'sqlite3'\n",
    "    user_initials  = 'KTU'\n",
    "    timezone = int(time.strftime(\"%z\", time.localtime()))\n",
    "    target_path = \"/Users/kurner/Documents/classroom_labs\"  # current directory\n",
    "    db_name = os.path.join(target_path, 'glossary.db')\n",
    "\n",
    "    @staticmethod\n",
    "    def mod_date():\n",
    "        return time.mktime(time.gmtime())  # GMT time\n",
    "\n",
    "    @classmethod\n",
    "    def connect(cls):\n",
    "        try:\n",
    "            if cls.backend == 'sqlite3':\n",
    "                cls.conn = sql.connect(cls.db_name)  # connection\n",
    "                cls.curs = cls.conn.cursor()         # cursor\n",
    "            elif cls.backend == 'mysql': # not using this, gives idea\n",
    "                cls.conn = sql.connect(host='localhost',\n",
    "                                      user='root', port='8889')\n",
    "                cls.curs = cls.conn.cursor()\n",
    "                \n",
    "        except Exception:\n",
    "            raise NoConnection\n",
    "            \n",
    "    @classmethod\n",
    "    def disconnect(cls):\n",
    "        cls.conn.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So far, this class hasn't done any work.  We'll use the context manager to make the actual connection, using this DB object.\n",
    "\n",
    "Upon initialization, this context manager expects a database class as an argument.  Context managers may be created without arguments as well.  The \\_\\_enter\\_\\_ method is then immediately executed, and what it returns (maybe None) becomes the object named by the optional \"as extension\", in this case the object named dbx.\n",
    "\n",
    "Dot notation takes us into the self of the DBcontext instance (dbx) which in turn anchors db, the database class, which was passed in to \\_\\_init\\_\\_.  Inside the database class are the connection and cursor objects required by the DB API.\n",
    "\n",
    "DBcontext has no specific knowledge about internal tables, simply connects and disconnects.  We can subclass this generic class if wishing to add more database-specific methods.  We'll start with a Glossary subclass, then move to Elements."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "class DBcontext:\n",
    "    \"\"\"\n",
    "    Generic parent class for connecting and disconnecting\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, db):\n",
    "        self.db = db      # references database class\n",
    "        \n",
    "    def __enter__(self):\n",
    "        self.db.connect() \n",
    "        return self       # <-- for use inside with scope\n",
    "\n",
    "    def __exit__(self, *stuff_happens):\n",
    "        self.db.disconnect()\n",
    "        if stuff_happens[0]:\n",
    "            print(\"Exception raised!\")\n",
    "            print(stuff_happens)\n",
    "            return True # <-- if considered handled, otherwise False\n",
    "        return True\n",
    "\n",
    "class Glossary(DBcontext):\n",
    "    \"\"\"\n",
    "    Subclass with custom methods for this particular database\n",
    "    \"\"\"\n",
    "    \n",
    "    def create_table(self):\n",
    "        # https://www.sqlite.org/lang_droptable.html\n",
    "        self.db.curs.execute(\"\"\"DROP TABLE IF EXISTS Glossary\"\"\")\n",
    "        self.db.curs.execute(\"\"\"CREATE TABLE Glossary\n",
    "            (gl_term text PRIMARY KEY,\n",
    "             gl_definition text,\n",
    "             updated_at int,\n",
    "             updated_by text)\"\"\")\n",
    "\n",
    "    def save_term(self, *the_data):\n",
    "        query = (\"INSERT INTO Glossary \"\n",
    "        \"(gl_term, gl_definition, updated_at, updated_by) \"\n",
    "        \"VALUES ('{}', '{}', {}, '{}')\".format(*the_data))\n",
    "        # print(query)\n",
    "        self.db.curs.execute(query)\n",
    "        self.db.conn.commit()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the \"with scope\" below, we create a new table by calling a method saved inside the context manager itself. The a text file is read in and each line gets saved to a row in the Glossary table.  Again, the context manager has a method for inserting rows."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "with Glossary(SQL_DB) as dbx:  # <--- dbx returned by __enter__\n",
    "    \n",
    "    # for testing __exit__ in case of an exception\n",
    "    # raise NoConnection\n",
    "    \n",
    "    dbx.create_table()\n",
    "    FILE = os.path.join(dbx.db.target_path, \"glossary.txt\")\n",
    "    \n",
    "    with open(FILE, 'r', encoding='UTF-8') as gloss:\n",
    "        lines = gloss.readlines()\n",
    "\n",
    "    for line in lines:\n",
    "        if len(line.strip()) == 0:\n",
    "            continue\n",
    "        term, definition = line.split(\":\", 1)\n",
    "        right_now = dbx.db.mod_date()\n",
    "        dbx.save_term(term[2:].strip(), definition.strip(), right_now, dbx.db.user_initials)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now have a resuable context that will connect and disconnect from our database.  \n",
    "\n",
    "Lets use it to query the Glossary table..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                               GLOSSARY OF TERMS                                \n",
      "--------------------------------------------------------------------------------\n",
      "Term                                |Abbreviated Definition   \n",
      "--------------------------------------------------------------------------------\n",
      ".NET                                | a virtual machine and the target runtime plat\n",
      "AJAX                                | asynchronous JavaScript and XML.  Loosely des\n",
      "API                                 | Application Program Interface.  Specifies how\n",
      "ASCII                               | American Standard Code for Information Interc\n",
      "Agile                               | a set of practices and work flows designed to\n",
      "Apache                              | a free / open source web server, highly confi\n",
      "Apache Foundation                   | an umbrella organization under which many ope\n",
      "C                                   | the core language of operating systems develo\n",
      "C#                                  | a Java-like language targeting the .NET virtu\n",
      "C++                                 | C turned into an Object Oriented language.  M\n"
     ]
    }
   ],
   "source": [
    "with Glossary(SQL_DB) as dbx:\n",
    "    \n",
    "    query = \"SELECT gl_term, gl_definition FROM Glossary ORDER BY gl_term\"\n",
    "    \n",
    "    dbx.db.curs.execute(query)  # gets the data\n",
    "    \n",
    "    print(\"{:^80}\".format(\"GLOSSARY OF TERMS\"))\n",
    "    print(\"-\" * 80)\n",
    "    print(\"Term                                |Abbreviated Definition   \" )\n",
    "    print(\"-\" * 80)\n",
    "                           \n",
    "    for term in dbx.db.curs.fetchmany(10):  # fetchone(), fetchmany(n), fetchall()\n",
    "        print(\"{:35} | {:45}\".format(term[0], term[1][:45]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, that was fun. We were able to obtain a partial listing of what's in the Glossary table.  Connecting and disconnecting was handled behind the scenes.\n",
    "\n",
    "Now lets subclass DBcontext once again and add methods specific to a Periodic Table of the Elements."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "File: /Users/kurner/Documents/classroom_labs/periodic_table.json loaded.\n"
     ]
    }
   ],
   "source": [
    "import chem_stuff\n",
    "\n",
    "# modify database class to point to a different database file\n",
    "SQL_DB.db_name = os.path.join(SQL_DB.target_path, 'periodic_table.db')\n",
    "\n",
    "class ChemContext(DBcontext):\n",
    "    \"\"\"\n",
    "    Subclass with custom methods for this particular database\n",
    "    \"\"\"\n",
    "    \n",
    "    def create_table(self):\n",
    "        # https://www.sqlite.org/lang_droptable.html\n",
    "        self.db.curs.execute(\"\"\"DROP TABLE IF EXISTS Elements\"\"\")\n",
    "        self.db.curs.execute(\"\"\"CREATE TABLE Elements\n",
    "            (elem_protons int PRIMARY KEY,\n",
    "             elem_symbol text,\n",
    "             elem_long_name text,\n",
    "             elem_mass float,\n",
    "             elem_series text,\n",
    "             updated_at int,\n",
    "             updated_by text)\"\"\")\n",
    "\n",
    "    def save_term(self, *the_data):\n",
    "        query = (\"INSERT INTO Elements \"\n",
    "        \"(elem_protons, elem_symbol, elem_long_name, elem_mass, elem_series,\"\n",
    "        \"updated_at, updated_by) \"\n",
    "        \"VALUES ({}, '{}', '{}', {}, '{}', {}, '{}')\".format(*the_data))\n",
    "        # print(query)\n",
    "        self.db.curs.execute(query)\n",
    "        self.db.conn.commit()\n",
    "        \n",
    "with ChemContext(SQL_DB) as dbx:\n",
    "    \n",
    "    dbx.create_table()\n",
    "    \n",
    "    FILE = os.path.join(dbx.db.target_path, \"periodic_table.json\")\n",
    "\n",
    "    chem_stuff.load_elements(FILE)  # uses imported module to read JSON\n",
    "\n",
    "    for atom in chem_stuff.all_elements.values():\n",
    "        right_now = dbx.db.mod_date()\n",
    "        dbx.save_term(atom.protons, atom.symbol, atom.long_name, atom.mass, atom.series,\n",
    "                     right_now, dbx.db.user_initials)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To test that our data table has indeed been populated, lets connect again and run a select query.  We'll be able to print the rows directly.  We're not bothering with an Atom class this time, just publishing row data directly from tuples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                    PERIODIC TABLE OF THE ELEMENTS                    \n",
      "----------------------------------------------------------------------\n",
      "Symbol |Long Name             |Protons |Mass   |Series  \n",
      "----------------------------------------------------------------------\n",
      "H      | Hydrogen             |      1 |  1.01 | Other nonmetal \n",
      "He     | Helium               |      2 |  4.00 | Nobel gas      \n",
      "Li     | Lithium              |      3 |  6.94 | Alkali metal   \n",
      "Be     | Beryllium            |      4 |  9.01 | Alkaline earth metal\n",
      "B      | Boron                |      5 | 10.81 | Metalloid      \n",
      "C      | Carbon               |      6 | 12.01 | Noble gas      \n",
      "N      | Nitrogen             |      7 | 14.01 | Other nonmetal \n",
      "O      | Oxygen               |      8 | 16.00 | Other nonmetal \n",
      "F      | Fluorine             |      9 | 19.00 | Metalloid      \n",
      "Ne     | Neon                 |     10 | 20.18 | Noble gas      \n",
      "Na     | Sodium               |     11 | 22.99 | Alkali metal   \n",
      "Mg     | Magnesium            |     12 | 24.30 | Alkaline earth metal\n",
      "Al     | Aluminum             |     13 | 26.98 | Post-transition metal\n",
      "Si     | Silicon              |     14 | 28.09 | Metalloid      \n",
      "P      | Phosphorous          |     15 | 30.97 | Other nonmetal \n",
      "S      | Sulfur               |     16 | 32.06 | Other nonmetal \n",
      "Cl     | Chlorine             |     17 | 35.45 | Halogen        \n",
      "Ar     | Argon                |     18 | 39.95 | Nobel gas      \n",
      "K      | Potassium            |     19 | 39.10 | Alkali metal   \n"
     ]
    }
   ],
   "source": [
    "with DBcontext(SQL_DB) as dbx:  # <--- dbx returned by __enter__\n",
    "    \n",
    "    query = (\"SELECT elem_symbol, elem_long_name, elem_protons, elem_mass, elem_series\" \n",
    "    \" FROM Elements ORDER BY elem_protons\")\n",
    "    dbx.db.curs.execute(query)\n",
    "    \n",
    "    print(\"{:^70}\".format(\"PERIODIC TABLE OF THE ELEMENTS\"))\n",
    "    print(\"-\" * 70)\n",
    "    print(\"Symbol |Long Name             |Protons |Mass   |Series  \" )\n",
    "    print(\"-\" * 70)\n",
    "   \n",
    "    for the_atom in dbx.db.curs.fetchall():\n",
    "        \n",
    "        print(\"{:6} | {:20} | {:6} | {:5.2f} | {:15}\".format(the_atom[0],\n",
    "                          the_atom[1],\n",
    "                          the_atom[2],\n",
    "                          the_atom[3],\n",
    "                          the_atom[4]))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.1"
  },
  "widgets": {
   "state": {},
   "version": "1.1.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}