From 78ad44bbb6703dedee2f31c34eb863b9fe870d53 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sun, 9 Apr 2017 17:42:00 -0400
Subject: [PATCH 01/35] added support for optional args for parity with newest
 C library

---
 cs50/cs50.py    | 40 +++++++++++++++++++++++++++-------------
 setup.py        |  2 +-
 test/python2.py |  2 --
 test/python3.py |  2 --
 4 files changed, 28 insertions(+), 18 deletions(-)

diff --git a/cs50/cs50.py b/cs50/cs50.py
index 657f405..d9c2923 100644
--- a/cs50/cs50.py
+++ b/cs50/cs50.py
@@ -20,20 +20,23 @@ def write(self, x):
 sys.stderr = flushfile(sys.stderr)
 sys.stdout = flushfile(sys.stdout)
 
-def get_char():
+def get_char(prompt=None):
     """Read a line of text from standard input and return the equivalent char."""
     while True:
-        s = get_string()
+        s = get_string(prompt)
         if s is None:
             return None
         if len(s) == 1:
             return s[0]
-        print("Retry: ", end="")
 
-def get_float():
+        # temporarily here for backwards compatibility
+        if prompt is None:
+            print("Retry: ", end="")
+
+def get_float(prompt=None):
     """Read a line of text from standard input and return the equivalent float."""
     while True:
-        s = get_string()
+        s = get_string(prompt)
         if s is None:
             return None
         if len(s) > 0 and re.search(r"^[+-]?\d*(?:\.\d*)?$", s):
@@ -41,12 +44,15 @@ def get_float():
                 return float(s)
             except ValueError:
                 pass
-        print("Retry: ", end="")
 
-def get_int():
+        # temporarily here for backwards compatibility
+        if prompt is None:
+            print("Retry: ", end="")
+
+def get_int(prompt=None):
     """Read a line of text from standard input and return the equivalent int."""
     while True:
-        s = get_string();
+        s = get_string(prompt);
         if s is None:
             return None
         if re.search(r"^[+-]?\d+$", s):
@@ -56,13 +62,16 @@ def get_int():
                     return i
             except ValueError:
                 pass
-        print("Retry: ", end="")
+
+        # temporarily here for backwards compatibility
+        if prompt is None:
+            print("Retry: ", end="")
 
 if sys.version_info.major != 3:
-    def get_long():
+    def get_long(prompt=None):
         """Read a line of text from standard input and return the equivalent long."""
         while True:
-            s = get_string();
+            s = get_string(prompt)
             if s is None:
                 return None
             if re.search(r"^[+-]?\d+$", s):
@@ -70,11 +79,16 @@ def get_long():
                     return long(s, 10)
                 except ValueError:
                     pass
-            print("Retry: ", end="")
 
-def get_string():
+            # temporarily here for backwards compatibility
+            if prompt is None:
+                print("Retry: ", end="")
+
+def get_string(prompt=None):
     """Read a line of text from standard input and return it as a string."""
     try:
+        if prompt is not None:
+            print(prompt, end="")
         s = sys.stdin.readline()
         return re.sub(r"(?:\r|\r\n|\n)$", "", s)
     except ValueError:
diff --git a/setup.py b/setup.py
index cee2266..09c4c5e 100644
--- a/setup.py
+++ b/setup.py
@@ -15,5 +15,5 @@
     name="cs50",
     packages=["cs50"],
     url="https://github.com/cs50/python-cs50",
-    version="1.3.0"
+    version="2.0.0"
 )
diff --git a/test/python2.py b/test/python2.py
index 10f19e4..69de822 100644
--- a/test/python2.py
+++ b/test/python2.py
@@ -1,6 +1,4 @@
 import cs50
 
-from cs50 import SQL
-
 l = cs50.get_long()
 print(l)
diff --git a/test/python3.py b/test/python3.py
index efef779..b545b98 100644
--- a/test/python3.py
+++ b/test/python3.py
@@ -1,6 +1,4 @@
 import cs50
-from cs50 import SQL
 
 i = cs50.get_int()
 print(i)
-

From ea74fd7a49c517a04ddae0366fa626c79f0eb058 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Fri, 14 Apr 2017 13:02:41 +0200
Subject: [PATCH 02/35] deploying on master and simplified makefile

---
 .travis.yml | 24 +++++-------------------
 Makefile    | 11 -----------
 2 files changed, 5 insertions(+), 30 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index b766e01..4e8c279 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,24 +11,10 @@ before_deploy: pip install twine
 deploy:
 
   # create github release
