diff --git a/.gitignore b/.gitignore index 663f268d..7b225ae2 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ bin build include lib +share cast-offs develop-eggs development @@ -41,3 +42,10 @@ development .mr.developer.cfg outline_improvements.txt src +html +slides +new_mash +.buildinfo +pip-selfcheck.json +.ipynb_checkpoints +testenvs diff --git a/Makefile b/Makefile index 20506467..84b07bcc 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,9 @@ # # You can set these variables from the command line. +BINDIR = ./bin SPHINXOPTS = -SPHINXBUILD = ./bin/sphinx-build +SPHINXBUILD = $(BINDIR)/sphinx-build PAPER = BUILDDIR = build diff --git a/README.rst b/README.rst index d79a8a4e..e7769775 100644 --- a/README.rst +++ b/README.rst @@ -10,35 +10,78 @@ This package provides the source for all lecture materials used for the `Internet Programming in Python`_ section of the `Certificate in Python Programming`_ offered by the `University of Washington Professional & Continuing Education`_ program. This version of the documentation is used for -the Winter 2015 instance of the course, Taught by `Cris Ewing`_ +the Winter 2016 instance of the course, Taught by `Cris Ewing`_ -.. _Internet Programming in Python: http://www.pce.uw.edu/courses/internet-programming-python/downtown-seattle-winter-2015/ +.. _Internet Programming in Python: http://www.pce.uw.edu/courses/internet-programming-python/downtown-seattle-winter-2016/ .. _Certificate in Python Programming: http://www.pce.uw.edu/certificates/python-programming.html .. _University of Washington Professional & Continuing Education: http://www.pce.uw.edu/ .. _Cris Ewing: http://www.linkedin.com/profile/view?id=19741495 +This course is taught using Python 3. + +This documentation builds both an HTML version of the course lectures (for the +students) and a set of slides (for the instructor). It uses the Python-based +documentation tool `Sphinx`_ and the `hieroglyph`_ sphinx extension. Shell +examples use `iPython` and tests are written for `pytest`. The build +environment is managed using `virtualenv` and `pip` + +.. _iPython: http://ipython.org/ +.. _Sphinx: http://sphinx-doc.org/ +.. _hieroglyph: http://docs.hieroglyph.io/en/latest/ +.. _pytest: http://pytest.org/latest/ +.. _virtualenv: https://virtualenv.pypa.io/en/latest/ +.. _pip: https://pip.pypa.io/en/stable + Building The Documentation -------------------------- -This documentation is built using docutils and Sphinx. The package uses -`zc.buildout` to manage setup and dependencies. This package uses the version -2 `bootstrap.py` script. This version of the script will attempt to use -setuptools 0.7 or better. If you have an earlier version of setuptools -installed, please upgrade prior to bootstrapping this buildout. +To build the documentation locally, begin by cloning the project to your +machine: + +.. code-block:: bash + + $ git clone https://github.com/cewing/training.python_web.git + +Change directories into the repository, then create a virtualenv using Python +3: + +.. code-block:: bash + + $ cd training.python_web + $ virtualenv --python /path/to/bin/python3.5 . + Running virtualenv with interpreter /path/to/bin/python3.5 + New python executable in training.python_web/bin/python3.5 + Also creating executable in training.python_web/bin/python + Installing setuptools, pip...done. + +Install the requirements for the documentation using pip: + +.. code-block:: bash + + $ bin/pip install -r requirements.pip + ... + + Successfully installed Babel-2.0 Jinja2-2.8 MarkupSafe-0.23 Pygments-2.0.2 Sphinx-1.3.1 alabaster-0.7.6 appnope-0.1.0 decorator-4.0.2 docutils-0.12 gnureadline-6.3.3 hieroglyph-0.7.1 ipython-4.0.0 ipython-genutils-0.1.0 path.py-8.1 pexpect-3.3 pickleshare-0.5 py-1.4.30 pytest-2.7.2 pytz-2015.4 simplegeneric-0.8.1 six-1.9.0 snowballstemmer-1.2.0 sphinx-rtd-theme-0.1.8 traitlets-4.0.0 + +Once that has successfully completed, you should be able to build both the html +documentation and the slides using the included Makefile. + +.. code-block:: bash + + $ make html + ... + + Build finished. The HTML pages are in build/html. + + (webdocs)$ make slides + ... -After cloning this package from the repository, do the following:: + Build finished. The HTML slides are in build/slides. - $ cd training.python_web # the location of your local copy - $ python bootstrap.py # must be Python 2.6 or 2.7 - $ bin/buildout - $ bin/sphinx # to build the main documentation and course outline - $ bin/build_s5 # to build the class session presentations +.. note:: If you prefer to build your virtualenvs in other ways, you will need + to adjust the `BINDIR` variable in `Makefile` to fit your reality. -At the end of a successful build, you will find a ``build/html`` directory, -containing the completed documentation and presentations. -.. _zc.buildout: https://pypi.python.org/pypi/zc.buildout/ -.. _bootstrap.py: http://downloads.buildout.org/2/bootstrap.py Reading The Documentation ------------------------- diff --git a/assignments/session01/echo_client.py b/assignments/session01/echo_client.py deleted file mode 100644 index 61616c36..00000000 --- a/assignments/session01/echo_client.py +++ /dev/null @@ -1,42 +0,0 @@ -import socket -import sys - - -def client(msg, log_buffer=sys.stderr): - server_address = ('localhost', 10000) - # TODO: Replace the following line with your code which will instantiate - # a TCP socket with IPv4 Addressing, call the socket you make 'sock' - sock = None - print >>log_buffer, 'connecting to {0} port {1}'.format(*server_address) - # TODO: connect your socket to the server here. - - # this try/finally block exists purely to allow us to close the socket - # when we are finished with it - try: - print >>log_buffer, 'sending "{0}"'.format(msg) - # TODO: send your message to the server here. - - # TODO: the server should be sending you back your message as a series - # of 16-byte chunks. You will want to log them as you receive - # each one. You will also need to check to make sure that - # you have received the entire message you sent __before__ - # closing the socket. - # - # Make sure that you log each chunk you receive. Use the print - # statement below to do it. (The tests expect this log format) - chunk = '' - print >>log_buffer, 'received "{0}"'.format(chunk) - finally: - # TODO: after you break out of the loop receiving echoed chunks from - # the server you will want to close your client socket. - print >>log_buffer, 'closing socket' - - -if __name__ == '__main__': - if len(sys.argv) != 2: - usg = '\nusage: python echo_client.py "this is my message"\n' - print >>sys.stderr, usg - sys.exit(1) - - msg = sys.argv[1] - client(msg) \ No newline at end of file diff --git a/assignments/session01/tests.py b/assignments/session01/tests.py deleted file mode 100644 index d0d4005a..00000000 --- a/assignments/session01/tests.py +++ /dev/null @@ -1,123 +0,0 @@ -from cStringIO import StringIO -from echo_client import client -import socket -import unittest - - -def make_buffers(string, buffsize=16): - for start in range(0, len(string), buffsize): - yield string[start:start+buffsize] - - -class EchoTestCase(unittest.TestCase): - """tests for the echo server and client""" - connection_msg = 'connecting to localhost port 10000' - sending_msg = 'sending "{0}"' - received_msg = 'received "{0}"' - closing_msg = 'closing socket' - - def setUp(self): - """set up our tests""" - if not hasattr(self, 'buff'): - # ensure we have a buffer for the client to write to - self.log = StringIO() - else: - # ensure that the buffer is set to the start for the next test - self.log.seek(0) - - def tearDown(self): - """clean up after ourselves""" - if hasattr(self, 'buff'): - # clear our buffer for the next test - self.log.seek(0) - self.log.truncate() - - def send_message(self, message): - """Attempt to send a message using the client and the test buffer - - In case of a socket error, fail and report the problem - """ - try: - client(message, self.log) - except socket.error, e: - if e.errno == 61: - msg = "Error: {0}, is the server running?" - self.fail(msg.format(e.strerror)) - else: - self.fail("Unexpected Error: {0}".format(str(e))) - - def process_log(self): - """process the buffer used by the client for logging - - The first and last lines of output will be checked to ensure that the - client started and terminated in the expected way - - The 'sending' message will be separated from the echoed message - returned from the server. - - Finally, the sending message, and the list of returned buffer lines - will be returned - """ - if self.log.tell() == 0: - self.fail("No bytes written to buffer") - - self.log.seek(0) - client_output = self.log.read() - lines = client_output.strip().split('\n') - first_line = lines.pop(0) - self.assertEqual(first_line, self.connection_msg, - "Unexpected connection message") - send_msg = lines.pop(0) - last_line = lines.pop() - self.assertEqual(last_line, self.closing_msg, - "Unexpected closing message") - return send_msg, lines - - def test_short_message_echo(self): - """test that a message short than 16 bytes echoes cleanly""" - short_message = "short message" - self.send_message(short_message) - actual_sent, actual_reply = self.process_log() - expected_sent = self.sending_msg.format(short_message) - self.assertEqual( - expected_sent, - actual_sent, - "expected {0}, got {1}".format(expected_sent, actual_sent) - ) - - self.assertEqual(len(actual_reply), 1, - "Short message was split unexpectedly") - - actual_line = actual_reply[0] - expected_line = self.received_msg.format(short_message) - self.assertEqual( - expected_line, - actual_line, - "expected {0} got {1}".format(expected_line, actual_line)) - - def test_long_message_echo(self): - """test that a message longer than 16 bytes echoes in 16-byte chunks""" - long_message = "Four score and seven years ago our fathers did stuff" - self.send_message(long_message) - actual_sent, actual_reply = self.process_log() - - expected_sent = self.sending_msg.format(long_message) - self.assertEqual( - expected_sent, - actual_sent, - "expected {0}, got {1}".format(expected_sent, actual_sent) - ) - - expected_buffers = make_buffers(long_message, 16) - for line_num, buff in enumerate(expected_buffers): - expected_line = self.received_msg.format(buff) - actual_line = actual_reply[line_num] - self.assertEqual( - expected_line, - actual_line, - "expected {0}, got {1}".format(expected_line, actual_line) - ) - - -if __name__ == '__main__': - unittest.main() diff --git a/assignments/session02/completed_http_server.py b/assignments/session02/completed_http_server.py deleted file mode 100644 index 0b096011..00000000 --- a/assignments/session02/completed_http_server.py +++ /dev/null @@ -1,102 +0,0 @@ -import socket -import sys -import os -import mimetypes - - -def response_ok(body, mimetype): - """returns a basic HTTP response""" - resp = [] - resp.append("HTTP/1.1 200 OK") - resp.append("Content-Type: %s" % mimetype) - resp.append("") - resp.append(body) - return "\r\n".join(resp) - - -def response_method_not_allowed(): - """returns a 405 Method Not Allowed response""" - resp = [] - resp.append("HTTP/1.1 405 Method Not Allowed") - resp.append("") - return "\r\n".join(resp) - - -def response_not_found(): - """return a 404 Not Found response""" - resp = [] - resp.append("HTTP/1.1 404 Not Found") - resp.append("") - return "\r\n".join(resp) - - -def parse_request(request): - first_line = request.split("\r\n", 1)[0] - method, uri, protocol = first_line.split() - if method != "GET": - raise NotImplementedError("We only accept GET") - print >>sys.stderr, 'serving request for %s' % uri - return uri - - -def resolve_uri(uri): - """return the filesystem resources identified by 'uri'""" - home = 'webroot' # this is relative to the location of - # the server script, could be a full path - filename = os.path.join(home, uri.lstrip('/')) - if os.path.isfile(filename): - ext = os.path.splitext(filename)[1] - mimetype = mimetypes.types_map.get(ext, 'text/plain') - contents = open(filename, 'rb').read() - return contents, mimetype - elif os.path.isdir(filename): - listing = "\n".join(os.listdir(filename)) - return listing, 'text/plain' - else: - raise ValueError("Not Found") - - -def server(): - address = ('127.0.0.1', 10000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - print >>sys.stderr, "making a server on %s:%s" % address - sock.bind(address) - sock.listen(1) - - try: - while True: - print >>sys.stderr, 'waiting for a connection' - conn, addr = sock.accept() # blocking - try: - print >>sys.stderr, 'connection - %s:%s' % addr - request = "" - while True: - data = conn.recv(1024) - request += data - if len(data) < 1024 or not data: - break - - try: - uri = parse_request(request) - content, mimetype = resolve_uri(uri) - except NotImplementedError: - response = response_method_not_allowed() - except ValueError: - response = response_not_found() - else: - response = response_ok(content, mimetype) - - print >>sys.stderr, 'sending response' - conn.sendall(response) - finally: - conn.close() - - except KeyboardInterrupt: - sock.close() - return - - -if __name__ == '__main__': - server() - sys.exit(0) diff --git a/assignments/session02/http_server.py b/assignments/session02/http_server.py deleted file mode 100644 index 12cbfeba..00000000 --- a/assignments/session02/http_server.py +++ /dev/null @@ -1,71 +0,0 @@ -import socket -import sys - - -def response_ok(): - """returns a basic HTTP response""" - resp = [] - resp.append("HTTP/1.1 200 OK") - resp.append("Content-Type: text/plain") - resp.append("") - resp.append("this is a pretty minimal response") - return "\r\n".join(resp) - - -def response_method_not_allowed(): - """returns a 405 Method Not Allowed response""" - resp = [] - resp.append("HTTP/1.1 405 Method Not Allowed") - resp.append("") - return "\r\n".join(resp) - - -def parse_request(request): - first_line = request.split("\r\n", 1)[0] - method, uri, protocol = first_line.split() - if method != "GET": - raise NotImplementedError("We only accept GET") - print >>sys.stderr, 'request is okay' - - -def server(): - address = ('127.0.0.1', 10000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - print >>sys.stderr, "making a server on %s:%s" % address - sock.bind(address) - sock.listen(1) - - try: - while True: - print >>sys.stderr, 'waiting for a connection' - conn, addr = sock.accept() # blocking - try: - print >>sys.stderr, 'connection - %s:%s' % addr - request = "" - while True: - data = conn.recv(1024) - request += data - if len(data) < 1024 or not data: - break - - try: - parse_request(request) - except NotImplementedError: - response = response_method_not_allowed() - else: - response = response_ok() - - print >>sys.stderr, 'sending response' - conn.sendall(response) - finally: - conn.close() - - except KeyboardInterrupt: - sock.close() - return - - -if __name__ == '__main__': - server() - sys.exit(0) diff --git a/assignments/session02/tasks.txt b/assignments/session02/tasks.txt deleted file mode 100644 index fcf3e582..00000000 --- a/assignments/session02/tasks.txt +++ /dev/null @@ -1,82 +0,0 @@ -Session 2 Homework -================== - -Required Tasks: ---------------- - -in assignments/session02/http_server.py: - -* Update the parse_request function to return the URI it parses from the - request. - -* Update the response_ok function so that it accepts a body and mimetype - argument and properly includes these in the response it generates. - -* Write a new function resolve_uri that handles looking up resources on - disk using the URI. - - * It should take a URI as the sole argument - - * It should map the pathname represented by the URI to a filesystem - location. - - * It should have a 'home directory', and look only in that location. - - * If the URI is a directory, it should return a plain-text listing and the - mimetype ``text/plain``. - - * If the URI is a file, it should return the contents of that file and its - correct mimetype. - - * If the URI does not map to a real location, it should raise an exception - that the server can catch to return a 404 response. - -* Write a new function response_not_found that returns a 404 response if the - resource does not exist. - -* Update the code in the server loop to use the new and changed functions you - completed for the tasks above. - -When you have successfully completed these tasks as described, all the tests -in assignments/session02/tests.py will pass as written. If you have to update -the tests to get them to pass, think again about how you are implementing the -feature under test. - -To run the tests: - -* Open one terminal while in this folder and execute this command: - - $ python http_server.py - -* Open a second terminal in this same folder and execute this command: - - $ python tests.py - -Make sure to run the tests early and often during your work. Remember, TDD -means that as soon as a test passes you are finished working. - - -Optional Tasks: ---------------- - -* Update all error responses so that they return something that can be seen in - a web browser. - -* Format directory listings as HTML, so you can link to files. Update the - mimetype appropriately. - -* Add a GMT ``Date:`` header in the proper format (RFC-1123) to responses. - *hint: see email.utils.formatdate in the python standard library* - -* Add a ``Content-Length:`` header for ``OK`` responses that provides a - correct value. - -* Protect your server against errors by providing, and using, a function that - returns a ``500 Internal Server Error`` response. - -* Instead of returning the python script in ``webroot`` as plain text, execute - the file and return the results as HTML. - -If you choose to take on any of these optional tasks, try start by writing -tests in tests.py that demostrate what the task should accomplish. Then write -code that makes the tests pass. diff --git a/assignments/session03/tasks.txt b/assignments/session03/tasks.txt deleted file mode 100644 index 03c92b3f..00000000 --- a/assignments/session03/tasks.txt +++ /dev/null @@ -1,38 +0,0 @@ -Session 3 Homework -================== - -Required Tasks: ---------------- - -Using what you've learned this week, create a more complex mashup of some data -that interests you. Map the locations of the breweries near your house. Chart -a multi-axial graph of the popularity of various cities across several -categories. Visualize the most effective legislators in Congress. You have -interests, the Web has tools. Put them together to make something. - -Use the API directory at http://www.programmableweb.com/apis/directory to get -some ideas of data sources and what you might do with them. - -Place the following in the assignments/session03 directory and make a pull -request: - -A textual description of your mashup. - What data sources did you scan, what tools did you use, what is the - outcome you wanted to create? - -Your source code. - Give me an executable python script that I can run to get output. - -Any instructions I need. - If I need instructions beyond 'python myscript.py' to get the right - output, let me know. - -The data you produce need not be pretty, or even particularly visible. In -class we only produced a simple dictionary of values for each listing. Focus -on getting data sources combined rather than on what the output looks like. - - -Optional Tasks: ---------------- - -Write unit tests supporting the functions of your mashup script. diff --git a/assignments/session04/flask_walkthrough-plain.html b/assignments/session04/flask_walkthrough-plain.html deleted file mode 100644 index 00050a4d..00000000 --- a/assignments/session04/flask_walkthrough-plain.html +++ /dev/null @@ -1,580 +0,0 @@ - - - -
- - -If you've already set up your virtualenv and installed flask, you can simply -activate it and skip down to Kicking the Tires
-If not...
-We are going to install Flask, and the packages it requires, into a -virtualenv.
-This will ensure that it is isolated from everything else we do in class (and -vice versa)
-Remember the basic format for creating a virtualenv:
--$ python virtualenv.py [options] <ENV> -<or> -$ virtualenv [options] <ENV> --
Start by creating your virtualenv:
--$ python virtualenv.py flaskenv -<or> -$ virtualenv flaskenv -... --
Then, activate it:
--$ source flaskenv/bin/activate -<or> -C:\> flaskenv\Scripts\activate --
Finally, install Flask using setuptools or pip:
--(flaskenv)$ pip install flask -Downloading/unpacking flask - Downloading Flask-0.10.1.tar.gz (544kB): 544kB downloaded -... -Installing collected packages: flask, Werkzeug, Jinja2, - itsdangerous, markupsafe -... -Successfully installed flask Werkzeug Jinja2 itsdangerous - markupsafe --
We've installed the Flask microframework and all of its dependencies.
-Now, let's see what it can do
-With your flaskenv activated, create a file called flask_intro.py and -open it in your text editor.
-Getting started with Flask is pretty straightforward. Here's a complete, -simple app. Type it into flask_intro.py:
--from flask import Flask -app = Flask(__name__) - -@app.route('/') -def hello_world(): - return 'Hello World!' - -if __name__ == '__main__': - app.run() --
As you might expect by now, the last block in our flask_intro.py file -allows us to run this as a python program. Save your file, and in your -terminal try this:
--(flaskenv)$ python flask_intro.py --
Load http://localhost:5000 in your browser to see it in action.
-Last week, cgitb provided us with useful feedback when building an app. -Flask has similar functionality. Make the following changes to your -flask_intro.py file:
--def hello_world(): - bar = 1 / 0 - return 'Hello World!' - -if __name__ == '__main__': - app.run(debug=True) --
Restart your app and then reload your browser to see what happens.
-Click in the stack trace that appears in your browser. Notice anything fun?
-(clean up the error when you're done playing).
-Let's take a look at how that last bit works for a moment...
-Remember our bookdb exercise? How did you end up solving the problem of -mapping an HTTP request to the right function?
-Flask solves this problem by using the route decorator from your app.
-A 'route' takes a URL rule (more on that in a minute) and maps it to an -endpoint and a function.
-When a request arrives at a URL that matches a known rule, the function is -called.
-URL Rules are strings that represent what environ['PATH_INFO'] will look like.
-They are added to a mapping on the Flask object called the url_map
-You can call app.add_url_rule() to add a new one
-Or you can use what we've used, the app.route() decorator
--def index(): - """some function that returns something""" - # ... - -app.add_url_rule('/', 'homepage', index) --
is identical to
--@app.route('/', 'homepage') -def index(): - """some function that returns something""" - # ... --
A placeholder in a URL rule becomes a named arg to your function (add these -to flask_intro.py):
--@app.route('/profile/<username>') -def show_profile(username): - return "My username is %s" % username --
And converters ensure the incoming argument is of the correct type.
--@app.route('/div/<float:val>/') -def divide(val): - return "%0.2f divided by 2 is %0.2f" % (val, val / 2) --
You can also determine which HTTP methods a given route will accept:
--@app.route('/blog/entry/<int:id>/', methods=['GET',]) -def read_entry(id): - return "reading entry %d" % id - -@app.route('/blog/entry/<int:id>/', methods=['POST', ]) -def write_entry(id): - return 'writing entry %d' % id --
After adding that to flask_intro.py and saving, try loading -http://localhost:5000/blog/entry/23/ into your browser. Which was called?
-Reversing a URL means the ability to generate the url that would result in a -given endpoint being called.
-This means you don't have to hard-code your URLs when building links
-That means you can change the URLs for your app without changing code or -templates
-This is called decoupling and it is a good thing
-In Flask, you reverse a url with the url_for function.
--from flask import url_for -with app.test_request_context(): - print url_for('endpoint', **kwargs) --
Quit your Flask app with ^C. Then start a python interpreter in that same -terminal and import your flask_intro.py module:
-->>> from flask_intro import app ->>> from flask import url_for ->>> with app.test_request_context(): -... print url_for('show_profile', username="cris") -... print url_for('divide', val=23.7) -... -'/profile/cris/' -'/div/23.7/' ->>> --
That will give you plenty to think about before class. We'll put this all to -good use building a real flask app in our next session.
-In this tutorial, you'll walk through some basic concepts of data persistence -using the Python stdlib implementation of DB API 2, sqlite3
-There are many models for persistance of data.
-It's also one of the most contentious issues in app design.
-For this reason, it's one of the things that most Small Frameworks leave -undecided.
-PEP 249 describes a -common API for database connections called DB-API 2.
-The goal was to
---achieve a consistency leading to more easily understood modules, code -that is generally more portable across databases, and a broader reach -of database connectivity from Python
- -
It is important to remember that PEP 249 is only a specification.
-There is no code or package for DB-API 2 on it's own.
-Since 2.5, the Python Standard Library has provided a reference -implementation of the api -based on SQLite3
-Before Python 2.5, this package was available as pysqlite
-To use the DB API with any database other than SQLite3, you must have an -underlying API package available.
-Implementations are available for:
-Most db api packages can be installed using typical Pythonic methods:
--$ easy_install psycopg2 -$ pip install mysql-python -... --
Most api packages will require that the development headers for the underlying -database system be available. Without these, the C symbols required for -communication with the db are not present and the wrapper cannot work.
-We don't want to spend the next hour getting a package installed, so let's use -sqlite3 instead.
-I do not recommend using sqlite3 for production web applications, there are -too many ways in which it falls short
-But it will provide a solid learning tool
-In the class resources folder, you'll find an sql directory. Copy that to -your working directory.
-Open the file createdb.py in your text editor. Edit main like so:
--def main(): - conn = sqlite3.connect(DB_FILENAME) - if DB_IS_NEW: - print 'Need to create database and schema' - else: - print 'Database exists, assume schema does, too.' - conn.close() --
Run the createdb.py script to see it in effect:
--$ python createdb.py -Need to create database and schema -$ python createdb.py -Database exists, assume schema does, too. -$ ls -books.db -... --
Sqlite3 will automatically create a new database when you connect for the -first time, if one does not exist.
-Make the following changes to createdb.py:
--DB_FILENAME = 'books.db' -SCHEMA_FILENAME = 'ddl.sql' # <- this is new -DB_IS_NEW = not os.path.exists(DB_FILENAME) - -def main(): - with sqlite3.connect(DB_FILENAME) as conn: # <- context mgr - if DB_IS_NEW: # A whole new if clause: - print 'Creating schema' - with open(SCHEMA_FILENAME, 'rt') as f: - schema = f.read() - conn.executescript(schema) - else: - print 'Database exists, assume schema does, too.' - # delete the `conn.close()` that was here. --
Quit your python interpreter and delete the file books.db
-Then run the script from the command line again to try it out:
--$ python createdb.py -Creating schema -$ python createdb.py -Database exists, assume schema does, too. --
Add the following to createdb.py:
--# in the imports, add this line: -from utils import show_table_metadata - -else: - # in the else clause, replace the print statement with this: - print "Database exists, introspecting:" - tablenames = ['author', 'book'] - cursor = conn.cursor() - for name in tablenames: - print "\n" - show_table_metadata(cursor, name) --
Then try running python createdb.py again
--$ python createdb.py -Table Metadata for 'author': -cid | name | type | notnull | dflt_value | pk | ------------+------------+------------+------------+------------+------------+- -0 | authorid | INTEGER | 1 | None | 1 | ------------+------------+------------+------------+------------+------------+- -1 | name | TEXT | 0 | None | 0 | ------------+------------+------------+------------+------------+------------+- - - -Table Metadata for 'book': -cid | name | type | notnull | dflt_value | pk | ------------+------------+------------+------------+------------+------------+- -0 | bookid | INTEGER | 1 | None | 1 | ------------+------------+------------+------------+------------+------------+- -1 | title | TEXT | 0 | None | 0 | ------------+------------+------------+------------+------------+------------+- -2 | author | INTEGER | 1 | None | 0 | ------------+------------+------------+------------+------------+------------+- --
Let's load up some data. Fire up your interpreter and type:
-->>> import sqlite3 ->>> insert = """ -... INSERT INTO author (name) VALUES("Iain M. Banks");""" ->>> with sqlite3.connect("books.db") as conn: -... cur = conn.cursor() -... cur.execute(insert) -... cur.rowcount -... cur.close() -... -<sqlite3.Cursor object at 0x10046e880> -1 ->>> --
Did that work?
-Let's query our database to find out:
-->>> query = """ -... SELECT * from author;""" ->>> with sqlite3.connect("books.db") as conn: -... cur = conn.cursor() -... cur.execute(query) -... rows = cur.fetchall() -... for row in rows: -... print row -... -<sqlite3.Cursor object at 0x10046e8f0> -(1, u'Iain M. Banks') --
Alright! We've got data in there. Let's make it more efficient
-Try this:
-->>> insert = """ -... INSERT INTO author (name) VALUES(?);""" ->>> authors = [["China Mieville"], ["Frank Herbert"], -... ["J.R.R. Tolkien"], ["Susan Cooper"], ["Madeline L'Engle"]] ->>> with sqlite3.connect("books.db") as conn: -... cur = conn.cursor() -... cur.executemany(insert, authors) -... print cur.rowcount -... cur.close() -... -<sqlite3.Cursor object at 0x10046e8f0> -5 --
Again, query the database:
-->>> query = """ -... SELECT * from author;""" ->>> with sqlite3.connect("books.db") as conn: -... cur = conn.cursor() -... cur.execute(query) -... rows = cur.fetchall() -... for row in rows: -... print row -... -<sqlite3.Cursor object at 0x10046e8f0> -(1, u'Iain M. Banks') -... -(4, u'J.R.R. Tolkien') -(5, u'Susan Cooper') -(6, u"Madeline L'Engle") --
Transactions group operations together, allowing you to verify them before -the results hit the database.
-In SQLite3, data-altering statements require an explicit commit unless -auto-commit has been enabled.
-The with statements we've used take care of committing when the context -manager closes.
-Let's change that so we can see what happens explicitly
-Let's start by seeing what happens when you try to look for newly added data -before the insert transaction is committed.
-Begin by quitting your interpreter and deleting books.db.
-Then re-create the database, empty:
--$ python createdb.py -Creating schema --
Open populatedb.py in your editor, replace the final print:
--conn1 = sqlite3.connect(DB_FILENAME) -conn2 = sqlite3.connect(DB_FILENAME) -print "\nOn conn1, before insert:" -show_authors(conn1) -authors = ([author] for author in AUTHORS_BOOKS.keys()) -cur = conn1.cursor() -cur.executemany(author_insert, authors) -print "\nOn conn1, after insert:" -show_authors(conn1) -print "\nOn conn2, before commit:" -show_authors(conn2) -conn1.commit() -print "\nOn conn2, after commit:" -show_authors(conn2) -conn1.close() -conn2.close() --
Quit your python interpreter and run the populatedb.py script:
--On conn1, before insert: -no rows returned -On conn1, after insert: -(1, u'China Mieville') -(2, u'Frank Herbert') -(3, u'Susan Cooper') -(4, u'J.R.R. Tolkien') -(5, u"Madeline L'Engle") - -On conn2, before commit: -no rows returned -On conn2, after commit: -(1, u'China Mieville') -(2, u'Frank Herbert') -(3, u'Susan Cooper') -(4, u'J.R.R. Tolkien') -(5, u"Madeline L'Engle") --
That's all well and good, but what happens if an error occurs?
-Transactions can be rolled back in order to wipe out partially completed work.
-Like with commit, using connect as a context manager in a with -statement will automatically rollback for exceptions.
-Let's rewrite our populatedb script so it explicitly commits or rolls back a -transaction depending on exceptions occurring
-First, add the following function above the if __name__ == '__main__' -block:
--def populate_db(conn): - authors = ([author] for author in AUTHORS_BOOKS.keys()) - cur = conn.cursor() - cur.executemany(author_insert, authors) - - for author in AUTHORS_BOOKS.keys(): - params = ([book, author] for book in AUTHORS_BOOKS[author]) - cur.executemany(book_insert, params) --
Then, in the runner:
--with sqlite3.connect(DB_FILENAME) as conn1: - with sqlite3.connect(DB_FILENAME) as conn2: - try: - populate_db(conn1) - print "\nauthors and books on conn2 before commit:" - show_authors(conn2) - show_books(conn2) - except sqlite3.Error: - conn1.rollback() - print "\nauthors and books on conn2 after rollback:" - show_authors(conn2) - show_books(conn2) - raise - else: - conn1.commit() - print "\nauthors and books on conn2 after commit:" - show_authors(conn2) - show_books(conn2) --
Remove books.db and recrete the database, then run our script:
--$ rm books.db -$ python createdb.py -Creating schema -$ python populatedb.py --
-authors and books on conn2 after rollback: -no rows returned -no rows returned -Traceback (most recent call last): - File "populatedb.py", line 57, in <module> - populate_db(conn1) - File "populatedb.py", line 46, in populate_db - cur.executemany(book_insert, params) -sqlite3.InterfaceError: Error binding parameter 0 - probably unsupported type. --
Okay, we got an error, and the transaction was rolled back correctly.
-Open utils.py and find this:
--'Susan Cooper': ["The Dark is Rising", ["The Greenwitch"]], --
Fix it like so:
--'Susan Cooper': ["The Dark is Rising", "The Greenwitch"], --
It appears that we were attempting to bind a list as a parameter. Ooops.
-Now that the error in our data is repaired, let's try again:
--$ python populatedb.py --
-Reporting authors and books on conn2 before commit: -no rows returned -no rows returned -Reporting authors and books on conn2 after commit: -(1, u'China Mieville') -(2, u'Frank Herbert') -(3, u'Susan Cooper') -(4, u'J.R.R. Tolkien') -(5, u"Madeline L'Engle") -(1, u'Perdido Street Station', 1) -(2, u'The Scar', 1) -(3, u'King Rat', 1) -(4, u'Dune', 2) -(5, u"Hellstrom's Hive", 2) -(6, u'The Dark is Rising', 3) -(7, u'The Greenwitch', 3) -(8, u'The Hobbit', 4) -(9, u'The Silmarillion', 4) -(10, u'A Wrinkle in Time', 5) -(11, u'A Swiftly Tilting Planet', 5) --
You've just created a small database of books and authors. The transactional -protections you've used let you rest comfortable, knowing that so long as the -process completed, you've got the data you sent.
-We'll see more of this when we build our flask app.
-When you installed flask into your virtualenv, along with it came a -Python-based templating engine called Jinja2.
-In this walkthrough, you'll see some basics about how templates work, and get -to know what sorts of options they provide you for creating HTML from a Python -process.
-"I enjoy writing HTML in Python"
--- nobody, ever
-A good framework will provide some way of generating HTML with a templating -system.
-There are nearly as many templating systems as there are frameworks
-Each has advantages and disadvantages
-Flask includes the Jinja2 templating system (perhaps because it's built by -the same folks)
-Let's start with the absolute basics.
-Fire up a Python interpreter, using your flask virtualenv:
--(flaskenv)$ python ->>> from jinja2 import Template --
A template is built of a simple string:
-->>> t1 = Template("Hello {{ name }}, how are you?") --
Call the render method, providing some context:
-->>> t1.render(name="Freddy") -u'Hello Freddy, how are you?' ->>> t1.render({'name': "Roberto"}) -u'Hello Roberto, how are you?' ->>> --
Context can either be keyword arguments, or a dictionary
-Dictionaries passed in as part of the context can be addressed with either -subscript or dotted notation:
-->>> person = {'first_name': 'Frank', -... 'last_name': 'Herbert'} ->>> t2 = Template("{{ person.last_name }}, {{ person['first_name'] }}") ->>> t2.render(person=person) -u'Herbert, Frank' --
The exact same is true of objects passed in as part of context:
-->>> t3 = Template("{{ obj.x }} + {{ obj['y'] }} = Fun!") ->>> class Game(object): -... x = 'babies' -... y = 'bubbles' -... ->>> bathtime = Game() ->>> t3.render(obj=bathtime) -u'babies + bubbles = Fun!' --
This means your templates can be a bit agnostic as to the nature of the things -in context
-You can apply filters to the data passed in context with the pipe ('|') -operator:
--t4 = Template("shouted: {{ phrase|upper }}") ->>> t4.render(phrase="this is very important") -u'shouted: THIS IS VERY IMPORTANT' --
You can also chain filters together:
--t5 = Template("confusing: {{ phrase|upper|reverse }}") ->>> t5.render(phrase="howdy doody") -u'confusing: YDOOD YDWOH' --
Logical control structures are also available:
--tmpl = """ -... {% for item in list %}{{ item }}, {% endfor %} -... """ ->>> t6 = Template(tmpl) ->>> t6.render(list=[1,2,3,4,5,6]) -u'\n1, 2, 3, 4, 5, 6, ' --
Any control structure introduced in a template must be paired with an -explicit closing tag ({% for %}...{% endfor %})
-There are a number of specialized tests available for use with the -if...elif...else control structure:
-->>> tmpl = """ -... {% if phrase is upper %} -... {{ phrase|lower }} -... {% elif phrase is lower %} -... {{ phrase|upper }} -... {% else %}{{ phrase }}{% endif %}""" ->>> t7 = Template(tmpl) ->>> t7.render(phrase="FOO") -u'\n\n foo\n' ->>> t7.render(phrase="bar") -u'\n\n BAR\n' ->>> t7.render(phrase="This should print as-is") -u'\nThis should print as-is' --
Basic Python expressions are also supported:
--tmpl = """ -... {% set sum = 0 %} -... {% for val in values %} -... {{ val }}: {{ sum + val }} -... {% set sum = sum + val %} -... {% endfor %} -... """ ->>> t8 = Template(tmpl) ->>> t8.render(values=range(1,11)) -u'\n\n\n1: 1\n \n\n2: 3\n \n\n3: 6\n \n\n4: 10\n - \n\n5: 15\n \n\n6: 21\n \n\n7: 28\n \n\n8: 36\n - \n\n9: 45\n \n\n10: 55\n \n' --
There's more that Jinja2 templates can do, and you'll see more in class -when we write templates for our Flask app.
-Make sure that you bookmark the Jinja2 documentation for later use:
--http://jinja.pocoo.org/docs/templates/ --
In this tutorial, you'll walk through creating a very simple microblog -application using Django.
-We'll install Django and any other packages we use with it in a virtualenv.
-This will ensure that it is isolated from everything else we do in class (and -vice versa)
-Remember the basic format for creating a virtualenv:
--$ python virtualenv.py [options] <ENV> -<or> -$ virtualenv [options] <ENV> --
Start by creating your virtualenv:
--$ python virtualenv.py djangoenv -<or> -$ virtualenv djangoenv -... --
Then, activate it:
--$ source djangoenv/bin/activate -<or> -C:\> djangoenv\Scripts\activate --
Finally, install Django 1.6.2 using pip:
--(djangoenv)$ pip install Django==1.6.2 -Downloading/unpacking Django==1.5.2 - Downloading Django-1.6.2.tar.gz (8.0MB): 8.0MB downloaded - Running setup.py egg_info for package Django - changing mode of /path/to/djangoenv/bin/django-admin.py to 755 -Successfully installed Django -Cleaning up... -(djangoenv)$ --
Everything in Django stems from the project
-To get started learning, we'll create one
-We'll use a script installed by Django, django-admin.py:
--(djangoenv)$ django-admin.py startproject mysite --
This will create a folder called 'mysite'. Let's take a look at it:
-The folder created by django-admin.py contains the following structure:
--mysite -├── manage.py -└── mysite - ├── __init__.py - ├── settings.py - ├── urls.py - └── wsgi.py --
If what you see doesn't match that, you're using an older version of Django. -Make sure you've installed 1.6.2.
-django-admin.py provides a hook for administrative tasks and abilities:
-manage.py wraps this functionality, adding the full environment of your -project.
-Look in the manage.py script Django created for you. You'll see this:
--#!/usr/bin/env python -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - ... --
The environmental var DJANGO_SETTINGS_MODULE is how the manage.py -script is made aware of your project's environment. This is why you shouldn't -rename the project package.
-At this point, you should be ready to use the development server:
--(djangoenv)$ cd mysite -(djangoenv)$ python manage.py runserver -... --
Load http://localhost:8000 in your browser.
-You should see this:
-
-Do you?
-Django supplies its own ORM (Object-Relational Mapper)
-This ORM sits on top of the DB-API implementation you choose.
-You must provide connection information through Django configuration.
-All Django configuration takes place in settings.py in your project -folder.
-Edit your settings.py to match:
--DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'mysite.db', - } -} --
There are other database settings, but they are not used with sqlite3, we'll -ignore them for now.
-Django's ORM provides a layer of abstraction between you and SQL
-You write Python classes called models describing the object that make up -your system.
-The ORM handles converting data from these objects into SQL statements (and -back)
-We'll learn much more about this in a bit
-We've created a Django project. In Django a project represents a whole -website:
-A Django app encapsulates a unit of functionality:
-One project can (and likely will) consist of many apps
-Django already includes some apps for you.
-They're in settings.py in the INSTALLED_APPS setting:
--INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', -) --
These apps define models of their own, tables must be created.
-You make them by running the syncdb management command:
--(djangoenv)$ python manage.py syncdb -Creating tables ... -Creating table auth_permission -Creating table auth_group_permissions -Creating table auth_group -... -You just installed Django's auth system, ... -Would you like to create one now? (yes/no): --
Add your first user at this prompt. I strongly suggest you use the username -'admin' and give it the password 'admin'. If you don't, make sure you remember -the values you use.
-We are going to build an app to add to our project. To start with our app -will be a lot like the Flask app we finished last time.
-As stated above, an app represents a unit within a system, the project. We -have a project, we need to create an app
-This is accomplished using manage.py.
-In your terminal, make sure you are in the outer mysite directory, where the -file manage.py is located. Then:
--(djangoenv)$ python manage.py startapp myblog --
This should leave you with the following structure:
--mysite -├── manage.py -├── myblog -│ ├── __init__.py -│ ├── admin.py -│ ├── models.py -│ ├── tests.py -│ └── views.py -└── mysite - ├── __init__.py - ... --
We'll start by defining the main Python class for our blog system, a Post.
-Any Python class in Django that is meant to be persisted must inherit from -the Django Model class.
-This base class hooks in to the ORM functionality converting Python code to -SQL.
-You can override methods from the base Model class to alter how this works -or write new methods to add functionality.
-Learn more about models
-Open the models.py file created in our myblog package. Add the -following:
--from django.db import models #<-- This is already in the file -from django.contrib.auth.models import User - -class Post(models.Model): - title = models.CharField(max_length=128) - text = models.TextField(blank=True) - author = models.ForeignKey(User) - created_date = models.DateTimeField(auto_now_add=True) - modified_date = models.DateTimeField(auto_now=True) - published_date = models.DateTimeField(blank=True, null=True) --
We've created a subclass of the Django Model class and added a bunch of -attributes.
-You can read much more about Model Fields and options
-There are some features of our fields worth mentioning in specific:
-Notice we have no field that is designated as the primary key
--title = models.CharField(max_length=128) --
The required max_length argument is specific to CharField fields.
-It affects both the Python and SQL behavior of a field.
-In python, it is used to validate supplied values during model validation
-In SQL it is used in the column definition: VARCHAR(128)
--author = models.ForeignKey(User) --
Django also models SQL relationships as specific field types.
-The required positional argument is the class of the related Model.
-By default, the reverse relation is implemented as the attribute -<fieldname>_set.
-You can override this by providing the related_name argument.
--created_date = models.DateTimeField(auto_now_add=True) -modified_date = models.DateTimeField(auto_now=True) --
auto_now_add is available on all date and time fields. It sets the value -of the field to now when an instance is first saved.
-auto_now is similar, but sets the value anew each time an instance is -saved.
-Setting either of these will cause the editable attribute of a field to be -set to False.
--text = models.TextField(blank=True) -# ... -published_date = models.DateTimeField(blank=True, null=True) --
The argument blank is shared across all field types. The default is -False
-This argument affects only the Python behavior of a field, determining if the -field is required
-The related null argument affects the SQL definition of a field: is the -column NULL or NOT NULL
-In order to use our new model, we need Django to know about our app
-This is accomplished by configuration in the settings.py file.
-Open that file now, in your editor, and find the INSTALLED_APPS setting.
-You extend Django functionality by installing apps. This is pretty simple:
--INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'myblog', # <- YOU ADD THIS PART -) --
You know what the next step will be:
--(djangoenv)$ python manage.py syncdb -Creating tables ... -Creating table myblog_post -Installing custom SQL ... -Installing indexes ... -Installed 0 object(s) from 0 fixture(s) --
Django has now created a table for our model.
-Notice that the table name is a combination of the name of our app and the -name of our model.
-Django provides a management command shell:
-Let's explore the Model Instance API directly using this shell:
--(djangoenv)$ python manage.py shell --
Instances of our model can be created by simple instantiation:
-->>> from myblog.models import Post ->>> p1 = Post(title="My first post", -... text="This is the first post I've written") ->>> p1 -<Post: Post object> --
We can also validate that our new object is okay before we try to save it:
-->>> p1.full_clean() -Traceback (most recent call last): - ... -ValidationError: {'author': [u'This field cannot be null.']} --
We have to hook our Post to an author, which must be a User.
-To do this, we need to have an instance of the User class.
-We can use the User model manager to run table-level operations like -SELECT:
-All Django models have a manager. By default it is accessed through the -objects class attribute.
-Let's use the manager to get an instance of the User class:
-->>> from django.contrib.auth.models import User ->>> all_users = User.objects.all() ->>> all_users -[<User: cewing>] ->>> u1 = all_users[0] ->>> p1.author = u1 --
And now our instance should validate properly:
-->>> p1.full_clean() ->>> --
Our model has three date fields, two of which are supposed to be -auto-populated:
-->>> print(p1.created_date) -None ->>> print(p1.modified_date) -None --
When we save our post, these fields will get values assigned:
-->>> p1.save() ->>> p1.created_date -datetime.datetime(2013, 7, 26, 20, 2, 38, 104217, tzinfo=<UTC>) ->>> p1.modified_date -datetime.datetime(2013, 7, 26, 20, 2, 38, 104826, tzinfo=<UTC>) --
Models operate much like 'normal' python objects.
-To change the value of a field, simply set the instance attribute to a new -value. Call save() to persist the change:
-->>> p1.title = p1.title + " (updated)" ->>> p1.save() ->>> p1.title -'My first post (updated)' --
Let's create a few more posts so we can explore the Django model manager query -API:
-->>> p2 = Post(title="Another post", -... text="The second one created", -... author=u1).save() ->>> p3 = Post(title="The third one", -... text="With the word 'heffalump'", -... author=u1).save() ->>> p4 = Post(title="Posters are great decoration", -... text="When you are a poor college student", -... author=u1).save() ->>> Post.objects.count() -4 --
The manager on each model class supports a full-featured query API.
-API methods take keyword arguments, where the keywords are special -constructions combining field names with field lookups:
-Each keyword argument generates an SQL clause.
-API methods can be divided into two basic groups: methods that return -QuerySets and those that do not.
-The former may be chained without hitting the database:
-->>> a = Post.objects.all() #<-- no query yet ->>> b = a.filter(title__icontains="post") #<-- not yet ->>> c = b.exclude(text__contains="created") #<-- nope ->>> [(p.title, p.text) for p in c] #<-- This will issue the query --
Conversely, the latter will issue an SQL query when executed.
-->>> a.count() # immediately executes an SQL query --
If you are curious, you can see the SQL that a given QuerySet will use:
-->>> print(c.query) -SELECT "myblog_post"."id", "myblog_post"."title", - "myblog_post"."text", "myblog_post"."author_id", - "myblog_post"."created_date", "myblog_post"."modified_date", - "myblog_post"."published_date" -FROM "myblog_post" -WHERE ("myblog_post"."title" LIKE %post% ESCAPE '\' - AND NOT ("myblog_post"."text" LIKE %created% ESCAPE '\' ) -) --
The SQL will vary depending on which DBAPI backend you use (yay ORM!!!)
-See https://docs.djangoproject.com/en/1.6/ref/models/querysets
-->>> [p.pk for p in Post.objects.all().order_by('created_date')] -[1, 2, 3, 4] ->>> [p.pk for p in Post.objects.all().order_by('-created_date')] -[4, 3, 2, 1] ->>> [p.pk for p in Post.objects.filter(title__contains='post')] -[1, 2, 4] ->>> [p.pk for p in Post.objects.exclude(title__contains='post')] -[3] ->>> qs = Post.objects.exclude(title__contains='post') ->>> qs = qs.exclude(id__exact=3) ->>> [p.pk for p in qs] -[] ->>> qs = Post.objects.exclude(title__contains='post', id__exact=3) ->>> [p.pk for p in qs] -[1, 2, 3, 4] --
You can update all selected objects at the same time.
-Changes are persisted without needing to call save.
-->>> qs = Post.objects.all() ->>> [p.published_date for p in qs] -[None, None, None, None] ->>> from datetime import datetime ->>> from django.utils.timezone import UTC ->>> utc = UTC() ->>> now = datetime.now(utc) ->>> qs.update(published_date=now) -4 ->>> [p.published_date for p in qs] -[datetime.datetime(2013, 7, 27, 1, 20, 30, 505307, tzinfo=<UTC>), - ...] --
As with any project, we want to test our work. Django provides a testing -framework to allow this.
-Django supports both unit tests and doctests. I strongly suggest using -unit tests.
-You add tests for your app to the file tests.py, which should be at the -same package level as models.py.
-Locate and open this file in your editor.
-SimpleTestCase is for basic unit testing with no ORM requirements
-TransactionTestCase is useful if you need to test transactional -actions (commit and rollback) in the ORM
-TestCase is used when you require ORM access and a test client
-LiveServerTestCase launches the django server during test runs for -front-end acceptance tests.
-Sometimes testing requires base data to be present. We need a User for ours.
-Django provides fixtures to handle this need.
-Create a directory called fixtures inside your myblog app directory.
-Copy the file myblog_test_fixture.json from the class resources into this -directory, it contains users for our tests.
-Now that we have a fixture, we need to instruct our tests to use it.
-Edit tests.py (which comes with one test already) to look like this:
--from django.test import TestCase -from django.contrib.auth.models import User - -class PostTestCase(TestCase): - fixtures = ['myblog_test_fixture.json', ] - - def setUp(self): - self.user = User.objects.get(pk=1) --
Look at the way our Post represents itself in the Django shell:
-->>> [p for p in Post.objects.all()] -[<Post: Post object>, <Post: Post object>, - <Post: Post object>, <Post: Post object>] --
Wouldn't it be nice if the posts showed their titles instead?
-In Django, the __unicode__ method is used to determine how a Model -instance represents itself.
-Then, calling unicode(instance) gives the desired result.
-Let's write a test that demonstrates our desired outcome:
--# add this import at the top -from myblog.models import Post - -# and this test method to the PostTestCase -def test_unicode(self): - expected = "This is a title" - p1 = Post(title=expected) - actual = unicode(p1) - self.assertEqual(expected, actual) --
To run tests, use the test management command
-Without arguments, it will run all TestCases it finds in all installed apps
-You can pass the name of a single app to focus on those tests
-Quit your Django shell and in your terminal run the test we wrote:
--(djangoenv)$ python manage.py test myblog --
We have yet to implement this enhancement, so our test should fail:
--Creating test database for alias 'default'... -F -====================================================================== -FAIL: test_unicode (myblog.tests.PostTestCase) ----------------------------------------------------------------------- -Traceback (most recent call last): - File "/Users/cewing/projects/training/uw_pce/training.python_web/scripts/session07/mysite/myblog/tests.py", line 15, in test_unicode - self.assertEqual(expected, actual) -AssertionError: 'This is a title' != u'Post object' - ----------------------------------------------------------------------- -Ran 1 test in 0.007s - -FAILED (failures=1) -Destroying test database for alias 'default'... --
Let's add an appropriate __unicode__ method to our Post class
-It will take self as its only argument
-And it should return its own title as the result
-Go ahead and take a stab at this in models.py
--class Post(models.Model): - #... - - def __unicode__(self): - return self.title --
Re-run the tests to see:
--(djangoenv)$ python manage.py test myblog -Creating test database for alias 'default'... -. ----------------------------------------------------------------------- -Ran 1 test in 0.007s - -OK -Destroying test database for alias 'default'... --
YIPEEEE!
-In any framework, the question arises of what to test. Much of your app's -functionality is provided by framework tools. Does that need testing?
-I usually don't write tests covering features provided directly by the -framework.
-I do write tests for functionality I add, and for places where I make -changes to how the default functionality works.
-This is largely a matter of style and taste (and of budget).
-We've only begun to test our blog app.
-We'll be adding many more tests later
-In between, you might want to take a look at the Django testing documentation:
- -There are some who believe that Django has been Python's killer app
-And without doubt the Django Admin is a killer feature for Django.
-To demonstrate this, we are going to set up the admin for our blog
-The Django Admin is, itself, an app, installed by default (as of 1.6).
-Open the settings.py file from our mysite project package and -verify that you see it in the list:
--INSTALLED_APPS = ( - 'django.contrib.admin', # <- already present - # ... - 'django.contrib.staticfiles', # <- already present - 'myblog', # <- already present -) --
What we need now is to allow the admin to be seen through a web browser.
-To do that, we'll have to add some URLs to our project.
-Django too has a system for dispatching requests to code: the urlconf.
-I said above that a urlconf is an iterable.
-That iterable is generally built by calling the django.conf.urls.patterns -function.
-It's best to build it that way, but in reality, any iterable will do.
-However, the name you give this iterable is not flexible.
-Django will load the urlconf named urlpatterns that it finds in the file -named in settings.ROOT_URLCONF.
-Many Django add-on apps, like the Django Admin, come with their own urlconf
-It is standard to include these urlconfs by rooting them at some path in your -site.
-You can do this by using the django.conf.urls.include function as the -callable in a url call:
--url(r'^forum/', include('random.forum.app.urls')) --
We can use this to add all the URLs provided by the Django admin in one -stroke.
-verify the following lines in urls.py:
--from django.contrib import admin #<- make sure these two are -admin.autodiscover() #<- present and uncommented - -urlpatterns = patterns('', - ... - url(r'^admin/', include(admin.site.urls)), #<- and this -) --
We can now view the admin. We'll use the Django development server.
-In your terminal, use the runserver management command to start the -development server:
--(djangoenv)$ python manage.py runserver -Validating models... - -0 errors found -Django version 1.4.3, using settings 'mysite.settings' -Development server is running at http://127.0.0.1:8000/ -Quit the server with CONTROL-C. --
Load http://localhost:8000/admin/. You should see this:
-
-Login with the name and password you created before.
-The index will provide a list of all the installed apps and each model -registered. You should see this:
-
-Click on Users. Find yourself? Edit yourself, but don't uncheck -superuser.
-Okay, let's add our app model to the admin.
-Find the admin.py file in the myblog package. Open it, add the -following and save the file:
--from django.contrib import admin # <- this is already there. -from myblog.models import Post - -admin.site.register(Post) --
Reload the admin index page.
-Visit the admin page for Posts. You should see the posts we created earlier in -the Django shell.
-Look at the listing of Posts. Because of our __unicode__ method we see a -nice title.
-Are there other fields you'd like to see listed?
-Click on a Post, note what is and is not shown.
-We've learned a great deal about Django's ORM and Models.
-We've also spent some time getting to know the Query API provided by model -managers and QuerySets.
-We've also hooked up the Django Admin and noted some shortcomings.
-In class we'll learn how to put a front end on this, add new models, and -customize the admin experience.
-%s
Hey there, this page has been generated by {software}, running {script}
+Today is {month} {date}, {year}.
+This page was requested by IP Address {client_ip}
+-"""% time_str - -print html - - +""" % time_str +print(html) diff --git a/assignments/session02/webroot/sample.txt b/resources/session02/homework/webroot/sample.txt similarity index 100% rename from assignments/session02/webroot/sample.txt rename to resources/session02/homework/webroot/sample.txt diff --git a/resources/session02/http_server.py b/resources/session02/http_server.py new file mode 100644 index 00000000..d5aaf480 --- /dev/null +++ b/resources/session02/http_server.py @@ -0,0 +1,39 @@ +import socket +import sys + + +def server(log_buffer=sys.stderr): + address = ('127.0.0.1', 10000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + print("making a server on {0}:{1}".format(*address), file=log_buffer) + sock.bind(address) + sock.listen(1) + + try: + while True: + print('waiting for a connection', file=log_buffer) + conn, addr = sock.accept() # blocking + try: + print('connection - {0}:{1}'.format(*addr), file=log_buffer) + while True: + data = conn.recv(16) + print('received "{0}"'.format(data), file=log_buffer) + if data: + print('sending data back to client', file=log_buffer) + conn.sendall(data) + else: + msg = 'no more data from {0}:{1}'.format(*addr) + print(msg, log_buffer) + break + finally: + conn.close() + + except KeyboardInterrupt: + sock.close() + return + + +if __name__ == '__main__': + server() + sys.exit(0) diff --git a/assignments/session02/simple_client.py b/resources/session02/simple_client.py similarity index 60% rename from assignments/session02/simple_client.py rename to resources/session02/simple_client.py index c0f0d6e2..74523a2a 100644 --- a/assignments/session02/simple_client.py +++ b/resources/session02/simple_client.py @@ -7,22 +7,25 @@ def client(msg): sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP ) - print >>sys.stderr, 'connecting to {0} port {1}'.format(*server_address) + print( + 'connecting to {0} port {1}'.format(*server_address), + file=sys.stderr + ) sock.connect(server_address) response = '' done = False bufsize = 1024 try: - print >>sys.stderr, 'sending "{0}"'.format(msg) - sock.sendall(msg) + print('sending "{0}"'.format(msg), file=sys.stderr) + sock.sendall(msg.encode('utf8')) while not done: chunk = sock.recv(bufsize) if len(chunk) < bufsize: done = True - response += chunk - print >>sys.stderr, 'received "{0}"'.format(response) + response += chunk.decode('utf8') + print('received "{0}"'.format(response), file=sys.stderr) finally: - print >>sys.stderr, 'closing socket' + print('closing socket', file=sys.stderr) sock.close() return response @@ -30,8 +33,8 @@ def client(msg): if __name__ == '__main__': if len(sys.argv) != 2: usg = '\nusage: python echo_client.py "this is my message"\n' - print >>sys.stderr, usg + print(usg, file=sys.stderr) sys.exit(1) - + msg = sys.argv[1] - client(msg) \ No newline at end of file + client(msg) diff --git a/resources/session02/tests.py b/resources/session02/tests.py new file mode 100644 index 00000000..a4da1793 --- /dev/null +++ b/resources/session02/tests.py @@ -0,0 +1,165 @@ +import mimetypes +import socket +import unittest + + +CRLF = '\r\n' +CRLF_BYTES = CRLF.encode('utf8') +KNOWN_TYPES = set( + map(lambda x: x.encode('utf8'), mimetypes.types_map.values()) +) + + +def extract_response_code(response): + return response.split(CRLF_BYTES, 1)[0].split(b' ', 1)[1].strip() + + +def extract_response_protocol(response): + return response.split(CRLF_BYTES, 1)[0].split(b' ', 1)[0].strip() + + +def extract_headers(response): + return response.split(CRLF_BYTES*2, 1)[0].split(CRLF_BYTES)[1:] + + +class ResponseOkTestCase(unittest.TestCase): + """unit tests for the response_ok method in our server + + Becase this is a unit test case, it does not require the server to be + running. + """ + + def call_function_under_test(self): + """call the `response_ok` function from our http_server module""" + from http_server import response_ok + return response_ok() + + def test_response_code(self): + ok = self.call_function_under_test() + expected = "200 OK" + actual = extract_response_code(ok) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_protocol(self): + ok = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = extract_response_protocol(ok) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_has_content_type_header(self): + ok = self.call_function_under_test() + headers = extract_headers(ok) + expected_name = 'content-type'.encode('utf8') + has_header = False + for header in headers: + name, value = header.split(b':') + actual_name = name.strip().lower() + if actual_name == expected_name: + has_header = True + break + self.assertTrue(has_header) + + def test_response_has_legitimate_content_type(self): + ok = self.call_function_under_test() + headers = extract_headers(ok) + expected_name = 'content-type'.encode('utf8') + for header in headers: + name, value = header.split(b':') + actual_name = name.strip().lower() + if actual_name == expected_name: + self.assertTrue(value.strip() in KNOWN_TYPES) + return + self.fail('no content type header found') + + +class ResponseMethodNotAllowedTestCase(unittest.TestCase): + """unit tests for the response_method_not_allowed function""" + + def call_function_under_test(self): + """call the `response_method_not_allowed` function""" + from http_server import response_method_not_allowed + return response_method_not_allowed() + + def test_response_code(self): + resp = self.call_function_under_test() + expected = "405 Method Not Allowed" + actual = extract_response_code(resp) + self.assertEqual(expected.encode('utf8'), actual) + + def test_response_method(self): + resp = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = extract_response_protocol(resp) + self.assertEqual(expected.encode('utf8'), actual) + + +class ParseRequestTestCase(unittest.TestCase): + """unit tests for the parse_request method""" + + def call_function_under_test(self, request): + """call the `parse_request` function""" + from http_server import parse_request + return parse_request(request) + + def test_get_method(self): + """verify that GET HTTP requests do not raise an error""" + request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + try: + self.call_function_under_test(request) + except (NotImplementedError, Exception) as e: + self.fail('GET method raises an error {0}'.format(str(e))) + + def test_bad_http_methods(self): + """verify that non-GET HTTP methods raise a NotImplementedError""" + methods = ['POST', 'PUT', 'DELETE', 'HEAD'] + request_template = "{0} / HTTP/1.1\r\nHost: example.com\r\n\r\n" + for method in methods: + request = request_template.format(method) + self.assertRaises( + NotImplementedError, self.call_function_under_test, request + ) + + +class HTTPServerFunctionalTestCase(unittest.TestCase): + """functional tests of the HTTP Server + + This test case interacts with the http server, and as such requires it to + be running in order for the tests to pass + """ + + def send_message(self, message): + """Attempt to send a message using the client and the test buffer + + In case of a socket error, fail and report the problem + """ + from simple_client import client + response = '' + try: + response = client(message) + except socket.error as e: + if e.errno == 61: + msg = "Error: {0}, is the server running?" + self.fail(msg.format(e.strerror)) + else: + self.fail("Unexpected Error: {0}".format(str(e))) + return response + + def test_get_request(self): + message = CRLF.join(['GET / HTTP/1.1', 'Host: example.com', '']) + expected = '200 OK' + actual = self.send_message(message) + self.assertTrue( + expected in actual, '"{0}" not in "{1}"'.format(expected, actual) + ) + + def test_post_request(self): + message = CRLF.join(['POST / HTTP/1.1', 'Host: example.com', '']) + expected = '405 Method Not Allowed' + actual = self.send_message(message) + self.assertTrue( + expected in actual, '"{0}" not in "{1}"'.format(expected, actual) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/resources/session03/cgi/cgi-bin/cgi_1.py b/resources/session03/cgi/cgi-bin/cgi_1.py new file mode 100755 index 00000000..baa5c3e9 --- /dev/null +++ b/resources/session03/cgi/cgi-bin/cgi_1.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +import cgi + + +cgi.test() diff --git a/resources/session03/cgi/cgi-bin/cgi_2.py b/resources/session03/cgi/cgi-bin/cgi_2.py new file mode 100755 index 00000000..100ccded --- /dev/null +++ b/resources/session03/cgi/cgi-bin/cgi_2.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +import cgi +import cgitb +cgitb.enable() +import os +import datetime + + +default = "No Value Present" + + +print("Content-Type: text/html") +print() + +body = """ +
+
+ +
+