-  - provider: releases
-
-    # GitHub access token
-    api_key:
-      secure: "pwBn1lky58GHp1qR4i0oSZyOJkGMTvRzt0EfESVtl2ZbRTVUE7UFQbk/cL1002zMOUkJ4c5IjRCg95NJZWUvOSHwt4cOyqDVzi/S6AqACOlOhfnkw3S6oLGgRT3IlXK2ng0Viiqc1+BlVdwwXURKyobirqFgr4MAlb5kh75WmV9Xs4GwIS+qPq9luv38Bls2US/mNt5KRV1DiePr2ZSCqFESFfoIz+QKhZtVdynEF4jJwevEwP4HrCoT3guIJlXcWhOG37n+e8S4YLwg+k3yYeQTmR/QMgjmQLwEBZ6v9bNjqXM3CMtn3KUryDzcp5Z5+Vv1p1uoDbmuK+Ll5nQttAp/gARk+IWZ/xWc8MuQpFvjRzafbtPiF7ZlqaYh1wCREuZTWDAk/UJgQxb81v0jo0iAPyk9HMfgK2CJuU8wDwraKZ5dKk4y45Zww1gSSzpJJ8xSrylKPn7Wnft617Lnc+O0X6DnIAFtHDAPu/lPFaaokn1TN9AOPXoxb2cEeh+oDcUQD4zZG6Ukvh9+Hw8XiFBG+jEm6ekCvawTjnlZmBIw8YPJKEjrZv8LWfKhnVebRbmehawmnrZxUALCp39EjrcsIltYw4gefbd/Z9kIr8r3yVZfuq7U6vd8PBuCiDZHlKM1Lz4Ns24WK96nYe6V9Lt3WUERh6xt8JtuFrNHBiQ="
-
-    # enable wildcards in filenames
-    file_glob: true
-
-    # upload sdist
-    file: dist/*
-
-    # avoid stashing sdist
-    skip_cleanup: true
-
-    # create releases on tags only
+  - provider: script
+    script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\", \"target_commitish\": \"$TRAVIS_COMMIT\", \"name\": \"v$(python setup.py --version)\" }" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases'
     on:
-      tags: true
+      branch: master
 
   # deploy to PyPI
   - provider: script
@@ -39,6 +25,6 @@ deploy:
     # avoid stashing sdist
     skip_cleanup: true
 
-    # deploy on tags only
+    # deploy on commits to master
     on:
-      tags: true
+      branch: master
diff --git a/Makefile b/Makefile
index 415b084..063b9de 100644
--- a/Makefile
+++ b/Makefile
@@ -9,14 +9,3 @@ clean:
 .PHONY: install
 install: build
 	pip install dist/*.tar.gz
-
-.PHONY: push
-push:
-	git push origin "v$$(python setup.py --version)"
-
-.PHONY: release
-release: tag push
-
-.PHONY: tag
-tag:
-	git tag "v$$(python setup.py --version)"

From a6a43ed3547b1f0a6842d90efc092934a650461c Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Tue, 18 Apr 2017 04:25:55 +0200
Subject: [PATCH 03/35] disabled tag builds

---
 .travis.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index b766e01..003bda6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,6 +2,10 @@ language: python
 
 python: "3.4"
 
+# don't build tags
+branches:
+  except: /^v\d/
+
 # build sdist
 script: make build
 

From d8a74a34fd5ce16c23b8a13b0dbbbc96d530bb19 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 29 Apr 2017 11:47:52 -0400
Subject: [PATCH 04/35] returning None for EOF, updated comments

---
 cs50/cs50.py | 33 ++++++++++++++++++++++++++++-----
 1 file changed, 28 insertions(+), 5 deletions(-)

diff --git a/cs50/cs50.py b/cs50/cs50.py
index d9c2923..edcb52d 100644
--- a/cs50/cs50.py
+++ b/cs50/cs50.py
@@ -21,7 +21,11 @@ def write(self, x):
 sys.stdout = flushfile(sys.stdout)
 
 def get_char(prompt=None):
-    """Read a line of text from standard input and return the equivalent char."""
+    """
+    Read a line of text from standard input and return the equivalent char;
+    if text is not a single char, user is prompted to retry. If line can't
+    be read, return None.
+    """
     while True:
         s = get_string(prompt)
         if s is None:
@@ -34,7 +38,11 @@ def get_char(prompt=None):
             print("Retry: ", end="")
 
 def get_float(prompt=None):
-    """Read a line of text from standard input and return the equivalent float."""
+    """
+    Read a line of text from standard input and return the equivalent float
+    as precisely as possible; if text does not represent a double, user is
+    prompted to retry. If line can't be read, return None.
+    """
     while True:
         s = get_string(prompt)
         if s is None:
@@ -50,7 +58,11 @@ def get_float(prompt=None):
             print("Retry: ", end="")
 
 def get_int(prompt=None):
-    """Read a line of text from standard input and return the equivalent int."""
+    """
+    Read a line of text from standard input and return the equivalent int;
+    if text does not represent an int, user is prompted to retry. If line
+    can't be read, return None.
+    """
     while True:
         s = get_string(prompt);
         if s is None:
@@ -69,7 +81,11 @@ def get_int(prompt=None):
 
 if sys.version_info.major != 3:
     def get_long(prompt=None):
-        """Read a line of text from standard input and return the equivalent long."""
+        """
+        Read a line of text from standard input and return the equivalent long;
+        if text does not represent a long, user is prompted to retry. If line
+        can't be read, return None.
+        """
         while True:
             s = get_string(prompt)
             if s is None:
@@ -85,11 +101,18 @@ def get_long(prompt=None):
                 print("Retry: ", end="")
 
 def get_string(prompt=None):
-    """Read a line of text from standard input and return it as a string."""
+    """
+    Read a line of text from standard input and return it as a string,
+    sans trailing line ending. Supports CR (\r), LF (\n), and CRLF (\r\n)
+    as line endings. If user inputs only a line ending, returns "", not None.
+    Returns None upon error or no input whatsoever (i.e., just EOF).
+    """
     try:
         if prompt is not None:
             print(prompt, end="")
         s = sys.stdin.readline()
+        if not s:
+            return None
         return re.sub(r"(?:\r|\r\n|\n)$", "", s)
     except ValueError:
         return None

From 9f7dca1715bc51416fa55b677c352675817a5d36 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 29 Apr 2017 12:11:32 -0400
Subject: [PATCH 05/35] added eprint

---
 cs50/cs50.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/cs50/cs50.py b/cs50/cs50.py
index 657f405..5f670ae 100644
--- a/cs50/cs50.py
+++ b/cs50/cs50.py
@@ -1,4 +1,5 @@
 from __future__ import print_function
+import inspect
 import re
 import sys
 
@@ -20,6 +21,15 @@ def write(self, x):
 sys.stderr = flushfile(sys.stderr)
 sys.stdout = flushfile(sys.stdout)
 
+def eprint(*objects, end="\n", sep=" "):
+    """
+    Print an error message to standard error, prefixing it with caller's
+    file name and line number.
+    """
+    (frame, filename, lineno, function, code_context, index) = inspect.stack()[1]
+    print("{}:{}: ".format(filename, lineno), end="")
+    print(*objects, end=end, file=sys.stderr, sep=sep)
+
 def get_char():
     """Read a line of text from standard input and return the equivalent char."""
     while True:

From bc1483abc61bc3aca029998583afbf2519f276a8 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 29 Apr 2017 12:14:46 -0400
Subject: [PATCH 06/35] clarified eprint's comments

---
 cs50/cs50.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/cs50/cs50.py b/cs50/cs50.py
index 5f670ae..bae76a7 100644
--- a/cs50/cs50.py
+++ b/cs50/cs50.py
@@ -23,8 +23,8 @@ def write(self, x):
 
 def eprint(*objects, end="\n", sep=" "):
     """
-    Print an error message to standard error, prefixing it with caller's
-    file name and line number.
+    Print an error message to standard error, prefixing it with
+    file name and line number from which method was called.
     """
     (frame, filename, lineno, function, code_context, index) = inspect.stack()[1]
     print("{}:{}: ".format(filename, lineno), end="")

From bfbe74d410e4dabb3ef200df914fc1de796c7f82 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Fri, 12 May 2017 22:34:29 +0200
Subject: [PATCH 07/35] configured slack notifications [skip ci]

---
 .travis.yml | 45 ++++++++++++++++-----------------------------
 1 file changed, 16 insertions(+), 29 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 6e383b5..b37c08e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,34 +1,21 @@
 language: python
-
-python: "3.4"
-
-# don't build tags
+python: '3.4'
 branches:
-  except: /^v\d/
-
-# build sdist
+  except: "/^v\\d/"
 script: make build
-
-# install twine for uploading to PyPI
 before_deploy: pip install twine
-
 deploy:
-
-  # create github release
-  - provider: script
-    script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\", \"target_commitish\": \"$TRAVIS_COMMIT\", \"name\": \"v$(python setup.py --version)\" }" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases'
-    on:
-      branch: master
-
-  # deploy to PyPI
-  - provider: script
-
-    # upload sdist to PyPI
-    script: twine upload -u $PYPI_USERNAME -p $PYPI_PASSWORD dist/*
-
-    # avoid stashing sdist
-    skip_cleanup: true
-
-    # deploy on commits to master
-    on:
-      branch: master
+- provider: script
+  script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\",
+    \"target_commitish\": \"$TRAVIS_COMMIT\", \"name\": \"v$(python setup.py --version)\"
+    }" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases'
+  on:
+    branch: master
+- provider: script
+  script: twine upload -u $PYPI_USERNAME -p $PYPI_PASSWORD dist/*
+  skip_cleanup: true
+  on:
+    branch: master
+notifications:
+  slack:
+    secure: lJklhcBVjDT6KzUNa3RFHXdXSeH7ytuuGrkZ5ZcR72CXMoTf2pMJTzPwRLWOp6lCSdDC9Y8MWLrcg/e33dJga4Jlp9alOmWqeqesaFjfee4st8vAsgNbv8/RajPH1gD2bnkt8oIwUzdHItdb5AucKFYjbH2g0d8ndoqYqUeBLrnsT1AP5G/Vi9OHC9OWNpR0FKaZIJE0Wt52vkPMH3sV2mFeIskByPB+56U5y547mualKxn61IVR/dhYBEtZQJuSvnwKHPOn9Pkk7cCa+SSSeTJ4w5LboY8T17otaYNauXo46i1bKIoGiBcCcrJyQHHiPQmcq/YU540MC5Wzt9YXUycmJzRi347oyQeDee27wV3XJlWMXuuhbtJiKCFny7BTQ160VATlj/dbwIzN99Ra6/BtTumv/6LyTdKIuVjdAkcN8dtdDW1nlrQ29zuPNCcXXzJ7zX7kQaOCUV1c2OrsbiH/0fE9nknUORn97txqhlYVi0QMS7764wFo6kg0vpmFQRkkQySsJl+TmgcZ01AlsJc2EMMWVuaj9Af9JU4/4yalqDiXIh1fOYYUZnLfOfWS+MsnI+/oLfqJFyMbrsQQTIjs+kTzbiEdhd2R4EZgusU/xRFWokS2NAvahexrRhRQ6tpAI+LezPrkNOR3aHiykBf+P9BkUa0wPp6V2Ayc6q0=

From a94b031774886395d0a5e1afbd47fefdd4a947ab Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Fri, 12 May 2017 22:40:03 +0200
Subject: [PATCH 08/35] deploying using pypi integration [skip ci]

---
 .travis.yml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index b37c08e..f097728 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,8 +2,8 @@ language: python
 python: '3.4'
 branches:
   except: "/^v\\d/"
-script: make build
-before_deploy: pip install twine
+install: true
+script: true
 deploy:
 - provider: script
   script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\",
@@ -11,9 +11,9 @@ deploy:
     }" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases'
   on:
     branch: master
-- provider: script
-  script: twine upload -u $PYPI_USERNAME -p $PYPI_PASSWORD dist/*
-  skip_cleanup: true
+- provider: pypi
+  user: "$PYPI_USERNAME"
+  password: "$PYPI_PASSWORD"
   on:
     branch: master
 notifications:

From 93fa5999eda49f8f4ff6d751c2a1732b2f8f086d Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Fri, 12 May 2017 23:42:21 +0200
Subject: [PATCH 09/35] removed makefile and updated readme

---
 Makefile  | 11 -----------
 README.md | 16 +++-------------
 2 files changed, 3 insertions(+), 24 deletions(-)
 delete mode 100644 Makefile

diff --git a/Makefile b/Makefile
deleted file mode 100644
index 063b9de..0000000
--- a/Makefile
+++ /dev/null
@@ -1,11 +0,0 @@
-.PHONY: build
-build: clean
-	python setup.py sdist
-
-.PHONY: clean
-clean:
-	rm -rf *.egg-info dist
-
-.PHONY: install
-install: build
-	pip install dist/*.tar.gz
diff --git a/README.md b/README.md
index 3f51734..fa498f1 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,11 @@
 
 Supports Python 2 and Python 3.
 
-## Development
-
-Requires [Docker Engine](https://docs.docker.com/engine/installation/).
-
-    make bash
-    make deb # builds .deb
-
 ## Installation
 
-1. Download the latest release per https://github.com/cs50/python-cs50/releases
-1. Extract `python-cs50-*`
-1. `cd python-cs50-*`
-1. `make install`
+```
+pip install cs50
+```
 
 ## Usage
 
@@ -31,8 +23,6 @@ Requires [Docker Engine](https://docs.docker.com/engine/installation/).
     s = cs50.get_string();
 
 ## TODO
-
-* Add install target to Makefile.
 * Conditionally install for Python 2 and/or Python 3.
 * Add targets for `pacman`, `rpm`.
 * Add tests.

From 19b33c09cf1e9c0cdf1bfdb303a0495e18539b80 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 20 May 2017 11:53:24 -0400
Subject: [PATCH 10/35] added dependency

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index cee2266..28ed25a 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@
         "Topic :: Software Development :: Libraries :: Python Modules"
     ],
     description="CS50 library for Python",
-    install_requires=["SQLAlchemy"],
+    install_requires=["SQLAlchemy", "sqlparse"],
     keywords="cs50",
     name="cs50",
     packages=["cs50"],

From 508937a54c09056b8f3d7c9c8306f213d2f56875 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 20 May 2017 19:51:12 -0400
Subject: [PATCH 11/35] trying support for all paramstyles

---
 cs50/sql.py | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/cs50/sql.py b/cs50/sql.py
index 2064133..1ad0664 100644
--- a/cs50/sql.py
+++ b/cs50/sql.py
@@ -1,4 +1,6 @@
+import re
 import sqlalchemy
+import sqlparse
 
 class SQL(object):
     """Wrap SQLAlchemy to provide a simple SQL API."""
@@ -20,8 +22,45 @@ def execute(self, text, *multiparams, **params):
         """
         Execute a SQL statement.
         """
+
+        # parse text
+        parsed = sqlparse.parse(text)
+        if len(parsed) == 0:
+            raise RuntimeError("missing statement")
+        elif len(parsed) > 1:
+            raise RuntimeError("too many statements")
+        statement = parsed[0]
+        if statement.get_type() == "UNKNOWN":
+            raise RuntimeError("unknown type of statement")
+
+        # infer paramstyle
+	# https://www.python.org/dev/peps/pep-0249/#paramstyle
+	paramstyle = None
+	for token in statement.flatten():
+	    if sqlparse.utils.imt(token.ttype, t=sqlparse.tokens.Token.Name.Placeholder):
+		_paramstyle = None
+		if re.search(r"^\?$", token.value):
+		    _paramstyle = "qmark"
+		elif re.search(r"^:\d+$", token.value):
+		    _paramstyle = "numeric"
+		elif re.search(r"^:\w+$", token.value):
+		    _paramstyle = "named"
+		elif re.search(r"^%s$", token.value):
+		    _paramstyle = "format"
+		elif re.search(r"^%\(\w+\)s$", token.value):
+		    _paramstyle = "pyformat"
+		else:
+		    raise RuntimeError("unknown paramstyle")
+		if paramstyle and paramstyle != _paramstyle:
+		    raise RuntimeError("inconsistent paramstyle")
+		paramstyle = _paramstyle
+
         try:
 
+            parsed = sqlparse.split("SELECT * FROM cs50 WHERE id IN (SELECT id FROM cs50); SELECT 1; CREATE TABLE foo")
+            print(parsed)
+            return 0
+
             # bind parameters before statement reaches database, so that bound parameters appear in exceptions
             # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text
             # https://groups.google.com/forum/#!topic/sqlalchemy/FfLwKT1yQlg

From ab5d8366c6af32eaf76b3e54b225b7f9f33f79e0 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 20 May 2017 21:51:39 -0400
Subject: [PATCH 12/35] added support for expandable parameters

---
 cs50/sql.py | 120 +++++++++++++++++++++++++++++++++-------------------
 1 file changed, 77 insertions(+), 43 deletions(-)

diff --git a/cs50/sql.py b/cs50/sql.py
index 1ad0664..fa9a864 100644
--- a/cs50/sql.py
+++ b/cs50/sql.py
@@ -1,6 +1,6 @@
-import re
+import datetime
 import sqlalchemy
-import sqlparse
+import sys
 
 class SQL(object):
     """Wrap SQLAlchemy to provide a simple SQL API."""
@@ -16,58 +16,91 @@ def __init__(self, url):
         try:
             self.engine = sqlalchemy.create_engine(url)
         except Exception as e:
+            e.__context__ = None
             raise RuntimeError(e)
 
-    def execute(self, text, *multiparams, **params):
+    def execute(self, text, **params):
         """
         Execute a SQL statement.
         """
 
-        # parse text
-        parsed = sqlparse.parse(text)
-        if len(parsed) == 0:
-            raise RuntimeError("missing statement")
-        elif len(parsed) > 1:
-            raise RuntimeError("too many statements")
-        statement = parsed[0]
-        if statement.get_type() == "UNKNOWN":
-            raise RuntimeError("unknown type of statement")
-
-        # infer paramstyle
-	# https://www.python.org/dev/peps/pep-0249/#paramstyle
-	paramstyle = None
-	for token in statement.flatten():
-	    if sqlparse.utils.imt(token.ttype, t=sqlparse.tokens.Token.Name.Placeholder):
-		_paramstyle = None
-		if re.search(r"^\?$", token.value):
-		    _paramstyle = "qmark"
-		elif re.search(r"^:\d+$", token.value):
-		    _paramstyle = "numeric"
-		elif re.search(r"^:\w+$", token.value):
-		    _paramstyle = "named"
-		elif re.search(r"^%s$", token.value):
-		    _paramstyle = "format"
-		elif re.search(r"^%\(\w+\)s$", token.value):
-		    _paramstyle = "pyformat"
-		else:
-		    raise RuntimeError("unknown paramstyle")
-		if paramstyle and paramstyle != _paramstyle:
-		    raise RuntimeError("inconsistent paramstyle")
-		paramstyle = _paramstyle
+        class UserDefinedType(sqlalchemy.TypeDecorator):
+            """
+            Add support for expandable values, a la https://bitbucket.org/zzzeek/sqlalchemy/issues/3953/expanding-parameter.
+            """
+            impl = sqlalchemy.types.UserDefinedType
+            def process_literal_param(self, value, dialect):
+                """Receive a literal parameter value to be rendered inline within a statement."""
+                def process(value):
+                    """Render a literal value, escaping as needed."""
+
+                    # bool
+                    if isinstance(value, bool):
+                        return sqlalchemy.types.Boolean().literal_processor(dialect)(value)
+
+                    # datetime.date
+                    elif isinstance(value, datetime.date):
+                        return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%Y-%m-%d"))
+
+                    # datetime.datetime
+                    elif isinstance(value, datetime.datetime):
+                        return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%Y-%m-%d %H:%M:%S"))
+
+                    # datetime.time
+                    elif isinstance(value, datetime.time):
+                        return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%H:%M:%S"))
+
+                    # float
+                    elif isinstance(value, float):
+                        return sqlalchemy.types.Float().literal_processor(dialect)(value)
+
+                    # int
+                    elif isinstance(value, int):
+                        return sqlalchemy.types.Integer().literal_processor(dialect)(value)
+
+                    # long
+                    elif sys.version_info.major != 3 and isinstance(value, long):
+                        return sqlalchemy.types.Integer().literal_processor(dialect)(value)
+
+                    # str
+                    elif isinstance(value, str):
+                        return sqlalchemy.types.String().literal_processor(dialect)(value)
+
+                    # None
+                    elif isinstance(value, sqlalchemy.sql.elements.Null):
+                        return sqlalchemy.types.NullType().literal_processor(dialect)(value)
+
+                    # unsupported value
+                    raise RuntimeError("unsupported value")
+
+                # process value(s), separating with commas as needed
+                if type(value) is list:
+                    return ", ".join([process(v) for v in value])
+                else:
+                    return process(value)
 
         try:
 
-            parsed = sqlparse.split("SELECT * FROM cs50 WHERE id IN (SELECT id FROM cs50); SELECT 1; CREATE TABLE foo")
-            print(parsed)
-            return 0
+            # construct a new TextClause clause
+            statement = sqlalchemy.text(text)
+
+            # iterate over parameters
+            for key, value in params.items():
 
-            # bind parameters before statement reaches database, so that bound parameters appear in exceptions
-            # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text
-            # https://groups.google.com/forum/#!topic/sqlalchemy/FfLwKT1yQlg
-            # http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine.execute
+                # translate None to NULL
+                if value is None:
+                    value = sqlalchemy.sql.null()
+
+                # bind parameters before statement reaches database, so that bound parameters appear in exceptions
+                # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text
+                statement = statement.bindparams(sqlalchemy.bindparam(key, value=value, type_=UserDefinedType()))
+
+            # stringify bound parameters
             # http://docs.sqlalchemy.org/en/latest/faq/sqlexpressions.html#how-do-i-render-sql-expressions-as-strings-possibly-with-bound-parameters-inlined
-            statement = sqlalchemy.text(text).bindparams(*multiparams, **params)
-            result = self.engine.execute(str(statement.compile(compile_kwargs={"literal_binds": True})))
+            self.statement = str(statement.compile(compile_kwargs={"literal_binds": True}))
+
+            # execute statement
+            result = self.engine.execute(self.statement)
 
             # if SELECT (or INSERT with RETURNING), return result set as list of dict objects
             if result.returns_rows:
@@ -88,4 +121,5 @@ def execute(self, text, *multiparams, **params):
 
         # else raise error
         except Exception as e:
+            e.__context__ = None
             raise RuntimeError(e)

From 64c1a04e2700378e37fb7853dc1a8fe89a4934e6 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 20 May 2017 21:51:45 -0400
Subject: [PATCH 13/35] removed sqlparse

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 28ed25a..cee2266 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@
         "Topic :: Software Development :: Libraries :: Python Modules"
     ],
     description="CS50 library for Python",
-    install_requires=["SQLAlchemy", "sqlparse"],
+    install_requires=["SQLAlchemy"],
     keywords="cs50",
     name="cs50",
     packages=["cs50"],

From 71dea16d78c8fa117162b937516079b4d44fe951 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sun, 21 May 2017 00:19:17 -0400
Subject: [PATCH 14/35] fixed support for PostgreSQL

---
 cs50/sql.py      |  41 ++++++++++-----
 test/sqltests.py | 129 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 157 insertions(+), 13 deletions(-)
 create mode 100644 test/sqltests.py

diff --git a/cs50/sql.py b/cs50/sql.py
index fa9a864..036e17c 100644
--- a/cs50/sql.py
+++ b/cs50/sql.py
@@ -1,22 +1,25 @@
 import datetime
+import re
 import sqlalchemy
 import sys
+import warnings
 
 class SQL(object):
     """Wrap SQLAlchemy to provide a simple SQL API."""
 
-    def __init__(self, url):
+    def __init__(self, url, **kwargs):
         """
         Create instance of sqlalchemy.engine.Engine.
 
         URL should be a string that indicates database dialect and connection arguments.
 
         http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine
+        http://docs.sqlalchemy.org/en/latest/dialects/index.html
         """
         try:
-            self.engine = sqlalchemy.create_engine(url)
+            self.engine = sqlalchemy.create_engine(url, **kwargs)
         except Exception as e:
-            e.__context__ = None
+            e.__cause__ = None
             raise RuntimeError(e)
 
     def execute(self, text, **params):
@@ -79,6 +82,10 @@ def process(value):
                 else:
                     return process(value)
 
+        # raise exceptions for warnings
+        warnings.filterwarnings("error")
+
+        # prepare, execute statement
         try:
 
             # construct a new TextClause clause
@@ -97,29 +104,37 @@ def process(value):
 
             # stringify bound parameters
             # http://docs.sqlalchemy.org/en/latest/faq/sqlexpressions.html#how-do-i-render-sql-expressions-as-strings-possibly-with-bound-parameters-inlined
-            self.statement = str(statement.compile(compile_kwargs={"literal_binds": True}))
+            statement = str(statement.compile(compile_kwargs={"literal_binds": True}))
 
             # execute statement
-            result = self.engine.execute(self.statement)
+            result = self.engine.execute(statement)
 
             # if SELECT (or INSERT with RETURNING), return result set as list of dict objects
-            if result.returns_rows:
+            if re.search(r"^\s*SELECT\s+", statement, re.I):
                 rows = result.fetchall()
                 return [dict(row) for row in rows]
 
             # if INSERT, return primary key value for a newly inserted row
-            elif result.lastrowid is not None:
-                return result.lastrowid
+            elif re.search(r"^\s*INSERT\s+", statement, re.I):
+                if self.engine.url.get_backend_name() == "postgresql":
+                    result = self.engine.execute(sqlalchemy.text("SELECT LASTVAL()"))
+                    return result.first()[0]
+                else:
+                    return result.lastrowid
 
-            # if DELETE or UPDATE (or INSERT without RETURNING), return number of rows matched
-            else:
+            # if DELETE or UPDATE, return number of rows matched
+            elif re.search(r"^\s*(?:DELETE|UPDATE)\s+", statement, re.I):
                 return result.rowcount
 
+            # if some other statement, return True unless exception
+            return True
+
         # if constraint violated, return None
         except sqlalchemy.exc.IntegrityError:
             return None
 
-        # else raise error
+        # else raise exception
         except Exception as e:
-            e.__context__ = None
-            raise RuntimeError(e)
+            _e = RuntimeError(e) # else Python 3 prints warnings' tracebacks
+            _e.__cause__ = None
+            raise _e
diff --git a/test/sqltests.py b/test/sqltests.py
new file mode 100644
index 0000000..d2204a1
--- /dev/null
+++ b/test/sqltests.py
@@ -0,0 +1,129 @@
+import unittest
+from cs50.sql import SQL
+
+class SQLTests(unittest.TestCase):
+    def test_delete_returns_affected_rows(self):
+        rows = [
+            {"id": 1, "val": "foo"},
+            {"id": 2, "val": "bar"},
+            {"id": 3, "val": "baz"}
+        ]
+        for row in rows:
+            self.db.execute("INSERT INTO cs50(val) VALUES(:val);", val=row["val"])
+
+        print(self.db.execute("DELETE FROM cs50 WHERE id = :id", id=rows[0]["id"]))
+        print(self.db.execute("SELECT * FROM cs50"))
+        return
+
+        self.assertEqual(self.db.execute("DELETE FROM cs50 WHERE id = :id", id=rows[0]["id"]), 1)
+        self.assertEqual(self.db.execute("DELETE FROM cs50 WHERE id = :a or id = :b", a=rows[1]["id"], b=rows[2]["id"]), 2)
+        self.assertEqual(self.db.execute("DELETE FROM cs50 WHERE id = -50"), 0)
+
+    def test_insert_returns_last_row_id(self):
+        self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('foo')"), 1)
+        self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('bar')"), 2)
+
+    def test_select_all(self):
+        self.assertEqual(self.db.execute("SELECT * FROM cs50"), [])
+
+        rows = [
+            {"id": 1, "val": "foo"},
+            {"id": 2, "val": "bar"},
+            {"id": 3, "val": "baz"}
+        ]
+        for row in rows:
+            self.db.execute("INSERT INTO cs50(val) VALUES(:val)", val=row["val"])
+
+        self.assertEqual(self.db.execute("SELECT * FROM cs50"), rows)
+
+    def test_select_cols(self):
+        rows = [
+            {"val": "foo"},
+            {"val": "bar"},
+            {"val": "baz"}
+        ]
+        for row in rows:
+            self.db.execute("INSERT INTO cs50(val) VALUES(:val)", val=row["val"])
+
+        self.assertEqual(self.db.execute("SELECT val FROM cs50"), rows)
+
+    def test_select_where(self):
+        rows = [
+            {"id": 1, "val": "foo"},
+            {"id": 2, "val": "bar"},
+            {"id": 3, "val": "baz"}
+        ]
+        for row in rows:
+            self.db.execute("INSERT INTO cs50(val) VALUES(:val)", val=row["val"])
+
+        self.assertEqual(self.db.execute("SELECT * FROM cs50 WHERE id = :id OR val = :val", id=rows[1]["id"], val=rows[2]["val"]), rows[1:3])
+
+    def test_update_returns_affected_rows(self):
+        rows = [
+            {"id": 1, "val": "foo"},
+            {"id": 2, "val": "bar"},
+            {"id": 3, "val": "baz"}
+        ]
+        for row in rows:
+            self.db.execute("INSERT INTO cs50(val) VALUES(:val)", val=row["val"])
+
+        self.assertEqual(self.db.execute("UPDATE cs50 SET val = 'foo' WHERE id > 1"), 2)
+        self.assertEqual(self.db.execute("UPDATE cs50 SET val = 'foo' WHERE id = -50"), 0)
+
+class MySQLTests(SQLTests):
+    @classmethod
+    def setUpClass(self):
+        self.db = SQL("mysql://root@localhost/cs50_sql_tests")
+
+    def setUp(self):
+        self.db.execute("CREATE TABLE cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), PRIMARY KEY (id))")
+
+    def tearDown(self):
+        self.db.execute("DROP TABLE cs50")
+
+    @classmethod
+    def tearDownClass(self):
+        self.db.execute("DROP TABLE IF EXISTS cs50")
+
+class PostgresTests(SQLTests):
+    @classmethod
+    def setUpClass(self):
+        self.db = SQL("postgresql://postgres:postgres@localhost/cs50_sql_tests")
+
+    def setUp(self):
+        self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))")
+
+    def tearDown(self):
+        self.db.execute("DROP TABLE cs50")
+
+    @classmethod
+    def tearDownClass(self):
+        self.db.execute("DROP TABLE IF EXISTS cs50")
+
+    def test_insert_returns_last_row_id(self):
+        self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('foo')"), 1)
+        self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('bar')"), 2)
+
+class SQLiteTests(SQLTests):
+    @classmethod
+    def setUpClass(self):
+        self.db = SQL("sqlite:///cs50_sql_tests.db")
+
+    def setUp(self):
+        self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)")
+
+    def tearDown(self):
+        self.db.execute("DROP TABLE cs50")
+
+    @classmethod
+    def tearDownClass(self):
+        self.db.execute("DROP TABLE IF EXISTS cs50")
+
+if __name__ == "__main__":
+    suite = unittest.TestSuite([
+        unittest.TestLoader().loadTestsFromTestCase(SQLiteTests),
+        unittest.TestLoader().loadTestsFromTestCase(MySQLTests),
+        unittest.TestLoader().loadTestsFromTestCase(PostgresTests)
+    ])
+
+    unittest.TextTestRunner(verbosity=2).run(suite)

From b63f5217f2e7d8cc7e4f83140105000ac1d511f9 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sun, 21 May 2017 00:35:14 -0400
Subject: [PATCH 15/35] added logging

---
 cs50/sql.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/cs50/sql.py b/cs50/sql.py
index 036e17c..93fd4e8 100644
--- a/cs50/sql.py
+++ b/cs50/sql.py
@@ -1,4 +1,5 @@
 import datetime
+import logging
 import re
 import sqlalchemy
 import sys
@@ -16,6 +17,8 @@ def __init__(self, url, **kwargs):
         http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine
         http://docs.sqlalchemy.org/en/latest/dialects/index.html
         """
+        logging.basicConfig(level=logging.DEBUG)
+        self.logger = logging.getLogger(__name__)
         try:
             self.engine = sqlalchemy.create_engine(url, **kwargs)
         except Exception as e:
@@ -109,6 +112,9 @@ def process(value):
             # execute statement
             result = self.engine.execute(statement)
 
+            # log statement
+            self.logger.debug(statement)
+
             # if SELECT (or INSERT with RETURNING), return result set as list of dict objects
             if re.search(r"^\s*SELECT\s+", statement, re.I):
                 rows = result.fetchall()

From bf2b82aa9a4193fb826adca19bf1a490f08b46db Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sun, 21 May 2017 00:40:11 -0400
Subject: [PATCH 16/35] upped version

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 09c4c5e..1b4c450 100644
--- a/setup.py
+++ b/setup.py
@@ -15,5 +15,5 @@
     name="cs50",
     packages=["cs50"],
     url="https://github.com/cs50/python-cs50",
-    version="2.0.0"
+    version="2.1.0"
 )

From be16156ff37137f9cf999ab94b24279942fbfb6e Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sun, 21 May 2017 00:49:52 -0400
Subject: [PATCH 17/35] fixed eprint for Python 2

---
 cs50/cs50.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/cs50/cs50.py b/cs50/cs50.py
index bae76a7..be6b402 100644
--- a/cs50/cs50.py
+++ b/cs50/cs50.py
@@ -21,14 +21,16 @@ def write(self, x):
 sys.stderr = flushfile(sys.stderr)
 sys.stdout = flushfile(sys.stdout)
 
-def eprint(*objects, end="\n", sep=" "):
+def eprint(*args, **kwargs):
     """
     Print an error message to standard error, prefixing it with
     file name and line number from which method was called.
     """
-    (frame, filename, lineno, function, code_context, index) = inspect.stack()[1]
+    end = kwargs.get("end", "\n")
+    sep = kwargs.get("sep", " ")
+    (filename, lineno) = inspect.stack()[1][1:3]
     print("{}:{}: ".format(filename, lineno), end="")
-    print(*objects, end=end, file=sys.stderr, sep=sep)
+    print(*args, end=end, file=sys.stderr, sep=sep)
 
 def get_char():
     """Read a line of text from standard input and return the equivalent char."""

From f98662bd0f840294e7905a3e9616d397d14a73a5 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sun, 21 May 2017 13:19:43 -0400
Subject: [PATCH 18/35] reorganized directories

---
 {cs50 => src/cs50}/__init__.py               |   0
 src/cs50/__pycache__/__init__.cpython-34.pyc | Bin 0 -> 1121 bytes
 src/cs50/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 1090 bytes
 src/cs50/__pycache__/cs50.cpython-34.pyc     | Bin 0 -> 3748 bytes
 src/cs50/__pycache__/cs50.cpython-36.pyc     | Bin 0 -> 4010 bytes
 src/cs50/__pycache__/sql.cpython-34.pyc      | Bin 0 -> 3943 bytes
 {cs50 => src/cs50}/cs50.py                   |   0
 {cs50 => src/cs50}/sql.py                    |   0
 {test => tests}/python2.py                   |   0
 {test => tests}/python3.py                   |   0
 {test => tests}/sqltests.py                  |   0
 11 files changed, 0 insertions(+), 0 deletions(-)
 rename {cs50 => src/cs50}/__init__.py (100%)
 create mode 100644 src/cs50/__pycache__/__init__.cpython-34.pyc
 create mode 100644 src/cs50/__pycache__/__init__.cpython-36.pyc
 create mode 100644 src/cs50/__pycache__/cs50.cpython-34.pyc
 create mode 100644 src/cs50/__pycache__/cs50.cpython-36.pyc
 create mode 100644 src/cs50/__pycache__/sql.cpython-34.pyc
 rename {cs50 => src/cs50}/cs50.py (100%)
 rename {cs50 => src/cs50}/sql.py (100%)
 rename {test => tests}/python2.py (100%)
 rename {test => tests}/python3.py (100%)
 rename {test => tests}/sqltests.py (100%)

diff --git a/cs50/__init__.py b/src/cs50/__init__.py
similarity index 100%
rename from cs50/__init__.py
rename to src/cs50/__init__.py
diff --git a/src/cs50/__pycache__/__init__.cpython-34.pyc b/src/cs50/__pycache__/__init__.cpython-34.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c4a23a20dcccc32310bcd1ab62ed9833c61d226c
GIT binary patch
literal 1121
zcmZuw&2G~`5T3OiCrwgXRj3j@`GjPo*lAHgf(jM&fRIoS3Kw!&Irb)Yy7s!e8<kY$
z)V>U_z>6UH%BdF~04~frX&VHq?3dY{+4<(rKf9al-`|{1-z|V2u(lkOk8!K_2nqfO
zaDdUBOkhMHu^_R5Tk!Es;y}`XYt%P@*>GjU$N@%R)_|D{%z-O_D*^=l41M<oox<RT
zAo`>tPhxYvT{a5GxYa%aLsBF{L@p$?SER^yjmMfbaSidPVN^rESv@$JD=qT3c`2l3
zGJ1?WZ4cM-#*5WqKNx;^=ke$w;|os-Pp6UgBvaZG6OU(O8OengM};?L-dwT7JI{0~
z=Gwz&uX&uZeBot<(h=t@3EH*yRO?c`2*X6gDkv9dUj#x<!>8fOVVLD*km{V@nWOdR
zY-tvhJUr;{AB6qGu>UMfqxdu{roE+~9=^m}RJi^!iHa$cqR=VkXvAR5#Z;9-2eHUk
z&Yp*<IPYoE!z_Agp>n39GSX?!B((B=AJZT<#W&0R<4nK_ZuJo161za))B?E;mjFX-
z$<Vh37p>cBeg)sI9L4ygYE9;x7g5eC$8gC8zOUieZYYJ&p?MQhniZL*6l+y=CRvft
zTqJYO9vDg&0Z5lP<YMpdb_Q#vu?f*E6Rz2HxGzz_a5caTBX&XVtz^7>G{h`$PH<>*
z8@^aTF0Fg*5gvxVJ&?vPPNt3R_MOVc!d1;>bZTU)GcG&Ez?LS=$}y?dH}{#!4WsFB
z5#6S>gBPle0A!1F$Tr#hH?=i$->q7d8qbtgZMv!r%3Vs&=22Z<JT)oBd81Ss&vMJa
zUA32z2}C*wCf2&H-KI;nochjhj%=JQ*+t#RF>R?_F`ltl*XnA+%nLP5tlX$9S&{tD
XebqWz7Uz}uqE!o_OS)Fu+THmB{Co}!

literal 0
HcmV?d00001

diff --git a/src/cs50/__pycache__/__init__.cpython-36.pyc b/src/cs50/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7eb8ca45c821c5d4b679a69e7913c6416a688c86
GIT binary patch
literal 1090
zcmZuw!EV$r5Ve!+Zo1t<RdIn+!~rQtn=GvgQdOa%9uN{*2^9|HVr66RCan{vwpV4V
z<<$NRzrc?m`O2xk02ju|wp|1h&3IzZyv&<%_O`bAzrTB*e>e#JL~G)Ke*~%yz)(an
zM@hTl1XG8)-1&&!BI?o3HKHBnUb%_K@M-6?%e*Ueg)!uups;%b`DlED!G_r2=b3%K
z6&IW%Q1uiHLx7ADhvLMgJ?c{L8Yh5HyVoe`PzTU_^Wb={v?$+|wUC<0^cev61CVwn
z$khHQI{ENE;OS+-7l9Ii&Ql#orgR{t0WT&pm5U%tt6;){xneZ9D0D97I)Jn{Jj+?R
z2#QMSlygR-euJKCU8{pQrXo{Oy@2>C5^@&r#Vf$LDC;QKCBF-Y=r2VZ7S%l79lh9%
zNBi;UWt^wkSy9b~EzS@!fs2aQGih4Qm=u-HfunV!2^TX}3ms*mT!Focb8#`$VhAjT
zYN1M|(>m4p&=Ojq-`6zGY`3l073PhOLDe>xC8qeyk%J{Vff+-eo$;lA+sSVzbd9GN
zpBjHU=e$ZwW<2{H*@4`~{0iewib)lhDNn0-Ma&|^jUl9{3QY*K&TLMLijq>$IcE=S
z1zj)*``E*mPyU{6v=*A#imlk{tv1z1;4HC5r}(TZAK7(@A6pi1N4ZnFUmc8=PP=~y
z9&mHV(n7&ybYbA3XIyBV>9qx^#8u~9ZrT7>+RBV)2{$|UeC38c+2kU<WwHYsst*R?
zK0E`^f7q;rhj<)zjZcULC&cv0sw3bJ2sxjpO`qYdM+jUxLZk(k53RXtd+XU+r2SuQ
zxlK3yo`?HhvlsRf7cQ6_z?t#vFiclWPFbd#W3yo|4YAWS-L$S*MgIqH{KK|CukC-V
Nd@w%t9pBmB`2!&k6t@5X

literal 0
HcmV?d00001

diff --git a/src/cs50/__pycache__/cs50.cpython-34.pyc b/src/cs50/__pycache__/cs50.cpython-34.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..dd8d33b93ba87f8c14dcf7963161fb808172e8cb
GIT binary patch
literal 3748
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
E0FF{prT_o{

literal 0
HcmV?d00001

diff --git a/src/cs50/__pycache__/cs50.cpython-36.pyc b/src/cs50/__pycache__/cs50.cpython-36.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b4444811a29651f884b781ed251824821529a236
GIT binary patch
literal 4010
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
X6n^gV3bu@HbuoG=T8-AQhSmQ7zgoyZ

literal 0
HcmV?d00001

diff --git a/src/cs50/__pycache__/sql.cpython-34.pyc b/src/cs50/__pycache__/sql.cpython-34.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8e567319c0fdc2669c45658d3518ccd7543a53ca
GIT binary patch
literal 3943
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

literal 0
HcmV?d00001

diff --git a/cs50/cs50.py b/src/cs50/cs50.py
similarity index 100%
rename from cs50/cs50.py
rename to src/cs50/cs50.py
diff --git a/cs50/sql.py b/src/cs50/sql.py
similarity index 100%
rename from cs50/sql.py
rename to src/cs50/sql.py
diff --git a/test/python2.py b/tests/python2.py
similarity index 100%
rename from test/python2.py
rename to tests/python2.py
diff --git a/test/python3.py b/tests/python3.py
similarity index 100%
rename from test/python3.py
rename to tests/python3.py
diff --git a/test/sqltests.py b/tests/sqltests.py
similarity index 100%
rename from test/sqltests.py
rename to tests/sqltests.py

From cc1f84042465312af705fe41b2cae87e4ecac762 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Wed, 24 May 2017 21:06:02 -0400
Subject: [PATCH 19/35] fixed naming conventions, fixed exception handling

---
 src/cs50/sql.py | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/cs50/sql.py b/src/cs50/sql.py
index 93fd4e8..7af6f39 100644
--- a/src/cs50/sql.py
+++ b/src/cs50/sql.py
@@ -1,4 +1,5 @@
 import datetime
+import importlib
 import logging
 import re
 import sqlalchemy
@@ -22,8 +23,9 @@ def __init__(self, url, **kwargs):
         try:
             self.engine = sqlalchemy.create_engine(url, **kwargs)
         except Exception as e:
-            e.__cause__ = None
-            raise RuntimeError(e)
+            e_ = RuntimeError(e) # else Python 3 prints warnings' tracebacks
+            e_.__cause__ = None
+            raise e_
 
     def execute(self, text, **params):
         """
@@ -141,6 +143,6 @@ def process(value):
 
         # else raise exception
         except Exception as e:
-            _e = RuntimeError(e) # else Python 3 prints warnings' tracebacks
-            _e.__cause__ = None
-            raise _e
+            e_ = RuntimeError(e) # else Python 3 prints warnings' tracebacks
+            e_.__cause__ = None
+            raise e_

From a115e9ae0a24721027afec4368c08bea0e7a012b Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Wed, 24 May 2017 21:11:29 -0400
Subject: [PATCH 20/35] no longer reraising RuntimeErrors

---
 src/cs50/sql.py | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/src/cs50/sql.py b/src/cs50/sql.py
index 7af6f39..6649d13 100644
--- a/src/cs50/sql.py
+++ b/src/cs50/sql.py
@@ -23,9 +23,8 @@ def __init__(self, url, **kwargs):
         try:
             self.engine = sqlalchemy.create_engine(url, **kwargs)
         except Exception as e:
-            e_ = RuntimeError(e) # else Python 3 prints warnings' tracebacks
-            e_.__cause__ = None
-            raise e_
+            e.__cause__ = None # else Python 3 prints warnings' tracebacks
+            raise e
 
     def execute(self, text, **params):
         """
@@ -143,6 +142,5 @@ def process(value):
 
         # else raise exception
         except Exception as e:
-            e_ = RuntimeError(e) # else Python 3 prints warnings' tracebacks
-            e_.__cause__ = None
-            raise e_
+            e.__cause__ = None # else Python 3 prints warnings' tracebacks
+            raise e

From 0032a54b67c0514834ea440aa10fdefb86003674 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Wed, 24 May 2017 21:11:42 -0400
Subject: [PATCH 21/35] upped version

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 1b4c450..2342a33 100644
--- a/setup.py
+++ b/setup.py
@@ -15,5 +15,5 @@
     name="cs50",
     packages=["cs50"],
     url="https://github.com/cs50/python-cs50",
-    version="2.1.0"
+    version="2.2.0"
 )

From 7ef9d6d4edc2a5b197a89eaeb258964b8ee677d6 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Wed, 24 May 2017 21:17:22 -0400
Subject: [PATCH 22/35] no longer reraising exceptions

---
 src/cs50/sql.py | 11 +----------
 1 file changed, 1 insertion(+), 10 deletions(-)

diff --git a/src/cs50/sql.py b/src/cs50/sql.py
index 6649d13..df5a20c 100644
--- a/src/cs50/sql.py
+++ b/src/cs50/sql.py
@@ -20,11 +20,7 @@ def __init__(self, url, **kwargs):
         """
         logging.basicConfig(level=logging.DEBUG)
         self.logger = logging.getLogger(__name__)
-        try:
-            self.engine = sqlalchemy.create_engine(url, **kwargs)
-        except Exception as e:
-            e.__cause__ = None # else Python 3 prints warnings' tracebacks
-            raise e
+        self.engine = sqlalchemy.create_engine(url, **kwargs)
 
     def execute(self, text, **params):
         """
@@ -139,8 +135,3 @@ def process(value):
         # if constraint violated, return None
         except sqlalchemy.exc.IntegrityError:
             return None
-
-        # else raise exception
-        except Exception as e:
-            e.__cause__ = None # else Python 3 prints warnings' tracebacks
-            raise e

From 22306093bae78b05ebf7a8e3b026af03d1f3c537 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Wed, 24 May 2017 21:23:02 -0400
Subject: [PATCH 23/35] added comments

---
 src/cs50/sql.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/cs50/sql.py b/src/cs50/sql.py
index df5a20c..bfa79f0 100644
--- a/src/cs50/sql.py
+++ b/src/cs50/sql.py
@@ -18,8 +18,12 @@ def __init__(self, url, **kwargs):
         http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine
         http://docs.sqlalchemy.org/en/latest/dialects/index.html
         """
+
+        # log statements to standard error
         logging.basicConfig(level=logging.DEBUG)
         self.logger = logging.getLogger(__name__)
+
+        # create engine, raising exception if back end's module not installed
         self.engine = sqlalchemy.create_engine(url, **kwargs)
 
     def execute(self, text, **params):

From 41f1027e8af3c0cfbf43f24b5bc44a8384cfa171 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 09:56:49 +0200
Subject: [PATCH 24/35] ignored *.pyc and *.db

---
 .gitignore | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitignore b/.gitignore
index f75ca9b..5a13495 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,6 @@
 !.gitignore
 !.travis.yml
 dist/
+*.db
 *.egg-info/
+*.pyc

From a1bbaab4704985c3c3f3d79e5653a4fc4b873e4c Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 10:06:24 +0200
Subject: [PATCH 25/35] exiting with error code if sql test failed

---
 tests/sqltests.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/tests/sqltests.py b/tests/sqltests.py
index d2204a1..6cddabb 100644
--- a/tests/sqltests.py
+++ b/tests/sqltests.py
@@ -1,3 +1,4 @@
+import sys
 import unittest
 from cs50.sql import SQL
 
@@ -126,4 +127,4 @@ def tearDownClass(self):
         unittest.TestLoader().loadTestsFromTestCase(PostgresTests)
     ])
 
-    unittest.TextTestRunner(verbosity=2).run(suite)
+    sys.exit(not unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful())

From f91a93ef359ec61c83743a5ac8a72a02550df693 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 10:36:23 +0200
Subject: [PATCH 26/35] suppressed unknown table in mysql's tearDownClass

---
 tests/sqltests.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/tests/sqltests.py b/tests/sqltests.py
index 6cddabb..7a569ad 100644
--- a/tests/sqltests.py
+++ b/tests/sqltests.py
@@ -84,7 +84,13 @@ def tearDown(self):
 
     @classmethod
     def tearDownClass(self):
-        self.db.execute("DROP TABLE IF EXISTS cs50")
+        try:
+            self.db.execute("DROP TABLE IF EXISTS cs50")
+        except RuntimeError as e:
+
+            # suppress "unknown table"
+            if not str(e).startswith("(1051L"):
+                raise e
 
 class PostgresTests(SQLTests):
     @classmethod

From d3c25fce4a0ca48b360ba3016a522f0f6109df99 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 10:36:59 +0200
Subject: [PATCH 27/35] dropped postgres password for travis

---
 tests/sqltests.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/sqltests.py b/tests/sqltests.py
index 7a569ad..cb8bec5 100644
--- a/tests/sqltests.py
+++ b/tests/sqltests.py
@@ -95,7 +95,7 @@ def tearDownClass(self):
 class PostgresTests(SQLTests):
     @classmethod
     def setUpClass(self):
-        self.db = SQL("postgresql://postgres:postgres@localhost/cs50_sql_tests")
+        self.db = SQL("postgresql://postgres@localhost/cs50_sql_tests")
 
     def setUp(self):
         self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))")

From c3253f9ee3c508d709906ffaff854d28e20a2242 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 10:51:20 +0200
Subject: [PATCH 28/35] running sql tests against python 2 and 3 on travis

---
 .travis.yml       | 17 ++++++++++++++---
 tests/sqltests.py |  6 +++---
 2 files changed, 17 insertions(+), 6 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index f097728..287bcab 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,20 @@
 language: python
-python: '3.4'
+python:
+- '2.7'
+- '3.4'
 branches:
   except: "/^v\\d/"
-install: true
-script: true
+services:
+- mysql
+- postgresql
+install:
+- python setup.py install
+- pip install mysqlclient
+- pip install psycopg2
+before_script:
+- mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
+- psql -c 'create database test;' -U postgres
+script: python test/sqltests.py
 deploy:
 - provider: script
   script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\",
diff --git a/tests/sqltests.py b/tests/sqltests.py
index cb8bec5..3cf5311 100644
--- a/tests/sqltests.py
+++ b/tests/sqltests.py
@@ -74,7 +74,7 @@ def test_update_returns_affected_rows(self):
 class MySQLTests(SQLTests):
     @classmethod
     def setUpClass(self):
-        self.db = SQL("mysql://root@localhost/cs50_sql_tests")
+        self.db = SQL("mysql://root@localhost/test")
 
     def setUp(self):
         self.db.execute("CREATE TABLE cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), PRIMARY KEY (id))")
@@ -95,7 +95,7 @@ def tearDownClass(self):
 class PostgresTests(SQLTests):
     @classmethod
     def setUpClass(self):
-        self.db = SQL("postgresql://postgres@localhost/cs50_sql_tests")
+        self.db = SQL("postgresql://postgres@localhost/test")
 
     def setUp(self):
         self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))")
@@ -114,7 +114,7 @@ def test_insert_returns_last_row_id(self):
 class SQLiteTests(SQLTests):
     @classmethod
     def setUpClass(self):
-        self.db = SQL("sqlite:///cs50_sql_tests.db")
+        self.db = SQL("sqlite:///test.db")
 
     def setUp(self):
         self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)")

From 2a07bc877fc0245347bfce1f7f67126fef602f89 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 11:17:11 +0200
Subject: [PATCH 29/35] removed __pycache__

---
 src/cs50/__pycache__/__init__.cpython-34.pyc | Bin 1121 -> 0 bytes
 src/cs50/__pycache__/__init__.cpython-36.pyc | Bin 1090 -> 0 bytes
 src/cs50/__pycache__/cs50.cpython-34.pyc     | Bin 3748 -> 0 bytes
 src/cs50/__pycache__/cs50.cpython-36.pyc     | Bin 4010 -> 0 bytes
 src/cs50/__pycache__/sql.cpython-34.pyc      | Bin 3943 -> 0 bytes
 5 files changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 src/cs50/__pycache__/__init__.cpython-34.pyc
 delete mode 100644 src/cs50/__pycache__/__init__.cpython-36.pyc
 delete mode 100644 src/cs50/__pycache__/cs50.cpython-34.pyc
 delete mode 100644 src/cs50/__pycache__/cs50.cpython-36.pyc
 delete mode 100644 src/cs50/__pycache__/sql.cpython-34.pyc

diff --git a/src/cs50/__pycache__/__init__.cpython-34.pyc b/src/cs50/__pycache__/__init__.cpython-34.pyc
deleted file mode 100644
index c4a23a20dcccc32310bcd1ab62ed9833c61d226c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1121
zcmZuw&2G~`5T3OiCrwgXRj3j@`GjPo*lAHgf(jM&fRIoS3Kw!&Irb)Yy7s!e8<kY$
z)V>U_z>6UH%BdF~04~frX&VHq?3dY{+4<(rKf9al-`|{1-z|V2u(lkOk8!K_2nqfO
zaDdUBOkhMHu^_R5Tk!Es;y}`XYt%P@*>GjU$N@%R)_|D{%z-O_D*^=l41M<oox<RT
zAo`>tPhxYvT{a5GxYa%aLsBF{L@p$?SER^yjmMfbaSidPVN^rESv@$JD=qT3c`2l3
zGJ1?WZ4cM-#*5WqKNx;^=ke$w;|os-Pp6UgBvaZG6OU(O8OengM};?L-dwT7JI{0~
z=Gwz&uX&uZeBot<(h=t@3EH*yRO?c`2*X6gDkv9dUj#x<!>8fOVVLD*km{V@nWOdR
zY-tvhJUr;{AB6qGu>UMfqxdu{roE+~9=^m}RJi^!iHa$cqR=VkXvAR5#Z;9-2eHUk
z&Yp*<IPYoE!z_Agp>n39GSX?!B((B=AJZT<#W&0R<4nK_ZuJo161za))B?E;mjFX-
z$<Vh37p>cBeg)sI9L4ygYE9;x7g5eC$8gC8zOUieZYYJ&p?MQhniZL*6l+y=CRvft
zTqJYO9vDg&0Z5lP<YMpdb_Q#vu?f*E6Rz2HxGzz_a5caTBX&XVtz^7>G{h`$PH<>*
z8@^aTF0Fg*5gvxVJ&?vPPNt3R_MOVc!d1;>bZTU)GcG&Ez?LS=$}y?dH}{#!4WsFB
z5#6S>gBPle0A!1F$Tr#hH?=i$->q7d8qbtgZMv!r%3Vs&=22Z<JT)oBd81Ss&vMJa
zUA32z2}C*wCf2&H-KI;nochjhj%=JQ*+t#RF>R?_F`ltl*XnA+%nLP5tlX$9S&{tD
XebqWz7Uz}uqE!o_OS)Fu+THmB{Co}!

diff --git a/src/cs50/__pycache__/__init__.cpython-36.pyc b/src/cs50/__pycache__/__init__.cpython-36.pyc
deleted file mode 100644
index 7eb8ca45c821c5d4b679a69e7913c6416a688c86..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1090
zcmZuw!EV$r5Ve!+Zo1t<RdIn+!~rQtn=GvgQdOa%9uN{*2^9|HVr66RCan{vwpV4V
z<<$NRzrc?m`O2xk02ju|wp|1h&3IzZyv&<%_O`bAzrTB*e>e#JL~G)Ke*~%yz)(an
zM@hTl1XG8)-1&&!BI?o3HKHBnUb%_K@M-6?%e*Ueg)!uups;%b`DlED!G_r2=b3%K
z6&IW%Q1uiHLx7ADhvLMgJ?c{L8Yh5HyVoe`PzTU_^Wb={v?$+|wUC<0^cev61CVwn
z$khHQI{ENE;OS+-7l9Ii&Ql#orgR{t0WT&pm5U%tt6;){xneZ9D0D97I)Jn{Jj+?R
z2#QMSlygR-euJKCU8{pQrXo{Oy@2>C5^@&r#Vf$LDC;QKCBF-Y=r2VZ7S%l79lh9%
zNBi;UWt^wkSy9b~EzS@!fs2aQGih4Qm=u-HfunV!2^TX}3ms*mT!Focb8#`$VhAjT
zYN1M|(>m4p&=Ojq-`6zGY`3l073PhOLDe>xC8qeyk%J{Vff+-eo$;lA+sSVzbd9GN
zpBjHU=e$ZwW<2{H*@4`~{0iewib)lhDNn0-Ma&|^jUl9{3QY*K&TLMLijq>$IcE=S
z1zj)*``E*mPyU{6v=*A#imlk{tv1z1;4HC5r}(TZAK7(@A6pi1N4ZnFUmc8=PP=~y
z9&mHV(n7&ybYbA3XIyBV>9qx^#8u~9ZrT7>+RBV)2{$|UeC38c+2kU<WwHYsst*R?
zK0E`^f7q;rhj<)zjZcULC&cv0sw3bJ2sxjpO`qYdM+jUxLZk(k53RXtd+XU+r2SuQ
zxlK3yo`?HhvlsRf7cQ6_z?t#vFiclWPFbd#W3yo|4YAWS-L$S*MgIqH{KK|CukC-V
Nd@w%t9pBmB`2!&k6t@5X

diff --git a/src/cs50/__pycache__/cs50.cpython-34.pyc b/src/cs50/__pycache__/cs50.cpython-34.pyc
deleted file mode 100644
index dd8d33b93ba87f8c14dcf7963161fb808172e8cb..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3748
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
E0FF{prT_o{

diff --git a/src/cs50/__pycache__/cs50.cpython-36.pyc b/src/cs50/__pycache__/cs50.cpython-36.pyc
deleted file mode 100644
index b4444811a29651f884b781ed251824821529a236..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4010
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
X6n^gV3bu@HbuoG=T8-AQhSmQ7zgoyZ

diff --git a/src/cs50/__pycache__/sql.cpython-34.pyc b/src/cs50/__pycache__/sql.cpython-34.pyc
deleted file mode 100644
index 8e567319c0fdc2669c45658d3518ccd7543a53ca..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3943
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


From 77dbb658ad1bb942954b8159e2cd75c227dde66f Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 11:17:31 +0200
Subject: [PATCH 30/35] fixed package path

---
 setup.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setup.py b/setup.py
index 2342a33..6b7fce7 100644
--- a/setup.py
+++ b/setup.py
@@ -13,6 +13,7 @@
     install_requires=["SQLAlchemy"],
     keywords="cs50",
     name="cs50",
+    package_dir={"": "src"},
     packages=["cs50"],
     url="https://github.com/cs50/python-cs50",
     version="2.2.0"

From 4bc77abd3be2c20dc166e197f8cabf012e869948 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 11:21:23 +0200
Subject: [PATCH 31/35] fixed sqltests.py path

---
 .travis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index 287bcab..a20df42 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,7 +14,7 @@ install:
 before_script:
 - mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
 - psql -c 'create database test;' -U postgres
-script: python test/sqltests.py
+script: python tests/sqltests.py
 deploy:
 - provider: script
   script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\",

From a45a3ec575b7fc0cf421d3c62ccabaf7663735f1 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 11:55:19 +0200
Subject: [PATCH 32/35] fixed unknown table suppression in sql tests

---
 tests/sqltests.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/sqltests.py b/tests/sqltests.py
index 3cf5311..26778dc 100644
--- a/tests/sqltests.py
+++ b/tests/sqltests.py
@@ -1,6 +1,7 @@
 import sys
 import unittest
 from cs50.sql import SQL
+import warnings
 
 class SQLTests(unittest.TestCase):
     def test_delete_returns_affected_rows(self):
@@ -86,10 +87,9 @@ def tearDown(self):
     def tearDownClass(self):
         try:
             self.db.execute("DROP TABLE IF EXISTS cs50")
-        except RuntimeError as e:
-
+        except Warning as e:
             # suppress "unknown table"
-            if not str(e).startswith("(1051L"):
+            if not str(e).startswith("(1051"):
                 raise e
 
 class PostgresTests(SQLTests):

From f7cf36992f9805ad6c37854bbcf814623161cbb2 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 12:03:29 +0200
Subject: [PATCH 33/35] simplified sqltests

---
 tests/sqltests.py | 34 ++++++++--------------------------
 1 file changed, 8 insertions(+), 26 deletions(-)

diff --git a/tests/sqltests.py b/tests/sqltests.py
index 26778dc..0ce8af0 100644
--- a/tests/sqltests.py
+++ b/tests/sqltests.py
@@ -72,14 +72,6 @@ def test_update_returns_affected_rows(self):
         self.assertEqual(self.db.execute("UPDATE cs50 SET val = 'foo' WHERE id > 1"), 2)
         self.assertEqual(self.db.execute("UPDATE cs50 SET val = 'foo' WHERE id = -50"), 0)
 
-class MySQLTests(SQLTests):
-    @classmethod
-    def setUpClass(self):
-        self.db = SQL("mysql://root@localhost/test")
-
-    def setUp(self):
-        self.db.execute("CREATE TABLE cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), PRIMARY KEY (id))")
-
     def tearDown(self):
         self.db.execute("DROP TABLE cs50")
 
@@ -92,24 +84,21 @@ def tearDownClass(self):
             if not str(e).startswith("(1051"):
                 raise e
 
-class PostgresTests(SQLTests):
+class MySQLTests(SQLTests):
     @classmethod
     def setUpClass(self):
-        self.db = SQL("postgresql://postgres@localhost/test")
+        self.db = SQL("mysql://root@localhost/test")
 
     def setUp(self):
-        self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))")
-
-    def tearDown(self):
-        self.db.execute("DROP TABLE cs50")
+        self.db.execute("CREATE TABLE cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), PRIMARY KEY (id))")
 
+class PostgresTests(SQLTests):
     @classmethod
-    def tearDownClass(self):
-        self.db.execute("DROP TABLE IF EXISTS cs50")
+    def setUpClass(self):
+        self.db = SQL("postgresql://postgres@localhost/test")
 
-    def test_insert_returns_last_row_id(self):
-        self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('foo')"), 1)
-        self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('bar')"), 2)
+    def setUp(self):
+        self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))")
 
 class SQLiteTests(SQLTests):
     @classmethod
@@ -119,13 +108,6 @@ def setUpClass(self):
     def setUp(self):
         self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)")
 
-    def tearDown(self):
-        self.db.execute("DROP TABLE cs50")
-
-    @classmethod
-    def tearDownClass(self):
-        self.db.execute("DROP TABLE IF EXISTS cs50")
-
 if __name__ == "__main__":
     suite = unittest.TestSuite([
         unittest.TestLoader().loadTestsFromTestCase(SQLiteTests),

From 5e2b03eee32a6c48f645cb586daabbb1715d7be7 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 14:50:00 +0200
Subject: [PATCH 34/35] added test for multi-insert statements

---
 tests/sqltests.py | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/tests/sqltests.py b/tests/sqltests.py
index 0ce8af0..8e518b4 100644
--- a/tests/sqltests.py
+++ b/tests/sqltests.py
@@ -1,9 +1,12 @@
+from cs50.sql import SQL
 import sys
 import unittest
-from cs50.sql import SQL
 import warnings
 
 class SQLTests(unittest.TestCase):
+    def multi_inserts_enabled(self):
+        return True
+
     def test_delete_returns_affected_rows(self):
         rows = [
             {"id": 1, "val": "foo"},
@@ -24,6 +27,8 @@ def test_delete_returns_affected_rows(self):
     def test_insert_returns_last_row_id(self):
         self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('foo')"), 1)
         self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('bar')"), 2)
+        if self.multi_inserts_enabled():
+            self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('baz'); INSERT INTO cs50(val) VALUES('qux')"), 4)
 
     def test_select_all(self):
         self.assertEqual(self.db.execute("SELECT * FROM cs50"), [])
@@ -108,6 +113,9 @@ def setUpClass(self):
     def setUp(self):
         self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)")
 
+    def multi_inserts_enabled(self):
+        return False
+
 if __name__ == "__main__":
     suite = unittest.TestSuite([
         unittest.TestLoader().loadTestsFromTestCase(SQLiteTests),

From da85752145d5937234959fd3d8e6479ca5e56877 Mon Sep 17 00:00:00 2001
From: Kareem Zidane <kzidane@cs50.harvard.edu>
Date: Sat, 27 May 2017 15:08:34 +0200
Subject: [PATCH 35/35] decreased version

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 6b7fce7..8440a86 100644
--- a/setup.py
+++ b/setup.py
@@ -16,5 +16,5 @@
     package_dir={"": "src"},
     packages=["cs50"],
     url="https://github.com/cs50/python-cs50",
-    version="2.2.0"
+    version="2.0.0"
 )