From 5459388e9583a5a59c8c5ef8fa59183cb720ecdd Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 30 Dec 2014 19:33:21 -0800 Subject: [PATCH 001/173] pep8 fix --- assignments/session02/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignments/session02/tests.py b/assignments/session02/tests.py index a74fe150..1438857c 100644 --- a/assignments/session02/tests.py +++ b/assignments/session02/tests.py @@ -77,7 +77,7 @@ def test_passed_mimetype_in_response(self): def test_passed_body_in_response(self): bodies = [ - "a body", + "a body", "a longer body\nwith two lines", open("webroot/sample.txt", 'r').read(), ] From dcdd8a0d6efd269d8bea8a1791853a12e6ec1316 Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 6 Jan 2015 00:54:31 -0800 Subject: [PATCH 002/173] removing more potentially obsolete material --- source/additional/flask_walkthrough.rst | 342 ---------- source/additional/git_cleanup.rst | 219 ------ .../additional/sql_persistence_tutorial.rst | 625 ------------------ source/additional/template_tutorial.rst | 244 ------- 4 files changed, 1430 deletions(-) delete mode 100644 source/additional/flask_walkthrough.rst delete mode 100644 source/additional/git_cleanup.rst delete mode 100644 source/additional/sql_persistence_tutorial.rst delete mode 100644 source/additional/template_tutorial.rst diff --git a/source/additional/flask_walkthrough.rst b/source/additional/flask_walkthrough.rst deleted file mode 100644 index eb5a408b..00000000 --- a/source/additional/flask_walkthrough.rst +++ /dev/null @@ -1,342 +0,0 @@ -.. slideconf:: - :autoslides: False - -A Quick Flask Walkthrough -========================= - -.. slide:: A Quick Flask Walkthrough - :level: 1 - - This document contains no slides. - -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... - -Practice Safe Development -------------------------- - -We are going to install Flask, and the packages it requires, into a -virtualenv. - -.. class:: incremental - -This will ensure that it is isolated from everything else we do in class (and -vice versa) - -.. container:: incremental - - Remember the basic format for creating a virtualenv: - - .. class:: small - - :: - - $ python virtualenv.py [options] - - $ virtualenv [options] - - -Set Up a VirtualEnv -------------------- - -Start by creating your virtualenv:: - - $ python virtualenv.py flaskenv - - $ virtualenv flaskenv - ... - -.. container:: incremental - - Then, activate it:: - - $ source flaskenv/bin/activate - - C:\> flaskenv\Scripts\activate - - -Install Flask -------------- - -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 - - -Kicking the Tires ------------------ - -We've installed the Flask microframework and all of its dependencies. - -.. class:: incremental - -Now, let's see what it can do - -.. class:: incremental - -With your flaskenv activated, create a file called ``flask_intro.py`` and -open it in your text editor. - - -Flask ------ - -Getting started with Flask is pretty straightforward. Here's a complete, -simple app. Type it into `flask_intro.py`: - -.. code-block:: python - :class: small - - from flask import Flask - app = Flask(__name__) - - @app.route('/') - def hello_world(): - return 'Hello World!' - - if __name__ == '__main__': - app.run() - - -Running our App ---------------- - -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 - -.. class:: incremental - -Load ``http://localhost:5000`` in your browser to see it in action. - - -Debugging our App ------------------ - -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: - -.. code-block:: python - :class: small - - def hello_world(): - bar = 1 / 0 - return 'Hello World!' - - if __name__ == '__main__': - app.run(debug=True) - -.. class:: incremental - -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). - - -Your work so far ----------------- - -.. class:: incremental - -* You instantiated a `Flask` app with a name that represents the package or - module containing the app - - * Because our app is a single Python module, this should be ``__name__`` - * This is used to help the `Flask` app figure out where to look for - *resources* - -* You defined a function that returned a response body -* You told the app which requests should use that function with a *route* - -.. class:: incremental - -Let's take a look at how that last bit works for a moment... - - -URL Routing ------------ - -Remember our bookdb exercise? How did you end up solving the problem of -mapping an HTTP request to the right function? - -.. class:: incremental - -Flask solves this problem by using the `route` decorator from your app. - -.. class:: incremental - -A 'route' takes a URL rule (more on that in a minute) and maps it to an -*endpoint* and a *function*. - -.. class:: incremental - -When a request arrives at a URL that matches a known rule, the function is -called. - - -URL Rules ---------- - -URL Rules are strings that represent what environ['PATH_INFO'] will look like. - -.. class:: incremental - -They are added to a *mapping* on the Flask object called the *url_map* - -.. class:: incremental - -You can call ``app.add_url_rule()`` to add a new one - -.. class:: incremental - -Or you can use what we've used, the ``app.route()`` decorator - - -Function or Decorator ---------------------- - -.. code-block:: python - :class: small - - def index(): - """some function that returns something""" - # ... - - app.add_url_rule('/', 'homepage', index) - -.. container:: incremental - - is identical to - - .. code-block:: python - :class: small - - @app.route('/', 'homepage') - def index(): - """some function that returns something""" - # ... - - -Routes Can Be Dynamic ---------------------- - -A *placeholder* in a URL rule becomes a named arg to your function (add these -to ``flask_intro.py``): - -.. code-block:: python - :class: incremental small - - @app.route('/profile/') - def show_profile(username): - return "My username is %s" % username - -.. class:: incremental - -And *converters* ensure the incoming argument is of the correct type. - -.. code-block:: python - :class: incremental small - - @app.route('/div//') - def divide(val): - return "%0.2f divided by 2 is %0.2f" % (val, val / 2) - - -Routes Can Be Filtered ----------------------- - -You can also determine which HTTP *methods* a given route will accept: - -.. code-block:: python - :class: small - - @app.route('/blog/entry//', methods=['GET',]) - def read_entry(id): - return "reading entry %d" % id - - @app.route('/blog/entry//', methods=['POST', ]) - def write_entry(id): - return 'writing entry %d' % id - -.. class:: incremental - -After adding that to ``flask_intro.py`` and saving, try loading -``http://localhost:5000/blog/entry/23/`` into your browser. Which was called? - -Routes Can Be Reversed ----------------------- - -Reversing a URL means the ability to generate the url that would result in a -given endpoint being called. - -.. class:: incremental - -This means *you don't have to hard-code your URLs when building links* - -.. class:: incremental - -That means *you can change the URLs for your app without changing code or -templates* - -.. class:: incremental - -This is called **decoupling** and it is a good thing - -Reversing URLs in Flask ------------------------ - -In Flask, you reverse a url with the ``url_for`` function. - -.. class:: incremental - -* ``url_for`` requires an HTTP request context to work -* You can fake an HTTP request when working in a terminal (or testing) -* Use the ``test_request_context`` method of your app object -* This is a great chance to use the Python ``with`` statement -* **Don't type this** - -.. code-block:: python - :class: small incremental - - from flask import url_for - with app.test_request_context(): - print url_for('endpoint', **kwargs) - -Reversing in Action -------------------- - -Quit your Flask app with ``^C``. Then start a python interpreter in that same -terminal and import your ``flask_intro.py`` module: - -.. code-block:: python - - >>> 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/' - >>> - -Enough for Now --------------- - -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. diff --git a/source/additional/git_cleanup.rst b/source/additional/git_cleanup.rst deleted file mode 100644 index 35338f79..00000000 --- a/source/additional/git_cleanup.rst +++ /dev/null @@ -1,219 +0,0 @@ -.. slideconf:: - :autoslides: False - -Steps to get a clean master branch for your git fork -==================================================== - -.. slide:: Steps to get a clean master branch for your git fork - :level: 1 - - This document contains no slides. - -Command line instructions for getting back to a pristine master branch - - -1. Make sure that you have the UWPCE repostitory set up as an additional - remote for your local repository:: - - $ git remote add uwpce git@github.com:UWPCE-PythonCert/training.python_web.git - -This will give you direct access to the original copy of the repository from -the command line. - -2. Verify this worked by checking your remotes:: - - $ git remote - origin - uwpce - -Now you have *two* remote repositories connected to your local repository. - -* *Origin* represents the copy of your fork of the UW PCE repository *on - github's servers*. -* *Uwpce* is the original UW PCE repository *on github's servers*. - -State the Problem ------------------ - -You have a series of changes *you* have made to the *master* branch of your -repository, both the local and the *origin* remote. - -Every time you make new changes for a homework and then submit a pull request, -all these old changes are included in the pull request. - -State the Goal --------------- - -You would like to get a *master* branch of your repository that exactly matches -the *master* branch of the UW PCE remote (*uwpce*). - -Once you have this, you can then keep that branch up to date with the UW PCE -copy - -And you can continue to make clean branches for each homework *starting from -that clean master*. - -Steps to get there ------------------- - -Preserve your Old Work -++++++++++++++++++++++ - -First, make a branch on your local machine of your current *master*, this will -be a branch you keep that contains all your homework up until today:: - - $ git branch -a - * master - remotes/origin/HEAD -> origin/master - remotes/origin/gh-pages - remotes/origin/instructor - remotes/origin/master - remotes/origin/week-long-format - remotes/uwpce/master - $ git branch keep-old-work - $ git branch -a - keep-old-work - * master - remotes/origin/HEAD -> origin/master - remotes/origin/gh-pages - remotes/origin/instructor - remotes/origin/master - remotes/origin/week-long-format - remotes/uwpce/master - -Now, you have a copy of all the work you've done to date. It's on the -*keep-old-work* branch. You have not yet pushed this branch up to your github -account, so let's do that next, making it safe:: - - $ git push -u origin keep-old-work - Total 0 (delta 0), reused 0 (delta 0) - To git@github.com:cewing/training.python_web.git - * [new branch] keep-old-work -> keep-old-work - Branch keep-old-work set up to track remote branch keep-old-work from origin. - -Okay, now there's a copy of your old work safe in a branch on *your* github -repository. - -Revert Your Master -++++++++++++++++++ - -The next step is to *roll back your master* to a point *before you made any -changes to it*. - -The key here is understanding that every change you commit to a repository in -git is associated with a *hash*, which is a big, unique identification number -you can use to refer to that specific change. You can see these numbers when -you look at the list of commits in github. - -You need to find the number of a commit by me that happened before you began -making changes. - -First, open the 'commits' page on github of your fork of the class repository. - -Then, scroll down until you find your first commit, which should be part of -work for session01 homework. - -Then, find the last commit *before* that commit, and click on the number in the -far right of that commit listing (it should be something like `b60ea2bb70`) - -This will open up that specific commit, and in the URL for that commit you will -find the full hash: `b60ea2bb7052a5bd300772d7d9d40b19b27f7a1b`. Copy that value. - -Now, we are going to reset your local *master* branch to that commit, -abandoning all the changes you (and I) have made between then and now:: - - $ git branch - keep-old-work - * master - $ git reset --hard b60ea2bb7052a5bd300772d7d9d40b19b27f7a1b - -Now, your *local master* has been reverted to a state before you did any work. -All your changes have been deleted, but so have all the changes I've made since -the start of class. - -Luckily, we can fix that. Our next step is to fetch the *uwpce* *master* -branch, which contains all those changes I've made, but none of the changes you -made: - - $ git fetch uwpce master - remote: Counting objects: 10, done. - remote: Compressing objects: 100% (10/10), done. - remote: Total 10 (delta 3), reused 7 (delta 0) - Unpacking objects: 100% (10/10), done. - From github.com:UWPCE-PythonCert/training.python_web - * branch master -> FETCH_HEAD - 8873ba1..75a8462 master -> uwpce/master - -And finally, we can merge the changes in the *uwpce* master into our local -*master*:: - - $ git branch - keep-old-work - * master - $ git merge uwpce/master - Merge made by the 'recursive' strategy. - source/presentations/session04.rst | 7 + - source/presentations/session06.rst | 1624 +----------------------------------- - 2 files changed, 40 insertions(+), 1591 deletions(-) - - -Forcibly Update -+++++++++++++++ - -Now, what we have is a situation where your local master has a history that is -completely different from the *origin* to which it is attached. Your -*origin/master* still has your work on it, interleaved with the changes I've -made along the way, but your *local* master contains only my work. - -If you were to try to push these changes up to *origin* (your repository) it would -fail because there's no way to reconcile the two histories. - -But we don't care about the history on your *origin*, we only want to keep the -history that is represented by what is currently in your *local* master branch. -To do that, we can push with the `--force` option:: - - $ git push --force origin master - Counting objects: 25, done. - Delta compression using up to 8 threads. - Compressing objects: 100% (11/11), done. - Writing objects: 100% (11/11), 2.04 KiB | 0 bytes/s, done. - Total 11 (delta 7), reused 0 (delta 0) - To git@github.com:cewing/training.python_web.git - + 782d17e...5fb97f3 master -> master (forced update) - -Okay. This means that now *master* both on your local machine and on the -*origin* remote (your github repository) is identical to (and up to date with) -the master in the *uwpce* repository. - - -Going Forward -------------- - -From now on, when you want to get the very latest copies of the *uwpce* -repository, you can issue these commands:: - - $ git checkout master - $ git fetch uwpce master - $ git merge uwpce/master - $ git push origin master - -That will fetch the changes from the *uwpce* remote *master* branch, merge them -into your *local* repository *master* branch, and then push those changes up to -your *origin* repository *master* branch. - -And when you are ready to start work on a new homework assignment, you can -simply start a new branch:: - - $ git checkout -b session05-homework - -Once you've completed your homework, and committed all the changes to your -*local* homework branch, you can push that branch up to your *origin* -repository:: - - $ git push origin session05-homework - -And then, when you open a pull request for me to review your homework, you can -select your *homework branch* as the source of the pull request, and my -*master* branch as the destination. The request will contain only those changes -that are germane to your homework. - diff --git a/source/additional/sql_persistence_tutorial.rst b/source/additional/sql_persistence_tutorial.rst deleted file mode 100644 index 5e47ba99..00000000 --- a/source/additional/sql_persistence_tutorial.rst +++ /dev/null @@ -1,625 +0,0 @@ -.. slideconf:: - :autoslides: False - -SQL Persistence in Python -========================= - -.. slide:: SQL Persistence in Python - :level: 1 - - This document contains no slides. - -In this tutorial, you'll walk through some basic concepts of data persistence -using the Python stdlib implementation of DB API 2, `sqlite3` - -Data Persistence ----------------- - -There are many models for persistance of data. - -.. class:: incremental - -* Flat files -* Relational Database (SQL RDBMs like PostgreSQL, MySQL, SQLServer, Oracle) -* Object Stores (Pickle, ZODB) -* NoSQL Databases (CouchDB, MongoDB, etc) - -.. class:: incremental - -It's also one of the most contentious issues in app design. - -.. class:: incremental - -For this reason, it's one of the things that most Small Frameworks leave -undecided. - - -Simple SQL ----------- - -`PEP 249 `_ describes a -common API for database connections called DB-API 2. - -.. container:: incremental - - 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 - - .. class:: image-credit - - source: http://www.python.org/dev/peps/pep-0248/ - - -A Note on DB API ----------------- - -.. class:: incremental center - -It is important to remember that PEP 249 is **only a specification**. - -.. class:: incremental - -There is no code or package for DB-API 2 on it's own. - -.. class:: incremental - -Since 2.5, the Python Standard Library has provided a `reference -implementation of the api `_ -based on SQLite3 - -.. class:: incremental - -Before Python 2.5, this package was available as ``pysqlite`` - - -Using DB API ------------- - -To use the DB API with any database other than SQLite3, you must have an -underlying API package available. - -.. container:: incremental - - Implementations are available for: - - * PostgreSQL (**psycopg2**, txpostgres, ...) - * MySQL (**mysql-python**, PyMySQL, ...) - * MS SQL Server (**adodbapi**, pymssql, mxODBC, pyodbc, ...) - * Oracle (**cx_Oracle**, mxODBC, pyodbc, ...) - * and many more... - - .. class:: image-credit - - source: http://wiki.python.org/moin/DatabaseInterfaces - - -Installing API Packages ------------------------ - -Most db api packages can be installed using typical Pythonic methods:: - - $ easy_install psycopg2 - $ pip install mysql-python - ... - -.. class:: incremental - -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. - - -Not Today ---------- - -We don't want to spend the next hour getting a package installed, so let's use -``sqlite3`` instead. - -.. class:: incremental - -I **do not** recommend using sqlite3 for production web applications, there are -too many ways in which it falls short - -.. class:: incremental - -But it will provide a solid learning tool - - -Getting Started ---------------- - -In the class resources folder, you'll find an ``sql`` directory. Copy that to -your working directory. - -.. class:: incremental - -Open the file ``createdb.py`` in your text editor. Edit ``main`` like so: - -.. code-block:: python - :class: incremental small - - 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() - - -Try It Out ----------- - -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 - ... - -.. class:: incremental - -Sqlite3 will automatically create a new database when you connect for the -first time, if one does not exist. - - -Set Up A Schema ---------------- - -Make the following changes to ``createdb.py``: - -.. code-block:: python - :class: small - - 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. - - -Verify Your Work ----------------- - -Quit your python interpreter and delete the file ``books.db`` - -.. container:: incremental - - 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. - -Introspect the Database ------------------------ - -Add the following to ``createdb.py``: - -.. code-block:: python - :class: small - - # 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) - -.. class:: incremental - -Then try running ``python createdb.py`` again - -My Results ----------- - -.. class:: small - -:: - - $ 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 | - -----------+------------+------------+------------+------------+------------+- - - -Inserting Data --------------- - -Let's load up some data. Fire up your interpreter and type: - -.. code-block:: python - :class: small - - >>> 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() - ... - - 1 - >>> - -.. class:: incremental - -Did that work? - - -Querying Data -------------- - -Let's query our database to find out: - -.. code-block:: python - :class: small - - >>> 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 - ... - - (1, u'Iain M. Banks') - -.. class:: incremental - -Alright! We've got data in there. Let's make it more efficient - - -Parameterized Statements ------------------------- - -Try this: - -.. code-block:: python - :class: small - - >>> 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() - ... - - 5 - - -Check Your Work ---------------- - -Again, query the database: - -.. code-block:: python - :class: small - - >>> 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 - ... - - (1, u'Iain M. Banks') - ... - (4, u'J.R.R. Tolkien') - (5, u'Susan Cooper') - (6, u"Madeline L'Engle") - - -Transactions ------------- - -Transactions group operations together, allowing you to verify them *before* -the results hit the database. - -.. class:: incremental - -In SQLite3, data-altering statements require an explicit ``commit`` unless -auto-commit has been enabled. - -.. class:: incremental - -The ``with`` statements we've used take care of committing when the context -manager closes. - -.. class:: incremental - -Let's change that so we can see what happens explicitly - - -Populating the Database ------------------------ - -Let's start by seeing what happens when you try to look for newly added data -before the ``insert`` transaction is committed. - -.. class:: incremental - -Begin by quitting your interpreter and deleting ``books.db``. - -.. container:: incremental - - Then re-create the database, empty:: - - $ python createdb.py - Creating schema - - -Setting Up the Test -------------------- - -.. class:: small - -Open ``populatedb.py`` in your editor, replace the final ``print``: - -.. code-block:: python - :class: small - - 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() - - -Running the Test ----------------- - -.. class:: small - -Quit your python interpreter and run the ``populatedb.py`` script: - -.. class:: small incremental - -:: - - 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") - - -Rollback --------- - -That's all well and good, but what happens if an error occurs? - -.. class:: incremental - -Transactions can be rolled back in order to wipe out partially completed work. - -.. class:: incremental - -Like with commit, using ``connect`` as a context manager in a ``with`` -statement will automatically rollback for exceptions. - -.. class:: incremental - -Let's rewrite our populatedb script so it explicitly commits or rolls back a -transaction depending on exceptions occurring - - -Edit populatedb.py (slide 1) ----------------------------- - -.. class:: small - -First, add the following function above the ``if __name__ == '__main__'`` -block: - -.. code-block:: python - :class: small - - 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) - - -Edit populatedb.py (slide 2) ----------------------------- - -.. class:: small - -Then, in the runner: - -.. code-block:: python - :class: small - - 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) - - -Try it Out ----------- - -Remove ``books.db`` and recrete the database, then run our script: - -.. class:: small - -:: - - $ rm books.db - $ python createdb.py - Creating schema - $ python populatedb.py - -.. class:: small incremental - -:: - - authors and books on conn2 after rollback: - no rows returned - no rows returned - Traceback (most recent call last): - File "populatedb.py", line 57, in - 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. - -Oooops, Fix It --------------- - -.. class:: small - -Okay, we got an error, and the transaction was rolled back correctly. - -.. container:: incremental small - - Open ``utils.py`` and find this: - - .. code-block:: python - - 'Susan Cooper': ["The Dark is Rising", ["The Greenwitch"]], - -.. container:: incremental small - - Fix it like so: - - .. code-block:: python - - 'Susan Cooper': ["The Dark is Rising", "The Greenwitch"], - -.. class:: small incremental - -It appears that we were attempting to bind a list as a parameter. Ooops. - - -Try It Again ------------- - -.. container:: small - - Now that the error in our data is repaired, let's try again:: - - $ python populatedb.py - -.. class:: small incremental - -:: - - 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) - -Congratulations ---------------- - -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. diff --git a/source/additional/template_tutorial.rst b/source/additional/template_tutorial.rst deleted file mode 100644 index 7eaa8065..00000000 --- a/source/additional/template_tutorial.rst +++ /dev/null @@ -1,244 +0,0 @@ -.. slideconf:: - :autoslides: False - -Jinja2 Template Introduction -============================ - -.. slide:: Jinja2 Template Introduction - :level: 1 - - This document contains no slides. - - -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. - -Generating HTML ---------------- - -.. class:: big-centered - -"I enjoy writing HTML in Python" - -.. class:: incremental right - --- nobody, ever - - -Templating ----------- - -A good framework will provide some way of generating HTML with a templating -system. - -.. class:: incremental - -There are nearly as many templating systems as there are frameworks - -.. class:: incremental - -Each has advantages and disadvantages - -.. class:: incremental - -Flask includes the *Jinja2* templating system (perhaps because it's built by -the same folks) - - -Jinja2 Template Basics ----------------------- - -Let's start with the absolute basics. - -.. container:: incremental - - Fire up a Python interpreter, using your flask virtualenv: - - .. code-block:: python - :class: small - - (flaskenv)$ python - >>> from jinja2 import Template - -.. container:: incremental - - A template is built of a simple string: - - .. code-block:: python - :class: small - - >>> t1 = Template("Hello {{ name }}, how are you?") - - -Rendering a Template --------------------- - -Call the ``render`` method, providing some *context*: - -.. code-block:: python - :class: incremental small - - >>> t1.render(name="Freddy") - u'Hello Freddy, how are you?' - >>> t1.render({'name': "Roberto"}) - u'Hello Roberto, how are you?' - >>> - -.. class:: incremental - -*Context* can either be keyword arguments, or a dictionary - - -Dictionaries in Context ------------------------ - -Dictionaries passed in as part of the *context* can be addressed with *either* -subscript or dotted notation: - -.. code-block:: python - :class: incremental small - - >>> person = {'first_name': 'Frank', - ... 'last_name': 'Herbert'} - >>> t2 = Template("{{ person.last_name }}, {{ person['first_name'] }}") - >>> t2.render(person=person) - u'Herbert, Frank' - -.. class:: incremental - -* Jinja2 will try the *correct* way first (attr for dotted, item for - subscript). -* If nothing is found, it will try the opposite. -* If nothing is found, it will return an *undefined* object. - - -Objects in Context ------------------- - -The exact same is true of objects passed in as part of *context*: - -.. code-block:: python - :class: incremental small - - >>> t3 = Template("{{ obj.x }} + {{ obj['y'] }} = Fun!") - >>> class Game(object): - ... x = 'babies' - ... y = 'bubbles' - ... - >>> bathtime = Game() - >>> t3.render(obj=bathtime) - u'babies + bubbles = Fun!' - -.. class:: incremental - -This means your templates can be a bit agnostic as to the nature of the things -in *context* - - -Filtering values in Templates ------------------------------ - -You can apply *filters* to the data passed in *context* with the pipe ('|') -operator: - -.. code-block:: python - :class: incremental small - - t4 = Template("shouted: {{ phrase|upper }}") - >>> t4.render(phrase="this is very important") - u'shouted: THIS IS VERY IMPORTANT' - -.. container:: incremental - - You can also chain filters together: - - .. code-block:: python - :class: small - - t5 = Template("confusing: {{ phrase|upper|reverse }}") - >>> t5.render(phrase="howdy doody") - u'confusing: YDOOD YDWOH' - - -Control Flow ------------- - -Logical control structures are also available: - -.. code-block:: python - :class: incremental small - - 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, ' - -.. class:: incremental - -Any control structure introduced in a template **must** be paired with an -explicit closing tag ({% for %}...{% endfor %}) - - -Template Tests --------------- - -There are a number of specialized *tests* available for use with the -``if...elif...else`` control structure: - -.. code-block:: python - :class: incremental small - - >>> 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 ------------------------- - -Basic Python expressions are also supported: - -.. code-block:: python - :class: incremental small - - 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' - - -Much, Much More ---------------- - -There's more that Jinja2 templates can do, and you'll see more in class -when we write templates for our Flask app. - -.. container:: incremental - - Make sure that you bookmark the Jinja2 documentation for later use:: - - http://jinja.pocoo.org/docs/templates/ From 12282a90cd80d46894edfec17547e67d4815bee2 Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 6 Jan 2015 17:49:44 -0800 Subject: [PATCH 003/173] remove obsolete assignments dir. Will replace soonish --- assignments/session01/echo_client.py | 42 - assignments/session01/echo_server.py | 69 - assignments/session01/tasks.txt | 49 - assignments/session01/tests.py | 123 -- .../session02/completed_http_server.py | 102 -- assignments/session02/http_server.py | 71 - assignments/session02/simple_client.py | 37 - assignments/session02/tasks.txt | 82 -- assignments/session02/tests.py | 370 ----- assignments/session02/webroot/a_web_page.html | 11 - .../session02/webroot/images/JPEG_example.jpg | Bin 15138 -> 0 bytes .../webroot/images/Sample_Scene_Balls.jpg | Bin 146534 -> 0 bytes .../session02/webroot/images/sample_1.png | Bin 8760 -> 0 bytes assignments/session02/webroot/make_time.py | 25 - assignments/session02/webroot/sample.txt | 3 - assignments/session03/tasks.txt | 38 - .../session04/flask_walkthrough-plain.html | 580 -------- assignments/session04/sql/createdb.py | 11 - assignments/session04/sql/ddl.sql | 15 - assignments/session04/sql/populatedb.py | 44 - assignments/session04/sql/utils.py | 31 - .../sql_persistence_tutorial-plain.rst | 798 ----------- assignments/session04/tasks.txt | 33 - .../session04/template_tutorial-plain.html | 514 ------- assignments/session05/microblog/microblog.cfg | 2 - assignments/session05/microblog/microblog.py | 70 - .../session05/microblog/microblog_tests.py | 77 - assignments/session05/microblog/schema.sql | 6 - .../session05/microblog/static/style.css | 20 - .../session05/microblog/templates/layout.html | 13 - .../microblog/templates/show_entries.html | 29 - assignments/session05/tasks.txt | 64 - assignments/session06/django_intro-plain.html | 1246 ----------------- assignments/session06/img/admin_index.png | Bin 27949 -> 0 bytes .../session06/img/django-admin-login.png | Bin 7749 -> 0 bytes assignments/session06/img/django-start.png | Bin 31371 -> 0 bytes assignments/session06/tasks.txt | 7 - assignments/session07/mysite/manage.py | 10 - .../session07/mysite/myblog/__init__.py | 0 assignments/session07/mysite/myblog/admin.py | 6 - .../myblog/fixtures/myblog_test_fixture.json | 38 - .../mysite/myblog/migrations/0001_initial.py | 78 -- .../migrations/0002_auto__add_category.py | 93 -- .../mysite/myblog/migrations/__init__.py | 0 assignments/session07/mysite/myblog/models.py | 22 - .../mysite/myblog/static/django_blog.css | 74 - .../mysite/myblog/templates/detail.html | 17 - .../mysite/myblog/templates/list.html | 25 - assignments/session07/mysite/myblog/tests.py | 65 - assignments/session07/mysite/myblog/urls.py | 10 - assignments/session07/mysite/myblog/views.py | 29 - assignments/session07/mysite/mysite.db | Bin 155648 -> 0 bytes .../session07/mysite/mysite/__init__.py | 0 .../session07/mysite/mysite/settings.py | 89 -- .../mysite/mysite/templates/base.html | 26 - .../mysite/mysite/templates/login.html | 9 - assignments/session07/mysite/mysite/urls.py | 20 - assignments/session07/mysite/mysite/wsgi.py | 14 - assignments/session07/tasks.txt | 49 - 59 files changed, 5256 deletions(-) delete mode 100644 assignments/session01/echo_client.py delete mode 100644 assignments/session01/echo_server.py delete mode 100644 assignments/session01/tasks.txt delete mode 100644 assignments/session01/tests.py delete mode 100644 assignments/session02/completed_http_server.py delete mode 100644 assignments/session02/http_server.py delete mode 100644 assignments/session02/simple_client.py delete mode 100644 assignments/session02/tasks.txt delete mode 100644 assignments/session02/tests.py delete mode 100644 assignments/session02/webroot/a_web_page.html delete mode 100644 assignments/session02/webroot/images/JPEG_example.jpg delete mode 100644 assignments/session02/webroot/images/Sample_Scene_Balls.jpg delete mode 100644 assignments/session02/webroot/images/sample_1.png delete mode 100644 assignments/session02/webroot/make_time.py delete mode 100644 assignments/session02/webroot/sample.txt delete mode 100644 assignments/session03/tasks.txt delete mode 100644 assignments/session04/flask_walkthrough-plain.html delete mode 100644 assignments/session04/sql/createdb.py delete mode 100644 assignments/session04/sql/ddl.sql delete mode 100644 assignments/session04/sql/populatedb.py delete mode 100644 assignments/session04/sql/utils.py delete mode 100644 assignments/session04/sql_persistence_tutorial-plain.rst delete mode 100644 assignments/session04/tasks.txt delete mode 100644 assignments/session04/template_tutorial-plain.html delete mode 100644 assignments/session05/microblog/microblog.cfg delete mode 100644 assignments/session05/microblog/microblog.py delete mode 100644 assignments/session05/microblog/microblog_tests.py delete mode 100644 assignments/session05/microblog/schema.sql delete mode 100644 assignments/session05/microblog/static/style.css delete mode 100644 assignments/session05/microblog/templates/layout.html delete mode 100644 assignments/session05/microblog/templates/show_entries.html delete mode 100644 assignments/session05/tasks.txt delete mode 100644 assignments/session06/django_intro-plain.html delete mode 100644 assignments/session06/img/admin_index.png delete mode 100644 assignments/session06/img/django-admin-login.png delete mode 100644 assignments/session06/img/django-start.png delete mode 100644 assignments/session06/tasks.txt delete mode 100755 assignments/session07/mysite/manage.py delete mode 100644 assignments/session07/mysite/myblog/__init__.py delete mode 100644 assignments/session07/mysite/myblog/admin.py delete mode 100644 assignments/session07/mysite/myblog/fixtures/myblog_test_fixture.json delete mode 100644 assignments/session07/mysite/myblog/migrations/0001_initial.py delete mode 100644 assignments/session07/mysite/myblog/migrations/0002_auto__add_category.py delete mode 100644 assignments/session07/mysite/myblog/migrations/__init__.py delete mode 100644 assignments/session07/mysite/myblog/models.py delete mode 100644 assignments/session07/mysite/myblog/static/django_blog.css delete mode 100644 assignments/session07/mysite/myblog/templates/detail.html delete mode 100644 assignments/session07/mysite/myblog/templates/list.html delete mode 100644 assignments/session07/mysite/myblog/tests.py delete mode 100644 assignments/session07/mysite/myblog/urls.py delete mode 100644 assignments/session07/mysite/myblog/views.py delete mode 100644 assignments/session07/mysite/mysite.db delete mode 100644 assignments/session07/mysite/mysite/__init__.py delete mode 100644 assignments/session07/mysite/mysite/settings.py delete mode 100644 assignments/session07/mysite/mysite/templates/base.html delete mode 100644 assignments/session07/mysite/mysite/templates/login.html delete mode 100644 assignments/session07/mysite/mysite/urls.py delete mode 100644 assignments/session07/mysite/mysite/wsgi.py delete mode 100644 assignments/session07/tasks.txt 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/echo_server.py b/assignments/session01/echo_server.py deleted file mode 100644 index 217380fb..00000000 --- a/assignments/session01/echo_server.py +++ /dev/null @@ -1,69 +0,0 @@ -import socket -import sys - - -def server(log_buffer=sys.stderr): - # set an address for our server - address = ('127.0.0.1', 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 - # TODO: Set an option to allow the socket address to be reused immediately - # see the end of http://docs.python.org/2/library/socket.html - - # log that we are building a server - print >>log_buffer, "making a server on {0}:{1}".format(*address) - - # TODO: bind your new sock 'sock' to the address above and begin to listen - # for incoming connections - - try: - # the outer loop controls the creation of new connection sockets. The - # server will handle each incoming connection one at a time. - while True: - print >>log_buffer, 'waiting for a connection' - - # TODO: make a new socket when a client connects, call it 'conn', - # at the same time you should be able to get the address of - # the client so we can report it below. Replace the - # following line with your code. It is only here to prevent - # syntax errors - addr = ('bar', 'baz') - try: - print >>log_buffer, 'connection - {0}:{1}'.format(*addr) - - # the inner loop will receive messages sent by the client in - # buffers. When a complete message has been received, the - # loop will exit - while True: - # TODO: receive 16 bytes of data from the client. Store - # the data you receive as 'data'. Replace the - # following line with your code. It's only here as - # a placeholder to prevent an error in string - # formatting - data = '' - print >>log_buffer, 'received "{0}"'.format(data) - # TODO: you will need to check here to see if any data was - # received. If so, send the data you got back to - # the client. If not, exit the inner loop and wait - # for a new connection from a client - - finally: - # TODO: When the inner loop exits, this 'finally' clause will - # be hit. Use that opportunity to close the socket you - # created above when a client connected. Replace the - # call to `pass` below, which is only there to prevent - # syntax problems - pass - - except KeyboardInterrupt: - # TODO: Use the python KeyboardIntterupt exception as a signal to - # close the server socket and exit from the server function. - # Replace the call to `pass` below, which is only there to - # prevent syntax problems - pass - - -if __name__ == '__main__': - server() - sys.exit(0) \ No newline at end of file diff --git a/assignments/session01/tasks.txt b/assignments/session01/tasks.txt deleted file mode 100644 index 9352a45f..00000000 --- a/assignments/session01/tasks.txt +++ /dev/null @@ -1,49 +0,0 @@ -Session 1 Homework -================== - -Required Tasks: ---------------- - -* Complete the code in ``echo_server.py`` to create a server that sends back - whatever messages it receives from a client - -* Complete the code in ``echo_client.py`` to create a client function that - can send a message and receive a reply. - -* Ensure that the tests in ``tests.py`` pass. - -To run the tests: - -* Open one terminal while in this folder and execute this command: - - $ python echo_server.py - -* Open a second terminal in this same folder and execute this command: - - $ python tests.py - - - - -Optional Tasks: ---------------- - -* Write a python function that lists the services provided by a given range of - ports. - - * accept the lower and upper bounds as arguments - * provide sensible defaults - * Ensure that it only accepts valid port numbers (0-65535) - -* The echo server as outlined will only process a connection from one client - at a time. If a second client were to attempt a connection, it would have to - wait until the first message was fully echoed before it could be dealt with. - - Python provides a module called `select` that allows waiting for I/O events - in order to control flow. The `select.select` method can be used to allow - our echo server to handle more than one incoming connection in "parallel". - - Read the documentation about the `select` module - (http://docs.python.org/2/library/select.html) and attempt to write a second - version of the echo server that can handle multiple client connections in - "parallel". You do not need to invoke threading of any kind to do this. 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/simple_client.py b/assignments/session02/simple_client.py deleted file mode 100644 index c0f0d6e2..00000000 --- a/assignments/session02/simple_client.py +++ /dev/null @@ -1,37 +0,0 @@ -import socket -import sys - - -def client(msg): - server_address = ('localhost', 10000) - sock = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP - ) - print >>sys.stderr, 'connecting to {0} port {1}'.format(*server_address) - sock.connect(server_address) - response = '' - done = False - bufsize = 1024 - try: - print >>sys.stderr, 'sending "{0}"'.format(msg) - sock.sendall(msg) - while not done: - chunk = sock.recv(bufsize) - if len(chunk) < bufsize: - done = True - response += chunk - print >>sys.stderr, 'received "{0}"'.format(response) - finally: - print >>sys.stderr, 'closing socket' - sock.close() - return response - - -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/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/session02/tests.py b/assignments/session02/tests.py deleted file mode 100644 index 1438857c..00000000 --- a/assignments/session02/tests.py +++ /dev/null @@ -1,370 +0,0 @@ -import mimetypes -import os -import socket -import unittest - - -CRLF = '\r\n' -KNOWN_TYPES = set(mimetypes.types_map.values()) - - -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, body="", mimetype="text/plain"): - """call the `response_ok` function from our http_server module""" - from http_server import response_ok - return response_ok(body=body, mimetype=mimetype) - - def test_response_code(self): - ok = self.call_function_under_test() - expected = "200 OK" - actual = ok.split(CRLF)[0].split(' ', 1)[1].strip() - self.assertEqual(expected, actual) - - def test_response_method(self): - ok = self.call_function_under_test() - expected = 'HTTP/1.1' - actual = ok.split(CRLF)[0].split(' ', 1)[0].strip() - self.assertEqual(expected, actual) - - def test_response_has_content_type_header(self): - ok = self.call_function_under_test() - headers = ok.split(CRLF+CRLF, 1)[0].split(CRLF)[1:] - expected_name = 'content-type' - has_header = False - for header in headers: - name, value = header.split(':') - 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 = ok.split(CRLF+CRLF, 1)[0].split(CRLF)[1:] - expected_name = 'content-type' - for header in headers: - name, value = header.split(':') - 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') - - def test_passed_mimetype_in_response(self): - mimetypes = [ - 'image/jpeg', 'text/html', 'text/x-python', - ] - header_name = 'content-type' - for expected in mimetypes: - ok = self.call_function_under_test(mimetype=expected) - headers = ok.split(CRLF+CRLF, 1)[0].split(CRLF)[1:] - for header in headers: - name, value = header.split(':') - if header_name == name.strip().lower(): - actual = value.strip() - self.assertEqual( - expected, - actual, - "expected {0}, got {1}".format(expected, actual) - ) - - def test_passed_body_in_response(self): - bodies = [ - "a body", - "a longer body\nwith two lines", - open("webroot/sample.txt", 'r').read(), - ] - for expected in bodies: - ok = self.call_function_under_test(body=expected) - actual = ok.split(CRLF+CRLF, 1)[1] - self.assertEqual( - expected, - actual, - "expected {0}, got {1}".format(expected, actual)) - - -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 = resp.split(CRLF)[0].split(' ', 1)[1].strip() - self.assertEqual(expected, actual) - - def test_response_method(self): - resp = self.call_function_under_test() - expected = 'HTTP/1.1' - actual = resp.split(CRLF)[0].split(' ', 1)[0].strip() - self.assertEqual(expected, actual) - - -class ResponseNotFoundTestCase(unittest.TestCase): - """unit tests for the response_not_found function""" - - def call_function_under_test(self): - """call the 'response_not_found' function""" - from http_server import response_not_found - return response_not_found() - - def test_response_code(self): - resp = self.call_function_under_test() - expected = "404 Not Found" - actual = resp.split(CRLF)[0].split(' ', 1)[1].strip() - self.assertEqual(expected, actual) - - def test_response_method(self): - resp = self.call_function_under_test() - expected = 'HTTP/1.1' - actual = resp.split(CRLF)[0].split(' ', 1)[0].strip() - self.assertEqual(expected, 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), 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 - ) - - def test_uri_returned(self): - """verify that the parse_request function returns a URI""" - URIs = [ - '/', '/a_web_page.html', '/sample.txt', '/images/sample_1.png', - ] - request_tmplt = "GET {0} HTTP/1.1" - for expected in URIs: - request = request_tmplt.format(expected) - actual = self.call_function_under_test(request) - self.assertEqual( - expected, - actual, - "expected {0}, got {1}".format(expected, actual) - ) - - -class ResolveURITestCase(unittest.TestCase): - """unit tests for the resolve_uri function""" - - def call_function_under_test(self, uri): - """call the resolve_uri function""" - from http_server import resolve_uri - return resolve_uri(uri) - - def test_directory_resource(self): - uri = '/' - expected_names = [ - 'a_web_page.html', 'images', 'make_time.py', 'sample.txt', - ] - expected_mimetype = "text/plain" - actual_body, actual_mimetype = self.call_function_under_test(uri) - self.assertEqual( - expected_mimetype, - actual_mimetype, - 'expected {0} got {1}'.format(expected_mimetype, actual_mimetype) - ) - for expected in expected_names: - self.assertTrue( - expected in actual_body, - '"{0}" not in "{1}"'.format(expected, actual_body) - ) - - def test_file_resource(self): - uris_types = { - '/a_web_page.html': 'text/html', - '/make_time.py': 'text/x-python', - '/sample.txt': 'text/plain', - } - for uri, expected_mimetype in uris_types.items(): - path = "webroot{0}".format(uri) - expected_body = open(path, 'rb').read() - actual_body, actual_mimetype = self.call_function_under_test(uri) - self.assertEqual( - expected_mimetype, - actual_mimetype, - 'expected {0} got {1}'.format( - expected_mimetype, actual_mimetype - ) - ) - self.assertEqual( - expected_body, - actual_body, - 'expected {0} got {1}'.format( - expected_mimetype, actual_mimetype - ) - ) - - def test_image_resource(self): - names_types = { - 'JPEG_example.jpg': 'image/jpeg', - 'sample_1.png': 'image/png', - } - for filename, expected_mimetype in names_types.items(): - uri = "/images/{0}".format(filename) - path = "webroot{0}".format(uri) - expected_body = open(path, 'rb').read() - actual_body, actual_mimetype = self.call_function_under_test(uri) - self.assertEqual( - expected_mimetype, - actual_mimetype, - 'expected {0} got {1}'.format( - expected_mimetype, actual_mimetype - ) - ) - self.assertEqual( - expected_body, - actual_body, - 'expected {0} got {1}'.format( - expected_mimetype, actual_mimetype - ) - ) - - def test_missing_resource(self): - uri = "/missing.html" - self.assertRaises(ValueError, self.call_function_under_test, uri) - - -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, 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) - ) - - def test_webroot_directory_resources(self): - """verify that directory uris are properly served""" - message_tmpl = CRLF.join(['GET {0} HTTP/1.1', 'Host: example.com', '']) - root = "webroot" - for directory, directories, files in os.walk(root): - directory_uri = "/{0}".format(directory[len(root):]) - message = message_tmpl.format(directory_uri) - actual = self.send_message(message) - # verify that directory listings are correct - self.assertTrue( - "200 OK" in actual, - "request for {0} did not result in OK".format(directory_uri)) - for expected in directories + files: - self.assertTrue( - expected in actual, - '"{0}" not in "{1}"'.format(expected, actual) - ) - - def test_webroot_file_uris(self): - """verify that file uris are properly served""" - message_tmpl = CRLF.join(['GET {0} HTTP/1.1', 'Host: example.com', '']) - root = "webroot" - for directory, directories, files in os.walk(root): - directory_uri = "/{0}".format(directory[len(root):]) - # verify that all files are delivered correctly - for filename in files: - # file as local resource and as web URI - file_path = os.path.sep.join([directory, filename]) - if directory_uri != '/': - file_uri = '/'.join([directory_uri, filename]) - else: - file_uri = '/{0}'.format(filename) - # set up expectations for this file - expected_body = open(file_path, 'rb').read() - expected_mimetype = mimetypes.types_map[ - os.path.splitext(filename)[1] - ] - # make a request for this file as a uri - message = message_tmpl.format(file_uri) - actual = self.send_message(message) - # verify that request is OK - self.assertTrue( - "200 OK" in actual, - "request for {0} did not result in OK".format( - directory_uri - ) - ) - self.assertTrue( - expected_mimetype in actual, - "mimetype {0} not in response for {1}".format( - expected_mimetype, file_uri - ) - ) - self.assertTrue( - expected_body in actual, - "body of {0} not in response for {1}".format( - file_path, file_uri - ) - ) - - def test_missing_resource(self): - message = CRLF.join( - ['GET /missing.html HTTP/1.1', 'Host: example.com', ''] - ) - expected = '404 Not Found' - 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/assignments/session02/webroot/a_web_page.html b/assignments/session02/webroot/a_web_page.html deleted file mode 100644 index 4635692d..00000000 --- a/assignments/session02/webroot/a_web_page.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - -

North Carolina

- -

A fine place to spend a week learning web programming!

- - - - diff --git a/assignments/session02/webroot/images/JPEG_example.jpg b/assignments/session02/webroot/images/JPEG_example.jpg deleted file mode 100644 index 13506f01e9a514c53dc52cc7cbf499f52a29b6f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15138 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 diff --git a/assignments/session02/webroot/images/Sample_Scene_Balls.jpg b/assignments/session02/webroot/images/Sample_Scene_Balls.jpg deleted file mode 100644 index 1c0ccade2de6ac61a164052ed4a7da9fe9ce2068..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146534 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%0AG!bll7=qnQE!?zJg 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 z z1n`K}5F9YiPwP-htXt+8`FKCXDo9kzaAIIo2ITwt)YGDi!VsJ&!vbk;Quk3+L^5!Z zFpv+Ko4pZ+X-Fz^pDqV+N**hFy{mH5F(daxSe3b#c7<-#CRiXR z*R4TzLAXVd77j-adK$JsT~(Pu+*b?_pr+igGKk#BTPL^5DR(PyXuoiqo1F1c$%kMQ z8<&E8D#+Z$ayJ;r&UmWRz|9iL5e18U*yw$zK+wmx5TJ1p<+)+dQ^JOVJf2C-Nikou z?UH7Vk0AiaV@`6uM0i|*(*lMhicb+AKG4h#-*74`vyOPnU!){_h z1J~&Gh zj>y0snaQ9NvMsIOl;v5nqtlv3xQbH8a}4{{V)q$4qre`V(|*$YhJ=X6ZL|lep%_*i zNW;@2gpuMUl0rw)szWJ|3{?6mPRR-B&u%HEcsF$*{b~o1 z`T(T%#YQ)l`O^UO;7}ISzZKxg+aUv{L8i%m(J%vhfzqm=5#_SGaZt6 z6Sw9#B>MB(x~~stD`PwpLvtfCfEW+}Z~nDaOkFHG=9Z*FgzCE@e+BqN>a$6wUdJ_^ z?0|V{@sLntWtbdbZO%Um_6-N&!+2}NhT6{cuQa48;hA=sNa$E}<0k^0qTR`>Uj4S@ z@3~+zA?k22KSBCeeW5Uwju&<@&3tA*h%psq?Be-j%c}fOZx5E!rD?rg&CNSXid*UL zb^~=e%#z>�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 x7uH_R#?^%;kmAuC#$UjQOmE(xFnt1Dwe_Dy| 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 KBK{cNAOG3+Y*1+c diff --git a/assignments/session02/webroot/images/sample_1.png b/assignments/session02/webroot/images/sample_1.png deleted file mode 100644 index 5b2f52dff2428c82276821909e466d877ea7608b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8760 zcmeAS@N?(olHy`uVBq!ia0y~yVA%k|9Be?5*NU6Z11XkbC(jTLAgJL;=>YOM3p^r= z85p>QK$!8;-MT+OLD3S|h!W?b)Wnj^{5*w_%-mE4V|^nFeFMvP`oBR6WIzgn^V3So z6N^$A98>a>QWe}Xi&D$;i?WLqoP&*LTytY)U{GN2ba4!+nDgeAVcuZ}0T#!32mjYs z%32kxRNW4`%B}XWG2_k5*R!}88m3$RTgSi<_bR&@XgUMKAGZTcKuW=Plo}0!(Nr*+ z5k|{`(c*9@)(CI!$uZcx&*5kIVImjTu;27HbHnnTqm|ldr3S0iKnWzk*!Z?ELj!;L zxtsGCCOl%|2Iln!6UISKZKj9gb3=AH^qts{+ zjHZIoj4)ajj24H3yhhl)ot=SU&s=aN_VY;fp4}h5#UwBS9X&w}LY2iEr&qIlcs$45 z&eQy=;qzU4_I|jxXEy_b{*xh6Sr7Ww#Ha%Ye~WK4jz;5X2y{$FlPT%R6x^9ppEdL5 zJz%x>A$|v?LljN#%ic<~CMs|=DX{7B1wh0THU0t`zyBS38eT2isM znHjRqSpqxO3=9>I8(D#r!>oo;YBUILZM9|i(Ye`{;lchhIr_lVA;hl(N{yFT1_QP3 zE#raZGl2s^2YjD{T_-k}TsJ6!Xw(sdBdLzY!)QDZQEKkJ#{jH^8CyYX0f-AXhEBaSd?N0SG)@{an^LB{Ts5b?$DF diff --git a/assignments/session02/webroot/make_time.py b/assignments/session02/webroot/make_time.py deleted file mode 100644 index d3064dd2..00000000 --- a/assignments/session02/webroot/make_time.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python - -""" -make_time.py - -simple script that returns and HTML page with the current time -""" - -import datetime - -time_str = datetime.datetime.now().isoformat() - -html = """ - - -

The time is:

-

%s

- - -"""% time_str - -print html - - - diff --git a/assignments/session02/webroot/sample.txt b/assignments/session02/webroot/sample.txt deleted file mode 100644 index 1965c7d3..00000000 --- a/assignments/session02/webroot/sample.txt +++ /dev/null @@ -1,3 +0,0 @@ -This is a very simple text file. -Just to show that we can server it up. -It is three lines long. 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 @@ - - - - - - -A Quick Flask Walkthrough - - - -

-

A Quick Flask Walkthrough

- -

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...

-
-

Practice Safe Development

-

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

Set Up a VirtualEnv

-

Start by creating your virtualenv:

-
-$ python virtualenv.py flaskenv
-<or>
-$ virtualenv flaskenv
-...
-
-
-

Then, activate it:

-
-$ source flaskenv/bin/activate
-<or>
-C:\> flaskenv\Scripts\activate
-
-
-
-
-

Install Flask

-

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

Kicking the Tires

-

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.

-
-
-

Flask

-

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()
-
-
-
-

Running our App

-

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.

-
-
-

Debugging our App

-

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).

-
-
-

Your work so far

-
    -
  • You instantiated a Flask app with a name that represents the package or -module containing the app
      -
    • Because our app is a single Python module, this should be __name__
    • -
    • This is used to help the Flask app figure out where to look for -resources
    • -
    -
  • -
  • You defined a function that returned a response body
  • -
  • You told the app which requests should use that function with a route
  • -
-

Let's take a look at how that last bit works for a moment...

-
-
-

URL Routing

-

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

-

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

-
-
-

Function or 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"""
-    # ...
-
-
-
-
-

Routes Can Be Dynamic

-

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)
-
-
-
-

Routes Can Be Filtered

-

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?

-
-
-

Routes Can Be Reversed

-

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

-
-
-

Reversing URLs in Flask

-

In Flask, you reverse a url with the url_for function.

-
    -
  • url_for requires an HTTP request context to work
  • -
  • You can fake an HTTP request when working in a terminal (or testing)
  • -
  • Use the test_request_context method of your app object
  • -
  • This is a great chance to use the Python with statement
  • -
  • Don't type this
  • -
-
-from flask import url_for
-with app.test_request_context():
-  print url_for('endpoint', **kwargs)
-
-
-
-

Reversing in Action

-

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/'
->>>
-
-
-
-

Enough for Now

-

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.

-
-
- - - diff --git a/assignments/session04/sql/createdb.py b/assignments/session04/sql/createdb.py deleted file mode 100644 index 429bbdbf..00000000 --- a/assignments/session04/sql/createdb.py +++ /dev/null @@ -1,11 +0,0 @@ -import os -import sqlite3 - -DB_FILENAME = 'books.db' -DB_IS_NEW = not os.path.exists(DB_FILENAME) - -def main(): - pass - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/assignments/session04/sql/ddl.sql b/assignments/session04/sql/ddl.sql deleted file mode 100644 index a41b6e9d..00000000 --- a/assignments/session04/sql/ddl.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Schema for a simple book database - --- Author table - -CREATE TABLE author( - authorid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - name TEXT -); - -CREATE TABLE book( - bookid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - title TEXT, - author INTEGER NOT NULL, - FOREIGN KEY(author) REFERENCES author(authorid) -); diff --git a/assignments/session04/sql/populatedb.py b/assignments/session04/sql/populatedb.py deleted file mode 100644 index 92baff4d..00000000 --- a/assignments/session04/sql/populatedb.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import sys -import sqlite3 -from utils import AUTHORS_BOOKS - -DB_FILENAME = 'books.db' -DB_IS_NEW = not os.path.exists(DB_FILENAME) - -author_insert = "INSERT INTO author (name) VALUES(?);" -author_query = "SELECT * FROM author;" -book_query = "SELECT * FROM book;" -book_insert = """ -INSERT INTO book (title, author) VALUES(?, ( - SELECT authorid FROM author WHERE name=? )); -""" - - -def show_query_results(conn, query): - cur = conn.cursor() - cur.execute(query) - had_rows = False - for row in cur.fetchall(): - print row - had_rows = True - if not had_rows: - print "no rows returned" - - -def show_authors(conn): - query = author_query - show_query_results(conn, query) - - -def show_books(conn): - query = book_query - show_query_results(conn, query) - - -if __name__ == '__main__': - if DB_IS_NEW: - print "Database does not yet exist, please import `createdb` first" - sys.exit(1) - - print "Do something cool here" diff --git a/assignments/session04/sql/utils.py b/assignments/session04/sql/utils.py deleted file mode 100644 index 750669b1..00000000 --- a/assignments/session04/sql/utils.py +++ /dev/null @@ -1,31 +0,0 @@ -TABLEPRAGMA = "PRAGMA table_info(%s);" - - -def print_table_metadata(cursor): - tmpl = "%-10s |" - rowdata = cursor.description - results = cursor.fetchall() - for col in rowdata: - print tmpl % col[0], - print '\n' + '-----------+-'*len(rowdata) - for row in results: - for value in row: - print tmpl % value, - print '\n' + '-----------+-'*len(rowdata) - print '\n' - - -def show_table_metadata(cursor, tablename): - stmt = TABLEPRAGMA % tablename - cursor.execute(stmt) - print "Table Metadata for '%s':" % tablename - print_table_metadata(cursor) - - -AUTHORS_BOOKS = { - 'China Mieville': ["Perdido Street Station", "The Scar", "King Rat"], - 'Frank Herbert': ["Dune", "Hellstrom's Hive"], - 'J.R.R. Tolkien': ["The Hobbit", "The Silmarillion"], - 'Susan Cooper': ["The Dark is Rising", ["The Greenwitch"]], - 'Madeline L\'Engle': ["A Wrinkle in Time", "A Swiftly Tilting Planet"] -} diff --git a/assignments/session04/sql_persistence_tutorial-plain.rst b/assignments/session04/sql_persistence_tutorial-plain.rst deleted file mode 100644 index cff12fa1..00000000 --- a/assignments/session04/sql_persistence_tutorial-plain.rst +++ /dev/null @@ -1,798 +0,0 @@ - - - - - - -SQL Persistence in Python - - - -
-

SQL Persistence in Python

- -

In this tutorial, you'll walk through some basic concepts of data persistence -using the Python stdlib implementation of DB API 2, sqlite3

-
-

Data Persistence

-

There are many models for persistance of data.

-
    -
  • Flat files
  • -
  • Relational Database (SQL RDBMs like PostgreSQL, MySQL, SQLServer, Oracle)
  • -
  • Object Stores (Pickle, ZODB)
  • -
  • NoSQL Databases (CouchDB, MongoDB, etc)
  • -
-

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.

-
-
-

Simple SQL

-

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

-

source: http://www.python.org/dev/peps/pep-0248/

-
-
-
-
-

A Note on DB API

-

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

-
-
-

Using DB API

-

To use the DB API with any database other than SQLite3, you must have an -underlying API package available.

-
-

Implementations are available for:

-
    -
  • PostgreSQL (psycopg2, txpostgres, ...)
  • -
  • MySQL (mysql-python, PyMySQL, ...)
  • -
  • MS SQL Server (adodbapi, pymssql, mxODBC, pyodbc, ...)
  • -
  • Oracle (cx_Oracle, mxODBC, pyodbc, ...)
  • -
  • and many more...
  • -
-

source: http://wiki.python.org/moin/DatabaseInterfaces

-
-
-
-

Installing API Packages

-

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.

-
-
-

Not Today

-

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

-
-
-

Getting Started

-

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()
-
-
-
-

Try It Out

-

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.

-
-
-

Set Up A Schema

-

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.
-
-
-
-

Verify Your Work

-

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.
-
-
-
-
-

Introspect the Database

-

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

-
-
-

My Results

-
-$ 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          |
------------+------------+------------+------------+------------+------------+-
-
-
-
-

Inserting Data

-

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?

-
-
-

Querying Data

-

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

-
-
-

Parameterized Statements

-

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

Check Your Work

-

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

-

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

-
-
-

Populating the Database

-

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

Setting Up the Test

-

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()
-
-
-
-

Running the Test

-

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")
-
-
-
-

Rollback

-

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

-
-
-

Edit populatedb.py (slide 1)

-

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)
-
-
-
-

Edit populatedb.py (slide 2)

-

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)
-
-
-
-

Try it Out

-

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.
-
-
-
-

Oooops, Fix It

-

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.

-
-
-

Try It Again

-
-

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)
-
-
-
-

Congratulations

-

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.

-
-
- - - diff --git a/assignments/session04/tasks.txt b/assignments/session04/tasks.txt deleted file mode 100644 index fb84f316..00000000 --- a/assignments/session04/tasks.txt +++ /dev/null @@ -1,33 +0,0 @@ -Session 4 Homework -================== - -Required Tasks: ---------------- - -The Mashup you created for the last session produces interesting data. This -session we learned how to build simple WSGI applications that expose data -using information from the request. - -For your homework this week, I want you to combine the two. Using your mashup -as a data source, build a simple WSGI application that shows that data to the -user. Make it minimally interactive. A user should be able to click links or -provide input via simple HTML forms. - -Place the following in the assignments/session04 directory and make a pull -request: - -Instructions - If any extra stuff needs to be installed or executed for your application - to work, make sure I know about it. - -Your source code. - Give me an executable python script that I can run to start a WSGI server - serving your application. Use the standard library wsgiref module. - -Your tests. - Last session, tests were optional. This week they are not. Write at least - two tests that prove some of your code works as intended. - -Your application should produce HTML output that I can view in a browser. That -output does not need to be attractive. Continue to focus on the mechanics of -making this work, rather than the aesthetics of making it pretty. diff --git a/assignments/session04/template_tutorial-plain.html b/assignments/session04/template_tutorial-plain.html deleted file mode 100644 index 999b16a4..00000000 --- a/assignments/session04/template_tutorial-plain.html +++ /dev/null @@ -1,514 +0,0 @@ - - - - - - -Jinja2 Template Introduction - - - -
-

Jinja2 Template Introduction

- -

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.

-
-

Generating HTML

-

"I enjoy writing HTML in Python"

-

-- nobody, ever

-
-
-

Templating

-

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)

-
-
-

Jinja2 Template Basics

-

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?")
-
-
-
-
-

Rendering a Template

-

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 in Context

-

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'
-
-
    -
  • Jinja2 will try the correct way first (attr for dotted, item for -subscript).
  • -
  • If nothing is found, it will try the opposite.
  • -
  • If nothing is found, it will return an undefined object.
  • -
-
-
-

Objects in Context

-

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

-
-
-

Filtering values in Templates

-

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'
-
-
-
-
-

Control Flow

-

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 %})

-
-
-

Template Tests

-

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

-

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'
-
-
-
-

Much, Much More

-

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/
-
-
-
-
- - - diff --git a/assignments/session05/microblog/microblog.cfg b/assignments/session05/microblog/microblog.cfg deleted file mode 100644 index fda11a39..00000000 --- a/assignments/session05/microblog/microblog.cfg +++ /dev/null @@ -1,2 +0,0 @@ -# application configuration for a Flask microblog -DATABASE = 'microblog.db' diff --git a/assignments/session05/microblog/microblog.py b/assignments/session05/microblog/microblog.py deleted file mode 100644 index fae4888a..00000000 --- a/assignments/session05/microblog/microblog.py +++ /dev/null @@ -1,70 +0,0 @@ -from flask import Flask -from flask import g -from flask import render_template -from flask import abort -from flask import request -from flask import url_for -from flask import redirect -import sqlite3 -from contextlib import closing - -app = Flask(__name__) - -app.config.from_pyfile('microblog.cfg') - - -def connect_db(): - return sqlite3.connect(app.config['DATABASE']) - - -def init_db(): - with closing(connect_db()) as db: - with app.open_resource('schema.sql') as f: - db.cursor().executescript(f.read()) - db.commit() - - -def get_database_connection(): - db = getattr(g, 'db', None) - if db is None: - g.db = db = connect_db() - return db - - -@app.teardown_request -def teardown_request(exception): - db = getattr(g, 'db', None) - if db is not None: - db.close() - - -def write_entry(title, text): - con = get_database_connection() - con.execute('insert into entries (title, text) values (?, ?)', - [title, text]) - con.commit() - - -def get_all_entries(): - con = get_database_connection() - cur = con.execute('SELECT title, text FROM entries ORDER BY id DESC') - return [dict(title=row[0], text=row[1]) for row in cur.fetchall()] - - -@app.route('/') -def show_entries(): - entries = get_all_entries() - return render_template('show_entries.html', entries=entries) - - -@app.route('/add', methods=['POST']) -def add_entry(): - try: - write_entry(request.form['title'], request.form['text']) - except sqlite3.Error: - abort(500) - return redirect(url_for('show_entries')) - - -if __name__ == '__main__': - app.run(debug=True) diff --git a/assignments/session05/microblog/microblog_tests.py b/assignments/session05/microblog/microblog_tests.py deleted file mode 100644 index 57a64f37..00000000 --- a/assignments/session05/microblog/microblog_tests.py +++ /dev/null @@ -1,77 +0,0 @@ -import os -import tempfile -import unittest -import microblog - - -class MicroblogTestCase(unittest.TestCase): - - def setUp(self): - db_fd = tempfile.mkstemp() - self.db_fd, microblog.app.config['DATABASE'] = db_fd - microblog.app.config['TESTING'] = True - self.client = microblog.app.test_client() - self.app = microblog.app - microblog.init_db() - - def tearDown(self): - os.close(self.db_fd) - os.unlink(microblog.app.config['DATABASE']) - - def test_database_setup(self): - con = microblog.connect_db() - cur = con.execute('PRAGMA table_info(entries);') - rows = cur.fetchall() - self.assertEquals(len(rows), 3) - - def test_write_entry(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - con = microblog.connect_db() - cur = con.execute("select * from entries;") - rows = cur.fetchall() - self.assertEquals(len(rows), 1) - for val in expected: - self.assertTrue(val in rows[0]) - - def test_get_all_entries_empty(self): - with self.app.test_request_context('/'): - entries = microblog.get_all_entries() - self.assertEquals(len(entries), 0) - - def test_get_all_entries(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - entries = microblog.get_all_entries() - self.assertEquals(len(entries), 1) - for entry in entries: - self.assertEquals(expected[0], entry['title']) - self.assertEquals(expected[1], entry['text']) - - def test_empty_listing(self): - actual = self.client.get('/').data - expected = 'No entries here so far' - self.assertTrue(expected in actual) - - def test_listing(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - actual = self.client.get('/').data - for value in expected: - self.assertTrue(value in actual) - - def test_add_entries(self): - actual = self.client.post('/add', data=dict( - title='Hello', - text='This is a post' - ), follow_redirects=True).data - self.assertFalse('No entries here so far' in actual) - self.assertTrue('Hello' in actual) - self.assertTrue('This is a post' in actual) - - -if __name__ == '__main__': - unittest.main() diff --git a/assignments/session05/microblog/schema.sql b/assignments/session05/microblog/schema.sql deleted file mode 100644 index 71fe0588..00000000 --- a/assignments/session05/microblog/schema.sql +++ /dev/null @@ -1,6 +0,0 @@ -drop table if exists entries; -create table entries ( - id integer primary key autoincrement, - title string not null, - text string not null -); diff --git a/assignments/session05/microblog/static/style.css b/assignments/session05/microblog/static/style.css deleted file mode 100644 index 80218c4f..00000000 --- a/assignments/session05/microblog/static/style.css +++ /dev/null @@ -1,20 +0,0 @@ -body { font-family: 'Helvetica', sans-serif; background: #eaeced; } -a, h1, h2 { color: #1E727F; } -h1, h2 { font-family: 'Helvetica', sans-serif; margin: 0; } -h1 { font-size: 2em; border-bottom: 2px solid #1E727F; padding-bottom: 0.25em; margin-bottom: 0.5em;} -h2 { font-size: 1.4em; } -.page { margin: 2em auto; width: 35em; border: 5px solid #1E727F; - padding: 0.8em; background: white; } -.entries { list-style: none; margin: 0; padding: 0; } -.entries li { margin: 0.8em 1.2em; } -.entries li h2 { margin-left: -1em; } -.add_entry { float: right; clear: right; width: 50%; font-size: 0.9em; - border: 1px solid #1E727F; padding: 1em; background: #fafafa;} -.add_entry dl { font-weight: bold; } -.add_entry label {display: block; } -.add_entry .field {margin-bottom: 0.25em;} -.metanav { text-align: left; font-size: 0.8em; padding: 0.3em; - margin-bottom: 1em; background: #fafafa; border: 1px solid #1E727F} -.flash { width: 30%; background: #00B0CC; padding: 1em; - border: 1px solid #1E727F; margin-bottom: 1em;} -.error { background: #F0D6D6; padding: 0.5em; } diff --git a/assignments/session05/microblog/templates/layout.html b/assignments/session05/microblog/templates/layout.html deleted file mode 100644 index e13b1287..00000000 --- a/assignments/session05/microblog/templates/layout.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - Microblog! - - - -

My Microblog

-
- {% block body %}{% endblock %} -
- - diff --git a/assignments/session05/microblog/templates/show_entries.html b/assignments/session05/microblog/templates/show_entries.html deleted file mode 100644 index 07738258..00000000 --- a/assignments/session05/microblog/templates/show_entries.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "layout.html" %} -{% block body %} -
-
- - -
-
- - -
-
- -
-
-

Posts

-
    - {% for entry in entries %} -
  • -

    {{ entry.title }}

    -
    - {{ entry.text|safe }} -
    -
  • - {% else %} -
  • No entries here so far
  • - {% endfor %} -
-{% endblock %} diff --git a/assignments/session05/tasks.txt b/assignments/session05/tasks.txt deleted file mode 100644 index ea269d25..00000000 --- a/assignments/session05/tasks.txt +++ /dev/null @@ -1,64 +0,0 @@ -Session 5 Homework -================== - -Required Tasks --------------- - -1. Add authentication so a user can log in and log out. -2. Add flash messaging so the app can inform a user about events that happen - - -Authentication Specifications ------------------------------ - -Writing new entries should be restricted to users who have logged in. This -means that: - -.. class:: incremental - -* The form to create a new entry should only be visible to logged in users -* There should be a visible link to allow a user to log in -* This link should display a login form that expects a username and password -* If the user provides incorrect login information, this form should tell her - so. -* If the user provides correct login information, she should end up at the - list page -* Once logged in, the user should see a link to log out. -* Upon clicking that link, the system should no longer show the entry form and - the log in link should re-appear. - - -Flash Messaging Specifications ------------------------------- - -A flask app provides a method called `flash` that allows passing messages from -a view function into a template context so that they can be viewed by a user. - -.. class:: incremental - -Use this method to provide the following messages to users: - -.. class:: incremental - -* Upon a successful login, display the message "You are logged in" -* Upon a successful logout, display the message "You have logged out" -* Upon posting a successful new entry, display the message "New entry posted" -* If adding an entry causes an error, instead of returning a 500 response, - alert the user to the error by displaying the error message to the user. - - -Resources to Use ----------------- - -The microblog we created today comes from the tutorial on the `flask` website. -I've modified that tutorial to omit authentication and flash messaging. You can -refer to the tutorial and to the flask api documentation to learn what you need -to accomplish these tasks. - -`The Flask Tutorial `_ - -`Flask API Documentation `_ - -Both features depend on *sessions*, so you will want to pay particular -attention to how a session is enabled and what you can do with it once it -exists. diff --git a/assignments/session06/django_intro-plain.html b/assignments/session06/django_intro-plain.html deleted file mode 100644 index ca60c56a..00000000 --- a/assignments/session06/django_intro-plain.html +++ /dev/null @@ -1,1246 +0,0 @@ - - - - - - -An Introduction To Django - - - -
-

An Introduction To Django

- -

In this tutorial, you'll walk through creating a very simple microblog -application using Django.

-
-

Practice Safe Development

-

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

Set Up a VirtualEnv

-

Start by creating your virtualenv:

-
-$ python virtualenv.py djangoenv
-<or>
-$ virtualenv djangoenv
-...
-
-
-

Then, activate it:

-
-$ source djangoenv/bin/activate
-<or>
-C:\> djangoenv\Scripts\activate
-
-
-
-
-

Install Django

-

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)$
-
-
-
-

Starting a Project

-

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:

-
-
-

Project Layout

-

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.

-
-
-

What Got Created

-
    -
  • outer *mysite* folder: this is just a container and can be renamed or -moved at will
  • -
  • inner *mysite* folder: this is your project directory. It should not be -renamed.
  • -
  • __init__.py: magic file that makes mysite a python package.
  • -
  • settings.py: file which holds configuration for your project, more soon.
  • -
  • urls.py: file which holds top-level URL configuration for your project, -more soon.
  • -
  • wsgi.py: binds a wsgi application created from your project to the -symbol application
  • -
  • manage.py: a management control script.
  • -
-
-
-

django-admin.py and manage.py

-

django-admin.py provides a hook for administrative tasks and abilities:

-
    -
  • creating a new project or app
  • -
  • running the development server
  • -
  • executing tests
  • -
  • entering a python interpreter
  • -
  • entering a database shell session with your database
  • -
  • much much more (run django-admin.py without an argument)
  • -
-

manage.py wraps this functionality, adding the full environment of your -project.

-
-
-

How manage.py Works

-

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.

-
-
-

Development Server

-

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.

-
-
-

A Blank Slate

-

You should see this:

-img/django-start.png -

Do you?

-
-
-

Connecting A Database

-

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.

-
-
-

Your Database Settings

-

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 and Your Database

-

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

-
-
-

Django Organization

-

We've created a Django project. In Django a project represents a whole -website:

-
    -
  • global configuration settings
  • -
  • inclusion points for additional functionality
  • -
  • master list of URL endpoints
  • -
-

A Django app encapsulates a unit of functionality:

-
    -
  • A blog section
  • -
  • A discussion forum
  • -
  • A content tagging system
  • -
-
-
-

Apps Make Up a Project

-

One project can (and likely will) consist of many apps

-
-
-

Core Django 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',
-)
-
-
-
-
-

Creating the Database

-

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.

-
-
-

Our Class App

-

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

-
-
-

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

What is Created

-

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.

-
-
-

Django Models

-

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

-
-
-

Our Post Model

-

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)
-
-
-
-

Model Fields

-

We've created a subclass of the Django Model class and added a bunch of -attributes.

-
    -
  • These attributes are all instances of Field classes defined in Django
  • -
  • Field attributes on a model map to columns in a database table
  • -
  • The arguments you provide to each Field customize how it works
      -
    • This means both how it operates in Django and how it is defined in SQL
    • -
    -
  • -
  • There are arguments shared by all Field types
  • -
  • There are also arguments specific to individual types
  • -
-

You can read much more about Model Fields and options

-
-
-

Field Details

-

There are some features of our fields worth mentioning in specific:

-

Notice we have no field that is designated as the primary key

-
    -
  • You can make a field the primary key by adding primary_key=True in the -arguments
  • -
  • If you do not, Django will automatically create one. This field is always -called id
  • -
  • No matter what the primary key field is called, its value is always -available on a model instance as the pk attribute.
  • -
-
-
-

Field Details

-
-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)

-
-
-

Field Details

-
-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.

-
-
-

Field Details

-
-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.

-
-
-

Field Details

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

-
-
-

Hooking it Up

-

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.

-
-
-

Installing Apps

-

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
-)
-
-
-
-

Setting Up the Database

-

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.

-
-
-

The Django Shell

-

Django provides a management command shell:

-
    -
  • Shares the same sys.path as your project, so all installed python -packages are present.
  • -
  • Imports the settings.py file from your project, and so shares all -installed apps and other settings.
  • -
  • Handles connections to your database, so you can interact with live data -directly.
  • -
-

Let's explore the Model Instance API directly using this shell:

-
-(djangoenv)$ python manage.py shell
-
-
-
-

Creating Instances

-

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.']}
-
-
-
-
-

Django Model Managers

-

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.

-
-
-

Making a ForeignKey Relation

-

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()
->>>
-
-
-
-
-

Saving New Objects

-

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>)
-
-
-
-
-

Updating An Instance

-

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)'
-
-
-
-
-

Create a Few Posts

-

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 Django Query API

-

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:

-
    -
  • title__exact="The exact title"
  • -
  • text__contains="decoration"
  • -
  • id__in=range(1,4)
  • -
  • published_date__lte=datetime.datetime.now()
  • -
-

Each keyword argument generates an SQL clause.

-
-
-

QuerySets

-

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

QuerySets and SQL

-

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!!!)

-
-
-

Exploring the QuerySet API

-

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]
-
-
-
-

Updating via QuerySets

-

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>),
- ...]
-
-
-
-

Testing Our Model

-

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.

-
-
-

Django TestCase Classes

-

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.

-
-
-

Testing Data

-

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.

-
-
-

Setting Up 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)
-
-
-
-
-

Our First Enhancement

-

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.

-
-
-

Write The Test

-

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)
-
-
-
-

Run The Test

-

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

The Result

-

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'...
-
-
-
-

Make it Pass

-

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

Did It Work?

-

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!

-
-
-

What to Test

-

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).

-
-
-

More Later

-

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:

-

https://docs.djangoproject.com/en/1.6/topics/testing/

-
-
-

The Django Admin

-

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

-
-
-

Using the Admin

-

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
-)
-
-
-
-

Accessing the Admin

-

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 URL Resolution

-

Django too has a system for dispatching requests to code: the urlconf.

-
    -
  • A urlconf is a an iterable of calls to the django.conf.urls.url function
  • -
  • This function takes:
      -
    • a regexp rule, representing the URL
    • -
    • a callable to be invoked (or a name identifying one)
    • -
    • an optional name kwarg, used to reverse the URL
    • -
    • other optional arguments we will skip for now
    • -
    -
  • -
  • The function returns a resolver that matches the request path to the -callable
  • -
-
-
-

urlpatterns

-

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.

-
-
-

Including URLs

-

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'))
-
-
-
-
-

Including the Admin

-

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
-)
-
-
-
-
-

Using the Development Server

-

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.
-
-
-
-

Viewing the Admin

-

Load http://localhost:8000/admin/. You should see this:

-img/django-admin-login.png -

Login with the name and password you created before.

-
-
-

The Admin Index

-

The index will provide a list of all the installed apps and each model -registered. You should see this:

-img/admin_index.png -

Click on Users. Find yourself? Edit yourself, but don't uncheck -superuser.

-
-
-

Add Posts to the Admin

-

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.

-
-
-

Play A Bit

-

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.

-
-
-

Next Steps

-

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.

-
-
- - - diff --git a/assignments/session06/img/admin_index.png b/assignments/session06/img/admin_index.png deleted file mode 100644 index ae7a19f986880cac0b47c2ea4cb48a722851c260..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27949 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 diff --git a/assignments/session06/img/django-admin-login.png b/assignments/session06/img/django-admin-login.png deleted file mode 100644 index 4ceb5f54b92922f5f82b8896c8dccfbcd1ad6bdf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7749 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 diff --git a/assignments/session06/tasks.txt b/assignments/session06/tasks.txt deleted file mode 100644 index 9cb9b211..00000000 --- a/assignments/session06/tasks.txt +++ /dev/null @@ -1,7 +0,0 @@ -Session 6 Homework -================== - -For your homework this week, walk through the Introduction to Django tutorial. - -Make sure to save your work and bring it to class. We'll be using it as a -starting point for our work in Session 7. diff --git a/assignments/session07/mysite/manage.py b/assignments/session07/mysite/manage.py deleted file mode 100755 index 8a50ec04..00000000 --- a/assignments/session07/mysite/manage.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/assignments/session07/mysite/myblog/__init__.py b/assignments/session07/mysite/myblog/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/assignments/session07/mysite/myblog/admin.py b/assignments/session07/mysite/myblog/admin.py deleted file mode 100644 index 67aec2d6..00000000 --- a/assignments/session07/mysite/myblog/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin -from myblog.models import Post -from myblog.models import Category - -admin.site.register(Post) -admin.site.register(Category) diff --git a/assignments/session07/mysite/myblog/fixtures/myblog_test_fixture.json b/assignments/session07/mysite/myblog/fixtures/myblog_test_fixture.json deleted file mode 100644 index 592dea17..00000000 --- a/assignments/session07/mysite/myblog/fixtures/myblog_test_fixture.json +++ /dev/null @@ -1,38 +0,0 @@ -[ -{ - "pk": 1, - "model": "auth.user", - "fields": { - "username": "admin", - "first_name": "Mr.", - "last_name": "Administrator", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2013-05-24T05:35:58.628Z", - "groups": [], - "user_permissions": [], - "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", - "email": "admin@example.com", - "date_joined": "2013-05-24T05:35:58.628Z" - } -}, -{ - "pk": 2, - "model": "auth.user", - "fields": { - "username": "noname", - "first_name": "", - "last_name": "", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2013-05-24T05:35:58.628Z", - "groups": [], - "user_permissions": [], - "password": "pbkdf2_sha256$10000$1rQazFNdOfFt$6aw/uIrv2uASkZ7moXMTajSN+ySYuowBnbP6ILNQntE=", - "email": "noname@example.com", - "date_joined": "2013-05-24T05:35:58.628Z" - } -} -] diff --git a/assignments/session07/mysite/myblog/migrations/0001_initial.py b/assignments/session07/mysite/myblog/migrations/0001_initial.py deleted file mode 100644 index 4e7a9de9..00000000 --- a/assignments/session07/mysite/myblog/migrations/0001_initial.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Post' - db.create_table(u'myblog_post', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=128)), - ('text', self.gf('django.db.models.fields.TextField')(blank=True)), - ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), - ('created_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('modified_date', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), - ('published_date', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), - )) - db.send_create_signal(u'myblog', ['Post']) - - - def backwards(self, orm): - # Deleting model 'Post' - db.delete_table(u'myblog_post') - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'myblog.post': { - 'Meta': {'object_name': 'Post'}, - 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), - 'created_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'published_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'}) - } - } - - complete_apps = ['myblog'] \ No newline at end of file diff --git a/assignments/session07/mysite/myblog/migrations/0002_auto__add_category.py b/assignments/session07/mysite/myblog/migrations/0002_auto__add_category.py deleted file mode 100644 index 1ecf7fcc..00000000 --- a/assignments/session07/mysite/myblog/migrations/0002_auto__add_category.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Category' - db.create_table(u'myblog_category', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('name', self.gf('django.db.models.fields.CharField')(max_length=128)), - ('description', self.gf('django.db.models.fields.TextField')(blank=True)), - )) - db.send_create_signal(u'myblog', ['Category']) - - # Adding M2M table for field posts on 'Category' - m2m_table_name = db.shorten_name(u'myblog_category_posts') - db.create_table(m2m_table_name, ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('category', models.ForeignKey(orm[u'myblog.category'], null=False)), - ('post', models.ForeignKey(orm[u'myblog.post'], null=False)) - )) - db.create_unique(m2m_table_name, ['category_id', 'post_id']) - - - def backwards(self, orm): - # Deleting model 'Category' - db.delete_table(u'myblog_category') - - # Removing M2M table for field posts on 'Category' - db.delete_table(db.shorten_name(u'myblog_category_posts')) - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'myblog.category': { - 'Meta': {'object_name': 'Category'}, - 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'categories'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['myblog.Post']"}) - }, - u'myblog.post': { - 'Meta': {'object_name': 'Post'}, - 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), - 'created_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'published_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'}) - } - } - - complete_apps = ['myblog'] \ No newline at end of file diff --git a/assignments/session07/mysite/myblog/migrations/__init__.py b/assignments/session07/mysite/myblog/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/assignments/session07/mysite/myblog/models.py b/assignments/session07/mysite/myblog/models.py deleted file mode 100644 index 29b851c7..00000000 --- a/assignments/session07/mysite/myblog/models.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.db import models -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) - - def __unicode__(self): - return self.title - -class Category(models.Model): - name = models.CharField(max_length=128) - description = models.TextField(blank=True) - posts = models.ManyToManyField(Post, blank=True, null=True, - related_name='categories') - - def __unicode__(self): - return self.name \ No newline at end of file diff --git a/assignments/session07/mysite/myblog/static/django_blog.css b/assignments/session07/mysite/myblog/static/django_blog.css deleted file mode 100644 index 64560dc0..00000000 --- a/assignments/session07/mysite/myblog/static/django_blog.css +++ /dev/null @@ -1,74 +0,0 @@ -body { - background-color: #eee; - color: #111; - font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; - margin:0; - padding:0; -} -#container { - margin:0; - padding:0; - margin-top: 0px; -} -#header { - background-color: #333; - border-botton: 1px solid #111; - margin:0; - padding:0; -} -#control-bar { - margin: 0em 0em 1em; - list-style: none; - list-style-type: none; - text-align: right; - color: #eee; - font-size: 80%; - padding-bottom: 0.4em; -} -#control-bar li { - display: inline-block; -} -#control-bar li a { - color: #eee; - padding: 0.5em; - text-decoration: none; -} -#control-bar li a:hover { - color: #cce; -} -#content { - margin: 0em 1em 1em; -} - -ul#entries { - list-style: none; - list-style-type: none; -} -div.entry { - margin-right: 2em; - margin-top: 1em; - border-top: 1px solid #cecece; -} -ul#entries li:first-child div.entry { - border-top: none; - margin-top: 0em; -} -div.entry-body { - margin-left: 2em; -} -.notification { - float: right; - text-align: center; - width: 25%; - padding: 1em; -} -.info { - background-color: #aae; -} -ul.categories { - list-style: none; - list-style-type: none; -} -ul.categories li { - display: inline; -} \ No newline at end of file diff --git a/assignments/session07/mysite/myblog/templates/detail.html b/assignments/session07/mysite/myblog/templates/detail.html deleted file mode 100644 index cd0322ff..00000000 --- a/assignments/session07/mysite/myblog/templates/detail.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Home -

{{ post }}

- -
- {{ post.text }} -
-
    - {% for category in post.categories.all %} -
  • {{ category }}
  • - {% endfor %} -
-{% endblock %} diff --git a/assignments/session07/mysite/myblog/templates/list.html b/assignments/session07/mysite/myblog/templates/list.html deleted file mode 100644 index 88920817..00000000 --- a/assignments/session07/mysite/myblog/templates/list.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

Recent Posts

- - {% comment %} here is where the query happens {% endcomment %} - {% for post in posts %} -
-

- {{ post }} -

- -
- {{ post.text }} -
-
    - {% for category in post.categories.all %} -
  • {{ category }}
  • - {% endfor %} -
-
- {% endfor %} -{% endblock %} diff --git a/assignments/session07/mysite/myblog/tests.py b/assignments/session07/mysite/myblog/tests.py deleted file mode 100644 index 413b2131..00000000 --- a/assignments/session07/mysite/myblog/tests.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.test import TestCase -from django.contrib.auth.models import User -from myblog.models import Post -from myblog.models import Category -import datetime -from django.utils.timezone import utc - -class PostTestCase(TestCase): - fixtures = ['myblog_test_fixture.json', ] - - def setUp(self): - self.user = User.objects.get(pk=1) - - def test_unicode(self): - expected = "This is a title" - p1 = Post(title=expected) - actual = unicode(p1) - self.assertEqual(expected, actual) - -class CategoryTestCase(TestCase): - - def test_unicode(self): - expected = "A Category" - c1 = Category(name=expected) - actual = unicode(c1) - self.assertEqual(expected, actual) - -class FrontEndTestCase(TestCase): - """test views provided in the front-end""" - fixtures = ['myblog_test_fixture.json', ] - - def setUp(self): - self.now = datetime.datetime.utcnow().replace(tzinfo=utc) - self.timedelta = datetime.timedelta(15) - author = User.objects.get(pk=1) - for count in range(1,11): - post = Post(title="Post %d Title" % count, - text="foo", - author=author) - if count < 6: - # publish the first five posts - pubdate = self.now - self.timedelta * count - post.published_date = pubdate - post.save() - - def test_list_only_published(self): - resp = self.client.get('/') - self.assertTrue("Recent Posts" in resp.content) - for count in range(1,11): - title = "Post %d Title" % count - if count < 6: - self.assertContains(resp, title, count=1) - else: - self.assertNotContains(resp, title) - - def test_details_only_published(self): - for count in range(1,11): - title = "Post %d Title" % count - post = Post.objects.get(title=title) - resp = self.client.get('/posts/%d/' % post.pk) - if count < 6: - self.assertEqual(resp.status_code, 200) - self.assertContains(resp, title) - else: - self.assertEqual(resp.status_code, 404) diff --git a/assignments/session07/mysite/myblog/urls.py b/assignments/session07/mysite/myblog/urls.py deleted file mode 100644 index ddce82fb..00000000 --- a/assignments/session07/mysite/myblog/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf.urls import patterns, url - -urlpatterns = patterns('myblog.views', - url(r'^$', - 'list_view', - name="blog_index"), - url(r'^posts/(?P\d+)/$', - 'detail_view', - name="blog_detail"), -) \ No newline at end of file diff --git a/assignments/session07/mysite/myblog/views.py b/assignments/session07/mysite/myblog/views.py deleted file mode 100644 index 54c4bb75..00000000 --- a/assignments/session07/mysite/myblog/views.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.shortcuts import render -from django.http import HttpResponse, HttpResponseRedirect, Http404 -from django.template import RequestContext, loader -from myblog.models import Post - -def stub_view(request, *args, **kwargs): - body = "Stub View\n\n" - if args: - body += "Args:\n" - body += "\n".join(["\t%s" % a for a in args]) - if kwargs: - body += "Kwargs:\n" - body += "\n".join(["\t%s: %s" % i for i in kwargs.items()]) - return HttpResponse(body, content_type="text/plain") - -def list_view(request): - published = Post.objects.exclude(published_date__exact=None) - posts = published.order_by('-published_date') - context = {'posts': posts} - return render(request, 'list.html', context) - -def detail_view(request, post_id): - published = Post.objects.exclude(published_date__exact=None) - try: - post = published.get(pk=post_id) - except Post.DoesNotExist: - raise Http404 - context = {'post': post} - return render(request, 'detail.html', context) \ No newline at end of file diff --git a/assignments/session07/mysite/mysite.db b/assignments/session07/mysite/mysite.db deleted file mode 100644 index 63c9e9a57dbf40bdfacafb2a0d046cc8f04f22df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155648 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 bAOHf7Isv}q=gWVIFaJH(@}GzE_y7MN3OLNV diff --git a/assignments/session07/mysite/mysite/__init__.py b/assignments/session07/mysite/mysite/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/assignments/session07/mysite/mysite/settings.py b/assignments/session07/mysite/mysite/settings.py deleted file mode 100644 index 78a3248e..00000000 --- a/assignments/session07/mysite/mysite/settings.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Django settings for mysite project. - -For more information on this file, see -https://docs.djangoproject.com/en/1.6/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.6/ref/settings/ -""" - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'o$)p$v##&^xobe)62v2mvh(+m297vqd-sqnma@c0@g#g&o_!bw' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -TEMPLATE_DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'south', - 'myblog', -) - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) - -ROOT_URLCONF = 'mysite.urls' - -WSGI_APPLICATION = 'mysite.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/1.6/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'mysite.db', - } -} - -# Internationalization -# https://docs.djangoproject.com/en/1.6/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.6/howto/static-files/ - -STATIC_URL = '/static/' - -TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'mysite/templates'),) - -LOGIN_URL = '/login/' -LOGIN_REDIRECT_URL = '/' diff --git a/assignments/session07/mysite/mysite/templates/base.html b/assignments/session07/mysite/mysite/templates/base.html deleted file mode 100644 index 2a01d991..00000000 --- a/assignments/session07/mysite/mysite/templates/base.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - My Django Blog - - - - -
-
- {% block content %} - [content will go here] - {% endblock %} -
-
- - diff --git a/assignments/session07/mysite/mysite/templates/login.html b/assignments/session07/mysite/mysite/templates/login.html deleted file mode 100644 index 1566d0f7..00000000 --- a/assignments/session07/mysite/mysite/templates/login.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

My Blog Login

-
{% csrf_token %} - {{ form.as_p }} -

-
-{% endblock %} diff --git a/assignments/session07/mysite/mysite/urls.py b/assignments/session07/mysite/mysite/urls.py deleted file mode 100644 index 9c67dfdb..00000000 --- a/assignments/session07/mysite/mysite/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.conf.urls import patterns, include, url - -from django.contrib import admin -admin.autodiscover() - -urlpatterns = patterns('', - # Examples: - # url(r'^$', 'mysite.views.home', name='home'), - # url(r'^blog/', include('blog.urls')), - url(r'^', include('myblog.urls')), - url(r'^admin/', include(admin.site.urls)), - url(r'^login/$', - 'django.contrib.auth.views.login', - {'template_name': 'login.html'}, - name="login"), - url(r'^logout/$', - 'django.contrib.auth.views.logout', - {'next_page': '/'}, - name="logout"), -) diff --git a/assignments/session07/mysite/mysite/wsgi.py b/assignments/session07/mysite/mysite/wsgi.py deleted file mode 100644 index 10ef32d9..00000000 --- a/assignments/session07/mysite/mysite/wsgi.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -WSGI config for mysite project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ -""" - -import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - -from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() diff --git a/assignments/session07/tasks.txt b/assignments/session07/tasks.txt deleted file mode 100644 index b3df2917..00000000 --- a/assignments/session07/tasks.txt +++ /dev/null @@ -1,49 +0,0 @@ -Session 7 Homework -================== - -We noted in class that it is awkward to have to add a post to a category, -instead of being able to designate a category for a post when authoring the -post. You will update your blog admin so that this is fixed. - -Required Tasks --------------- - -Take the following steps: - -1. Read the documentation about the Django admin. -2. You'll need to create a customized ModelAdmin class for the Post and - Category models. -3. And you'll need to create an InlineModelAdmin to represent Categories on the - Post admin view. -4. Finally, you'll need to suppress the display of the 'posts' field on your - Category admin view. - -resources: - -https://docs.djangoproject.com/en/1.6/ref/contrib/admin/ -https://docs.djangoproject.com/en/1.6/ref/contrib/admin/#modeladmin-objects -https://docs.djangoproject.com/en/1.6/ref/contrib/admin/#inlinemodeladmin-objects -https://docs.djangoproject.com/en/1.6/ref/contrib/admin/#modeladmin-options - - -Optional Tasks --------------- - -If you complete the above in less than 3-4 hours of work, consider looking into -other ways of customizing the admin. - -Tasks you might consider: - -* Change the admin index to say 'Categories' instead of 'Categorys'. -* Add columns for the date fields to the list display of Posts. -* Display the created and modified dates for your posts when viewing them in - the admin. -* Add a column to the list display of Posts that shows the author. For more - fun, make this a link that takes you to the admin page for that user. -* For the biggest challenge, look into `admin actions`_ and add an action to - the Post admin that allows you to bulk publish posts from the Post list - display - -resources: - -https://docs.djangoproject.com/en/1.6/ref/contrib/admin/actions/ From 2df88e70d2a0759ff17856ed8f49f7dd2cbc75dc Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 6 Jan 2015 17:53:12 -0800 Subject: [PATCH 004/173] update session 1 with contact and docs info --- source/presentations/session01.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/source/presentations/session01.rst b/source/presentations/session01.rst index 4eb0983c..00248064 100644 --- a/source/presentations/session01.rst +++ b/source/presentations/session01.rst @@ -45,6 +45,14 @@ But First .. nextslide:: +The rendered documentation is available as well: + +http://uwpce-pythoncert.github.io + +Please check frequently. I will update with great regularity + +.. nextslide:: + **Classroom Protocol** .. rst-class:: build @@ -1786,7 +1794,8 @@ Do not delay working on this until the last moment. Do not skip this assignment. -Do ask questions frequently via email. +Do ask questions frequently via email (use the `class google group`_). See you next week! +.. _class google group: https://groups.google.com/forum/#!forum/programming-in-python From 1e66d85562a3116c9e5c3dcd8a424bf080a44511 Mon Sep 17 00:00:00 2001 From: cewing Date: Thu, 8 Jan 2015 18:59:46 -0800 Subject: [PATCH 005/173] fixes for formatting and a typo in a code example --- source/presentations/session01.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/source/presentations/session01.rst b/source/presentations/session01.rst index 00248064..d0d5e7be 100644 --- a/source/presentations/session01.rst +++ b/source/presentations/session01.rst @@ -1394,7 +1394,7 @@ It's pretty easy to play with your models from in an interpreter. >>> from sqlalchemy.orm import sessionmaker >>> Session = sessionmaker(bind=engine) >>> session = Session() - >>> from learning_journal.models MyModel + >>> from learning_journal.models import MyModel >>> session.query(MyModel).all() [] @@ -1711,9 +1711,11 @@ Since methods in this category return ``Query`` objects, they can be safely Homework ======== +.. rst-class:: left + Okay, that's enough for the moment. -.. rst-class:: build +.. rst-class:: build left .. container:: You've learned quite a bit about how *models* work in SQLAlchemy @@ -1728,7 +1730,8 @@ Okay, that's enough for the moment. I'll also ask you to define a few methods to complete the first part of our API. -.. nextslide:: The Model +The Model +--------- Our model will be called an ``Entry``. Here's what you need to know: From ec72851bc614a50a9d279ae6ec8ed631df13029e Mon Sep 17 00:00:00 2001 From: cewing Date: Thu, 8 Jan 2015 19:37:21 -0800 Subject: [PATCH 006/173] start creating session 2 slides --- source/_static/lj_entry.png | Bin 0 -> 160553 bytes source/presentations/index.rst | 1 + source/presentations/session02.rst | 16 ++++++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 source/_static/lj_entry.png create mode 100644 source/presentations/session02.rst diff --git a/source/_static/lj_entry.png b/source/_static/lj_entry.png new file mode 100644 index 0000000000000000000000000000000000000000..4224d059b3efb03bd8fe8a846b9a3e2ada2d6064 GIT binary patch literal 160553 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≪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

w_C}^zj{y%>}{r(NV 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 BsOA6w literal 0 HcmV?d00001 diff --git a/source/presentations/index.rst b/source/presentations/index.rst index 9140ae4b..5fae542a 100644 --- a/source/presentations/index.rst +++ b/source/presentations/index.rst @@ -16,3 +16,4 @@ course. :maxdepth: 2 session01 + session02 diff --git a/source/presentations/session02.rst b/source/presentations/session02.rst new file mode 100644 index 00000000..a8fc65c3 --- /dev/null +++ b/source/presentations/session02.rst @@ -0,0 +1,16 @@ +.. slideconf:: + :autoslides: True + +********** +Session 02 +********** + +.. image:: /_static/lj_entry.png + :width: 65% + :align: center + +Views and Controllers +===================== + +**Wherein we learn to show our data, and to create and edit it too!** + From 2e9f0a958512bd44aa85e8ff3018fb0f645fce10 Mon Sep 17 00:00:00 2001 From: cewing Date: Fri, 9 Jan 2015 15:08:33 -0800 Subject: [PATCH 007/173] fix up a small issue with figure caption text size --- source/_static/custom.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/_static/custom.css b/source/_static/custom.css index de26ee70..44274406 100644 --- a/source/_static/custom.css +++ b/source/_static/custom.css @@ -159,3 +159,8 @@ article table.docutils tr td { text-decoration: none; border: none; } +.figure p.caption { + font-size: 75%; + text-align: center; +} + From b8ae7aca758b6f4d0086c6f1a40cee2bda663dfb Mon Sep 17 00:00:00 2001 From: cewing Date: Fri, 9 Jan 2015 15:08:58 -0800 Subject: [PATCH 008/173] progress on session 2 slides --- source/presentations/session02.rst | 222 ++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 2 deletions(-) diff --git a/source/presentations/session02.rst b/source/presentations/session02.rst index a8fc65c3..4321a283 100644 --- a/source/presentations/session02.rst +++ b/source/presentations/session02.rst @@ -9,8 +9,226 @@ Session 02 :width: 65% :align: center -Views and Controllers +Interacting with Data ===================== -**Wherein we learn to show our data, and to create and edit it too!** +**Wherein we learn to display our data, and to create and edit it too!** + + +But First +--------- + +Last week we discussed the **model** part of the *MVC* application design +pattern. + +.. rst-class:: build +.. container:: + + We set up a project using the `Pyramid`_ web framework and the `SQLAlchemy`_ + library for persisting our data to a database. + + We looked at how to define a simple model by investigating the demo model + created on our behalf. + + And we went over, briefly, the way we can interact with this model at the + command line to make sure we've got it right. + + Finally, we defined what attributes a learning journal entry would have, + and a pair of methods we think we will need to make the model complete. + +.. _Pyramid: http://www.pylonsproject.org/projects/pyramid/about +.. _SQLAlchemy: http://docs.sqlalchemy.org/en/rel_0_9/ + +Our Data Model +-------------- + +Over the last week, your assignment was to create the new model. + +.. rst-class:: build +.. container:: + + Did you get that done? + + If not, what stopped you? + + Let's take a few minutes here to answer questions about this task so you + are more comfortable. + + Questions? + +.. nextslide:: A Complete Example + +I have added a new folder to our `class repository`_, ``resources``. + +.. _class repository: https://github.com/UWPCE-PythonCert/training.python_web/ + +.. rst-class:: build +.. container:: + + If you clone the repository to your local machine you can get to it. + + You can also just browse the repository in github to view it. + + In this folder, I added a ``session02`` folder that contains resources for + today. + + Among these resources is the completed ``models.py`` file with this new + model added. + + Let's review how it works. + +.. nextslide:: Demo Interaction + +Another resource I've added is the ``ljshell.py`` script. + +.. rst-class:: build +.. container:: + + That script will allow you to interact with a db session just like I showed + in class last week: + + .. code-block:: python + + # the script + from pyramid.paster import get_appsettings, setup_logging + from sqlalchemy import engine_from_config + from sqlalchemy.orm import sessionmaker + + config_uri = 'development.ini' + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + Session = sessionmaker(bind=engine) + + Just copy the file into your learning_journal Pyramid project folder (where + ``setup.py`` is) + +.. nextslide:: Using the ``ljshell.py`` script + +Here's a demo interaction using the script to set up a session maker + +.. rst-class:: build +.. container:: + + First ``cd`` to your project code, fire up your project virtualenv and + start python: + + .. code-block:: bash + + $ cd projects/learning-journal/learning_journal + $ source ../ljenv/bin/activate + (ljenv)$ python + >>> + + Then, you can import the ``Session`` symbol from ``ljshell`` and you're off + to the races: + + .. code-block:: pycon + + >>> from ljshell import Session + >>> from learning_journal.models import MyModel + >>> session = Session() + >>> session.query(MyModel).all() + [] + ... + +Pyramid Views +============= + +.. rst-class:: left +.. container:: + + Let's go back to thinking for a bit about the *Model-View-Controller* + pattern. + + .. rst-class:: build + .. container:: + + .. figure:: http://upload.wikimedia.org/wikipedia/commons/4/40/MVC_passive_view.png + :align: center + :width: 25% + + By Alan Evangelista (Own work) [CC0], via Wikimedia Commons + + We talked last week (and today) about the *model* + +outline +------- + +views are "controllers" + +requests come in with user input, data sent out + +views are connected to the outside world via "routes" and these determine URLs + +see how it works for the current MyModel and my_view + +add route to config tells the application which urls will work + try urls that are not in config, see what happens + +view_config tells the view what renderer to use, which route to connect to, and +can help discriminate between views that share the same route + +renderers are the "view" in mvc + +our data model is the program's api for our application + +Think of routes as the user API for the application, it determines what the +user can do. + +Add routes for our application, what do we need to be able to do? + +Add stub views for our application, we can see our routes, and can tell when +we've succeeded in getting past them. + +Test the application routes + +Create a view to view all entries + +create a view to view one entry by id + +Templates +========= + +We want to use Jinja, add jinja 2 as template engine and `python setup.py +develop` to install + +quick intro to jinja2 templates. + +create a nice basic html outline, see how it works + +create a template to show a single entry, hook it up to your view/route and +test it by viewing it. + +create a template to show a list of entries, hook it up and test by viewing. + + + +Adding New Entries +================== + +Add route, and view for creating new entry. + +Discuss forms. + +Create form for creating a new entry + +use form in template. + + +homework +-------- + +What's the difference between creating new and editing existing? + +add route and view for editing + +create form for editing (subclass) + +use form in template + + +homework + + From cedd311c57ddbe01f52bd1b7dd42bbc63eacc005 Mon Sep 17 00:00:00 2001 From: cewing Date: Fri, 9 Jan 2015 15:09:11 -0800 Subject: [PATCH 009/173] begin adding class resources for session 2 --- resources/session02/ljshell.py | 9 ++++++ resources/session02/models.py | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 resources/session02/ljshell.py create mode 100644 resources/session02/models.py diff --git a/resources/session02/ljshell.py b/resources/session02/ljshell.py new file mode 100644 index 00000000..40a8b080 --- /dev/null +++ b/resources/session02/ljshell.py @@ -0,0 +1,9 @@ +from pyramid.paster import get_appsettings, setup_logging +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker + +config_uri = 'development.ini' +setup_logging(config_uri) +settings = get_appsettings(config_uri) +engine = engine_from_config(settings, 'sqlalchemy.') +Session = sessionmaker(bind=engine) diff --git a/resources/session02/models.py b/resources/session02/models.py new file mode 100644 index 00000000..4d689df5 --- /dev/null +++ b/resources/session02/models.py @@ -0,0 +1,55 @@ +import datetime +from sqlalchemy import ( + Column, + DateTime, + Index, + Integer, + Text, + Unicode, + UnicodeText, + ) + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + +Index('my_index', MyModel.name, unique=True, mysql_length=255) + + +class Entry(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode(255), unique=True, nullable=False) + body = Column(UnicodeText, default=u'') + created = Column(DateTime, default=datetime.datetime.utcnow) + edited = Column(DateTime, default=datetime.datetime.utcnow) + + @classmethod + def all(cls): + """return a query with all entries, ordered by creation date reversed + """ + return DBSession.query(cls).order_by(sa.desc(cls.created)).all() + + @classmethod + def by_id(cls, id): + """return a single entry identified by id + + If no entry exists with the provided id, return None + """ + return DBSession.query(cls).get(id) From 7870507def5cabb83095c20d76c6f14bf438cfa6 Mon Sep 17 00:00:00 2001 From: cewing Date: Fri, 9 Jan 2015 20:06:51 -0800 Subject: [PATCH 010/173] more session 2 slides --- source/presentations/session02.rst | 310 +++++++++++++++++++++++++++-- 1 file changed, 296 insertions(+), 14 deletions(-) diff --git a/source/presentations/session02.rst b/source/presentations/session02.rst index 4321a283..10527aa1 100644 --- a/source/presentations/session02.rst +++ b/source/presentations/session02.rst @@ -88,7 +88,7 @@ Another resource I've added is the ``ljshell.py`` script. in class last week: .. code-block:: python - + # the script from pyramid.paster import get_appsettings, setup_logging from sqlalchemy import engine_from_config @@ -132,8 +132,8 @@ Here's a demo interaction using the script to set up a session maker [] ... -Pyramid Views -============= +The Controller +============== .. rst-class:: left .. container:: @@ -141,25 +141,307 @@ Pyramid Views Let's go back to thinking for a bit about the *Model-View-Controller* pattern. + .. figure:: http://upload.wikimedia.org/wikipedia/commons/4/40/MVC_passive_view.png + :align: center + :width: 25% + + By Alan Evangelista (Own work) [CC0], via Wikimedia Commons + .. rst-class:: build .. container:: - - .. figure:: http://upload.wikimedia.org/wikipedia/commons/4/40/MVC_passive_view.png - :align: center - :width: 25% - - By Alan Evangelista (Own work) [CC0], via Wikimedia Commons We talked last week (and today) about the *model* -outline -------- + Today, we'll dig into *controllers* and *views* + + or as we will know them in Pyramid: *views* and *renderers* + + +HTTP Request/Response +--------------------- + +Internet software is driven by the HTTP Request/Response cycle. + +.. rst-class:: build +.. container:: + + A *client* (perhaps a user with a web browser) makes a **request** + + A *server* receives and handles that request and returns a **response** + + The *client* receives the response and views it, perhaps making a new + **request** + + And around and around it goes. + +.. nextslide:: URLs + +An HTTP request arrives at a server through the magic of a **URL** + +.. code-block:: bash + + http://uwpce-pythoncert.github.io/training.python_web/html/index.html + +.. rst-class:: build +.. container:: + + Let's break that up into its constituent parts: + + .. rst-class:: build + + \http://: + This part is the *protocol*, it determines how the request will be sent + + uwpce-pythoncert.github.io: + This is a *domain name*. It's the human-facing address for a server + somewhere. + + /training.python_web/html/index.html: + This part is the *path*. It serves as a locator for a resource *on the + server* + +.. nextslide:: Paths + +In a static website (like our documentation) the *path* identifies a **physical +location** in the server's filesystem. + +.. rst-class:: build +.. container:: + + Some directory on the server is the *home* for the web process, and the + *path* is looked up there. + + Whatever resource (a file, an image, whatever) is located there is returned + to the user as a response. + + If the path leads to a location that doesn't exist, the server responds + with a **404 Not Found** error. + + In the golden days of yore, this was the only way content was served via + HTTP. + +.. nextslide:: Paths in an MVC System + +In todays world we have dynamic systems, server-side web frameworks like +Pyramid. + +.. rst-class:: build +.. container:: + + The requests that you send to a server are handled by a software process + that assembles a response instead of looking up a physical location. + + But we still have URLs, with *protocol*, *domain* and *path*. + + What is the role for a path in a process that doesn't refer to a physical + file system? + + Most web frameworks now call the *path* a **route**. + + They provide a way of matching *routes* to the code that will be run to + handle requests. + +Routes in Pyramid +----------------- + +In Pyramid, routes are handled as *configuration* and are set up in the *main* +function in ``__init__.py``: + +.. code-block:: python + + # learning_journal/__init__.py + def main(global_config, **settings): + # ... + config.add_route('home', '/') + # ... + +.. rst-class:: build +.. container:: + + Our code template created a sample route for us, using the ``add_route`` + method of the ``Configurator`` class. + + The ``add_route`` method has two required arguments: a *name* and a + *pattern* + + In our sample route, the *name* is ``'home'`` + + In our sample route, the *pattern* is ``'/'`` + +.. nextslide:: + +When a request comes in to a Pyramid application, the framework looks at all +the *routes* that have been configured. + +.. rst-class:: build +.. container:: + + One by one, in order, it tries to match the *path* of the incoming request + against the *pattern* of the route. + + As soon as a *pattern* matches the *path* from the incoming request, that + route is used and no further matching is performed. + + If no route is found that matches, then the request will automatically get + a **404 Not Found** error response. + + In our sample app, we have one sample *route* named ``'home'``, with a + pattern of ``/``. + + This means that any request that comes in for ``/`` will be matched to this + route, and any other request will be **404**. + +.. nextslide:: Routes as API + +In a very real sense, the *routes* defined in an application *are* the public +API. + +.. rst-class:: build +.. container:: + + Any route that is present represents something the user can do. + + Any route that is not present is something the user cannot do. + + You can use the proper definition of routes to help conceptualize what your + app will do. + + What routes might we want for a learning journal application? + + What will our application do? + +.. nextslide:: Defining our Routes + +Let's add routes for our application. + +.. rst-class:: build +.. container:: + + Open ``learning_journal/__init__.py``. + + For our list page, the existing ``'home'`` route will do fine, leave it. + + For a detail page, we want a URL that captures the identifier for our + journal entries. + + That way, we can use the captured identifier to pick the correct entry + using our models ``by_id`` api. + + We'll need the Pyramid pattern syntax. + +.. nextslide:: Matching an ID + +In a pattern, you can capture a ``path segment`` *replacement +marker*, a valid Python symbol surrounded by curly braces: + +.. rst-class:: build +.. container:: + + :: + + /home/{foo}/ + + Matched path segments are captured in a ``matchdict``:: + + # pattern # actual url # matchdict + /home/{foo}/ /home/an_id/ {'foo': 'an_id'} + + If you want to match a particular pattern, like digits only, add a + *regexp*:: + + /journal/{id:\d+} + + Add this new route to our configuration as ``'detail'``:: + + config.add_route('detail', '/journal/{id:\d+}') + + +.. nextslide:: Connecting Routes to Views + +In Pyramid, a *route* is connected by configuration to a *view*. + +.. rst-class:: build +.. container:: + + In our app, a sample view has been created for us, in ``views.py``: + + .. code-block:: python + + @view_config(route_name='home', renderer='templates/mytemplate.pt') + def my_view(request): + # ... + + The order in which *routes* are configured *is important*, so that must be + done in ``__init__.py``. + + The order in which views are connected to routes *is not important*, so the + *declarative* ``@view_config`` decorator can be used. + + When ``config.scan`` is called, all files in our application are searched + for such *declarative configuration* and it is added. + +The Pyramid View +---------------- + +Let's imagine that a *request* has come to our application for the path +``'/'``. + +.. rst-class:: build +.. container:: -views are "controllers" + The framework made a match of that path to a *route* with the pattern ``'/'``. -requests come in with user input, data sent out + Configuration connected that route to a *view* in our application. + + Now, the view that was connected will be *called*, which brings us to the + nature of *views* + + .. rst-class:: centered + + --A Pyramid view is a *callable* that takes *request* as an argument-- + + Remember what a *callable* is? + +.. nextslide:: What the View Does + +So, a *view* is a callable that takes the *request* as an argument. + +.. rst-class:: build +.. container:: -views are connected to the outside world via "routes" and these determine URLs + It can then use information from that request to build appropriate data, + perhaps using the application's *models*. + + Then, it returns the data it assembled, passing it on to a `renderer`_. + + Which *renderer* to use is determined, again, by configuration: + + .. code-block:: python + + @view_config(route_name='home', renderer='templates/mytemplate.pt') + def my_view(request): + # ... + + More about this in a moment. + + The *view* stands at the intersection of *input data*, the application + *model* and *renderers* that offer rendering of the results. + + It is the *Controller* in our MVC application. + + + Here, we'll use a *page template*, which renders HTML. + + But Pyramid has other possible renderers: ``string``, ``json``, ``jsonp``. + + And you can build your own. + +.. _renderer: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html + + + +outline +------- see how it works for the current MyModel and my_view From 9ca44f16654d9ec6f95d8d317876d284b529e9a4 Mon Sep 17 00:00:00 2001 From: cewing Date: Fri, 9 Jan 2015 22:37:04 -0800 Subject: [PATCH 011/173] further up and further in --- source/presentations/session02.rst | 174 +++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 33 deletions(-) diff --git a/source/presentations/session02.rst b/source/presentations/session02.rst index 10527aa1..83089361 100644 --- a/source/presentations/session02.rst +++ b/source/presentations/session02.rst @@ -132,6 +132,8 @@ Here's a demo interaction using the script to set up a session maker [] ... + [demo] + The Controller ============== @@ -321,13 +323,22 @@ Let's add routes for our application. For our list page, the existing ``'home'`` route will do fine, leave it. - For a detail page, we want a URL that captures the identifier for our - journal entries. + Add the following two routes: + + .. code-block:: python + + config.add_route('home', '/') # already there + config.add_route('detail', '/journal/{id:\d+}') + config.add_route('action', '/journal/{action}') + + The ``'detail'`` route will serve a single journal entry, identified by an + ``id``. - That way, we can use the captured identifier to pick the correct entry - using our models ``by_id`` api. + The ``action`` route will serve ``create`` and ``edit`` views, depending on + the ``action`` specified. - We'll need the Pyramid pattern syntax. + In both cases, we want to capture a portion of the matched path to use + information it provides. .. nextslide:: Matching an ID @@ -341,19 +352,17 @@ marker*, a valid Python symbol surrounded by curly braces: /home/{foo}/ - Matched path segments are captured in a ``matchdict``:: - - # pattern # actual url # matchdict - /home/{foo}/ /home/an_id/ {'foo': 'an_id'} - If you want to match a particular pattern, like digits only, add a *regexp*:: /journal/{id:\d+} - Add this new route to our configuration as ``'detail'``:: + Matched path segments are captured in a ``matchdict``:: + + # pattern # actual url # matchdict + /journal/{id:\d+} /journal/27 {'id': '27'} - config.add_route('detail', '/journal/{id:\d+}') + The ``matchdict`` is made available as an attribute of the *request* .. nextslide:: Connecting Routes to Views @@ -429,45 +438,144 @@ So, a *view* is a callable that takes the *request* as an argument. It is the *Controller* in our MVC application. +.. nextslide:: Adding Stub Views - Here, we'll use a *page template*, which renders HTML. +Add temporary views to our application in ``views.py`` (and comment out the +sample view): - But Pyramid has other possible renderers: ``string``, ``json``, ``jsonp``. +.. code-block:: python - And you can build your own. + @view_config(route_name='home', renderer='string') + def index_page(request): + return 'list page' -.. _renderer: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html + @view_config(route_name='blog', renderer='string') + def blog_view(request): + return 'detail page' + @view_config(route_name='blog_action', match_param='action=create', renderer='string') + def blog_create(request): + return 'create page' + @view_config(route_name='blog_action', match_param='action=edit', renderer='string') + def blog_update(request): + return 'edit page' -outline -------- +.. nextslide:: Testing Our Views -see how it works for the current MyModel and my_view +Now we can verify that our view configuration has worked. -add route to config tells the application which urls will work - try urls that are not in config, see what happens +.. rst-class:: build +.. container:: + + Make sure your virtualenv is properly activated, and start the web server: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 84467. + serving on http://0.0.0.0:6543 + + Then try viewing some of the expected application urls: + + .. rst-class:: build + + * http://localhost:6543/ + * http://localhost:6543/journal/1 + * http://localhost:6543/journal/create + * http://localhost:6543/journal/edit + + What happens if you visit a URL that *isn't* in our configuration? + +.. nextslide:: Interacting With the Model -view_config tells the view what renderer to use, which route to connect to, and -can help discriminate between views that share the same route +Now that we've got temporary views that work, we can fix them to get +information from our database -renderers are the "view" in mvc +.. rst-class:: build +.. container:: -our data model is the program's api for our application + We'll begin with the list view. -Think of routes as the user API for the application, it determines what the -user can do. + We need some code that will fetch all the journal entries we've written, in + reverse order, and hand that collection back for rendering. + + .. code-block:: python -Add routes for our application, what do we need to be able to do? + from .models import ( + DBSession, + MyModel, + Entry, # <- Add this import + ) -Add stub views for our application, we can see our routes, and can tell when -we've succeeded in getting past them. + # and update this view function + def index_page(request): + entries = Entry.all() + return {'entries': entries} -Test the application routes +.. nextslide:: Using the ``matchdict`` -Create a view to view all entries +Next, we want to write the view for a single entry. + +.. rst-class:: build +.. container:: + + We'll need to use the ``id`` value our route captures into the + ``matchdict``. + + Remember that the ``matchdict`` is an attribute of the request. + + We'll get the ``id`` from there, and use it to get the correct entry. + + .. code-block:: python + + # add this import at the top + from pyramid.exceptions import HTTPNotFound + + # and update this view function: + def blog_view(request): + this_id = request.matchdict.get('id', -1) + entry = Entry.by_id(this_id) + if not entry: + return HTTPNotFound() + return {'entry': entry} + +.. nextslide:: Testing Our Views + +We can now verify that these views work correctly. + +.. rst-class:: build +.. container:: + + Make sure your virtualenv is properly activated, and start the web server: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 84467. + serving on http://0.0.0.0:6543 + + Then try viewing the list page and an entry page: + + * http://localhost:6543 + * http://localhost:6543/journal/1 + + What happens when you request an entry with an id that isn't in the + database? + + * http://localhost:6543/journal/100 + +outline +------- + +Here, we'll use a *page template*, which renders HTML. + +But Pyramid has other possible renderers: ``string``, ``json``, ``jsonp``. + +And you can build your own. + +.. _renderer: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html -create a view to view one entry by id Templates ========= From 605fb4a00d7915733f4a0707b8ec28547fca7397 Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 10 Jan 2015 10:03:12 -0800 Subject: [PATCH 012/173] update readings a bit --- source/readings.rst | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/source/readings.rst b/source/readings.rst index 52d1d782..d3b8b243 100644 --- a/source/readings.rst +++ b/source/readings.rst @@ -15,7 +15,7 @@ readings that will support the information you'll learn in class. Think of this as supplemental materials. You can read it at your leisure to help increase both the depth and breadth of your knowledge. -The readings are organized like the class, by session and topic. +The readings are organized like the class, by session and topic. Session 1 - MVC Applications and Data Persistence @@ -46,6 +46,26 @@ understanding of how the SQLAlchemy ORM works. Session 2 - Pyramid Views, Renderers and Forms ---------------------------------------------- +This week we'll be focusing on the connection of an HTTP request to the code +that handles that request using `URL Dispatch`_. Quite a lot is possible with +the Pyramid route system. You may wish to read a bit more about it in one of +the following documentation sections: + +* `Route Pattern Syntax + `_ + discusses the syntax for pattern matching and extraction in Pyramid routes. + +In Pyramid, the code that handles requests is called `a view`_. + +A view passes data to `a renderer`_, which is responsible for turning the data +into a response to send back. + + +.. _URL Dispatch: http://docs.pylonsproject.org/docs/pyramid/en/latest/narr/urldispatch.html +.. _a view: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/views.html +.. _a renderer: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html + + Sesstion 3 - Pyramid Authentication and Deployment -------------------------------------------------- From c71218123f74249fdb6abb6ec6ad77e6b4851ae5 Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 10 Jan 2015 16:09:48 -0800 Subject: [PATCH 013/173] pushing further into this presentation --- source/presentations/session02.rst | 580 ++++++++++++++++++++++++++++- 1 file changed, 572 insertions(+), 8 deletions(-) diff --git a/source/presentations/session02.rst b/source/presentations/session02.rst index 83089361..f25e479e 100644 --- a/source/presentations/session02.rst +++ b/source/presentations/session02.rst @@ -134,8 +134,8 @@ Here's a demo interaction using the script to set up a session maker [demo] -The Controller -============== +The MVC Controller +================== .. rst-class:: left .. container:: @@ -438,6 +438,9 @@ So, a *view* is a callable that takes the *request* as an argument. It is the *Controller* in our MVC application. +.. _renderer: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html + + .. nextslide:: Adding Stub Views Add temporary views to our application in ``views.py`` (and comment out the @@ -565,17 +568,578 @@ We can now verify that these views work correctly. * http://localhost:6543/journal/100 -outline -------- +The MVC View +============ + +.. rst-class:: left +.. container:: + + Again, back to the *Model-View-Controller* pattern. + + .. figure:: http://upload.wikimedia.org/wikipedia/commons/4/40/MVC_passive_view.png + :align: center + :width: 25% -Here, we'll use a *page template*, which renders HTML. + By Alan Evangelista (Own work) [CC0], via Wikimedia Commons -But Pyramid has other possible renderers: ``string``, ``json``, ``jsonp``. + .. rst-class:: build + .. container:: -And you can build your own. + We've built a *model* and we've created some *controllers* that use it. -.. _renderer: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html + In Pyramid, we call *controllers* **views** and they are callables that + take *request* as an argument. + + Let's turn to the last piece of the *MVC* patter, the *view* + +Presenting Data +--------------- + +The job of the *view* in the *MVC* pattern is to present data in a format that +is readable to the user of the system. + +.. rst-class:: build +.. container:: + + There are many ways to present data. + + Some are readable by humans (tables, charts, graphs, HTML pages, text + files). + + Some are more for machines (xml files, csv, json). + + Which of these formats is the *right one* depends on your purpose. + + What is the purpose of our learning journal? + +Pyramid Renderers +----------------- + +In Pyramid, the job of presenting data is performed by a *renderer*. + +.. rst-class:: build +.. container:: + + So we can consider the Pyramid **renderer** to be the *view* in our *MVC* + app. + + We've already seen how we can connect a *renderer* to a Pyramid *view* with + configuration. + + In fact, we have already done so, using a built-in renderer called + ``'string'``. + + This renderer converts the return value of its *view* to a string and sends + that back to the client as an HTTP response. + + But the result isn't so nice looking. + +.. nextslide:: Template Renderers + +The `built-in renderers` (``'string'``, ``'json'``, ``'jsonp'``) in Pyramid are +not the only ones available. + +.. _built-in renderers: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html#built-in-renderers + +.. rst-class:: build +.. container:: + + There are add-ons to Pyramid that support using various *template + languages* as renderers. + + In fact, one of these was installed by default when you created this + project. + +.. nextslide:: Configuring a Template Renderer + +.. code-block:: python + + # in setup.py + requires = [ + # ... + 'pyramid_chameleon', + # ... + ] + + # in learning_journal/__init__.py + def main(global_config, **settings): + # ... + config.include('pyramid_chameleon') + +.. rst-class:: build +.. container:: + + The `pyramid_chameleon` package supports using the `chameleon` template + language. + + The language is quite nice and powerful, but not so easy to learn. + + Let's use a different one, *jinja2* + +.. nextslide:: Changing Template Renderers + +Change ``pyramid_chameleon`` to ``pyramid_jinja2`` in both of these files: + +.. code-block:: python + + # in setup.py + requires = [ + # ... + 'pyramid_jinja2', + # ... + ] + + # in learning_journal/__init__.py + def main(global_config, **settings): + # ... + config.include('pyramid_jinja2') + +.. nextslide:: Picking up the Changes + +We've changed the dependencies for our Pyramid project. + +.. rst-class:: build +.. container:: + + As a result, we will need to re-install it so the new dependencies are also + installed: + + .. code-block:: bash + + (ljenv)$ python setup.py develop + ... + Finished processing dependencies for learning-journal==0.0 + (ljenv)$ + + Now, we can use *Jinja2* templates in our project. + + Let's learn a bit about how `Jinja2 templates`_ work. + +.. _Jinja2 templates: http://jinja.pocoo.org/docs/templates/ + +Jinja2 Template Basics +---------------------- + +We'll start with the absolute basics. + +.. rst-class:: build +.. container:: + + Fire up a Python interpreter, using your `ljenv` virtualenv: + + .. code-block:: bash + + (ljenv)$ python + >>> + + Then import the ``Template`` class from the ``jinja2`` package: + + .. code-block:: pycon + + >>> from jinja2 import Template + +.. nextslide:: Templates are Strings + +A template is constructed with a simple string: + +.. code-block:: python + + >>> t1 = Template("Hello {{ name }}, how are you?") + +.. rst-class:: build +.. container:: + + Here, we've simply typed the string directly, but it is more common to + build a template from the contents of a *file*. + + Notice that our string has some odd stuff in it: ``{{ name }}``. + + This is called a placeholder and when the template is *rendered* it is + replaced. + +.. nextslide:: Rendering a Template + +Call the ``render`` method, providing *context*: + +.. code-block:: python + + >>> t1.render(name="Freddy") + u'Hello Freddy, how are you?' + >>> t1.render({'name': "Roberto"}) + u'Hello Roberto, how are you?' + >>> + +.. rst-class:: build +.. container:: + + *Context* can either be keyword arguments, or a dictionary + + Note the resemblance to something you've seen before: + + .. code-block:: python + + >>> "This is {owner}'s string".format(owner="Cris") + 'This is Cris's string' + + +.. nextslide:: Dictionaries in Context + +Dictionaries passed in as part of the *context* can be addressed with *either* +subscript or dotted notation: + +.. code-block:: python + + >>> person = {'first_name': 'Frank', + ... 'last_name': 'Herbert'} + >>> t2 = Template("{{ person.last_name }}, {{ person['first_name'] }}") + >>> t2.render(person=person) + u'Herbert, Frank' + +.. rst-class:: build + +* Jinja2 will try the *correct* way first (attr for dotted, item for + subscript). +* If nothing is found, it will try the opposite. +* If nothing is found, it will return an *undefined* object. + + +.. nextslide:: Objects in Context + +The exact same is true of objects passed in as part of *context*: + +.. rst-class:: build +.. container:: + + .. code-block:: python + + >>> 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* + +.. nextslide:: Filtering values in Templates + +You can apply `filters`_ to the data passed in *context* with the pipe ('|') +operator: + +.. _filters: http://jinja.pocoo.org/docs/dev/templates/#filters + +.. code-block:: python + + t4 = Template("shouted: {{ phrase|upper }}") + >>> t4.render(phrase="this is very important") + u'shouted: THIS IS VERY IMPORTANT' + +.. rst-class:: build +.. container:: + + You can also chain filters together: + + .. code-block:: python + + t5 = Template("confusing: {{ phrase|upper|reverse }}") + >>> t5.render(phrase="howdy doody") + u'confusing: YDOOD YDWOH' + +.. nextslide:: Control Flow + +Logical `control structures`_ are also available: + +.. _control structures: http://jinja.pocoo.org/docs/dev/templates/#list-of-control-structures + +.. rst-class:: build +.. container:: + + .. code-block:: python + + 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 %}) + + Remember, although template tags like ``{% for %}`` or ``{% if %}`` look a + lot like Python, they are not. + + The syntax is specific and must be followed correctly. + +.. nextslide:: Template Tests + +There are a number of specialized *tests* available for use with the +``if...elif...else`` control structure: + +.. code-block:: python + + >>> 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' + + +.. nextslide:: Basic Expressions + +Basic `Python-like expressions`_ are also supported: + +.. _Python-like expressions: http://jinja.pocoo.org/docs/dev/templates/#expressions + +.. code-block:: python + + 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' + + +Our Templates +------------- + +There's more that Jinja2 templates can do, but it will be easier to introduce +you to that in the context of a working template. So let's make some. + +.. nextslide:: Detail Template + +We have a Pyramid view that returns a single entry. Let's create a template to +show it. + +.. rst-class:: build +.. container:: + + In ``learning_journal/templates`` create a new file ``detail.jinja2``: + + .. code-block:: jinja + +

+

{{ entry.title }}

+
+

{{ entry.body }}

+
+

Created {{entry.created}}

+
+ + Then wire it up to the detail view in ``views.py``: + + .. code-block:: python + + # views.py + @view_config(route_name='detail', renderer='templates/detail.jinja2') + def blog_view(request): + # ... + +.. nextslide:: Try It Out + +Now we should be able to see some rendered HTML for our journal entry details. + +.. rst-class:: build +.. container:: + + Start up your server: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 90536. + serving on http://0.0.0.0:6543 + + Then try viewing an individual journal entry + + * http://localhost:6543/journal/1 + +.. nextslide:: Listing Page + +The index page of our journal should show a list of journal entries, let's do +that next. + +.. rst-class:: build +.. container:: + + In ``learning_journal/templates`` create a new file ``list.jinja2``: + + .. code-block:: jinja + + {% if entries %} +

Journal Entries

+
+ {% else %} +

This journal is empty

+ {% endif %} + +.. nextslide:: + +It's worth taking a look at a few specifics of this template. + +.. rst-class:: build +.. container:: + + .. code-block:: jinja + + {{ entry.title }} + + Jinja2 templates are rendered with a *context*. + + The return values of the Pyramid *view* for a template get included in that + context. + + So does *request*, which is placed there by the framework. + + Request has a method ``route_url`` that will create a URL for a named + route. + + This allows you to include URLs in your template without needing to know + exactly what they will be. + + This process is called *reversing*, since it's a bit like a reverse phone + book lookup. + +.. nextslide:: + +Finally, you'll need to connect this new renderer to your listing view: + +.. code-block:: python + + @view_config(route_name='home', renderer='templates/list.jinja2') + def index_page(request): + # ... + +.. nextslide:: Try It Out + +We can now see our list page too. Let's try starting the server: + +.. rst-class:: build +.. container:: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 90536. + serving on http://0.0.0.0:6543 + + Then try viewing the home page of your journal: + + * http://localhost:6543/ + + Click on the link to an entry, it should work. + + Can you add a link back to the homepage on your detail page? + +.. nextslide:: Sharing Structure + +These views are reasonable, if quite plain. + +.. rst-class:: build +.. container:: + + It'd be nice to put them into something that looks a bit more like a + website. + + Jinja2 allows you to combine templates using something called + `template inheritance`_. + + You can create a basic page structure, and then *inherit* that structure in + other templates. + + In our class resources I've added a page template ``layout.jinja2``. Copy + that page to your templates directory + +.. _template inheritance: http://jinja.pocoo.org/docs/dev/templates/#template-inheritance + +.. nextslide:: ``layout.jinja2`` + +.. code-block:: jinja + + + + + + Python Learning Journal + + + +
+ +
+
+

My Python Journal

+
{% block body %}{% endblock %}
+
+

Created in the UW PCE Python Certificate Program

+ + + +.. nextslide:: Template Blocks + +The important part here is the ``{% block body %}{% endblock %}`` expression. + +.. rst-class:: build +.. container:: + + This is a template **block** and it is a kind of placeholder. + + Other templates can inherit from this one, and fill that block with + additional HTML. + + Let's update our detail and list templates: + + .. code-block:: jinja + + {% extends "layout.jinja2" %} + {% block body %} + + {% endblock %} + +.. nextslide:: Try It Out + +Let's try starting the server so we can see the result: + +.. rst-class:: build +.. container:: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 90536. + serving on http://0.0.0.0:6543 + + Then try viewing the home page of your journal: + + * http://localhost:6543/ + + Click on the link to an entry, it should work. + Now you have shared page structure that is in both. Templates ========= From 1fd6adf75a836040ec915e36a073e403802dac4c Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 10 Jan 2015 16:10:12 -0800 Subject: [PATCH 014/173] add a basic html layout skeleton. --- resources/session02/layout.jinja2 | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 resources/session02/layout.jinja2 diff --git a/resources/session02/layout.jinja2 b/resources/session02/layout.jinja2 new file mode 100644 index 00000000..0bc21c6a --- /dev/null +++ b/resources/session02/layout.jinja2 @@ -0,0 +1,28 @@ + + + + + Python Learning Journal + + + +
+ +
+
+

My Python Journal

+
+ {% block body %}{% endblock %} +
+
+
+

Created in the UW PCE Python Certificate Program

+
+ + From 74817ad3d16d77f4bf10e91040dd73ccc99f8aa8 Mon Sep 17 00:00:00 2001 From: cewing Date: Sun, 11 Jan 2015 22:26:51 -0800 Subject: [PATCH 015/173] complete session 2 slides --- source/presentations/session02.rst | 450 +++++++++++++++++++++++++++-- 1 file changed, 419 insertions(+), 31 deletions(-) diff --git a/source/presentations/session02.rst b/source/presentations/session02.rst index f25e479e..3d1c6b8e 100644 --- a/source/presentations/session02.rst +++ b/source/presentations/session02.rst @@ -452,16 +452,16 @@ sample view): def index_page(request): return 'list page' - @view_config(route_name='blog', renderer='string') - def blog_view(request): + @view_config(route_name='detail', renderer='string') + def view(request): return 'detail page' - @view_config(route_name='blog_action', match_param='action=create', renderer='string') - def blog_create(request): + @view_config(route_name='action', match_param='action=create', renderer='string') + def create(request): return 'create page' - @view_config(route_name='blog_action', match_param='action=edit', renderer='string') - def blog_update(request): + @view_config(route_name='action', match_param='action=edit', renderer='string') + def update(request): return 'edit page' .. nextslide:: Testing Our Views @@ -1053,8 +1053,6 @@ We can now see our list page too. Let's try starting the server: Click on the link to an entry, it should work. - Can you add a link back to the homepage on your detail page? - .. nextslide:: Sharing Structure These views are reasonable, if quite plain. @@ -1139,50 +1137,440 @@ Let's try starting the server so we can see the result: Click on the link to an entry, it should work. - Now you have shared page structure that is in both. + And now you have shared page structure that is in both. -Templates -========= +Static Assets +------------- -We want to use Jinja, add jinja 2 as template engine and `python setup.py -develop` to install +Although we have a shared structure, it isn't particularly nice to look at. -quick intro to jinja2 templates. +.. rst-class:: build +.. container:: -create a nice basic html outline, see how it works + Aspects of how a website looks are controlled by CSS (*Cascading Style + Sheets*). -create a template to show a single entry, hook it up to your view/route and -test it by viewing it. + Stylesheets are one of what we generally speak of as *static assets*. -create a template to show a list of entries, hook it up and test by viewing. + Other static assets include *images* that are part of the look and feel of + the site (logos, button images, etc) and the *JavaScript* files that add + client-side dynamic behavior to the site. +.. nextslide:: Static Assets in Pyramid +Serving static assets in Pyramid requires a *static view* to configuration. +Luckily, ``pcreate`` already handled that for us: -Adding New Entries -================== +.. rst-class:: build +.. container:: + + .. code-block:: python + + # in learning_journal/__init__.py + def main(global_config, **settings): + # ... + config.add_static_view('static', 'static', cache_max_age=3600) + # ... + + The first argument to ``add_static_view`` is a *name* that will need to + appear in the path of URLs requesting assets. + + The second argument is a *path* that is relative to the package being + configured. + + Assets referenced by the *name* in a URL will be searched for in the + location defined by the *path* + + Additional keyword arguments control other aspects of how the view works. + +.. nextslide:: Static Assets in Templates + +Once you have a static view configured, you can use assets in that location in +templates. + +.. rst-class:: build +.. container:: + + The *request* object in Pyramid provides a ``static_url`` method that + builds appropriate URLs + + Add the following to our ``layout.jinja2`` template: + + .. code-block:: jinja + + + + + + + The one required argument to ``request.static_url`` is a *path* to an + asset. + + Note that because any package *might* define a static view, we have to + specify which package we want to look in. + + That's why we have ``learning_journal:static/styles.css`` in our call. + +.. nextslide:: Basic Styles + +I've created some very very basic styles for our learning journal. + +.. rst-class:: build +.. container:: + + You can find them in ``resources/session02/styles.css``. Go ahead and copy + that file. + + Add it to ``learning_journal/static``. + + Then restart your web server and see what a difference a little style + makes: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 90536. + serving on http://0.0.0.0:6543 + +.. nextslide:: The Outcome + +Your site should look something like this: + +.. figure:: /_static/learning_journal_styled.png + :align: center + :width: 75% + + The learning journal with basic styles applied + +Getting Interactive +=================== + +.. rst-class:: left +.. container:: + + We have a site that allows us to view a list of journal entries. + + .. rst-class:: build + .. container:: + + We can also view the details of a single entry. + + But as yet, we don't really have any *interaction* in our site yet. + + We can't create new entries. + + Let's add that functionality next. + +User Input +---------- + +In HTML websites, the traditional way of getting input from users is via +`HTML forms`_. + +.. rst-class:: build +.. container:: + + Forms use *input elements* to allow users to enter data, pick from + drop-down lists, or choose items via checkbox or radio button. + + It is possible to create plain HTML forms in templates and use them with + Pyramid. + + It's a lot easier, however, to work with a *form library* to create forms, + render them in templates and interact with data sent by a client. + + We'll be using a form library called `WTForms`_ in our project + +.. _HTML forms: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms +.. _WTForms: http://wtforms.readthedocs.org/en/latest/ + +.. nextslide:: Installing WTForms + +The first step to working with this library is to install it. + +.. rst-class:: build +.. container:: + + Start by makin the library as a *dependency* of our package by adding it to + the *requires* list in ``setup.py``: + + .. code-block:: python + + requires = [ + # ... + 'wtforms', # <- add this to the list + ] + + Then, re-install our package to download and install the new dependency: + + .. code-block:: bash + + (ljenv)$ python setup.py develop + ... + Finished processing dependencies for learning-journal==0.0 + +Using WTForms +------------- + +We'll want a form to allow a user to create a new Journal Entry. + +.. rst-class:: build +.. container:: + + Add a new file called ``forms.py`` in our learning_journal package, next to + ``models.py``: + + .. code-block:: python + + from wtforms import Form, TextField, TextAreaField, validators + + strip_filter = lambda x: x.strip() if x else None + + class EntryCreateForm(Form): + title = TextField( + 'Entry title', + [validators.Length(min=1, max=255)], + filters=[strip_filter]) + body = TextAreaField( + 'Entry body', + [validators.Length(min=1)], + filters=[strip_filter]) + +.. nextslide:: Using a Form in a View + +Next, we need to add a new view that uses this form to create a new entry. + +.. rst-class:: build +.. container:: + + Add this to ``views.py``: + + .. code-block:: python + + # add these imports + from pyramid.exceptions import HTTPFound + from .forms import EntryCreateForm + + # and update this view function + def create(request): + entry = Entry() + form = EntryCreateForm(request.POST) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + DBSession.add(entry) + return HTTPFound(location=request.route_url('home')) + return {'form': form, 'action': request.matchdict.get('action')} + +.. nextslide:: Testing the Route/View Connection + +We already have a route that connects here. Let's test it. + +.. rst-class:: build +.. container:: + + Start your server: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 90536. + serving on http://0.0.0.0:6543 + + And then try connecting to the ``action`` route: + + * http://localhost:6543/journal/create + + You should see something like this:: + + {'action': u'create', 'form': } + +.. nextslide:: Rendering A Form + +Finally, we need to create a template that will render our form. + +.. rst-class:: build +.. container:: + + Add a new template called ``edit.jinja2`` in + ``learning_journal/templates``: + + .. code-block:: jinja + + {% extends "templates/layout.jinja2" %} + {% block body %} +
+ {% for field in form %} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +

{{ field.label }}: {{ field }}

+ {% endfor %} +

+
+ {% endblock %} + +.. nextslide:: Connecting the Renderer + +You'll need to update the view configuration to use this new renderer. + +.. rst-class:: build +.. container:: + + Update the configuration in ``learning_journal/views.py``: + + .. code-block:: python + + @view_config(route_name='action', match_param='action=create', + renderer='templates/edit.jinja2') + def create(request): + # ... + + And then you should be able to start your server and test: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 90536. + serving on http://0.0.0.0:6543 + + * http://localhost:6543/create + +.. nextslide:: Providing Access + +Great! Now you can add new entries to your journal. + +.. rst-class:: build +.. container:: + + But in order to do so, you have to hand-enter the url. + + You should add a new link in the UI somewhere that helps you get there more + easily. + + Add the following to ``list.jinja2``: + + .. code-block:: jinja + + {% extends "layout.jinja2" %} + {% block body %} + {% if entries %} + ... + {% else %} + ... + {% endif %} + +

New Entry

+ {% endblock %} + +Homework +======== + +.. rst-class:: left +.. container:: + + You have a website now that allows you to create, view and list journal + entries + + .. rst-class:: build + .. container:: + + However, there are still a few flaws in this system. + + You should be able to edit a journal entry that already exists, in case + you make a spelling error. + + It would also be nice to see a prettier site. + + Let's handle that for homework this week. + +Part 1: Add Editing +------------------- + +For part one of your assignment, add editing of existing entries. You will need: + +* A form that shows an existing entry (what is different about this form from + one for creating a new entry?) +* A pyramid view that handles that form. It should: + + * Show the form with the requested entry when the page is first loaded + * Accept edits only on POST + * Update an existing entry with new data from the form + * Show the view of the entry after editing so that the user can see the edits + saved correctly + * Show errors from form validation, if any are present + +* A link somewhere that leads to the editing page for a single entry (probably + on the view page for a entry) + +You'll need to update a bit of configuration, but not much. Use the create +form we did here in class as an example. + +Part 2: Make it Yours +--------------------- + +I've created for you a very bare-bones layout and stylesheet. + +You will certainly want to add a bit of your own style and panache. + +Spend a few hours this week playing with the styles and getting a site that +looks more like you want it to look. + +The Mozilla Developer Network has `some excellent resources`_ for learning CSS. + +In particular, the `Getting Started with CSS`_ tutorial is a thorough +introduction to the basics. + +You might also look at their `CSS 3 Demos`_ to help fire up your creative +juices. + +Here are a few more resources: + +* `A List Apart `_ offers outstanding articles. Their + `Topics list `_ is worth a browse. +* `Smashing Magazine `_ is another excellent + resource for articles on design. + +.. _some excellent resources: https://developer.mozilla.org/en-US/docs/Web/CSS +.. _Getting Started with CSS: https://developer.mozilla.org/en-US/docs/CSS/Getting_Started +.. _CSS 3 Demos: https://developer.mozilla.org/en-US/demos/tag/tech:css3 -Add route, and view for creating new entry. -Discuss forms. +Part 3: User Model +------------------ -Create form for creating a new entry +As it stands, our journal accepts entries from anyone who comes by. -use form in template. +Next week we will add security to allow only logged-in users to create and edit +entries. +To do so, we'll need a user model -homework --------- +The model should have: -What's the difference between creating new and editing existing? +* An ``id`` field that is a primary key +* A ``username`` field that is unicode, no more than 255 characters, not + nullable, unique and indexed. +* A ``password`` field that is unicode and not nullable -add route and view for editing +In addition, the model should have a classmethod that retrieves a specific user +when given a username. -create form for editing (subclass) +Part 4: Preparation for Deployment +---------------------------------- -use form in template +At the end of class next week we will be deploying our application to Heroku. +You will need to get a free account. -homework +Once you have your free account set up and you have logged in, run through the +`getting started with Python`_ tutorial. +Be sure to at least complete the *set up* step. It will have you install the +Heroku Toolbelt, which you will need to have ready in class. +.. _getting started with Python: https://devcenter.heroku.com/articles/getting-started-with-python#introduction From 2a49396ec9cabb7be5da2cd9f154071a05adcaec Mon Sep 17 00:00:00 2001 From: cewing Date: Sun, 11 Jan 2015 22:27:14 -0800 Subject: [PATCH 016/173] add readings for session 2 --- source/readings.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/source/readings.rst b/source/readings.rst index d3b8b243..8bc1f971 100644 --- a/source/readings.rst +++ b/source/readings.rst @@ -60,10 +60,25 @@ In Pyramid, the code that handles requests is called `a view`_. A view passes data to `a renderer`_, which is responsible for turning the data into a response to send back. +Getting information from a client to the server is generally handled by +`HTML forms`_. Working with forms in a framework like Pyramid can be +facilitated by using a *form library* like `WTForms`_. .. _URL Dispatch: http://docs.pylonsproject.org/docs/pyramid/en/latest/narr/urldispatch.html .. _a view: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/views.html .. _a renderer: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html +.. _HTML forms: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms +.. _WTForms: http://wtforms.readthedocs.org/en/latest/ + +For layout and design, CSS will be your tool of choice. There is no better tool +for learning CSS than trying things out, but you need a good reference to get +started. You can learn a great deal from the `Mozilla Developer Network`_ CSS +pages. I also find `A List Apart`_ and `Smashing Magazine`_ to be fantastic +resources. + +.. _Smashing Magazine: http://www.smashingmagazine.com +.. _A List Apart: http://alistapart.com +.. _Mozilla Developer Network: https://developer.mozilla.org/en-US/docs/Web/CSS Sesstion 3 - Pyramid Authentication and Deployment From edc2800981adf179a40c1ba2d6e4ea428b6203de Mon Sep 17 00:00:00 2001 From: cewing Date: Sun, 11 Jan 2015 22:27:47 -0800 Subject: [PATCH 017/173] add resources needed for class session 2 --- resources/session02/forms.py | 21 +++++++++ resources/session02/layout.jinja2 | 1 + resources/session02/styles.css | 73 +++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 resources/session02/forms.py create mode 100644 resources/session02/styles.css diff --git a/resources/session02/forms.py b/resources/session02/forms.py new file mode 100644 index 00000000..5629d7a9 --- /dev/null +++ b/resources/session02/forms.py @@ -0,0 +1,21 @@ +from wtforms import ( + Form, + TextField, + TextAreaField, + validators, +) + +strip_filter = lambda x: x.strip() if x else None + + +class BlogCreateForm(Form): + title = TextField( + 'Entry title', + [validators.Length(min=1, max=255)], + filters=[strip_filter] + ) + body = TextAreaField( + 'Entry body', + [validators.Length(min=1)], + filters=[strip_filter] + ) diff --git a/resources/session02/layout.jinja2 b/resources/session02/layout.jinja2 index 0bc21c6a..93e462c0 100644 --- a/resources/session02/layout.jinja2 +++ b/resources/session02/layout.jinja2 @@ -6,6 +6,7 @@ +
diff --git a/resources/session02/styles.css b/resources/session02/styles.css new file mode 100644 index 00000000..951ac84f --- /dev/null +++ b/resources/session02/styles.css @@ -0,0 +1,73 @@ +body{ + color:#111; + padding:0; + margin:0; + background-color: #eee;} +header{ + margin:0; + padding:0 0.75em; + width:100%; + background: #222; + color: #ccc; + border-bottom: 3px solid #fff;} +header:after{ + content:""; + display:table; + clear:both;} +header a, +footer a{ + text-decoration:none} +header a:hover, +footer a:hover { + color:#fff; +} +header a:visited, +footer a:visited { + color:#eee; +} +header aside{ + float:right; + text-align:right; + padding-right:0.75em} +header ul{ + list-style:none; + list-style-type:none; + display:inline-block} +header ul li{ + margin:0 0.25em 0 0} +header ul li a{ + padding:0; + display:inline-block} +main{padding:0 0.75em 1em} +main:after{ + content:""; + display:table; + clear:both} +main article{ + margin-bottom:1em; + padding-left:0.5em} +main article h3{margin-top:0} +main article .entry_body{ + margin:0.5em} +main aside{float:right} +main aside .field{ + margin-bottom:1em} +main aside .field input, +main aside .field label, +main aside .field textarea{ + vertical-align:top} +main aside .field label{ + display:inline-block; + width:15%; + padding-top:2px} +main aside .field input, +main aside .field textarea{ + width:83%} +main aside .control_row input{ + margin-left:16%} +footer{ + padding: 1em 0.75em; + background: #222; + color: #ccc; + border-top: 3px solid #fff; + border-bottom: 3px solid #fff;} From bbbd3bafdfefc79bb22265c2b51c33c74a77bac8 Mon Sep 17 00:00:00 2001 From: cewing Date: Sun, 11 Jan 2015 22:28:11 -0800 Subject: [PATCH 018/173] add image of styled learning journal for reference --- source/_static/learning_journal_styled.png | Bin 0 -> 60697 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 source/_static/learning_journal_styled.png diff --git a/source/_static/learning_journal_styled.png b/source/_static/learning_journal_styled.png new file mode 100644 index 0000000000000000000000000000000000000000..1bd091be7d05d98d90efd8421000c6c5c537b343 GIT binary patch literal 60697 zcmagEcRbtg_cv~}s)OpXMXTzql-fI}mfA|~Rbmr6#E4NXRjX>mR#jAqEg?qKO6@I( zQDVp5M7Vv%{k`u$e&c#P@_4+ilXK2>&h98ms<*DQUZq<__%DQUG%3Qi$?)FZub`%t^Z)K(Uz15$15ZT`-**I|L z5f{%sYXBYBknHRGG;gBRLUpMkK0NV#c}3+OBcGw|+h$7D$VZ@8_ilv0x53fv-q!8- z{pRJX#pxIqx-EqG-b^|fYce~rjKWPgY{7773f;5!G%Y^Hl%Zm|#`iXfzs>L5?QT1w zV!lmDZ%o-dXKzoVdi;`VtW#%e|={v{K-Ntt4SYAgIIP< zz3xmp&s<&%O|E$=&tHeUAX>Y|w`%DWi%0(q47(xu?9MVB|?o4 zC=MT+W^hzBB^yvwgRd$s&XwqFaZ&7Ep{Sn8+~bUJwfx#Pf0Oq{^!mE1H)W2CApeRa zDd)X=7bZgbD6M;wWGGv}q!7lfc3;%))nY+mfyT?8o}2XWep~X zGtOH-!Ky7}4yu@by1U4UUL}GVgeifb->3zzy@yZgZMSkMx%gZ^qkt@q8Cx$(#M) z`kU&`7h0ch*TupFpHm6EyTu+V>OnojMe!l@^=;}DYQfi3PKq>@O2F~U5ELG#^bL3R zpcn}Y7@*O9FX3_3oW`j6mdA~op{k14BYtZ^7U} zHGgF#U8#H2?aEPKZhuug8F=!O`DysCTxDJPeQqCC^fim{({Ho`Tw+(rl&}wssZ_(( zx5g=up%dk@?AKIVy8pg}-p{0EYR1p0*FFH!qgt5e*tRJIK3cUTUQ_&Yje1bU_!`^J z%NJjaA5$_ubV+#hgR1CN`WN~iY<01(6Fe8FuSVutr!qh2U?D`B#LU?kT94VNKOap6 zr5b87ElJql77aUjpKmi-QDDqlqTov>!I%;Itj+LU{5!L9cm?&i^|~xOJ+_%;p4}t3 zmf2jyO}vx_#ej>c4<)wVntQSXZFo9!Z|gn7Z|`lJpodM@L)b$&XX61K7~U^*rZg_^ z86Ku^J$8Nk_BQLa{k$=+oFtJm{R@IByMA1^G^l=WC6%WH#1>K~7p4`(>XMgo#LR(tBPSIb{ zUpZgz)=#$)<6o)hEK{AvmoRmhkZPhRd=F0OL?jj`xFi$dfjl}%IJh6&N-&4l>S2ib zdBIsN)tldnzxBiN$_;Z>=BiudKdQY-|DJ5~$tK+<$|ihXy!{PN!`HB{s5?4EimT;A zJoyhwdMJ{ zV^KYp{Fua;`k44Vv8B3xLO))j%*pbifiL;wsoT?_v2Yf-R*sGes6BN)XkC+EOJq%FZDM6%HDHB{ zb%?X1yQO!gFQzYxVXGZYZ`9D3E}AaaSUyiq+w6I3&r=mGor@^lD10^u{4rfDj{yCw zGIq$*uIjsM@m0`FQVC^_()W9zqakE0WL5TND;M}C2a)pwqlz&L>1rO}ntT_nl=1s# z?&n;Lb+&ch9Ekf@Lh<0!Jkvbc4k-}Q^YadnVD_XuDogFFT5PA5{`knu+jr>x>i(N9 zOO8*(ipA@(-^-^=OiEP7zDc*r{RztR&p6?~As=lVo%LY)ftA39fc`V00KkOL+P<9_0xfSKZ^f`$;cS*kA79_CzPuYZnYNimo-un3dd}6d zdnE|k3dUGLJ#t43@W}*I;%Sc-E_T_s4cs<8Eh)M z?S9~6U$3I0p=pgs!utGm!lt08c}ms#q*u*iQFnjo7pvbUo=-X1035ToFQV?Y6fJ6& z?pxLQEz;l;>mx~Dmpk~EV|Of$?amTt;2$bK48DZ7Sww-G=BXd=dgxxlX{w!2ljW9`^x-9FEZy!rN~yy=}|)z(tSi@FzY z<{F>%nbEtpyWV%*n6|G%IOE}uEC&y4y5~R7>+no{CC-|_j83L|4nJ%(<{##h{Qml0 z^P8GpMeiA>*_c>P43z%0KrHs`e&0pa8?84PTAO*kV{nSMjjtB@S=p#w=Bzqexb|Z$ zj5R)esc|h3wUIT6#p5uu;E-X;Sn76KAm=^yhLAr;x-JMMaJ%)?TuEy`@%tJY;}BvM z@^a(#PC~!xN7a06OFU#$b_6H+Q!+`SCH8Aa1;G>RPX2qK9C){QRaZ2x?wCgEq4dr? z_8730eN%42|1<2@Dla+b5IJD_*)(@-YAg!*L&7OgGjMh_bU2HnkK>s_#zoM6%wbx3 zRhNTCe?s5=%na!qzu?^$9Uph1qnILPjC_FT>V=`K((=-o?l5?nAj zubyrSHU(Z-PRXyrX2^rIMGTe-Jpoc=M>1%g@SXR%&eQB)3JCJ(G0wiIaew_A?p=S? zCx`fmT4kY&3;)&7{1nCXCu{r2td82B*ZBZluBd6Ya@oD=Lv zYILyaY}y*FWasx4Es546%>A+DQ;3Y3?t`~K>OH))pea?o#dQY z1-~G=vt8&Du>I2P(T6>fh>S__<(ZPs5x>B{v0y98~7=5p3sU%>@O!x_Qe$obAagl#y ztfkAP?Cxd9B`zo?`1}#@Iu{q0oR_V=jNVI?|B7Gs57y2LFm!fk2qRQwxy8F0adgbK=QgidM z^YZYz^zdJ!0Y&8gE%*Py@qcXik1icAC%a28{w*#1U+Mpw_FsNEp?^8@e>wJ_S^GEj za@v5`<%Is{6#%b`!2DS$C=@BwUMjtXQm$vwHe?wjAL5>^#@^=oV3|+%b7a8QmHUCB zF8gm0o8J+Du{S>Ks+TP*N9OQ-*R{`7+|yT|z7qIGlknh@Upc_b8-H|$3(U^Qf_r;! zek`VMo#?C!+^R9cyI>ZjiO&iN9c=FG>nkgtoQ4b9_g9BOEg$H1P*20d!^4C|?xBh@ zk_UGshu;i!m|7JDKHeCd1Uw7j2VC~}0}poo-J7;-R2Vy5J2g8{bqT3H1A#HgO@ndq zzQDXi_r-o=qKjP_5NJxuTP(vDA7M?cEJBLU9XH}rLP|DJLz0s_gMALwXNWTv7Z_rF z6#H)-kidg;V`4GUeH4Ho5AHHJUyr@dxJP68yP(sYT~UcX51 zvDu)2OJMIlYDhSR00~(`V<$1_#)DN!_vyllb^KLG)liQ@w-~|wRaMD``0%Vc);JDF zE+TTeT&x|A70xz5aylyA{~wHthp=Z98zMfFwlE4E@c_UyMT`Bn+timWttQXSP}e@& zo`7)deL{~Au){0};puJpRO=Ut;s;gx|ws< zgi7f;?!;^Z1352M#xOgp&hm=fU>1JD3O4O+e1T>~2To#mvl1?a5L` zp7N^l$$*?L;vNobrqz;I)2KJIVJI^uX^FF1uDRvhMVKoi{=_?vT}B*fShL`Mugu?K zVp(@OYE#y$SDpX-6e`NWgV?m<+w$#HnDn&>v6zwg$rm4qDa~^$Ho>@+ZR&`o8Hy1@ z%SthVE;Hz@KjQp|VATQlqRbjH{eXD5b}p9a^s0*f zc|~MqTw5yYWa1oE04P|Kpflnxwm>Pl)|`OpYZD2Xh1b9AT7DZ9E5O3Ecp^EUjB!K=&cx&G$n!0A>0Gb z@}{w6#R!Od$w3Wre972~#uTAiymPSXCLu9)nm)mNYvTxW0rM%h4_;kq0p}xa*QGWAVVwN_ zs!!dU=GOEIH=51d&6@h$(Jt}Be)t;yu1U~{9bS$H;TrEV6?A|ujRxi%o*K$6uU zk%e}J6FAx2*e&;J!6$~0qN>t7_Nwf!OGxZbjdXme1`s8$Vu8duPX(+&rc*W)Ob#!a znwnOOD{R`!S(K^tzMG(mE*>ma9D~xZ^JR8Le7#HV*@maj@Sojbgz>fY^`5);g2q-4 zJaQ@$POofvw9EYD8*>kA?Ih=~#J2;w3e$qrs$A9Bz-(q42kvDG6d&ioUHCjwOsr4+2ftbyj*)vrRO8&j zx!yi@KHiQqS|`O~_N;eU9RtGs+mut6^$WOFu)j~hP%r!LRmF{{H*C_}X2`*c%PG%u zJ1pwXn3Y%~?03&|bfCM1C&!H9G7RI)+-gtEW-2!&hpjCjoJZYg!TwYyx4^hDvoTK5 zGP_=o3}oUE+Q{j4Zpq-2v~d~iO+y8+e~9RY6L22za^dyOR`SF#y)px+)|Z!7685Mco`h2i_9)#oPCx>(T5&;M*-goUAgrq^Q<1kPXsqc;NN(8Rr(}?@Sez2 z2{_|mCmn6Qymg?cGvSus-!vZFSo$A7AtBu-X2=EMUSHy=oLkyu6wz_bH%A`p2FkC% zHv;51RB>@akSZOEnKR@Y0Ll`#aZuLuT*~bLnznhlk8xmonAh4PMj2J#Ze6oi43oU( z=!QG#nf=9y0P3^_5D(2_&H5msBv;LE+xG1sB&ZrVd4hMxs|tw?M-Dp#<5BTTk#0H6 zF8O6Zz#i@ZZH00V#!Jq=CJwH-NVpT5?TdHNRw*^?dmfr=4g2!;!PtQ6J-;c}%P{8d zR+QSbWWx^bxQn|<O?6~RKaJG6H8yQEhW>nBZI-JU#Qm~ zAWw*=8bA^iqR*YQgmo+G_Q5R`E`bz^_&D9mLTcpi!Vgc&rnMV5>JATIXcf8V3^afT zOW`shby_J65b})M93RRLWdWDz^0ml0$sIm~29k01f7ufQ*t?;18_*WK<%Nh15t-3m z9K$hXH$^;~u|y;r;}+%ZEylu1j=0Mb4#@d&7wBit`((EoxCqNew`{ zinqnCF}*yJ4&Xz2G3)E7f4L+R9Ktw*kLQ9Cp&=n5?dIDVx^NT%orP_2pH2ZAW<;jz zp)R1hjfCZ_s0v!)k6E= z!jU&|W)YJCCG{)__B^l!lCTp1o`wX+^{WD4KIZ<5$tQU{i~Ve~-u?2m<$dfWk*sVp z7fybG)XP|Oj8dCk1N|EF}RQ#-$IZT6jN z$W8j^?g{UB9}l%N*D&uFIZkM^lAoH5_P;3R9~S<3HQ<`d(BI_kZ&zi0IDD)6ZB`LY z3+h@}Et4BmSp5-RzQ&HYI%*USseCE%eC%)GgwU-lXdg!1sw*l#{hh3e&Ijg${lhjg z!xTURE*RS@Icu&_*xt)ivbHXE4$avHz6Dgd*14N3Mb%ye0<#O3Hzs}h2lsq)JeMqX z>$S8hG$$mdL7m2De5=5e7oyV=8fGzdN#NnN8ZzH%O4ZD1s@*y*A;qaIU zt2Wt_U2yDjlPixvRf%ZFflOr2TieHWdpn*H(F--Pr>RG;NXwpUdIjqlZh&Ua!1W5% zDPKI;`rS1JmbQdag`hOyYA@NZmSX4QToyFuWtNRHoY22vXD9bWZ13GBMzFatXSh~& z$|zLfT)*dA+8zMgq1(S{MTZU7GQ?dBT^n{fggxim9=;?tbE#GcI@9`9pRUOu;FF-S z4XGr#0Z#vggnjS6C2yN@l+aZY>ARK7b>=nNKa7~Tz7;0twYOGpt)C4%0C7|lgm9r{ zu3TGnX~@e%g_X65e%aRKqAzA0Q!rG{9PEss{u=`vwb8|3^wH&fSsax`80irq07dv9 z&+6~ew*wUg3a9lnH2SZuOkG`I3y`_8GDN@rGlKK$aK(*nR%#16fx)!8H>)e_qu+^I{YA5Lo?U=e$(E5ngDv0?{%*eyI|;@5B%fCOu7Pl&_xy{RhIBwPX1+Z)|j$I;RO9|ka~y)Ip1~K zFsmP(KDYq@32x(sNCc^7P#{l8BbbvYQ8uRc9{1`2lWey)Nsxpk{aFK0QG zDpn!oC>5e#pIT@sYFU`P_rJ}Rzf?PiI@e1P+Q5*ex9)@|6WsLSU|YU{b%2p{WT-W4 zpv7K$wIioJnhD$T@kY4IAjR)z$ni(29-jf$SGf{|@$bTKsaL2<+p=vl6mB(toBVkRqI{U-H1?gN!d{76{@h9X;vDmgBzL<%1xP(IgFx(s=^mN zUgig;Fp1Gpnl#sUUpHyXDL2tqZu4-H251MOI6L+3Q26?4-1)-muzfen)`{gxiLVdO zGoEde^TRx42^l*!MLL!B^%R9i3|~3!CHSva_}w~GyXzIIx0?R|Af{`5xyoahE9_6N zGkP!e#IcoPgpUBohxEqxw%872XjP>)$%8UM&jlf=STX0ldWqhK0VlJZ)l!XfJ=*=s zo05h@W;NZOKEv)ys!k|&)J<`p1&u5ou$1(%2}&={m(Ogia6@mc`X*@1pzN7ZVZBwH z84BBA*$dUolfbwPtHY`h{XRIJkRs{;lL$8cRm9|?6na&1FR$9{a@S>*+0F0!Dv`;4 zd88is|u05vesbGF3;{%dJM$Uv;DrmIj5wXd5KBYlP$@| z(JMQZmb0gRW_;o?>rxUcS%1X)5~8TvtX)ylzTG`s;Omm|BEiF2lI`Lg~C9F zhMDJ!%%5*hDC{;Sg#%jJU(>sAa4)&a@QyE?c(peTmL!o3syv6g$Wh36rFApGg8IYr z6;>chqyB(Aar)P~hg4>FnW)cx5pv3x1C1%;^M53R``h=b75=34i;Fb{wcG%-?~dGq zMi$S_fjB2uL~osXpgHdD>EYv?)P9nccrQzA&oD+#k`mYww_U5XfwYs; z*0RnNb}fm=Y_e7IAd*m)9o;roYlQ5?Wv3AJCOu^1xlE1(k{a6%k+&~Um>A0ef6(|7 z`l7U9^K$ulPD5}!f?hE{qX-@(g$lMV=9X)@oVrxR7G@SXN82e6ibaYS*JZv5ON5-C z^>Dh?Zy+B4hU8!Bg2YZY8;@obbNn9f|4npRZ>0wRI0sktO01$FwaA&bmLx)jh`sF_ zzey)vPYIg@+?OyAl}5|!G$op=RAX(!zwM@!-sL`p)owiho$DA?NmKgg4prajl@au7 z+?TAowFHs<&@aY?icf46zi+4c6ekC*QMY=@?|k$0Na1WYgHx~+sYi)E`tyWya62WC z(z|!?nFo$f2H~Rt+Q0MZbE)n3j)OuSUVg36H;QxLmd4dmJalK@+R9&5ib)or(7$F3 zns>*p(6MkMpqg0{@|ze)a8IhR@~mI=v#OrSnQ`7@uMT|=b~a9zou*OEY|u?qie|dl z>KjrBY*yf4py1e6X%@ValJCnF&X5U&bunk}H*uf@|{xBN&|{IVEdJaVW_H$Wr1jgU%<6bc{b&;-T_|U6L{Y~7?5sN5(MzR zA9)VXsKI!v_myyl-gwSiCwS6!U_mZZzlQy3lyd8>MT&(Y*bF%I7^++`V~JQ?8$?1Aa7h*;cxZEh8# zn3%41I{R=SfyFqcP;kNt`-y_A1riGmIvSAF4^2PvJfuBTawBoM^y}*fC4_2AWZ)lq z)&C|bS%AYBU~o=)xS(X~*lO}U%8enN{0p_J?}~+}X+(77 z=t=&l+(Nqe_`4OD6jZk|!}gNE#~e2%7spSJ5wmBTc!0BGff>q$SfG#@GG=jE0Vq`y zwaA|t;>90jZp(l5Xhfzf=0;HMAYywFd!g;Qy?sESC5kwAb3KR`j6%K`U(T#&o)ae z5cG`5^`V3IPqSEKDb+~tWHWT>6?Idq-4t4kfbXrh%IH@ExUvktCN zlw5{o_r5t#Ow!ku=IJ{fH1XVjqZ+Ly(>6uPOhuEcTG3TiAV{L3t@APCcv5`#8RN=5 z3U)4nsu@jXs@+3hHE4J$hhk% zybUkhWpJL+{;5pH+rB?4aI;?Zh`rCmL19>A87TTK{7#xHj1Zb+Wjp@vxbh1ZgNS6I zy>;tCcf^<>p51!+SJEuXENx!i@Bexm`k4V{-5uEo24LYBO}d#EtFf;7Fx?< zvzy0V2acbt-i5x6<0cB@@-r7RCf>#96y+u5YgNqX!55h&8%qXF zwq)=2(B(+fK=QEdUNOm6WCAp%-RdRcUAl3kf!w+g{7PP`8=2=B3u}m+Td?!+JEyUT zC1K}oiM&z5+pd5fNKv;;?^culx`U_*^dHWul4U*Iswc7J2v4*EyB{2n5?=QZ#mXo_ z_u};fd&CKL>%IMmK)8jk$$AaW(sR_n=wG)Ad*U{dGf>caEg??X$V0=#wgl(nI)r2> z0I8yy=;XwF1#mZYnY}CY=&6_~KYsrLgiP4qThW=KvdrbjtmY3)L9;L3cR=jEb67%TtkxG<4l5ga=5J>n{7 zI#Xe`C#2*K;G)^WU)m^m_g)q9YF1FP(g;hMz+>JJ>9X$e%+f0~`9jM-q>276Z?FGQ z!%T7n{JDZbrvbc~Y67-@bTLDGR(GWX?v#`&sElh~dHG<`i%W%M2(OEAI3aLkipQoW!`wK2ixcU5?U!vM6b=MZ3Nm_eMm|(zh|`ywc#{B5Oq|S8@>iT|4b#jiowNnN_HVsbytH z>CxZ_qDjYc|D=2)xm|3n5=uYN1DZY{17yUyo3|u(m&)+Z0dh-`hQXSCu5 zwfo&@a?x)+`7~JC-`sb@hX8BJsN_I$R>iIXH;xyoQlzwxfzaVC=SlB-tL|8M}O@q7# z+$*f;%cN$rCo=Cm7SnN}zIs(OB|T&0hgN%dcNB-~REbglePO?j>RMMPpb?@ce?hF! z{MzKW1D^^r_qrfy*a)W+7@hL_yO(^tDG4sLHPSvGpg3+T|$h^U}pG_Re z!jFpmvgn`NT61t`Kd^#g!9kdWy!e^8PmD+K%Bxk9Sy}M73HJu~N(L_{ZY5dD$DijN z>TSrPKh^U4kv9xn)P$-!@ALc66bIUg3)Txj9&awqcf82QZ&5b#ST0TD(Ut` z^BUmG z7sQM8o+mfB>Ep2ax#x-~El0NuH|h0pUg!S%GIIA&-JZuQdliCECo|Q~c>0jF0K{y- znm8e8Ze7@LEuzfwTXKK^$gH=cq$)H1Aa&MPJ;RL~OUqVZ!YRFyqz4D5+h3?#qKb%w zBHfU0;Z+ooQ>Cq=cU<>g1c@VS)aQtmp2$0b6^a2alQL;;NIF%7_bZ%DWFli!LS*xDzv z40gQFoT0n+^)t&Q?> z(9`6?^DoO#1nx#_U<7m&LK(!kwp)mhTJxS_&ykX5gq)hrD_*NqU{p)73kBOHoB+NS z9bWsjqcKM2&)@p70{u_1t~$lci{0gQLT_XN0g9g{O`pmS@$%lk4r63&^ls57$igi- z>UC)PemYtvYrT_tB~yLX^nHQw9rVx8#}s$;=br=#*9t&lVKK%JP3G8M7Bg#QW)kxU z7TLQt9L0nmQ8)DYj(lP?X<*yG8n)LbD;W$~U3{rV2pKYMA9bNZYNzG|FG zx*gpnQM>`3Z=>^vI6ZL( zI-?RzLRR$6%@o9lK_Uo+D5=dk`@Y9>(3KBD3vpLGb8kC@Jnfof9^%bDIM88z0kJcD zhjyWX`qx--#=Zaw_Ob5j(#mZ=3YP<*icWM@^4yV|guHP8PHhC>)3RrLrW`40IC%(1 zk_GVt#r>hx%xPawk#+MkwTXZ4;#K-Mh<$t5=t z>6w7jvrH>jlt`%*DNopL@}6CLT=X7OX%Z&1bJVkU6)-fcJ)9hyd8hxRa2WLaX}c1I z-#LTM=eroMH<^1|s)bY41R#?nVu1XCirw7)iT7~+Mkte$euw9Grk0f<$581Ti?S6=kVm7KJ+l#g9lnSnb&P}#r zP_V_IUbAz~vl`yQZ~M|_>&fwDK>>dKKhm;pHKTJJ_}dEEz9gusK3Eb`)gEy}Qi zQQ*^jdi=Wvf&9$X%d!h~IC+jC zCD5cYnYtq<*t29iQ+L1ye_vvU1Z(yTM5&m=<-X+JWRo0rcu+Ct{?zqs?uTJ8S0RIK zz#oCtshd3uPV=j*W+krcyRt&DI_xJ_jAg*5mS;zaguB>m4h@OP~)%~7yBL1HImRgR^z+hDtIUBs6n7F`!KF{vB0W5iV4Qmhqm@iJ{giSX{NMe z+iQ4sakUPiD5hl=chu1in3A6XuHLP1ocMe^ZRYmL0YlDQ;c*dn>xORN+y`Mc{PRo~ zO>#3Qr8#Y0A_!`Y`IfClS)kAIjrk6<3)yTL19;IE=AW6skp$tBUR+zRQSh_iU3b7I z1)usFQmd^6OCZ~=$jSjKRA4S*5JIB(dOtTg?9gQ1rmsquJFtIa?eEB z)`pKe_~E$d5ZFfiJY)OMGSD7$gKrPiK4iBOts@YfI78N_G(>Wr4M>dI%C6np?cPxD z2y3fj+AjavfW7GXTRH7aF}sFm1bR!SMn=$&hg?W4d zEY?75zT4QIWixC(t}H@S{ip+5q*TS(LyEA# zWUrNdx!hqSXNS7~X^fKSbj_l=MT(?O5!|<^A5~DE|gh%9q4s4IDw(>(aBZ&?@KJS zU#%6$t2$N48NFFYTn?6H=poIUm*DmT;SSHZ435rg+w?7FZgXvx97!@J1lnF9U8x>fP%`DAhp535Ie8WG3!Va^RDU_0Mpi5&n%nRB{3G zBez)#*9LgD;rpW9F&no}D&%~p#BBy?dUij2-Yz}^!4*R|dabME_|ICx#8-z{S4%MC*<2+`q>-kT4FEq~1T7^Ex z(omvbDAYk6+SvLnZ@a^mN>9`+`x2FTebd1C^t~odv*7(o z=#t@ZwY~m)&Be|Rv|q`=-W-|!Y2$jzj*Yj+^AKaqWj6O{W{+_EN8@l1`SgWOc zp~Xck^l)8iTCltd%Dar#POVa1ubuyVe9p(zCVwLtHlu4L_T%vuQ?kyBPw|s+6EY_-i0MGj#b zfQh6^i4-=xspq*+7DjB#-x^?d5tbulIYnxLh1RLBj35m+T84O?1+CIsj7kjTMk@^) zG9+=pO6hb&;pn-$Ww#+oO#7*= zc;o)b2=<#btqE>P&7NGr4s+F0mv$`B$1^xH9ruDv8znC5v$JB(D3(HSV4P%tR^F}n z8zN(7YrC}&?yY0^nH?JoF=7)5#slKtn9Ydcy}>eQ@N9raV0EvH30^!0YhiYY=s1B+ zTbO01+elBYd{6Qk&@4m(Mg%V}{i_4OoH!ng)5ztw8h^;{zMrnb#>r$`f*M)$B&y8p z2%U6Gh>YEu2IG+moO#FwC*<;^j}N}-Amz~U61RYK0mj!OIrsGUcF^@mIq`O{>cLlG zhr4i$oXfD6Vo0i_k?;=o%H<^n);t3%enxlGT9FU_o@IeRr9XW;G(Pb9B`2Dz8@7cbG?_WaUzEq<3cg_(z z?JO0lYv)4nsry@#4ruLhQ%TB_-?2*_PA$@a(~hE@=35txLMQVL<1hA%Ib(5G5Q7oo z*?}B94Z{V(X{)O$XmKiPkw^0UHXkh+Cn|_ic2VQ+^1T%&+QaMy`Q^>~@LtGZ;B!1| zkUM0-!e0IUi}o8_-^6>0qO5q`K1y0e<^_RDQqq<=f}!kze|wCjJ>E8~wX$J-dN(45 zEm=rS5U!>Z0zO@ggYmEb!Ho54YX7VcM}y$MtpphwotL+oX63Kp?0K9?d{*|FLVS#6u-;zAJ$#- zxYm4~-C-(f_Hgp!^MaEjM3b3Fn?}`GPOrr6)DhnV;rVDK?8AzKdw&ZpuM272qu}%$ zCxOX3HPPqd=fU0h`5d9G(M2#q*^(13{cP$TO!m%g&JU#<+&MRmVxY~uR;%10$7-!g z6H6S)2OFPon0<|uKt2-$|NdII!ujf?)NwQQtCg7UU<9q@{;mCHW%^|8Y<(=#YD!sL z!p&6{-Z0;!fBL5Gv@r2BiQVx#Rf%Z8*|;T1hNswtyQ6y zJg)tbyGdrh>e2_;^xK4J1bAQKey@*3#Vqf z#rGsOJ)=jDE^;+GXDCYAs4f9rzAB-taD_V5rxFUlq)N#_>=k*4gZlJJHE`(rSfuvW z*#UK=q_%TNDi_$e1ZtISvQt->>M;6D{0f&~sO_2Zu!krve(O-vel)dg5O#}W zeD_BQ?(i|x+JSlrsB- zw6M1*4eUud0^b(`KAbPapt%dS_G88gt2)sLd!58Bjd@9{<2P+)mP|tU5Ug?IcEA&! z=Es=rJ>>PrK3`$?DY%ig4XJ)jvy4m>D)VqNV%xRn*`FSUDPQk+&<`yY%)^KRceA=d zGsy~{@))F~ZMNu!oIG+WQWIukvvQJqZ z-EL(*Rv~s%$XbO>^uC7Y-^k5qiziYltgXR}+{k1+3+U-^4r8D^!AaN;L+z6@2e3KU3NV$f`rT{uc(ucqh9awGuw;sNTr!oH^b7D} z)uhg}$uZ&A>O$r(LC)G@rOqX4-Y;HezZ|z6Uq6nmipu{K^p&II>i)=~&vaEMTzu=I zd;$2FC1^KT75~w9LNaTT)JT*mThw$P69T7Srdi+PZvmIAdx8o^4_c{jN$;6Px3{pqG%fm46PBz_tV8lZ24fs)O8U2~g4?`^6lJ^Q=H?hkZ(r)H!er$|#-V z_n($%G{ICmT_#x3K#AkjNa4LSLmUHx*g}sBN9kOyF)bLHae%=3{zWjw=jM&*BNLR2 zE+Ku>OYGh@1`kf#x?K!An1S7Cn@!jTw|zvbxR^Shz7_w`rHIdc#N?`gYyM_^N9f0* z=_N(Y!&n9|keNWR;qZOm6z!ioh@>Zm_oD?EdS%QrBCOoZ9Ex!09s=vkZaBbhHs}v^ zaOdmYXolU#O3Y=jIUsp$8Wwv(SNqTC_q(Z~+-`LLeD{8Q1=tsBUaDW7=8=7^Gcty) zkXBagr}F#qj~!yb6DMq&0)N{5EvWapsKwux69s)6^szdazdAefV2PMKojOYt^JhO< zy9ob3FDA4q4T4geGr55e3bUYK_Hq^muq$I(!(!tOvA;n|34BE3WiI1EC$%HnMX;3s z+#U#m7^;McIF2^D2_F|q1w~3%blPXE8a&BpztJSGki9pj(9Tql%`DrO&MKPpYH~f< zp<^wsXlE+EL2@1wH_*x@Iv>54bEn>lv?;abg1df=ZthRN_I6ef!D))loYB|*Nw`US zczGX}ZY=X}+L@s>1L5~d!fR7&`JR{64wj>!)pDu7guQw3St&JKYhZH!+lOPe%dFqD z9*kx)ULq>^1TQ!X8_AQY#kHDgK;F3e4S5#08|8eFEE(1ixI9ewhOqAtyr}pGc$> zQxWt~#p3!M_&ZA5I7SpDg|vU8P7|+Vpb6{vx6iFi@guFz*gBY&w(ix_HFJ^|U1|o> zalUsPD>-hhpnmzYHpC>9&`sGW>EGen;41!EdpjF~~u{*7U4nw>g{k8jesSxw!-S~ zLsA{P^YM>X8|12n7WMd3TW5Xd<>1zi-3Qd`fT8^OmP{SqDy!X($$Fa(2cQ=s_v99X zHxE5XAw@VWw=+s!JJy0&rRD6nROs#>pImu39W-XN%m+!4gUAG^V@hU47E_%@Qd*6m z5rjSSpB5#w>3B|2%qnsb=v&gg`zIx6?8>ckuS{RHD)UZ|nccKy$cEA9LWMYP7e%=f zXh}(9wxuYRg|iF*X4-?xsI#ma!x!Z($K2p9w_^2%)bVa`UGL9XwlM0<_p!I~l03g%g5zUM9Hz7V)yV^2iiW3zAQL)!^+@`VVV_xtCQ+r=l^{ND1vFEk{A6TLiu(#kU}}# zp^s4_tft4lO9eR6Tqh6W4W_)Kz3^-3YIB?>d*aq;+1vd;g?#Ac9^o~!8qK5o+EJZe zZn`EN*-`qS?^9*LuRh}0XtH=V2?k>UI3GVK$!QGt% zcfYv1UEDQ6gS*?sor^mk&%85JQ(sm8IKNI++uq%4ueCZLWDVmH;%LMtj#6?AP`j9U zjKy+&Ic4Ns>fkFtcG13##cIHct{Im#^{)N8)Ya3rFD#4!=-JQKz6Jjre3XoN)?`-G z@A$HLi^&L{_PbeaC)8;-w}@HtZ}4e-HH(|c`~6g8G60yUc&l zMnM8MHa3Y)tt#u^_V8at)HKw_B)s-p`&n_U=Dref%^rF#NcGZX`-sD{L)m z;EJ~v7??FcSJ?(3&2ipZ67X<%P4B8Y?3KhEU)>aFZ=O?fy)Sz-u5xJ0t(A1SufZ4-AdD7tx%7h^32zgb7eBj-58R7@Mj^@QNpNscI zzvdnk8-+s<#_b+EDHdnHVtEGONg&I{H!%Fx2Uwod*Ui;<9x5#DD;qL?N9j{;s9}bG z9!4)?SYD(R?x*463`Tpb?Cq!?VTZYb2A)-J2-+tl8ROHM&Qm4u-~~LbmuT(F z8wX@xu~Gv@jGd30QAi+$XTy;aX!4DVSx@xYh2Oo$eh$#-#xv$j#azC=N2IStpee;V z*4@pSm91e9y_XqbZw2-fL(R@^MW{T$bjwQOi7!GsjuvUKsH*4D z;yP%Q<{ULzmg9!lBv%nR8tD)6#acL9(5zxGS$`#wHuMnPc^(RezSp>zuK<=kNAM2- z6$*!_oE&>CgMF&RIqutC`D}e>nVyZrjdV*3fa; zgce_fpC2D_cXk!ob)Uyziy%aeElO}L`l&cOMhf8tsFn;k}q+8Umz$>vgc_+O+CK4 z>-`Rx%$S*iHpZB`os2C&5;B4$Ph$v4#WYc))5x~yrWi@*Gg7QJBHwDD0mht95O;sZ zOaQ4z2}1~89PN%Hd=Dg}3KY;WPLi8A20n&fjnG4(LKNYVl+t&0)%ifo)35}$jNeYr z;wo`nIf=OQ0VY%i;PCQsdM6OszIe%XTNOg-!z{Z{FPp&PO2niIbxEW2#l-7mO)bt% zo#UbjvxRKNU5i%6JRP+bIn+FDcy1&EioNH89;ogq*dqNZR;_kp?`{$?ngg(`Qt0e| zt=2Y;*l{nkjAGOA>3uh%ysEE%fBg5Ja0&$98e~CceaQfSlFrykB9gs`x&7oTE)Q=7 zHJ^{UuJ7LR>aWv(WSK#7n(gykW1i+uw7oQ)<`-PV+p7SsM^ptIaz=`holi|;;w@C` zz-5ZRfhoU{h{pius?7GXbb3n)Th9M+OHY{&P5D0w(Eag5vUGA=F2v04nM*Ezve0@s#0g9@pM*t%nURnWgYdu81-N;=*>@UdZQY;T zi1ajByf=If692*PvUz)7V(!3-F?*XR?~?|qykU8<$#d0BJ_Ef;ftIEJBE9e0Rg0>F zS`*!Vi$a!ZfTLP%fgX*YgP8Dh3DD@J1D_#ZvI4JW{hR&tZG*=e@@yHYt#|7Nw++*I zXDp-rSl#bsA$#+uAxV3~8?{fdjA>TW%v?-dwtb(JC^Ehh7X`a$ZwpJgTucMgcG;Ja z)6qw|zOomHQJZ}eagj*5d`b|6oVkS?Cl7P8$I%XCfPaL**!_`Fs=$QcBPiL~^Wn+}LFgmQjlwzTw%Vds zhSK^$A!kC^dE`dWnWEbJbjq+&f@v3l+}ptrv1a1wr)@D6x(?Jz4g)(N&BqUjn_p>B+)HL5rrecm5N;+ z*KWzweZOB!=Xpo535NfjDZy3dmQdR3^vP~XZ_Lhrzjv)!bKl~tn9*7dk=!zyFqhRm zrnq%+aL_Ztlk=+YA0BMk`H7s%U_bnc8S0#J#UDEGbsbqtN`?P7RdFbx;Mt!qecBaU zukQF7C%W)O6q>VAJ5sCe-3*T-awy#L$g;m> zx1URuz_?b4s)cgXWegU*N=RcD&2}Q-p=WlsjttVqH`%RJRP|9pU6v7?5Ov?B3gV^D z)|lbUM`(lcsb~X@A?XqT{e~nh!7#m;QDpwdVud_t?Ec}9$Lb!A$`8k2q4tnkd}uh? z$UgFb9Wx7tW*K20XWyd+77tf^{JsA8aZDTi$znSAM^(>Co8coFtfGtBi^EtZqyOUt z(6#Yv{F!Ubt|(T@2=x|57-KX1MYUaQbZn?C`0qL*f;xCF*2bv{+;1nphL&99**){h34jSyf%de8wDGc;Wq& z9u0p6u#$kQ{OAj{6!XN7Vj5qEEGO3mDPHN4hooB!TiKLJ1$IF zbm_5a+opeNAR(D^Q5Rw8zxns2yhEZl9yK)9X?|I~o&UDdHok3*s+?i0NUAb3FBq&G zVP-SU!?*mXE(0qgAJ9skhp)KrBp2^T*&EE&5Xx)h5)IYy#8J8TxDf4kt=*;XG6ZE< zf@0&6XC{$^jW@nD24~z5c)d0vU5xxm$zoz2+|}r&?M!8^NJ7^qT-u zroNnG*nccpc23rh4r81=cL{vmFq=Pck5z2-UoV;(9wvI=mF$SkzWc%OvNA(*dd1_m zR(_Knr?|Q-SzA2E?4IT$*!r2oPK%Iw{qMDjJCALu)onrPpSOWe<+TBTVRt;eHPFUf;DXf#NM-MJrsa>=G0dp8 zSn~SOayQ(1;eAu9GC0}=1~dUwg|pW=-QO&#XXerm^sv5A=_CV1T{Y;EQ)FAHm@RM3 z`^61vS@*zU##C{ctK`M2vU>d#-LvQQ4Qam!tJi;umt;cxb1pwQgqV=UXH(K}NW0u$ zQwON0+7qBlDH0^aTqi`f(7=C#lZi0O-acM|5hFMEK-rszFn)3hKhnQY*u5s}ec&)Z zGB0FVfVc*J?W3{kNGY8h)?Y zwHmAkUUoZA33?8@beCEczhvUNp5i}ez0gV_w@AJ>xO4j8e;P3r+Sje-C|FHb&|Bze z*pD6x%`Qt*{n~VwHKVoW<=9hkdH)_Sl{=jvb&k8wmO6aq5rc_-jt>!Mc?_siQ(uilTUbx1>p0pfQbA6VtxyGYI!dQeA-FK# zxoej~bG5t}cpeDGJ_7xrf!|+S&3d{`9S{uWRq2!u^Cb?vq_R=_6%uA4KT^Ia{|m^x5kQ}|Qa7d#PR^|y_`Ew8 z)w3W`%a_JS&Q3{}i5-6^$6V(MKr*d(tAB#)ArB;Rw2}?L_Jc1b;6pLm_PT5L z99-*!^gl)IEbS`8(vv9ubD}*liFS#>0c zkJafD$e9{XiZcKJlON$DZNGO1B6{9u?4|d+AU)xyA?mJigs(ZdSY&7emlj#kt@BN| z^9+Y@@P0@ofdvl-OYA4|;6XUSYBC}fa1HkwF^`-`rj*?*W938*+2w4m@aNLk99 zZ84=Sm+K%Sj(plhhCVW82o?~u`E{e!1{7HrTt*)B7?kmBM)+8Lkg%INK-EGBGAVZ` z)kuJMyxW&o>S<}A#usuFikzH%gfwdfskwfN*`_c4{J%-Iag;juukjZEA(*|SPbf(@ zPuYs+`(J$YM#T4N5=CP(&(tyyXprLsxOjh;`W~twD51t>s$Pu^E|Hc(jh1QKp%=K?ZR& z!3I(3D)cw4&fe|U03`K{<*?wrs#l2UT_Zym2c87xfiONyHT&nAkz25UPnYN1b! z9@jr${zzqqcD}s0-D4$;m=hR_7%OEN{0bRhJW0$LyRv0{Y>rp7m~pdFx&qDl_T-Aw zyu$dXjyp~r{Kf~3Y2^j|yJI#QuLcKfh(!?RZcfbQnBRC%ibMw~y;tMY3PFm0f!dF{ zy=J$=y6UHE$V7@E6NI3=yP<#>bHs$cfS`(jJWJV^Pk_cnQ-v&c$;d?d9vH6ux~{)c zB5%?AnDL-J2se1+*uk3y)F<{m@79vf;3$Ez;@M<6hijskjuYXB`tRckCm!NqrbfS> z+@brerEPyv4@9T5qA0vV*BDsf`hfvYvM(5B9@fTK_3=XlOB%ya4U#7qZ*);z8Le+| zG?|n>LuIyE*^rM3jyi^_FAW5m0J;Tn!zseg12FSk3rRL z$=32PBPn=NfX6CW3uA&T+)tKmuUr~)blF*xnqBSER?OVgNweJlbm@Qh?e)_wri#I% z%kN`x4cq)zshZZ!yXxUP4Z4si)-AFWM3OF)@Do#qWEvt202Ngn7NL)^09ORjsZ_VT zQ%r=u(QQ;6k^=&ZCuR$3Qm#-{{1+3mply&`YRsWpv@s{-ETjmTWXScE%pU#k5n1>v z=Rh*%Y|1rm(!LBWk{O0}WWk^$Ks^SfjXqlt?>pq1m!py4H_JF)y7Jg@az69;cHvwVyQ_Fu%6&?1muJ1nLHQ!FRnx zBpP^PB}y3L&hVU&sRFQ%)+Lo^x%g1xn3dzLk=&Qz7|;)$iOzQBCjCb6^Uv<107PRU zjzZ^ONzCqC36vszTvU)ZG+WY4pO71-kX1d*W7{FB>AOvY_E*Y0lxPUt5`b3y9)m4< zdnC1?lfkAPV>pJ^H|-=u3q=mE0`~kwUohM55i3*jU&=3ChSVDeE^c|RDNKekH(sU# znBGZQOUq6FzX>@fhoGfgfmuG@i`v`edH5~(49W3iCsuM384<4?>p=WtJIo=MOR)>8 z61@l1!`~2$H480YVHNXo3$t>K?t+uSwfd_+XZ@v2Tm2HVn)rE}%~>ciiACNllCjDc zstQL@j>*2v_!kdeMV}=U`{?gUa~cw_ewt%>QrcbuoMemSv3_$b(RWZ{HEEt-TD7#( z^}wyMC3Z8})i+{PW9T|4jJaCrOy@};dMY#MSlPHP-T_)_n3g%v)!U#)Mql9XuvM2~ z<1Va}u|2>8W);m!DxWC$zYi}zG}5ubxuzwfK!3aT{WS1DEn)y5QmT*>NPe>SU7N-2 z;&268@u}u&rV>l=xqbb`^mw+U{>KAj5=$13b726wo24Ds&)xLT5L&|SAlsxM9%)xm)&kNECNUNH+d|o*UTA?i}5MGp>L%*q?23RW5#gnY( zp%S9(rYo1MphYCC%xBTd6G3wy^z0d4at0z6yJ8+nW6LNc(y3aJocDHeYsEg(t@HZgrb&+~u=5Sh=?RZkEmB z1x@eICT`E6>Q>NjKF$uoXNLmJm#KU1!gsj)j+A340d%1PF5#wWu1Ugov!Al>-jBD- zrBSiBzbL{2fk5wwX@~ze22sZMeX8zvQ34;uvfXAsqS+@OMc1AFBGtcK_^h@`?C1;7 zuJ`X7IKeuSeU?{=v3F8(;0sKyW}s0vNf2;0$sp(u3qf0&7snNSpx^jgrqL5-jT5aS zuEH`!^?g!4CQ%|OLSl~*3}s#r5fxX7mif+9r|4R&94>w^ZJOWY*q)8U!&$ukwEB37 zOQyh&X=T7&-NFaC%kcs|cfQQ{e;fL8sE@j2;A>ClcGd5EL;_05oyZw4oaDA0bs?C(zdo^= z{1NvbI{XDRY^Se%nR5q+SJlQDcx-$(eEjnl6lG+D*ZVec3DTMJQDl1UU8$G*@OI`*B z+?(S3%MNpmhO`trx#M`iSoN_n3omAl&bRn4T8yz03@?SYe?rn({bd8naJ$p)gkqmF40fjidjiD!P?NVAS)@q3 zLWEk=!O*zbt(?nXD^y{YJic#X}$ z?njYSxw@If^H(PeRc6;IqfiCV_1pJrAOfQg$?6jwqPd^)f$A|489!23_j3ZCFu8=R zzpeAP_rw(R`rqE$T*R8<2Z7a}Jogf%Vpv?;*lM5G^*}`Neq@F>S`cCW4+cau-Iu*) zTihCbb(;@DvH zXH05K=wZ5Y10T6OAGP)fL}&<8FVcRXKfd8@Kb&3@mU@^C>o3oKEjqOPpJPRg2F$@! zFIDdI{D$hMpz|DCOW!%vP$ zuRC;ZO5MNky@ILqZV}Vo2T;QxXkp5xP0TV+qlQygGWP9gKsD0T4|Mi58LwXEAXV(dquQ=V89++MQhXN{$T` zc`-fA@lYh?8tJqh>4k@c{B_Np`egya0)o$^uWRqy^j!W?CS*`zk7c4OdMx99KAV5n zJboPj#h+y@WDt%0r#+^vCNK(#JH6KSW$dous54}Qd%})d*dUDf1oF64;eg2zkaD3| zaL`4f%2f|7^yOE-=T0b&L_AvpV6gV_e2Y;BJw>Q69UdU@n?|KVD0hE zW->t;n75vTNxCx>AFR>38B>VlmBD*JcNsOPxefJY<`2DZ|8V9prnQ_9h143Ywr^Q{ zyE)P7m0kIJIiModZ7#m;^vlWCO}@MbvbxIV%y)Mr)*e%gd8guGcpY@|xN)2p+eor0 zEaV3&IGu>tbQ_Y|t?KpT;o3Z0<^h-ZN`Abx4on65P4E%l)y^ok^lO4|KdB#_wj9O6 zuG9_ALLH2+U@d%7^r2=8r*+Hv2ej}(g!>ITzKaigzq^~C!@!=q)MeMHz(|9&ioeal zHm(0VSF%R*fd->*(tv>Ovyuw?`!fa8>%CF$1;_s+xxmn_lxkPbD{`{5lh^l(&A z`v}fziD$RTFl*agy<0rvx9a+MB9Q!7J4-0fFJmHVQOk{4oim3A3G{a`xxphNWB@@-Z9 z+dBM}{pXOm1)P!FGJ+Xs9)6a+T)r^B$Zx}^(;l=5OA@2kOq=XZr@C6Cl+k_C14Bp& z|4rl!o8L)|eOmvk7g8Ksik(f)Gj)TI@zELSV*{ftjH=K>`xH{*B;V;-*PguOO`yLu z>0qMYZ-R6E1OJ;h@Ms*6hJ{eGCD26wYAn1kaPDwem?+T6{Fl=+J1CW3$jkuE;4P&>y=FUK**S4zL;}1S37d)y^t%9pwgQFxyYm;$&O83+uw*4= z{0R5@I4m4#n+mzB52E}GWS3oXc1<&qV!vQX{FJHdnA*1g6oV)K1ni&_%kTWlx`eHs z9OPq9R+ww&%gdNAG^m732F5&0{kw-f-l(?;`1IqIrrv|5SsK8-pzgJp8pINc3hCOU(NjqHvk?E z^ZJ%Wz#F5C%_1W++3YRmw;?+k1%Jju)fr2$pA!s3Bud>0e)~HO&;I_Liv}caM275y zVkVCU!H9@_mXP33zE89wuN`xxq1tz!9*0;5o`k&1*G!E#WzSGm2ls-_dcM za%VLQH^=CE0cNeJXA74%iP$jR8(FlNGftr2XI1HYpT} z;H}et&THT}&umPZyw~@~ZCGKJTi%N{vnBZ1PkR`Gk6We7SkK-UgD3Bgmn}dS0yg9H z`A>wUopW@d9ZnZ`blz6a<7(!vZC7FU$aqcRup6enCM7McU|4~(!m&DddYDFYPq#-a zWkC2#YQo9ssYZh$-^-$SUXqr-zo4usJj6q7q@n-w5~8_k6^gQSX=Dr;E;B3K4bfMU zQ7P|`7?JD{nF4fV5kjA@p$^xEf)yRxzYt7!h&(`C}tK2HE zV$@6MMn9}qpLn%VN(lW%M4{wVtE!1EpGIjCu~12I@Hmw1p!B~ZQFqNJpF-P-RJU3~-kIc3KcY~MRCQE5?Yc|8!`+Tl6NezzNbu82ReDpV}KKJkiNj>R2Zlu!b5p zi5oj-u|H5XTk@gPrmt|2T9)f=;sL8ffslf(A7tVXgtX~1_*xa;EFnLOG2^FCeT?Si z^|;8<(ZDwuI9%-o{z+j79&H2_w;MQ5_7dU&S3l;xwz;Esb6zGQ!kM3x?cUDfZN|K0 z{GPHgOxHLOK->?LwM-}v-(y_g?L`S+He7cbvzQ{;-_JF`i$4QdkaeGPmd$E>Pdny) zZ3r9wR-jG!{PcTw-31ZQ__G;40>g(Y)r@Kxb!`K`VjJSV5Iq2$r(b$8a7bF5*Pq7Z z!if*kTi!(LrG{H~S2rGFD2xI`VzQbIo>zLW3$(kyrIE*>{kn_iE^5Di1Hq6)$=XQL!)w+3f6xk+6F4`EAC@eixY88(woKT5 z{+fHs&((7BpAq4%zI=n2{Yu58qKA^e*1ZSq^TM z&*YDLN^U}-A6c7baK=f*UqWa?Ze(;V7TU)3-b^EcVuB{l5gej*WET{1g$+%|>(m~W z6=EZOYycwWr7Seeq@kYlwjoJ617f*!#b%0X^|A1i^tx zfH%bzvhV8^ya&M2k;5*M#IcR*#H9_lstcT13$xL}rUOXW$%VNk*wSJ6K%J2xNf=og z$v?j{!-Ggk^?v=<=hDo{XTg#oA=M8+(Tbo79lJPXIM5?e^0(VwB%z{RKiD%GI==L` zM85Rh;RIhbl?`?Zc{_CNoKb+z;{DZan}7O!46*O-ui5^h56la^`w0?qwwW5d807NI z#xUx()2TkPu#0#t9}1?)Bf{}NqheJMaT;P4!UMMd)Tu^adQ5xoqUR$qOn5G}YxER2 z-8P^=7P6E6$bO59ejouqdkr=m4iPAM?I$3t^*)M$Q*lu02gM#XvT~{>Uk&Z*rx11* zn9n0!r(V7NUUs>~FQtSqVD0rMTo>c`2jb!x;~4_HMFV=;7otI1TOWVY7nC`j4wZK2 zeCo^_4}19I)-_u-#~l&(mKfIbh;jUG_Z|mrfZ%bj$zXn;I)m2#b50isgBzc>H~hFl z0Y;F#9Z5NVnxX9z@JS~%(PuL-_dKEtCS0F-%|sg(YmL~kQvPYj75VD6V?L1)q*gce zHgSdK$e>eWjthu;8qXEf12lLN&ptKgy)mx29!?T3JfL#}ZF4d*Y z54i?+g(g#H3-gWUTR8ZHzYiku(DCzOAIAlWr!!)b)@w#QP z4qKt>A<|}=OUsmQ4ee27=T2rF;&Y8Jjx}ngg1oN5_cmPL0BKRtx0&2JTAO9*56mOT zN)GXexV7CW<6#B?S0D8de>LQYB{>uYIv!g%o!5A_CCHiD*wcU=yiBhptG+#Cxl0hC zCpDiu(k{N9ho^_ZDkFC*yN6YFtPyKl5-y$a#+hS355E)tpdbHMVLAI$YhEEiN-KA~ z&cD(FUH&-CfUC8J=>Uu(DxU__R{AyJfs1M>nq_Bh11})x;a=JUp_ik_<#59^yfIg% z_7XB4PGsJ9L2#|-o2}o|SYFgF$~t4m1&{EjHYYR|3=8r#;*&d%Wc38pZYK>WcnTE1 zS898`eXqBYF#8!_;XGc}i<|d$Mzag;$V=Z6B?USq@zTW0G;dduN8*UF^0SMDP-BP*MG@m%9SaiRUp6)vhiPiRlCpeKl zsxKEcs}5(oTuVndSm?V5EXwbQW;s&$ahX>|TNaH`k z1LnpNb0O3spElFJ2fzp}@=x|-?iy7_0zz&L?xn>i!0SDGegFFr=Vu*yc{?v25ZU?~Czytwg4s zX%}#OQ&lbd#eACkVzNUh?0Y}dF`t zU?h3U1OQ;r!qNXRA~Fa#%gdDKk!&q{DV%{qgKtM@F-z=`NBG{du*?ybcFxIS&t&+TR z&AQm8j+jZDU_aYuI%`B=o%W>%dlGUeuOvE<%I184}sPIhFOJXXbxZcrKy z2o#mrpHmcC7X`i1gu5%(BS_-1yqr#hYQ9vqM&l4x-k->UPGC>*HHR&>aA>n1Yqg!g zKS35#d_1}J?$Ad9)O%%i#t4CykehUKEs;fA?@#Hi2UB3=mFIpGCN1JxdO?`3L1@xB z?wsWD){TyGbkJo;?Xt*%O^w`KgSiUhWUwTY%fDRkj#40>(8i!OBH~N|Qwius!rvCx+B{3iy+_PcB`SYMw(@qH|7;OK&e3K){AleIPmPdtH zmcA~Zl#8;Rdi%)hzJZ9T{P*Wdm~1pCAxKt)j22ZKj#i^QLC>Qp3LEY6EBDoB|2G*rMsOjakW{hHFFCtS&q?7;Es=gHd9ZU zBE40~wIN^7yRPA*qfwG%9aKJ44#&vo@f6QiA!Pe=oZy->Sr*1C_^BODn_=g~sg38T zVYIHP)V4D;)tJkXnPXRVya2leEEh{F{bvn7+Z>7k>q#r(beDd_!^PcHjSyV^K?Qq2Vqu1jlW_v zi|uFVo7sq}(l}_wUPFgs33feo@>G7X?EbapF`~9_alm4h2#Zb_A6g{|?7wyd$Ou`N zMJ(Dk*eS{7=;g1@vCL-A3O>!`m=xpNN6s_9VTo4r8RWJ8AvT}TGiFWkz;se3 ze=qIc>lc!kXTH1mzTph&S6D969!POKuC~hd1-JJ_yqJ%)^4Icqk|{bW!&)=E|8CV< z>?3+Gh@TrcrZ}5tYAbxVTd6qw6x0SXGewU5<^ISp4cWRgAh4C&TMo}#jBwRxoKqP~ z40}=&X2Tx5pBPLW^~*%XmQ4`Zkfv z2Al+(AYV#zmR<-Wp4GOlavPa1n3c>Tq4p_v1>9Izb^fl0I*8untrSY~>)6gkW9u-? zPt#LqiAlx_R*jfEN*0@lFSK-HGp$929&7f&J7k6p02F9#JW=kqV(PnqONIn0eUb5Z zN~#x%tyisiJ@%s6)KaT$Veps42w_h(>)vc>Z65MuD-R2spS5g&pOmMnozN4uwACOSMFy?B=u^h)=RFUAQto zKRqBN@dR-RS1P9|5~Ai zV3a8gKC~haOkeoVFqsL8yUH4TxnY*K1fEJ~S4IY~v0gjh-XC=h$b57t1Y~=!3Pex= zsd)|=p(bi^W2;Eouu*OuVqt9rJN2a`DU@r z?hAR-&lSDUnecdioXnIZz``d$z$b`U!{!mk`fy+Q3 zqR+q|JCAANl?x}9flhgfa_ZsT6Yh-@Ej_b;pze&@nQ8^Ou`c!{r3TKwc#@@Au{?Cc zch;gP7LhQ;-qk-|ZGe@k(&zgORUw)>eyT1Br0~Bho6V z^u(M!qmwkkZy}1H)N1hHj1=`p?MCNduXv3P`MbDRg{~??RurDu%3RKmCMg#Rx7vb# z^zQl7tXrH_n&cK^*iBXe)T=G_Kh^6qU)>-yC+by$Fe#oWMY@CTl$r2>i_-SxrK*s$ zsp-#^GW#8i1)JD%O)_n?fkc~{{pIXW_ZH`WMOo)G0s9t;d^!i1#EA{r+?r#vf_V!+ zthyYE^=qWo3PB2|oO0XJ5f|FzgBe`q&8j%yMU%7CDovBqmYN~NDo^#@c>-w(3v+Xr zPkHk%MI!#O=6MF9e<*#@avIUa4r0lU2TBKjna&m7Iq~eL9Lxqvt<#3X6;pEf?gV3S zS=T8zu8s#HlUba$&&ic-VHKTHrvI|R{`h(n9D%XlkCgD}g#9j5toLmcMiU4VZb9`R zR*vRgh$haf6S#Ysje+V~DChT;whC8NqLl_#M`w}Q0BOa3NGzOJZjp;fv`xMz_FCOK z;O@W_c;bLfFEW1{dykmlj zEit7C)Mv907dgC}09f1o4M*V?YiYEcV0%HYqPbEc4^ijY^x8$@xtmu5KOMT7i_?sI zQNCybZ6SdGI%mFhKXF32iH_kyZuOi9dnvaVKiGo!#kaaegHXPTBi$+b^;?#>u9A7F zj-AUWPTRVt)-7=qUIc9G!VWpkxXXgf(uo!O!Ji<;$TYMa)1xAd29CV%{!7Gk3UJbp znmc^ib}?S2M#&fyQZ-Qv&OfNKN|Tl9gCgbCBG};PGUaH+{${G{m6c?&Nkojc$Ju8U zauj(;!vHdQHGWr0HCWdbFLhbM>DeeZ*3xsEZOlo^K=NS&`Kyo81LGKP%ab%~(pO1k z$R?R=HkusDt;@bv@}x3jrAuB#sW$(c=duZDkt!)KPT~rUmbkj?y^lJyrhi{;93Ib8 zN7L1M2U)EaS1=Y~#M))VmqRttN}d?hp!|-~JvIGTn2$L6Ue4tIS^2Ay3cLR3Zk z;(OiXyoy|P>V)}IBTGiufjVbl)jvrR>$N{_hQ%t`X)Ve%MN5kf!J4k_9BTGn?R+#U z*-BAW@tXdfva!tc6YA>~w5k#%nnH=vLqDjO=s__Ev0+tFs_a$N$+xE1Du)Py8lq+A z>M8VLZs(#zxwX_tE!EzdLRgVL<$YsVqGi+7{f09~^u-+AA(@%Q#0GbRZ92BTP2ZDf zw8DyA2casW3^mH0X1S!PH;4wkwvG~Nj>E;vw^O<-WeDk{!W){a^+%0Yi~cAOshcwu z=_1CDm`@%Ztv1{%`5vbf&j`t6-2y)^Zfj&H$tx!pv@yl|rQQFe8!`_FFn|XLPUXvL zjtn`C%ISbqOV>fIi1RC`+ho!IS?iR_s!;Z;*|8`>j}TehW$9Gl&k6|?>%I1+h5^&T z^%yg^Ey83L_m(#1Cujo-(y-38;R|34k0Y%NRW^LSuGKuv?0NaUAO&Keo16HI2aF}s zzQqDJ7fY&R(j-JPoPfdMe!DdqKItVhu0P75pLS5l_Rw0Eh{WkLV@*UvO)E))(a)(>IT^DuNrB8 ztk>?(f(%tVk|Q%)q8p1Mqkb6BM1rQ*y#xNsEB~byWf-?AzMMwSp^S;d@28({@@({K zc!cxUn26Cye)X5oQWy7cvT4>voKXLf!$9*(PQ2kCw;88~dlhI}`*yz2GR#+lQl?FJ zH|9>U+K#;NRz1|XM|VTqz9LA~q(p4Wv_D09qU@he2%uQYz0^k^JxW5w&%Fr(E7($6 z+f>}xW+QV_n}kY8YcSVFfKDyk(ftEm)P$g8Yk0tjCt6kq=ZT>JnDpQ#tZx-@*D{eT0Ay z`4!X?y}(rbEN1_^#Q!00F+cH$Xa-BQ7V$i?v^?lpng!Q4%VDfglQU6jmIlibpMvjS z1UYY_^n(3KSHUJfb+BI+nc*P)Aw6R=3@2kHPx3hZ42LFYZ%&)=OV1*OTx+>l^iXB0 zVqi?u+<0oJb*2K#KW`M5HR3W-_D1=kE5D?U<4dea3DdZ~0c5&Q0{ zaX+19KKalpM2RZSSwytclikdbBPpaArwS$cCi=Z527FEGnBrm^Hty}dVCc{&F?%MI z|4yR+wcu$qWw3`xHd2*@?od%e&kI9>kSb_L(yyZ9^PO~hTB+i~CBz}a2uWGF zm_U4s%14(%v}NFtV?SYXu_>&g{w4d}oqk*0xynlQG#6g22|YhaPAVpLTotXq=1N7XG`19GP83<)4Q6GUv`i|yTa>S(=3U`blRPETD;=D9x@>o} z2$Cxe!+LK%cZof|D5`3$8L|pnO@0*8sfoJMCHAxC_VIY{n$+|osfn~E&2^R1+=_za ze$AgIM02`3z#GvTREy#VS<|M#B%|<^33NW)JI$;1zaEcN{Mwj+wpZ- z_Bdvl&(S zW{cBq!cwaYsaHt~!WN+Kc!_9v6i-^oy|%9p5g(R-$L`W1&t{vY9~&L4JZA|TqCXxl zfXdI(o6S3twCS3s2zo{n*RwMy{uou6(|_S0Y%dDnlK?GD+tq8Up`+2dUvg`)b7`>o zG&VXM@&;rj6Mw3vF`-xZ$QxbSP|aVlG%p{IZAB2BE&z*OJN~sLLR!M$<8Pr-JOFXjkqM;17{dty#sM)T?*l7;kp0x5Z`qh-MJ+~;UcpIt`0Th?Z z@8>k`wjK&K4+_xDs1%I*J=%fr+IQtgfS2MUje)F|vGx{&?wOkVk@gg`t`Q_?>tyrk zpTO<4l>F2LG(0db=o0@!5Y)T!%*O(hJ{2TqM!Yel@Ap;Ml^DE~k9-ZCi8=7|GLkOT|v?(Xgo+$9iPg1ZHG3GObz9fG?{7H4sHg1ful=6(Nf zNZt3ly1Tln{jl}y&eJp9Jv}YInbF2#o~U+^7oIcJ72Z0u%(b8*MBAq>$TzpDW(Sym zOEY`f62?@&7CcIU(MAiRzJ?<~GhG00qtFOcGNZCKpi;I^&ExP?K1H@+VxEU1c_LGU zEjCVIk)$d9;C;t#m55DRGC*lJIPVf_5zVf10rN%nK&~KSyqQcxvuHG+6t-Np&Kxnj z_(tUA*O-<@LvPG)?#7x3dJ}A7`2A0dfjlC*i05DFTW-E5Vl6m#xDp+-l%*YzJSCAY zRgVNCej+1gaI7OJMr3SBlXyQQAi_;C77Y1RC`sq2|C7naT9z(H&@0>@9ip545DWExq1Rw>Ipn7WeeiN!3_u>n*DiMN(N&%8(&DK~nrkrsl(O zsihZ-m%85C8e%&!SbBM|QYAg&J&C4tb)Ql!JPEt^^T_9LWlMzQ3DBTt^oLNzs=gG1 z2Wvbz6l2!-D_mx^AfKz0Kg``){6k?e-)y*= z*HA>Pw;H$;{GU{&JlbbL)+Fy<0!{hWS-_)kl~=+C;?L&?=UdIm0@;X&vQYakH@jg7Dkx=-Tyqoe`27(HvvQh5dSTbzn4m3A3!t$*#EqN z-+qq|k)U4xd*4^85}k-yjGN#7Y5%`>eSuQIJpcG-MFM?cgrJ1GBmAH1^bdVZFw{T~ z#6NupeS!Pk9jN~&cSq30Xg%73hrnjk8-8ah!oe9>zrzD}z=t`H&>A%WUADC51Ab6| z(5KDt{0pfUbO_JC&c<8hqGkx{gq})`=LXEB64-4UKK*0U>lxSGF$*Y_&BfEFu!gnp zpbliNB60F|%$dNPvC3b6iAoLBn9*T*%5rBQvtz^_QhjvxKU8p;SXrMdXB2x?!rNIocY}NRL0&&!Gx4~Z4oGt#QUusVHD6#Kl*D& zGAZYNq-Xc*7D>UWhVHWu?19ErtCK1R*MP9jvNY3^BaMtes&k_T|13JhY54107{v0G zqJ#6!bjJYQgqM!vY8>XhQHa1G?r0)W0!+n$%`xy!{h?ko8#+!l!S;Bc&C??H860uewqfaq;E)i{rt#xZT?-iOwU+$ z^xI!#Nq&<>QXdU-eWrp0&Y6mYFE`hVhbG(O@6SJO%vjc**09d7Qyr$Z_rWjun5w?J z8*%=iuwz1^$(ln)4QqqYzUU&{61&A_6Dh@KeX>v4@yB{l)B66%|Lox*->%aT#92ko9>R`GD7% zh;?>GpC*RE(xArNF?!sC%;Q>Q*|^}SxjKKJo7>ki2`Um8t4dS?dPNh|U*YW)Uyf@B zk}2^=H3<%bI}(SwmmZ};7B7F2W5 z4UF1Ot_TaPug&4Z?%$q6++N(?w~dzW72M4tu-MJ>p)Aev68&chK$#A)U-?NoLixyn z`EQqs<-tbGypXS7&hS=JtD0FwQ(8?N4#MoQrvL;3tx;sD#8TS$uz&0bp|1jR79$*) zcB1U>l-0i$-gB%*)!kV*$# z?ynvOy$R6kX;QVMdL`i>4|*nXR&D#*;UZ4ZDnT>SrtjPRW#_-M0Hj~Z5$1t@7Y
?{@LHp)IryChMu3R7cN=JfF8De`Vr7L?= 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 Date: Tue, 13 Jan 2015 10:40:56 -0800 Subject: [PATCH 019/173] begin reworking the session 4 slides --- source/presentations/{session04.rst.norender => session04.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename source/presentations/{session04.rst.norender => session04.rst} (100%) diff --git a/source/presentations/session04.rst.norender b/source/presentations/session04.rst similarity index 100% rename from source/presentations/session04.rst.norender rename to source/presentations/session04.rst From fdfdb997c0478aa2a5bd6dc2a71d7340f6afe90f Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 13 Jan 2015 10:41:23 -0800 Subject: [PATCH 020/173] * continue reworking sesion 4 * add a few style rules to support the images in the session 4 deck --- source/_static/custom.css | 8 + source/presentations/session04.rst | 513 +++++++++++++++-------------- 2 files changed, 271 insertions(+), 250 deletions(-) diff --git a/source/_static/custom.css b/source/_static/custom.css index 44274406..f0ae2b41 100644 --- a/source/_static/custom.css +++ b/source/_static/custom.css @@ -102,6 +102,10 @@ article .medium { font-weight: bold; font-size: 45px; line-height: 45px; } +article .small { + font-weight: normal; + font-size: 30px; + line-height: 30px; } article .credit { font-size: 75%; text-align: left; } @@ -163,4 +167,8 @@ article table.docutils tr td { font-size: 75%; text-align: center; } +.figure.align-left { + text-align: left; + float: left; +} diff --git a/source/presentations/session04.rst b/source/presentations/session04.rst index 39b36076..3cf6f66f 100644 --- a/source/presentations/session04.rst +++ b/source/presentations/session04.rst @@ -1,63 +1,72 @@ +********************** Python Web Programming -====================== +********************** -.. image:: img/python.png - :align: left +.. figure:: /_static/python.png + :align: center :width: 33% -Session 1: Networking and Sockets +Session 4: Networking and Sockets Computer Communications ------------------------ +======================= -.. image:: img/network_topology.png - :align: left - :width: 40% +.. rst-class:: left +.. container:: -.. class:: incremental + We've spent the first few weeks of this course building and deploying a + simple web application. -* processes can communicate + .. rst-class:: build + .. container:: -* inside one machine + now it's time to step back and look at the technologies underlying the + work we've done. -* between two machines + We'll begin by discussing the basics of networking computers. -* among many machines + You'll learn a bit here about how computers talk to each other across a + distance. -.. class:: image-credit +TCP/IP +------ -image: http://en.wikipedia.org/wiki/Internet_Protocol_Suite +.. figure:: /_static/network_topology.png + :align: left + http://en.wikipedia.org/wiki/Internet_Protocol_Suite + +.. rst-class:: build + +* processes can communicate +* inside one machine +* between two machines +* among many machines -Computer Communications ------------------------ -.. image:: img/data_in_tcpip_stack.png +.. nextslide:: + +.. figure:: /_static/data_in_tcpip_stack.png :align: left - :width: 55% + :width: 100% -.. class:: incremental + http://en.wikipedia.org/wiki/Internet_Protocol_Suite -* Process divided into 'layers' +.. rst-class:: build +* Process divided into 'layers' * 'Layers' are mostly arbitrary - * Different descriptions have different layers - * Most common is the 'TCP/IP Stack' -.. class:: image-credit - -image: http://en.wikipedia.org/wiki/Internet_Protocol_Suite - The TCP/IP Stack - Link ----------------------- The bottom layer is the 'Link Layer' -.. class:: incremental +.. rst-class:: build * Deals with the physical connections between machines, 'the wire' @@ -65,6 +74,8 @@ The bottom layer is the 'Link Layer' * Executes transmission over a physical medium + .. rst-class:: build + * what that medium is is arbitrary * Implemented in the Network Interface Card(s) (NIC) in your computer @@ -75,10 +86,12 @@ The TCP/IP Stack - Internet Moving up, we have the 'Internet Layer' -.. class:: incremental +.. rst-class:: build * Deals with addressing and routing + .. rst-class:: build + * Where are we going and how do we get there? * Agnostic as to physical medium (IP over Avian Carrier - IPoAC) @@ -87,17 +100,16 @@ Moving up, we have the 'Internet Layer' * Two addressing systems - .. class:: incremental + .. rst-class:: build * IPv4 (current, limited '192.168.1.100') * IPv6 (future, 3.4 x 10^38 addresses, '2001:0db8:85a3:0042:0000:8a2e:0370:7334') -The TCP/IP Stack - Internet ---------------------------- +.. nextslide:: -.. class:: big-centered +.. rst-class:: large center That's 4.3 x 10^28 addresses *per person alive today* @@ -107,7 +119,7 @@ The TCP/IP Stack - Transport Next up is the 'Transport Layer' -.. class:: incremental +.. rst-class:: build * Deals with transmission and reception of data @@ -121,66 +133,65 @@ Next up is the 'Transport Layer' * Not all Transport Protocols are 'reliable' - .. class:: incremental + .. rst-class:: build * TCP ensures that dropped packets are resent * UDP makes no such assurance - + * Reliability is slow and expensive -The TCP/IP Stack - Transport ----------------------------- +.. nextslide:: The 'Transport Layer' also establishes the concept of a **port** -.. class:: incremental +.. rst-class:: build +.. container:: -* IP Addresses designate a specific *machine* on the network + .. rst-class:: build -* A **port** provides addressing for individual *applications* in a single host + * IP Addresses designate a specific *machine* on the network -* 192.168.1.100:80 (the *:80* part is the **port**) + * A **port** provides addressing for individual *applications* in a single + host -* [2001:db8:85a3:8d3:1319:8a2e:370:7348]:443 (*:443* is the **port**) + * 192.168.1.100:80 (the *:80* part is the **port**) -.. class:: incremental + * [2001:db8:85a3:8d3:1319:8a2e:370:7348]:443 (*:443* is the **port**) -This means that you don't have to worry about information intended for your -web browser being accidentally read by your email client. + This means that you don't have to worry about information intended for your + web browser being accidentally read by your email client. -The TCP/IP Stack - Transport ----------------------------- +.. nextslide:: There are certain **ports** which are commonly understood to belong to given applications or protocols: -.. class:: incremental - -* 80/443 - HTTP/HTTPS -* 20 - FTP -* 22 - SSH -* 23 - Telnet -* 25 - SMTP -* ... +.. rst-class:: build +.. container:: -.. class:: incremental + .. rst-class:: build -These ports are often referred to as **well-known ports** + * 80/443 - HTTP/HTTPS + * 20 - FTP + * 22 - SSH + * 23 - Telnet + * 25 - SMTP + * ... -.. class:: small + These ports are often referred to as **well-known ports** -(see http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers) + .. rst-class:: small + (see http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers) -The TCP/IP Stack - Transport ----------------------------- +.. nextslide:: Ports are grouped into a few different classes -.. class:: incremental +.. rst-class:: build * Ports numbered 0 - 1023 are *reserved* @@ -196,25 +207,24 @@ The TCP/IP Stack - Application The topmost layer is the 'Application Layer' -.. class:: incremental +.. rst-class:: build +.. container:: -* Deals directly with data produced or consumed by an application + .. rst-class:: build -* Reads or writes data using a set of understood, well-defined **protocols** + * Deals directly with data produced or consumed by an application - * HTTP, SMTP, FTP etc. + * Reads or writes data using a set of understood, well-defined **protocols** -* Does not know (or need to know) about lower layer functionality + * HTTP, SMTP, FTP etc. - * The exception to this rule is **endpoint** data (or IP:Port) + * Does not know (or need to know) about lower layer functionality + * The exception to this rule is **endpoint** data (or IP:Port) -The TCP/IP Stack - Application ------------------------------- + .. rst-class:: centered -.. class:: big-centered - -this is where we live and work + **this is where we live and work** Sockets @@ -222,86 +232,83 @@ Sockets Think back for a second to what we just finished discussing, the TCP/IP stack. -.. class:: incremental - -* The *Internet* layer gives us an **IP Address** +.. rst-class:: build +.. container:: -* The *Transport* layer establishes the idea of a **port**. + .. rst-class:: build -* The *Application* layer doesn't care about what happens below... + * The *Internet* layer gives us an **IP Address** -* *Except for* **endpoint data** (IP:Port) + * The *Transport* layer establishes the idea of a **port**. -.. class:: incremental + * The *Application* layer doesn't care about what happens below... -A **Socket** is the software representation of that endpoint. + * *Except for* **endpoint data** (IP:Port) -.. class:: incremental + A **Socket** is the software representation of that endpoint. -Opening a **socket** creates a kind of transceiver that can send and/or -receive *bytes* at a given IP address and Port. + Opening a **socket** creates a kind of transceiver that can send and/or + receive *bytes* at a given IP address and Port. Sockets in Python ----------------- Python provides a standard library module which provides socket functionality. -It is called **socket**. +It is called **socket**. -.. class:: incremental +.. rst-class:: build +.. container:: -The library is really just a very thin wrapper around the system -implementation of *BSD Sockets* + The library is really just a very thin wrapper around the system + implementation of *BSD Sockets* -.. class:: incremental + Let's spend a few minutes getting to know this module. -Let's spend a few minutes getting to know this module. + We're going to do this next part together, so open up a terminal and start + a python interpreter -.. class:: incremental -We're going to do this next part together, so open up a terminal and start a -python interpreter - - -Sockets in Python ------------------ +.. nextslide:: The Python sockets library allows us to find out what port a *service* uses: -.. class:: small +.. rst-class:: build +.. container:: - >>> import socket - >>> socket.getservbyname('ssh') - 22 + .. code-block:: pycon -.. class:: incremental + >>> import socket + >>> socket.getservbyname('ssh') + 22 -You can also do a *reverse lookup*, finding what service uses a given *port*: + You can also do a *reverse lookup*, finding what service uses a given *port*: -.. class:: incremental small + .. code-block:: pycon - >>> socket.getservbyport(80) - 'http' + >>> socket.getservbyport(80) + 'http' -Sockets in Python ------------------ +.. nextslide:: The sockets library also provides tools for finding out information about *hosts*. For example, you can find out about the hostname and IP address of -the machine you are currently using:: +the machine you are currently using: + +.. code-block:: pycon >>> socket.gethostname() 'heffalump.local' >>> socket.gethostbyname(socket.gethostname()) '10.211.55.2' - -Sockets in Python ------------------ +.. nextslide:: You can also find out about machines that are located elsewhere, assuming you -know their hostname. For example:: +know their hostname. For example: + +.. code-block:: pycon >>> socket.gethostbyname('google.com') '173.194.33.4' @@ -311,11 +318,12 @@ know their hostname. For example:: '108.59.11.99' -Sockets in Python ------------------ +.. nextslide:: The ``gethostbyname_ex`` method of the ``socket`` library provides more -information about the machines we are exploring:: +information about the machines we are exploring: + +.. code-block:: pycon >>> socket.gethostbyname_ex('google.com') ('google.com', [], ['173.194.33.9', '173.194.33.14', @@ -329,62 +337,60 @@ information about the machines we are exploring:: ['www.rad.washington.edu'], # <- any machine aliases ['128.95.247.84']) # <- all active IP addresses - -Sockets in Python ------------------ +.. nextslide:: To create a socket, you use the **socket** method of the ``socket`` library. It takes up to three optional positional arguments (here we use none to get -the default behavior):: +the default behavior): + +.. code-block:: pycon >>> foo = socket.socket() >>> foo - -Sockets in Python ------------------ +.. nextslide:: A socket has some properties that are immediately important to us. These -include the *family*, *type* and *protocol* of the socket:: +include the *family*, *type* and *protocol* of the socket: - >>> foo.family - 2 - >>> foo.type - 1 - >>> foo.proto - 0 +.. rst-class:: build +.. container:: -.. class:: incremental + .. code-block:: pycon -You might notice that the values for these properties are integers. In fact, -these integers are **constants** defined in the socket library. + >>> foo.family + 2 + >>> foo.type + 1 + >>> foo.proto + 0 + You might notice that the values for these properties are integers. In + fact, these integers are **constants** defined in the socket library. -A quick utility method ----------------------- + +.. nextslide:: A quick utility method Let's define a method in place to help us see these constants. It will take a single argument, the shared prefix for a defined set of constants: -.. class:: small +.. rst-class:: build +.. container:: -:: - - >>> def get_constants(prefix): - ... """mapping of socket module constants to their names.""" - ... return dict( - ... (getattr(socket, n), n) - ... for n in dir(socket) - ... if n.startswith(prefix) - ... ) - ... - >>> + (you can also find this in ``resources/session04/socket_tools.py``) -.. class:: small - -(you can also find this in ``resources/session01/session1.py``) + .. code-block:: pycon + >>> def get_constants(prefix): + ... """mapping of socket module constants to their names.""" + ... return dict( + ... (getattr(socket, n), n) + ... for n in dir(socket) + ... if n.startswith(prefix) + ... ) + ... + >>> Socket Families --------------- @@ -392,46 +398,48 @@ Socket Families Think back a moment to our discussion of the *Internet* layer of the TCP/IP stack. There were a couple of different types of IP addresses: -.. class:: incremental +.. rst-class:: build +.. container:: -* IPv4 ('192.168.1.100') + .. rst-class:: build -* IPv6 ('2001:0db8:85a3:0042:0000:8a2e:0370:7334') + * IPv4 ('192.168.1.100') -.. class:: incremental + * IPv6 ('2001:0db8:85a3:0042:0000:8a2e:0370:7334') -The **family** of a socket corresponds to the *addressing system* it uses for -connecting. + The **family** of a socket corresponds to the *addressing system* it uses + for connecting. -Socket Families ---------------- +.. nextslide:: -Families defined in the ``socket`` library are prefixed by ``AF_``:: +Families defined in the ``socket`` library are prefixed by ``AF_``: - >>> families = get_constants('AF_') - >>> families - {0: 'AF_UNSPEC', 1: 'AF_UNIX', 2: 'AF_INET', - 11: 'AF_SNA', 12: 'AF_DECnet', 16: 'AF_APPLETALK', - 17: 'AF_ROUTE', 23: 'AF_IPX', 30: 'AF_INET6'} +.. rst-class:: build +.. container:: -.. class:: small incremental + .. code-block:: pycon + + >>> families = get_constants('AF_') + >>> families + {0: 'AF_UNSPEC', 1: 'AF_UNIX', 2: 'AF_INET', + 11: 'AF_SNA', 12: 'AF_DECnet', 16: 'AF_APPLETALK', + 17: 'AF_ROUTE', 23: 'AF_IPX', 30: 'AF_INET6'} -*Your results may vary* + *Your results may vary* -.. class:: incremental + Of all of these, the ones we care most about are ``2`` (IPv4) and ``30`` + (IPv6). -Of all of these, the ones we care most about are ``2`` (IPv4) and ``30`` (IPv6). +.. nextslide:: Unix Domain Sockets -Unix Domain Sockets -------------------- When you are on a machine with an operating system that is Unix-like, you will find another generally useful socket family: ``AF_UNIX``, or Unix Domain Sockets. Sockets in this family: -.. class:: incremental +.. rst-class:: build * connect processes **on the same machine** @@ -443,18 +451,16 @@ Sockets. Sockets in this family: * use an 'address' that looks like a pathname ('/tmp/foo.sock') -Test your skills ----------------- +.. nextslide:: Test your skills What is the *default* family for the socket we created just a moment ago? -.. class:: incremental +.. rst-class:: build +.. container:: -(remember we bound the socket to the symbol ``foo``) + (remember we bound the socket to the symbol ``foo``) -.. class:: incremental center - -How did you figure this out? + How did you figure this out? Socket Types @@ -462,21 +468,23 @@ Socket Types The socket *type* determines the semantics of socket communications. -Look up socket type constants with the ``SOCK_`` prefix:: +.. rst-class:: build +.. container:: - >>> types = get_constants('SOCK_') - >>> types - {1: 'SOCK_STREAM', 2: 'SOCK_DGRAM', - ...} + Look up socket type constants with the ``SOCK_`` prefix: -.. class:: incremental + .. code-block:: pycon + + >>> types = get_constants('SOCK_') + >>> types + {1: 'SOCK_STREAM', 2: 'SOCK_DGRAM', + ...} -The most common are ``1`` (Stream communication (TCP)) and ``2`` (Datagram -communication (UDP)). + The most common are ``1`` (Stream communication (TCP)) and ``2`` (Datagram + communication (UDP)). -Test your skills ----------------- +.. nextslide:: Test your skills What is the *default* type for our generic socket, ``foo``? @@ -485,40 +493,45 @@ Socket Protocols ---------------- A socket also has a designated *protocol*. The constants for these are -prefixed by ``IPPROTO_``:: +prefixed by ``IPPROTO_``: - >>> protocols = get_constants('IPPROTO_') - >>> protocols - {0: 'IPPROTO_IP', 1: 'IPPROTO_ICMP', - ..., - 255: 'IPPROTO_RAW'} +.. rst-class:: build +.. container:: -.. class:: incremental + .. code-block:: pycon -The choice of which protocol to use for a socket is determined by the -*internet layer* protocol you intend to use. ``TCP``? ``UDP``? ``ICMP``? -``IGMP``? + >>> protocols = get_constants('IPPROTO_') + >>> protocols + {0: 'IPPROTO_IP', 1: 'IPPROTO_ICMP', + ..., + 255: 'IPPROTO_RAW'} + The choice of which protocol to use for a socket is determined by the + *internet layer* protocol you intend to use. ``TCP``? ``UDP``? ``ICMP``? + ``IGMP``? -Test your skills ----------------- + +.. nextslide:: Test your skills What is the *default* protocol used by our generic socket, ``foo``? -Custom Sockets --------------- +Customizing Sockets +------------------- These three properties of a socket correspond to the three positional -arguments you may pass to the socket constructor. +arguments you may pass to the socket constructor. -.. container:: incremental +.. rst-class:: build +.. container:: Using them allows you to create sockets with specific communications - profiles:: + profiles: + + .. code-block:: pycon >>> bar = socket.socket(socket.AF_INET, - ... socket.SOCK_DGRAM, + ... socket.SOCK_DGRAM, ... socket.IPPROTO_UDP) ... >>> bar @@ -530,18 +543,18 @@ Break Time So far we have: -.. class:: incremental +.. rst-class:: build * learned about the "layers" of the TCP/IP Stack * discussed *families*, *types* and *protocols* in sockets * learned how to create sockets with a specific communications profile. -.. class:: incremental +.. rst-class:: build When we return we'll learn how to find the communcations profiles of remote sockets, how to connect to them, and how to send and receive messages. -.. class:: incremental +.. rst-class:: build Take a few minutes now to clear your head (do not quit your python interpreter). @@ -553,15 +566,15 @@ Address Information When you are creating a socket to communicate with a remote service, the remote socket will have a specific communications profile. -.. class:: incremental +.. rst-class:: build The local socket you create must match that communications profile. -.. class:: incremental +.. rst-class:: build How can you determine the *correct* values to use? -.. class:: incremental center +.. rst-class:: build center You ask. @@ -577,12 +590,12 @@ connections on a given host. socket.getaddrinfo('127.0.0.1', 80) -.. class:: incremental +.. rst-class:: build This provides all you need to make a proper connection to a socket on a remote host. The value returned is a tuple of: -.. class:: incremental +.. rst-class:: build * socket family * socket type @@ -626,14 +639,14 @@ Now, ask your own machine what possible connections are available for 'http':: family: AF_INET type: SOCK_DGRAM protocol: IPPROTO_UDP - canonical name: + canonical name: socket address: ('10.211.55.2', 80) - + family: AF_INET ... >>> -.. class:: incremental +.. rst-class:: build What answers do you get? @@ -653,7 +666,7 @@ On the Internet ... >>> -.. class:: incremental +.. rst-class:: build Try a few other servers you know about. @@ -693,9 +706,9 @@ Once the socket is constructed with the appropriate *family*, *type* and *protocol*, we can connect it to the address of our remote server:: >>> cewing_socket.connect(info[-1]) - >>> + >>> -.. class:: incremental +.. rst-class:: build * a successful connection returns ``None`` @@ -715,7 +728,7 @@ learn in session 2 about the message we are sending):: >>> cewing_socket.sendall(msg) >>> -.. class:: incremental small +.. rst-class:: build small * the transmission continues until all data is sent or an error occurs @@ -740,7 +753,7 @@ back out (again, **do not type this yet**):: 'HTTP/1.1 200 OK\r\nDate: Thu, 03 Jan 2013 05:56:53 ... -.. class:: incremental small +.. rst-class:: build small * The sole required argument is ``buffer_size`` (an integer). It should be a power of 2 and smallish (~4096) @@ -794,7 +807,7 @@ Then, receive a reply, iterating until it is complete: ... done = True ... cewing_socket.close() ... response += msg_part - ... + ... >>> len(response) 19427 @@ -820,7 +833,7 @@ Construct a Socket ... socket.AF_INET, ... socket.SOCK_STREAM, ... socket.IPPROTO_TCP) - ... + ... >>> server_socket @@ -834,7 +847,7 @@ Port to which clients must connect:: >>> address = ('127.0.0.1', 50000) >>> server_socket.bind(address) -.. class:: incremental +.. rst-class:: build **Terminology Note**: In a server/client relationship, the server *binds* to an address and port. The client *connects* @@ -848,7 +861,7 @@ connections:: >>> server_socket.listen(1) -.. class:: incremental +.. rst-class:: build * The argument to ``listen`` is the *backlog* @@ -867,7 +880,7 @@ When a socket is listening, it can receive incoming connection requests:: ... # this blocks until a client connects >>> connection.recv(16) -.. class:: incremental +.. rst-class:: build * The ``connection`` returned by a call to ``accept`` is a **new socket**. This new socket is used to communicate with the client @@ -896,7 +909,7 @@ Once a transaction between the client and server is complete, the >>> connection.close() -.. class:: incremental +.. rst-class:: build Note that the ``server_socket`` is *never* closed as long as the server continues to run. @@ -908,7 +921,7 @@ Getting the Flow The flow of this interaction can be a bit confusing. Let's see it in action step-by-step. -.. class:: incremental +.. rst-class:: build Open a second python interpreter and place it next to your first so you can see both of them at the same time. @@ -927,8 +940,8 @@ connections:: >>> server_socket.bind(('127.0.0.1', 50000)) >>> server_socket.listen(1) >>> conn, addr = server_socket.accept() - -.. class:: incremental + +.. rst-class:: build At this point, you should **not** get back a prompt. The server socket is waiting for a connection to be made. @@ -960,11 +973,11 @@ As soon as you made the connection above, you should have seen the prompt return in your server interpreter. The ``accept`` method finally returned a new connection socket. -.. class:: incremental +.. rst-class:: build When you're ready, type the following in the *client* interpreter. -.. class:: incremental +.. rst-class:: build :: @@ -1016,17 +1029,17 @@ Homework Your homework assignment for this week is to take what you've learned here and build a simple "echo" server. -.. class:: incremental +.. rst-class:: build The server should automatically return to any client that connects *exactly* what it receives (it should **echo** all messages). -.. class:: incremental +.. rst-class:: build You will also write a python script that, when run, will send a message to the server and receive the reply, printing it to ``stdout``. -.. class:: incremental +.. rst-class:: build Finally, you'll do all of this so that it can be tested. @@ -1036,11 +1049,11 @@ What You Have In our class repository, there is a folder ``assignments/session01``. -.. class:: incremental +.. rst-class:: build Inside that folder, you should find: -.. class:: incremental +.. rst-class:: build * A file ``tasks.txt`` that contains these instructions @@ -1050,7 +1063,7 @@ Inside that folder, you should find: * Some simple tests in ``tests.py`` -.. class:: incremental +.. rst-class:: build Your task is to make the tests pass. @@ -1069,21 +1082,21 @@ To run the tests, you'll have to set the server running in one terminal: .. container:: incremental Then, in a second terminal, you will execute the tests: - + .. class:: small - + :: - + $ python tests.py .. container:: incremental You should see output like this: - + .. class:: small - + :: - + [...] FAILED (failures=2) @@ -1093,7 +1106,7 @@ Submitting Your Homework To submit your homework: -.. class:: incremental +.. rst-class:: build * In github, make a fork of my repository into *your* account. @@ -1105,7 +1118,7 @@ To submit your homework: * When you are finished and your tests are passing, you will open a pull request in github.com from your fork to mine. -.. class:: incremental +.. rst-class:: build I will review your work when I receive your pull requests, make comments on it there, and then close the pull request. @@ -1116,12 +1129,12 @@ Going Further In ``assignments/session01/tasks.txt`` you'll find a few extra problems to try. -.. class:: incremental +.. rst-class:: build If you finish the first part of the homework in less than 3-4 hours give one or more of these a whirl. -.. class:: incremental +.. rst-class:: build They are not required, but if you include solutions in your pull request, I'll review your work. From 22f2b66cd03e5da5ae772ab85a8f6398a3c8fb12 Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 13 Jan 2015 10:42:05 -0800 Subject: [PATCH 021/173] begin adding supporting materials for the session 4 lecture --- resources/session04/socket_tools.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 resources/session04/socket_tools.py diff --git a/resources/session04/socket_tools.py b/resources/session04/socket_tools.py new file mode 100644 index 00000000..1aaf7229 --- /dev/null +++ b/resources/session04/socket_tools.py @@ -0,0 +1,29 @@ +import socket + + +def get_constants_26(prefix): + return dict( + (getattr(socket, n), n) + for n in dir(socket) + if n.startswith(prefix) + ) + + +# this example is more 'pythonic' for 2.7 and above (where dictionary +# comprehensions exist) It will not work in Python 2.6 or below. +def get_constants(prefix): + return {getattr(socket, n): n for n in dir(socket) if n.startswith(prefix)} + + +def get_address_info(host, port): + families = get_constants('AF_') + types = get_constants('SOCK_') + protocols = get_constants('IPPROTO_') + for response in socket.getaddrinfo(host, port): + fam, typ, pro, nam, add = response + print 'family: ', families[fam] + print 'type: ', types[typ] + print 'protocol: ', protocols[pro] + print 'canonical name: ', nam + print 'socket address: ', add + print From 81614b9852ae59a331889a31291d3056516d2ef7 Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 13 Jan 2015 22:16:11 -0800 Subject: [PATCH 022/173] fix name error --- resources/session02/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/session02/forms.py b/resources/session02/forms.py index 5629d7a9..88d9d348 100644 --- a/resources/session02/forms.py +++ b/resources/session02/forms.py @@ -8,7 +8,7 @@ strip_filter = lambda x: x.strip() if x else None -class BlogCreateForm(Form): +class EntryCreateForm(Form): title = TextField( 'Entry title', [validators.Length(min=1, max=255)], From 0509669471ba6fa6581174d457cae26275374c8d Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 13 Jan 2015 22:17:34 -0800 Subject: [PATCH 023/173] make Entry object classmethods more usable in the interpreter. --- resources/session02/models.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/session02/models.py b/resources/session02/models.py index 4d689df5..e87ac2c8 100644 --- a/resources/session02/models.py +++ b/resources/session02/models.py @@ -41,15 +41,19 @@ class Entry(Base): edited = Column(DateTime, default=datetime.datetime.utcnow) @classmethod - def all(cls): + def all(cls, session=None): """return a query with all entries, ordered by creation date reversed """ - return DBSession.query(cls).order_by(sa.desc(cls.created)).all() + if session is None: + session = DBSession + return session.query(cls).order_by(sa.desc(cls.created)).all() @classmethod - def by_id(cls, id): + def by_id(cls, id, session=None): """return a single entry identified by id If no entry exists with the provided id, return None """ - return DBSession.query(cls).get(id) + if session is None: + session = DBSession + return session.query(cls).get(id) From 533b6a87452b58c84ec0700ca9a488f0a3d54b8f Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 13 Jan 2015 22:19:36 -0800 Subject: [PATCH 024/173] Add a working, known-good version of the application as of the end of session 2 --- .../session03/learning_journal/.gitignore | 3 + .../session03/learning_journal/CHANGES.txt | 4 + .../session03/learning_journal/MANIFEST.in | 2 + .../session03/learning_journal/README.txt | 14 ++ .../learning_journal/development.ini | 71 ++++++++ .../learning_journal/__init__.py | 23 +++ .../learning_journal/forms.py | 21 +++ .../learning_journal/models.py | 64 ++++++++ .../learning_journal/scripts/__init__.py | 1 + .../learning_journal/scripts/initializedb.py | 40 +++++ .../learning_journal/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../learning_journal/static/pyramid.png | Bin 0 -> 12901 bytes .../learning_journal/static/styles.css | 73 +++++++++ .../learning_journal/static/theme.css | 152 ++++++++++++++++++ .../learning_journal/static/theme.min.css | 1 + .../learning_journal/templates/detail.jinja2 | 11 ++ .../learning_journal/templates/edit.jinja2 | 17 ++ .../learning_journal/templates/layout.jinja2 | 29 ++++ .../learning_journal/templates/list.jinja2 | 16 ++ .../learning_journal/templates/mytemplate.pt | 66 ++++++++ .../learning_journal/tests.py | 55 +++++++ .../learning_journal/views.py | 43 +++++ .../session03/learning_journal/ljshell.py | 17 ++ .../session03/learning_journal/production.ini | 62 +++++++ resources/session03/learning_journal/setup.py | 48 ++++++ 25 files changed, 833 insertions(+) create mode 100644 resources/session03/learning_journal/.gitignore create mode 100644 resources/session03/learning_journal/CHANGES.txt create mode 100644 resources/session03/learning_journal/MANIFEST.in create mode 100644 resources/session03/learning_journal/README.txt create mode 100644 resources/session03/learning_journal/development.ini create mode 100644 resources/session03/learning_journal/learning_journal/__init__.py create mode 100644 resources/session03/learning_journal/learning_journal/forms.py create mode 100644 resources/session03/learning_journal/learning_journal/models.py create mode 100644 resources/session03/learning_journal/learning_journal/scripts/__init__.py create mode 100644 resources/session03/learning_journal/learning_journal/scripts/initializedb.py create mode 100644 resources/session03/learning_journal/learning_journal/static/pyramid-16x16.png create mode 100644 resources/session03/learning_journal/learning_journal/static/pyramid.png create mode 100644 resources/session03/learning_journal/learning_journal/static/styles.css create mode 100644 resources/session03/learning_journal/learning_journal/static/theme.css create mode 100644 resources/session03/learning_journal/learning_journal/static/theme.min.css create mode 100644 resources/session03/learning_journal/learning_journal/templates/detail.jinja2 create mode 100644 resources/session03/learning_journal/learning_journal/templates/edit.jinja2 create mode 100644 resources/session03/learning_journal/learning_journal/templates/layout.jinja2 create mode 100644 resources/session03/learning_journal/learning_journal/templates/list.jinja2 create mode 100644 resources/session03/learning_journal/learning_journal/templates/mytemplate.pt create mode 100644 resources/session03/learning_journal/learning_journal/tests.py create mode 100644 resources/session03/learning_journal/learning_journal/views.py create mode 100644 resources/session03/learning_journal/ljshell.py create mode 100644 resources/session03/learning_journal/production.ini create mode 100644 resources/session03/learning_journal/setup.py diff --git a/resources/session03/learning_journal/.gitignore b/resources/session03/learning_journal/.gitignore new file mode 100644 index 00000000..c7332211 --- /dev/null +++ b/resources/session03/learning_journal/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.DS_Store +*.egg-info diff --git a/resources/session03/learning_journal/CHANGES.txt b/resources/session03/learning_journal/CHANGES.txt new file mode 100644 index 00000000..35a34f33 --- /dev/null +++ b/resources/session03/learning_journal/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/resources/session03/learning_journal/MANIFEST.in b/resources/session03/learning_journal/MANIFEST.in new file mode 100644 index 00000000..3a0de395 --- /dev/null +++ b/resources/session03/learning_journal/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include learning_journal *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/resources/session03/learning_journal/README.txt b/resources/session03/learning_journal/README.txt new file mode 100644 index 00000000..f49a002c --- /dev/null +++ b/resources/session03/learning_journal/README.txt @@ -0,0 +1,14 @@ +learning_journal README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/python setup.py develop + +- $VENV/bin/initialize_learning_journal_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/resources/session03/learning_journal/development.ini b/resources/session03/learning_journal/development.ini new file mode 100644 index 00000000..a184061e --- /dev/null +++ b/resources/session03/learning_journal/development.ini @@ -0,0 +1,71 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html +### + +[app:main] +use = egg:learning_journal + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/learning_journal.sqlite + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html +### + +[loggers] +keys = root, learning_journal, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_learning_journal] +level = DEBUG +handlers = +qualname = learning_journal + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/resources/session03/learning_journal/learning_journal/__init__.py b/resources/session03/learning_journal/learning_journal/__init__.py new file mode 100644 index 00000000..601a0017 --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/__init__.py @@ -0,0 +1,23 @@ +from pyramid.config import Configurator +from sqlalchemy import engine_from_config + +from .models import ( + DBSession, + Base, + ) + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.bind = engine + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.add_route('detail', '/journal/{id:\d+}') + config.add_route('action', '/journal/{action}') + config.scan() + return config.make_wsgi_app() diff --git a/resources/session03/learning_journal/learning_journal/forms.py b/resources/session03/learning_journal/learning_journal/forms.py new file mode 100644 index 00000000..88d9d348 --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/forms.py @@ -0,0 +1,21 @@ +from wtforms import ( + Form, + TextField, + TextAreaField, + validators, +) + +strip_filter = lambda x: x.strip() if x else None + + +class EntryCreateForm(Form): + title = TextField( + 'Entry title', + [validators.Length(min=1, max=255)], + filters=[strip_filter] + ) + body = TextAreaField( + 'Entry body', + [validators.Length(min=1)], + filters=[strip_filter] + ) diff --git a/resources/session03/learning_journal/learning_journal/models.py b/resources/session03/learning_journal/learning_journal/models.py new file mode 100644 index 00000000..7afb0ddb --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/models.py @@ -0,0 +1,64 @@ +import datetime +from sqlalchemy import ( + Column, + DateTime, + Index, + Integer, + Text, + Unicode, + UnicodeText, + ) + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + +Index('my_index', MyModel.name, unique=True, mysql_length=255) + + +class Entry(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode(255), unique=True, nullable=False) + body = Column(UnicodeText, default=u'') + created = Column(DateTime, default=datetime.datetime.utcnow) + edited = Column(DateTime, default=datetime.datetime.utcnow) + + @classmethod + def all(cls, session=None): + """return a query with all entries, ordered by creation date reversed + """ + if session is None: + session = DBSession + return session.query(cls).order_by(sa.desc(cls.created)).all() + + @classmethod + def by_id(cls, id, session=None): + """return a single entry identified by id + + If no entry exists with the provided id, return None + """ + if session is None: + session = DBSession + return session.query(cls).get(id) + + + + + diff --git a/resources/session03/learning_journal/learning_journal/scripts/__init__.py b/resources/session03/learning_journal/learning_journal/scripts/__init__.py new file mode 100644 index 00000000..5bb534f7 --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/resources/session03/learning_journal/learning_journal/scripts/initializedb.py b/resources/session03/learning_journal/learning_journal/scripts/initializedb.py new file mode 100644 index 00000000..7dfdece1 --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/scripts/initializedb.py @@ -0,0 +1,40 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models import ( + DBSession, + MyModel, + Base, + ) + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=1) + DBSession.add(model) diff --git a/resources/session03/learning_journal/learning_journal/static/pyramid-16x16.png b/resources/session03/learning_journal/learning_journal/static/pyramid-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..979203112e76ba4cfdb8cd6f108f4275e987d99a GIT binary patch literal 1319 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xd_B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxR5#hc&_u!9QqR!T z(8R(}N5ROz&{*HVSl`fC*U-qyz|zXlQ~?TIxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr7KMf`)Hma%LWg zuL;)R>ucqiS6q^qmz?V9Vygr+LN7Bj#mdRl*wM}0&C<--)xyxw)!5S9(bdAp)YZVy zz`)Yd%><^`B|o_|H#M&WrZ)wl*Ab^)P+G_>0NU)5T9jFqn&MWJpQ`}&vsET;x0vHJ z52`l>w_7Z5>eUB2MjsTjNHGl)0wy026P|8?9C*r4%>yR)B4E1CQ{KVEz`!`m)5S5Q z;#SXOUk#T)k>lxKj4~&v95M;sSJp8lNikLV)bX~~P1`qaNLZPZ<6^QgASli8jmk<% zZdJ~1%}JBkHhu^^q;ghbe{lMjv_0X)ug#yI+xz_S{hiM(g*saf_f6Pt#!wis>2u+! z{;RjXUlmP?^Kq|yJMn}2uJ~!L3EU-*{+zn6Ya6$jYueOB4G#Lx+=R;oM4sqe*Sq0$ zQ2Nf{-BFQxlX@Dog;`l=SkyQh`W)n22F}BoCTQYYC=p8g*kiK(1`FEnD$cY`NZX8<>GJicU)2$Vj5iRl-7k*od z3K)m{Gsp@4JM)eDo7z=gtG;>hu{QvcXxLU8eD@D+$BKJ@RM`zyYKvG z-8a3ur|aAMt1Vr-yH|CEDauQLkO+_f002lzQdIf%fB4Ui0QY*V)U3(^0FZ<%L_`#& zL`1-fj&^1i)}{b}Bq%eIA9uoG!laCfFcwoR zb`OTl9xm%u?u}UK6Z+-0LfvF1uNzRlu;BSs+a-xXQEAzvn#Z125}lrEE$o@!cYog? z@lko^ANF`uyQDsu%o2*s(%P^-sbKEJ1>90;Svb?qHr@sbgo4>hFv21pO(baNe1U?G_am z$%uaYhJupCKc=2k-Lpftu1m0%A~@dHZKRf6W*s6Qm&D`7K|3 zP8#?(KABe7=AZNd-k*6CTcqHJ?f3yA6ws8mf*wHc;}7VpNW)zn=9RJ4PSI>0zxN+V zk#)jtw`7ILRrYRCqD>sB@)+LaZvtH~TpCmeT z5;T(}&;kNeCnT`+Is{plpj-ki?E!QC9#b�i5=5IxreNAbVsKKM4p@aIXvt)VjX~ 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 +

{{ entry.title }}

+
+

{{ entry.body }}

+
+

Created {{entry.created}}

+ +

Go Back

+{% endblock %} diff --git a/resources/session03/learning_journal/learning_journal/templates/edit.jinja2 b/resources/session03/learning_journal/learning_journal/templates/edit.jinja2 new file mode 100644 index 00000000..ebe0f6b9 --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/templates/edit.jinja2 @@ -0,0 +1,17 @@ +{% extends "templates/layout.jinja2" %} +{% block body %} +

Create a Journal Entry

+
+{% for field in form %} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +

{{ field.label }}: {{ field }}

+{% endfor %} +

+
+{% endblock %} diff --git a/resources/session03/learning_journal/learning_journal/templates/layout.jinja2 b/resources/session03/learning_journal/learning_journal/templates/layout.jinja2 new file mode 100644 index 00000000..52e9047f --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/templates/layout.jinja2 @@ -0,0 +1,29 @@ + + + + + Python Learning Journal + + + + +
+ +
+
+

My Python Journal

+
+ {% block body %}{% endblock %} +
+
+
+

Created in the UW PCE Python Certificate Program

+
+ + diff --git a/resources/session03/learning_journal/learning_journal/templates/list.jinja2 b/resources/session03/learning_journal/learning_journal/templates/list.jinja2 new file mode 100644 index 00000000..09c835a8 --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/templates/list.jinja2 @@ -0,0 +1,16 @@ +{% extends "layout.jinja2" %} +{% block body %} +{% if entries %} +

Journal Entries

+ +{% else %} +

This journal is empty

+{% endif %} +

New Entry

+{% endblock %} diff --git a/resources/session03/learning_journal/learning_journal/templates/mytemplate.pt b/resources/session03/learning_journal/learning_journal/templates/mytemplate.pt new file mode 100644 index 00000000..9e88dc4b --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/templates/mytemplate.pt @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid Alchemy scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework 1.5.2.

+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/resources/session03/learning_journal/learning_journal/tests.py b/resources/session03/learning_journal/learning_journal/tests.py new file mode 100644 index 00000000..4fc444a6 --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/tests.py @@ -0,0 +1,55 @@ +import unittest +import transaction + +from pyramid import testing + +from .models import DBSession + + +class TestMyViewSuccessCondition(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=55) + DBSession.add(model) + + def tearDown(self): + DBSession.remove() + testing.tearDown() + + def test_passing_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'learning_journal') + + +class TestMyViewFailureCondition(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + + def tearDown(self): + DBSession.remove() + testing.tearDown() + + def test_failing_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info.status_int, 500) diff --git a/resources/session03/learning_journal/learning_journal/views.py b/resources/session03/learning_journal/learning_journal/views.py new file mode 100644 index 00000000..ad76afb5 --- /dev/null +++ b/resources/session03/learning_journal/learning_journal/views.py @@ -0,0 +1,43 @@ +from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid.view import view_config + +from .models import ( + DBSession, + MyModel, + Entry, + ) + +from .forms import EntryCreateForm + + +@view_config(route_name='home', renderer='templates/list.jinja2') +def index_page(request): + entries = Entry.all() + return {'entries': entries} + + +@view_config(route_name='detail', renderer='templates/detail.jinja2') +def view(request): + this_id = request.matchdict.get('id', -1) + entry = Entry.by_id(this_id) + if not entry: + return HTTPNotFound() + return {'entry': entry} + + +@view_config(route_name='action', match_param='action=create', + renderer='templates/edit.jinja2') +def create(request): + entry = Entry() + form = EntryCreateForm(request.POST) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + DBSession.add(entry) + return HTTPFound(location=request.route_url('home')) + return {'form': form, 'action': request.matchdict.get('action')} + + +@view_config(route_name='action', match_param='action=edit', + renderer='string') +def update(request): + return 'edit page' diff --git a/resources/session03/learning_journal/ljshell.py b/resources/session03/learning_journal/ljshell.py new file mode 100644 index 00000000..9be82c72 --- /dev/null +++ b/resources/session03/learning_journal/ljshell.py @@ -0,0 +1,17 @@ +import os +import sys + +from pyramid.paster import ( + get_appsettings, + setup_logging, +) +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker + + +config_uri = 'development.ini' +setup_logging(config_uri) +settings = get_appsettings(config_uri) +engine = engine_from_config(settings, 'sqlalchemy.') +Session = sessionmaker(bind=engine) +session = Session() diff --git a/resources/session03/learning_journal/production.ini b/resources/session03/learning_journal/production.ini new file mode 100644 index 00000000..1db7a630 --- /dev/null +++ b/resources/session03/learning_journal/production.ini @@ -0,0 +1,62 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html +### + +[app:main] +use = egg:learning_journal + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/learning_journal.sqlite + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html +### + +[loggers] +keys = root, learning_journal, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_learning_journal] +level = WARN +handlers = +qualname = learning_journal + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/resources/session03/learning_journal/setup.py b/resources/session03/learning_journal/setup.py new file mode 100644 index 00000000..e4bb0bcd --- /dev/null +++ b/resources/session03/learning_journal/setup.py @@ -0,0 +1,48 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + 'wtforms', + ] + +setup(name='learning_journal', + version='0.0', + description='learning_journal', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + test_suite='learning_journal', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = learning_journal:main + [console_scripts] + initialize_learning_journal_db = learning_journal.scripts.initializedb:main + """, + ) From 1572c7c413aee9cd44d49bb550552dce2567d709 Mon Sep 17 00:00:00 2001 From: cewing Date: Wed, 14 Jan 2015 19:00:25 -0800 Subject: [PATCH 025/173] fix references to 'blog_view' in code examples, we actually called it 'view' --- source/presentations/session02.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/presentations/session02.rst b/source/presentations/session02.rst index 3d1c6b8e..35529bca 100644 --- a/source/presentations/session02.rst +++ b/source/presentations/session02.rst @@ -536,7 +536,7 @@ Next, we want to write the view for a single entry. from pyramid.exceptions import HTTPNotFound # and update this view function: - def blog_view(request): + def view(request): this_id = request.matchdict.get('id', -1) entry = Entry.by_id(this_id) if not entry: @@ -950,7 +950,7 @@ show it. # views.py @view_config(route_name='detail', renderer='templates/detail.jinja2') - def blog_view(request): + def view(request): # ... .. nextslide:: Try It Out From 06d6fdbf6726b0a8e29133d65b8bbf97f2fe93ae Mon Sep 17 00:00:00 2001 From: cewing Date: Wed, 14 Jan 2015 19:03:41 -0800 Subject: [PATCH 026/173] fix import errors by providing the fully canonical import location for http exceptions. The old pyramid.exception location for HTTPNotFound is an artifact of backward compatibility with an earlier version of the package. --- source/presentations/session02.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/presentations/session02.rst b/source/presentations/session02.rst index 35529bca..f141f8a8 100644 --- a/source/presentations/session02.rst +++ b/source/presentations/session02.rst @@ -533,7 +533,7 @@ Next, we want to write the view for a single entry. .. code-block:: python # add this import at the top - from pyramid.exceptions import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound # and update this view function: def view(request): @@ -1348,7 +1348,7 @@ Next, we need to add a new view that uses this form to create a new entry. .. code-block:: python # add these imports - from pyramid.exceptions import HTTPFound + from pyramid.httpexceptions import HTTPFound from .forms import EntryCreateForm # and update this view function From 749ce67d1adffd7ef2e352932bc71b8f899d6009 Mon Sep 17 00:00:00 2001 From: cewing Date: Wed, 14 Jan 2015 20:09:09 -0800 Subject: [PATCH 027/173] complete session 4 re-write and add resources to support homework and in-class activities --- resources/session04/echo_client.py | 42 +++ resources/session04/echo_server.py | 69 +++++ resources/session04/tasks.txt | 49 ++++ resources/session04/tests.py | 123 +++++++++ source/presentations/session04.rst | 421 ++++++++++++++++------------- 5 files changed, 512 insertions(+), 192 deletions(-) create mode 100644 resources/session04/echo_client.py create mode 100644 resources/session04/echo_server.py create mode 100644 resources/session04/tasks.txt create mode 100644 resources/session04/tests.py diff --git a/resources/session04/echo_client.py b/resources/session04/echo_client.py new file mode 100644 index 00000000..02d42eaf --- /dev/null +++ b/resources/session04/echo_client.py @@ -0,0 +1,42 @@ +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) diff --git a/resources/session04/echo_server.py b/resources/session04/echo_server.py new file mode 100644 index 00000000..91f25e89 --- /dev/null +++ b/resources/session04/echo_server.py @@ -0,0 +1,69 @@ +import socket +import sys + + +def server(log_buffer=sys.stderr): + # set an address for our server + address = ('127.0.0.1', 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 + # TODO: Set an option to allow the socket address to be reused immediately + # see the end of http://docs.python.org/2/library/socket.html + + # log that we are building a server + print >>log_buffer, "making a server on {0}:{1}".format(*address) + + # TODO: bind your new sock 'sock' to the address above and begin to listen + # for incoming connections + + try: + # the outer loop controls the creation of new connection sockets. The + # server will handle each incoming connection one at a time. + while True: + print >>log_buffer, 'waiting for a connection' + + # TODO: make a new socket when a client connects, call it 'conn', + # at the same time you should be able to get the address of + # the client so we can report it below. Replace the + # following line with your code. It is only here to prevent + # syntax errors + addr = ('bar', 'baz') + try: + print >>log_buffer, 'connection - {0}:{1}'.format(*addr) + + # the inner loop will receive messages sent by the client in + # buffers. When a complete message has been received, the + # loop will exit + while True: + # TODO: receive 16 bytes of data from the client. Store + # the data you receive as 'data'. Replace the + # following line with your code. It's only here as + # a placeholder to prevent an error in string + # formatting + data = '' + print >>log_buffer, 'received "{0}"'.format(data) + # TODO: you will need to check here to see if any data was + # received. If so, send the data you got back to + # the client. If not, exit the inner loop and wait + # for a new connection from a client + + finally: + # TODO: When the inner loop exits, this 'finally' clause will + # be hit. Use that opportunity to close the socket you + # created above when a client connected. Replace the + # call to `pass` below, which is only there to prevent + # syntax problems + pass + + except KeyboardInterrupt: + # TODO: Use the python KeyboardIntterupt exception as a signal to + # close the server socket and exit from the server function. + # Replace the call to `pass` below, which is only there to + # prevent syntax problems + pass + + +if __name__ == '__main__': + server() + sys.exit(0) diff --git a/resources/session04/tasks.txt b/resources/session04/tasks.txt new file mode 100644 index 00000000..16849442 --- /dev/null +++ b/resources/session04/tasks.txt @@ -0,0 +1,49 @@ +Session 1 Homework +================== + +Required Tasks: +--------------- + +* Complete the code in ``echo_server.py`` to create a server that sends back + whatever messages it receives from a client + +* Complete the code in ``echo_client.py`` to create a client function that + can send a message and receive a reply. + +* Ensure that the tests in ``tests.py`` pass. + +To run the tests: + +* Open one terminal while in this folder and execute this command: + + $ python echo_server.py + +* Open a second terminal in this same folder and execute this command: + + $ python tests.py + + + + +Optional Tasks: +--------------- + +* Write a python function that lists the services provided by a given range of + ports. + + * accept the lower and upper bounds as arguments + * provide sensible defaults + * Ensure that it only accepts valid port numbers (0-65535) + +* The echo server as outlined will only process a connection from one client + at a time. If a second client were to attempt a connection, it would have to + wait until the first message was fully echoed before it could be dealt with. + + Python provides a module called `select` that allows waiting for I/O events + in order to control flow. The `select.select` method can be used to allow + our echo server to handle more than one incoming connection in "parallel". + + Read the documentation about the `select` module + (http://docs.python.org/2/library/select.html) and attempt to write a second + version of the echo server that can handle multiple client connections in + "parallel". You do not need to invoke threading of any kind to do this. diff --git a/resources/session04/tests.py b/resources/session04/tests.py new file mode 100644 index 00000000..d0d4005a --- /dev/null +++ b/resources/session04/tests.py @@ -0,0 +1,123 @@ +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/source/presentations/session04.rst b/source/presentations/session04.rst index 3cf6f66f..62570249 100644 --- a/source/presentations/session04.rst +++ b/source/presentations/session04.rst @@ -419,7 +419,7 @@ Families defined in the ``socket`` library are prefixed by ``AF_``: .. container:: .. code-block:: pycon - + >>> families = get_constants('AF_') >>> families {0: 'AF_UNSPEC', 1: 'AF_UNIX', 2: 'AF_INET', @@ -474,7 +474,7 @@ The socket *type* determines the semantics of socket communications. Look up socket type constants with the ``SOCK_`` prefix: .. code-block:: pycon - + >>> types = get_constants('SOCK_') >>> types {1: 'SOCK_STREAM', 2: 'SOCK_DGRAM', @@ -529,7 +529,7 @@ arguments you may pass to the socket constructor. profiles: .. code-block:: pycon - + >>> bar = socket.socket(socket.AF_INET, ... socket.SOCK_DGRAM, ... socket.IPPROTO_UDP) @@ -544,20 +544,19 @@ Break Time So far we have: .. rst-class:: build +.. container:: -* learned about the "layers" of the TCP/IP Stack -* discussed *families*, *types* and *protocols* in sockets -* learned how to create sockets with a specific communications profile. - -.. rst-class:: build + .. rst-class:: build -When we return we'll learn how to find the communcations profiles of remote -sockets, how to connect to them, and how to send and receive messages. + * learned about the "layers" of the TCP/IP Stack + * discussed *families*, *types* and *protocols* in sockets + * learned how to create sockets with a specific communications profile. -.. rst-class:: build + When we return we'll learn how to find the communcations profiles of remote + sockets, how to connect to them, and how to send and receive messages. -Take a few minutes now to clear your head (do not quit your python -interpreter). + Take a few minutes now to clear your head (do not quit your python + interpreter). Address Information @@ -567,51 +566,45 @@ When you are creating a socket to communicate with a remote service, the remote socket will have a specific communications profile. .. rst-class:: build +.. container:: -The local socket you create must match that communications profile. - -.. rst-class:: build - -How can you determine the *correct* values to use? + The local socket you create must match that communications profile. -.. rst-class:: build center + How can you determine the *correct* values to use? -You ask. + .. rst-class:: centered + **You ask.** -Address Information -------------------- +.. nextslide:: The function ``socket.getaddrinfo`` provides information about available connections on a given host. .. code-block:: python - :class: small socket.getaddrinfo('127.0.0.1', 80) .. rst-class:: build +.. container:: -This provides all you need to make a proper connection to a socket on a remote -host. The value returned is a tuple of: + This provides all you need to make a proper connection to a socket on a + remote host. The value returned is a tuple of: -.. rst-class:: build + .. rst-class:: build -* socket family -* socket type -* socket protocol -* canonical name (usually empty, unless requested by flag) -* socket address (tuple of IP and Port) + * socket family + * socket type + * socket protocol + * canonical name (usually empty, unless requested by flag) + * socket address (tuple of IP and Port) -A quick utility method ----------------------- +.. nextslide:: A quick utility method Again, let's create a utility method in-place so we can see this in action: -.. class:: small - -:: +.. code-block:: pycon >>> def get_address_info(host, port): ... for response in socket.getaddrinfo(host, port): @@ -625,36 +618,35 @@ Again, let's create a utility method in-place so we can see this in action: ... >>> -.. class:: small - (you can also find this in ``resources/session01/session1.py``) -On Your Own Machine -------------------- +.. nextslide:: On Your Own Machine -Now, ask your own machine what possible connections are available for 'http':: +Now, ask your own machine what possible connections are available for 'http': - >>> get_address_info(socket.gethostname(), 'http') - family: AF_INET - type: SOCK_DGRAM - protocol: IPPROTO_UDP - canonical name: - socket address: ('10.211.55.2', 80) +.. rst-class:: build +.. container:: - family: AF_INET - ... - >>> + .. code-block:: pycon -.. rst-class:: build + >>> get_address_info(socket.gethostname(), 'http') + family: AF_INET + type: SOCK_DGRAM + protocol: IPPROTO_UDP + canonical name: + socket address: ('10.211.55.2', 80) + + family: AF_INET + ... + >>> -What answers do you get? + What answers do you get? -On the Internet ---------------- +.. nextslide:: On the Internet -:: +.. code-block:: pycon >>> get_address_info('crisewing.com', 'http') family: AF_INET @@ -667,16 +659,22 @@ On the Internet >>> .. rst-class:: build +.. container:: -Try a few other servers you know about. + Try a few other servers you know about. -First Steps ------------ +Client Side +=========== -.. class:: big-centered +.. rst-class:: build +.. container:: + + .. rst-class:: large -Let's put this to use + Let's put this to use + + We'll communicate with a remote server as a *client* Construct a Socket @@ -686,9 +684,7 @@ We've already made a socket ``foo`` using the generic constructor without any arguments. We can make a better one now by using real address information from a real server online [**do not type this yet**]: -.. class:: small - -:: +.. code-block:: pycon >>> streams = [info ... for info in socket.getaddrinfo('crisewing.com', 'http') @@ -703,7 +699,9 @@ Connecting a Socket ------------------- Once the socket is constructed with the appropriate *family*, *type* and -*protocol*, we can connect it to the address of our remote server:: +*protocol*, we can connect it to the address of our remote server: + +.. code-block:: pycon >>> cewing_socket.connect(info[-1]) >>> @@ -721,7 +719,9 @@ Sending a Message ----------------- Send a message to the server on the other end of our connection (we'll -learn in session 2 about the message we are sending):: +learn in session 2 about the message we are sending): + +.. code-block:: pycon >>> msg = "GET / HTTP/1.1\r\n" >>> msg += "Host: crisewing.com\r\n\r\n" @@ -746,14 +746,16 @@ Receiving a Reply ----------------- Whatever reply we get is received by the socket we created. We can read it -back out (again, **do not type this yet**):: +back out (again, **do not type this yet**): + +.. code-block:: pycon >>> response = cewing_socket.recv(4096) >>> response 'HTTP/1.1 200 OK\r\nDate: Thu, 03 Jan 2013 05:56:53 ... -.. rst-class:: build small +.. rst-class:: build * The sole required argument is ``buffer_size`` (an integer). It should be a power of 2 and smallish (~4096) @@ -776,9 +778,7 @@ Putting it all together First, connect and send a message: -.. class:: small - -:: +.. code-block:: pycon >>> streams = [info ... for info in socket.getaddrinfo('crisewing.com', 'http') @@ -791,12 +791,11 @@ First, connect and send a message: >>> cewing_socket.sendall(msg) -Putting it all together ------------------------ +.. nextslide:: Then, receive a reply, iterating until it is complete: -:: +.. code-block:: pycon >>> buffsize = 4096 >>> response = '' @@ -813,21 +812,29 @@ Then, receive a reply, iterating until it is complete: Server Side ------------ +=========== + +.. rst-class:: build +.. container:: + + .. rst-class:: large -.. class:: big-centered + What about the other half of the equation? -What about the other half of the equation? + Let's build a server and see how that part works. Construct a Socket ------------------ **For the moment, stop typing this into your interpreter.** -.. container:: incremental +.. rst-class:: build +.. container:: Again, we begin by constructing a socket. Since we are actually the server - this time, we get to choose family, type and protocol:: + this time, we get to choose family, type and protocol: + + .. code-block:: pycon >>> server_socket = socket.socket( ... socket.AF_INET, @@ -841,23 +848,27 @@ Construct a Socket Bind the Socket --------------- -Our server socket needs to be bound to an address. This is the IP Address and -Port to which clients must connect:: - - >>> address = ('127.0.0.1', 50000) - >>> server_socket.bind(address) +Our server socket needs to be **bound** to an address. This is the IP Address +and Port to which clients must connect: .. rst-class:: build +.. container:: + + .. code-block:: pycon -**Terminology Note**: In a server/client relationship, the server *binds* to -an address and port. The client *connects* + >>> address = ('127.0.0.1', 50000) + >>> server_socket.bind(address) + **Terminology Note**: In a server/client relationship, the server *binds* + to an address and port. The client *connects* Listen for Connections ---------------------- Once our socket is bound to an address, we can listen for attempted -connections:: +connections: + +.. code-block:: pycon >>> server_socket.listen(1) @@ -874,7 +885,9 @@ connections:: Accept Incoming Messages ------------------------ -When a socket is listening, it can receive incoming connection requests:: +When a socket is listening, it can receive incoming connection requests: + +.. code-block:: pycon >>> connection, client_address = server_socket.accept() ... # this blocks until a client connects @@ -896,7 +909,9 @@ Send a Reply ------------ The same socket that received a message from the client may be used to return -a reply:: +a reply: + +.. code-block:: pycon >>> connection.sendall("message received") @@ -905,63 +920,82 @@ Clean Up -------- Once a transaction between the client and server is complete, the -``connection`` socket should be closed:: - - >>> connection.close() +``connection`` socket should be closed: .. rst-class:: build +.. container:: + + .. code-block:: pycon -Note that the ``server_socket`` is *never* closed as long as the server -continues to run. + >>> connection.close() + + Note that the ``server_socket`` is *never* closed as long as the server + continues to run. Getting the Flow ----------------- +================ -The flow of this interaction can be a bit confusing. Let's see it in action -step-by-step. +.. rst-class:: left +.. container:: -.. rst-class:: build -Open a second python interpreter and place it next to your first so you can -see both of them at the same time. + + The flow of this interaction can be a bit confusing. Let's see it in + action step-by-step. + + .. rst-class:: build + .. container:: + + .. container:: + + Open a second python interpreter and place it next to your first so + you can see both of them at the same time. Create a Server --------------- In your first python interpreter, create a server socket and prepare it for -connections:: - - >>> server_socket = socket.socket( - ... socket.AF_INET, - ... socket.SOCK_STREAM, - ... socket.IPPROTO_IP) - >>> server_socket.bind(('127.0.0.1', 50000)) - >>> server_socket.listen(1) - >>> conn, addr = server_socket.accept() +connections: .. rst-class:: build +.. container:: + + .. code-block:: pycon + + >>> server_socket = socket.socket( + ... socket.AF_INET, + ... socket.SOCK_STREAM, + ... socket.IPPROTO_IP) + >>> server_socket.bind(('127.0.0.1', 50000)) + >>> server_socket.listen(1) + >>> conn, addr = server_socket.accept() -At this point, you should **not** get back a prompt. The server socket is -waiting for a connection to be made. + At this point, you should **not** get back a prompt. The server socket is + waiting for a connection to be made. Create a Client --------------- In your second interpreter, create a client socket and prepare to send a -message:: +message: + +.. rst-class:: build +.. container:: + + .. code-block:: pycon - >>> import socket - >>> client_socket = socket.socket( - ... socket.AF_INET, - ... socket.SOCK_STREAM, - ... socket.IPPROTO_IP) + >>> import socket + >>> client_socket = socket.socket( + ... socket.AF_INET, + ... socket.SOCK_STREAM, + ... socket.IPPROTO_IP) -.. container:: incremental + Before connecting, keep your eye on the server interpreter: - Before connecting, keep your eye on the server interpreter:: + .. code-block:: pycon >>> client_socket.connect(('127.0.0.1', 50000)) @@ -974,128 +1008,132 @@ return in your server interpreter. The ``accept`` method finally returned a new connection socket. .. rst-class:: build +.. container:: -When you're ready, type the following in the *client* interpreter. - -.. rst-class:: build + When you're ready, type the following in the *client* interpreter: -:: + .. code-block:: pycon - >>> client_socket.sendall("Hey, can you hear me?") + >>> client_socket.sendall("Hey, can you hear me?") Receive and Respond ------------------- Back in your server interpreter, go ahead and receive the message from your -client:: +client: + +.. rst-class:: build +.. container:: + + .. code-block:: pycon + + >>> conn.recv(32) + 'Hey, can you hear me?' - >>> conn.recv(32) - 'Hey, can you hear me?' + Send a message back, and then close up your connection: -Send a message back, and then close up your connection:: + .. code-block:: pycon - >>> conn.sendall("Yes, I hear you.") - >>> conn.close() + >>> conn.sendall("Yes, I hear you.") + >>> conn.close() Finish Up --------- Back in your client interpreter, take a look at the response to your message, -then be sure to close your client socket too:: +then be sure to close your client socket too: - >>> client_socket.recv(32) - 'Yes, I hear you.' - >>> client_socket.close() +.. rst-class:: build +.. container:: -And now that we're done, we can close up the server too (back in the server -interpreter):: + .. code-block:: pycon - >>> server_socket.close() + >>> client_socket.recv(32) + 'Yes, I hear you.' + >>> client_socket.close() + And now that we're done, we can close up the server too (back in the server + interpreter): -Congratulations! ----------------- + .. code-block:: pycon -.. class:: big-centered + >>> server_socket.close() -You've run your first client-server interaction +.. nextslide:: Congratulations! -Homework --------- +.. rst-class:: large center -Your homework assignment for this week is to take what you've learned here -and build a simple "echo" server. +You've run your first client-server interaction -.. rst-class:: build -The server should automatically return to any client that connects *exactly* -what it receives (it should **echo** all messages). +Homework +======== -.. rst-class:: build +.. rst-class:: left +.. container:: -You will also write a python script that, when run, will send a message to the -server and receive the reply, printing it to ``stdout``. + Your homework assignment for this week is to take what you've learned here + and build a simple "echo" server. -.. rst-class:: build + .. rst-class:: build + .. container:: -Finally, you'll do all of this so that it can be tested. + The server should automatically return to any client that connects *exactly* + what it receives (it should **echo** all messages). + You will also write a python script that, when run, will send a message to the + server and receive the reply, printing it to ``stdout``. -What You Have -------------- + Finally, you'll do all of this so that it can be tested. -In our class repository, there is a folder ``assignments/session01``. -.. rst-class:: build +Your Task +--------- -Inside that folder, you should find: +In our class repository, there is a folder ``resources/session04``. .. rst-class:: build +.. container:: -* A file ``tasks.txt`` that contains these instructions + Inside that folder, you should find: -* A skeleton for your server in ``echo_server.py`` + .. rst-class:: build -* A skeleton for your client script in ``echo_client.py`` + * A file ``tasks.txt`` that contains these instructions -* Some simple tests in ``tests.py`` + * A skeleton for your server in ``echo_server.py`` -.. rst-class:: build + * A skeleton for your client script in ``echo_client.py`` + + * Some simple tests in ``tests.py`` -Your task is to make the tests pass. + Your task is to make the tests pass. -Running the tests +Running the Tests ----------------- To run the tests, you'll have to set the server running in one terminal: -.. class:: small - -:: +.. rst-class:: build +.. container:: - $ python echo_server.py + .. code-block:: bash -.. container:: incremental + $ python echo_server.py Then, in a second terminal, you will execute the tests: - .. class:: small - - :: + .. code-block:: bash $ python tests.py -.. container:: incremental - You should see output like this: - .. class:: small - - :: + .. code-block:: bash [...] FAILED (failures=2) @@ -1107,21 +1145,21 @@ Submitting Your Homework To submit your homework: .. rst-class:: build +.. container:: -* In github, make a fork of my repository into *your* account. + .. rst-class:: build -* Clone your fork of my repository to your computer. + * Create a new repository in GitHub. Call it ``echo_sockets``. -* Do your work in the ``assignments/session01/`` folder on your computer and - commit your changes to your fork. + * Put the ``echo_server.py``, ``echo_client.py`` and ``tests.py`` files in + this repository. -* When you are finished and your tests are passing, you will open a pull - request in github.com from your fork to mine. + * Send Maria and I an email with a link to your repository when you are + done. -.. rst-class:: build + We will clone your repository and run the tests as described above. -I will review your work when I receive your pull requests, make comments on it -there, and then close the pull request. + And we'll make comments inline on your repository. Going Further @@ -1130,11 +1168,10 @@ Going Further In ``assignments/session01/tasks.txt`` you'll find a few extra problems to try. .. rst-class:: build +.. container:: -If you finish the first part of the homework in less than 3-4 hours give one -or more of these a whirl. - -.. rst-class:: build + If you finish the first part of the homework in less than 3-4 hours give + one or more of these a whirl. -They are not required, but if you include solutions in your pull request, I'll -review your work. + They are not required, but if you include solutions in your repository, + we'll review your work. From 5eba4b98e7cd421438f8a419aac3f51d8233a6c7 Mon Sep 17 00:00:00 2001 From: cewing Date: Fri, 16 Jan 2015 18:11:10 -0800 Subject: [PATCH 028/173] update task list for formatting --- resources/session04/tasks.txt | 2 +- source/presentations/{session02.rst.norender => session05.rst} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename source/presentations/{session02.rst.norender => session05.rst} (100%) diff --git a/resources/session04/tasks.txt b/resources/session04/tasks.txt index 16849442..3ceefcc6 100644 --- a/resources/session04/tasks.txt +++ b/resources/session04/tasks.txt @@ -1,4 +1,4 @@ -Session 1 Homework +Session 4 Homework ================== Required Tasks: diff --git a/source/presentations/session02.rst.norender b/source/presentations/session05.rst similarity index 100% rename from source/presentations/session02.rst.norender rename to source/presentations/session05.rst From 69cd1448cc4ded8d531fa42e8017d94fa4d5dff6 Mon Sep 17 00:00:00 2001 From: cewing Date: Fri, 16 Jan 2015 18:12:01 -0800 Subject: [PATCH 029/173] add materials for new session 5 --- resources/session05/http_server.py | 40 + resources/session05/simple_client.py | 37 + resources/session05/tests.py | 147 +++ source/presentations/session05.rst | 1285 ++++++++++++-------------- 4 files changed, 824 insertions(+), 685 deletions(-) create mode 100644 resources/session05/http_server.py create mode 100644 resources/session05/simple_client.py create mode 100644 resources/session05/tests.py diff --git a/resources/session05/http_server.py b/resources/session05/http_server.py new file mode 100644 index 00000000..bfda1f98 --- /dev/null +++ b/resources/session05/http_server.py @@ -0,0 +1,40 @@ +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 >>log_buffer, "making a server on {0}:{1}".format(*address) + sock.bind(address) + sock.listen(1) + + try: + while True: + print >>log_buffer, 'waiting for a connection' + conn, addr = sock.accept() # blocking + try: + print >>log_buffer, 'connection - {0}:{1}'.format(*addr) + while True: + data = conn.recv(16) + print >>log_buffer, 'received "{0}"'.format(data) + if data: + msg = 'sending data back to client' + print >>log_buffer, msg + conn.sendall(data) + else: + msg = 'no more data from {0}:{1}'.format(*addr) + print >>log_buffer, msg + break + finally: + conn.close() + + except KeyboardInterrupt: + sock.close() + return + + +if __name__ == '__main__': + server() + sys.exit(0) diff --git a/resources/session05/simple_client.py b/resources/session05/simple_client.py new file mode 100644 index 00000000..af7d548c --- /dev/null +++ b/resources/session05/simple_client.py @@ -0,0 +1,37 @@ +import socket +import sys + + +def client(msg): + server_address = ('localhost', 10000) + sock = socket.socket( + socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP + ) + print >>sys.stderr, 'connecting to {0} port {1}'.format(*server_address) + sock.connect(server_address) + response = '' + done = False + bufsize = 1024 + try: + print >>sys.stderr, 'sending "{0}"'.format(msg) + sock.sendall(msg) + while not done: + chunk = sock.recv(bufsize) + if len(chunk) < bufsize: + done = True + response += chunk + print >>sys.stderr, 'received "{0}"'.format(response) + finally: + print >>sys.stderr, 'closing socket' + sock.close() + return response + + +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) diff --git a/resources/session05/tests.py b/resources/session05/tests.py new file mode 100644 index 00000000..46624d8f --- /dev/null +++ b/resources/session05/tests.py @@ -0,0 +1,147 @@ +import mimetypes +import socket +import unittest + + +CRLF = '\r\n' +KNOWN_TYPES = set(mimetypes.types_map.values()) + + +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 = ok.split(CRLF)[0].split(' ', 1)[1].strip() + self.assertEqual(expected, actual) + + def test_response_method(self): + ok = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = ok.split(CRLF)[0].split(' ', 1)[0].strip() + self.assertEqual(expected, actual) + + def test_response_has_content_type_header(self): + ok = self.call_function_under_test() + headers = ok.split(CRLF+CRLF, 1)[0].split(CRLF)[1:] + expected_name = 'content-type' + has_header = False + for header in headers: + name, value = header.split(':') + 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 = ok.split(CRLF+CRLF, 1)[0].split(CRLF)[1:] + expected_name = 'content-type' + for header in headers: + name, value = header.split(':') + 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 = resp.split(CRLF)[0].split(' ', 1)[1].strip() + self.assertEqual(expected, actual) + + def test_response_method(self): + resp = self.call_function_under_test() + expected = 'HTTP/1.1' + actual = resp.split(CRLF)[0].split(' ', 1)[0].strip() + self.assertEqual(expected, 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): + request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + try: + self.call_function_under_test(request) + except (NotImplementedError, Exception), e: + self.fail('GET method raises an error {0}'.format(str(e))) + + def test_bad_http_methods(self): + 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, 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/source/presentations/session05.rst b/source/presentations/session05.rst index 1ab82949..f18ed3aa 100644 --- a/source/presentations/session05.rst +++ b/source/presentations/session05.rst @@ -1,37 +1,44 @@ +********************** Python Web Programming -====================== +********************** -.. image:: img/protocol.png - :align: left - :width: 45% +.. figure:: /_static/protocol.png + :align: center + :width: 40% -Session 2: Web Protocols + **Session 2: Web Protocols** -.. class:: intro-blurb +The Languages Computers Speak +============================= -Wherein we learn about the languages that machines speak to each other +.. rst-class:: build left +.. container:: + Programming languages like Python are the languages we speak to computers. -But First ---------- + *Protocols* are the languages that computers speak to each-other. -.. class:: big-centered + This sesson we'll look at a few of them and -Some boring business of identification + .. rst-class:: build + * Learn what makes them similar + * Learn what makes them different + * Learn about Python's tools for speaking them + * Learn how to speak one (HTTP) ourselves -But Second + +But First ---------- -.. class:: big-centered +.. rst-class:: large centered Questions from the Homework? -And Third ---------- +.. nextslide:: -.. class:: big-centered +.. rst-class:: large centered Examples of an echo server using ``select`` @@ -39,21 +46,19 @@ Examples of an echo server using ``select`` What is a Protocol? ------------------- -.. class:: incremental center +.. rst-class:: build large centered +.. container:: -a set of rules or conventions + **a set of rules or conventions** -.. class:: incremental center + **governing communications** -governing communications - -Protocols IRL -------------- +.. nextslide:: Protocols IRL Life has lots of sets of rules for how to do things. -.. class:: incremental +.. rst-class:: build * What do you say when you get on the elevator? @@ -66,24 +71,20 @@ Life has lots of sets of rules for how to do things. * ...? -Protocols IRL -------------- +.. nextslide:: Protocols IRL -.. image:: img/icup.png +.. figure:: /_static/icup.png :align: center - :width: 58% - -.. class:: image-credit + :width: 65% -http://blog.xkcd.com/2009/09/02/urinal-protocol-vulnerability/ + http://blog.xkcd.com/2009/09/02/urinal-protocol-vulnerability/ -Protocols In Computers ----------------------- +.. nextslide:: Protocols In Computers Digital life has lots of rules too: -.. class:: incremental +.. rst-class:: build * how to say hello @@ -99,15 +100,9 @@ Digital life has lots of rules too: Real Protocol Examples ---------------------- -.. class:: big-centered - What does this look like in practice? - -Real Protocol Examples ----------------------- - -.. class:: incremental +.. rst-class:: build * SMTP (Simple Message Transfer Protocol) http://tools.ietf.org/html/rfc5321#appendix-D @@ -122,51 +117,53 @@ Real Protocol Examples http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol +SMTP +---- + What does SMTP look like? -------------------------- -SMTP (Say hello and identify yourself):: +.. rst-class:: build +.. container:: - S: 220 foo.com Simple Mail Transfer Service Ready - C: EHLO bar.com - S: 250-foo.com greets bar.com - S: 250-8BITMIME - S: 250-SIZE - S: 250-DSN - S: 250 HELP + SMTP (Say hello and identify yourself):: + S (<--): 220 foo.com Simple Mail Transfer Service Ready + C (-->): EHLO bar.com + S (<--): 250-foo.com greets bar.com + S (<--): 250-8BITMIME + S (<--): 250-SIZE + S (<--): 250-DSN + S (<--): 250 HELP -What does SMTP look like? -------------------------- -SMTP (Ask for information, provide answers):: +.. nextslide:: - C: MAIL FROM: - S: 250 OK - C: RCPT TO: - S: 250 OK - C: RCPT TO: - S: 550 No such user here - C: DATA - S: 354 Start mail input; end with . - C: Blah blah blah... - C: ...etc. etc. etc. - C: . - S: 250 OK +SMTP (Ask for information, provide answers):: -What does SMTP look like? -------------------------- + C (-->): MAIL FROM: + S (<--): 250 OK + C (-->): RCPT TO: + S (<--): 250 OK + C (-->): RCPT TO: + S (<--): 550 No such user here + C (-->): DATA + S (<--): 354 Start mail input; end with . + C (-->): Blah blah blah... + C (-->): ...etc. etc. etc. + C (-->): . + S (<--): 250 OK + +.. nextslide:: SMTP (Say goodbye):: - C: QUIT - S: 221 foo.com Service closing transmission channel + C (-->): QUIT + S (<--): 221 foo.com Service closing transmission channel -SMTP Characteristics --------------------- +.. nextslide:: SMTP Characteristics -.. class:: incremental +.. rst-class:: build * Interaction consists of commands and replies * Each command or reply is *one line* terminated by @@ -175,241 +172,241 @@ SMTP Characteristics * Each reply has a formal *code* and an informal *explanation* +POP3 +---- + What does POP3 look like? -------------------------- -POP3 (Say hello and identify yourself):: +.. rst-class:: build +.. container:: - C: - S: +OK POP3 server ready <1896.6971@mailgate.dobbs.org> - C: USER bob - S: +OK bob - C: PASS redqueen - S: +OK bob's maildrop has 2 messages (320 octets) + POP3 (Say hello and identify yourself):: + C (-->): + S (<--): +OK POP3 server ready <1896.6971@mailgate.dobbs.org> + C (-->): USER bob + S (<--): +OK bob + C (-->): PASS redqueen + S (<--): +OK bob's maildrop has 2 messages (320 octets) -What does POP3 look like? -------------------------- + +.. nextslide:: POP3 (Ask for information, provide answers):: - C: STAT - S: +OK 2 320 - C: LIST - S: +OK 1 messages (120 octets) - S: 1 120 - S: . + C (-->): STAT + S (<--): +OK 2 320 + C (-->): LIST + S (<--): +OK 1 messages (120 octets) + S (<--): 1 120 + S (<--): . -What does POP3 look like? -------------------------- +.. nextslide:: POP3 (Ask for information, provide answers):: - C: RETR 1 - S: +OK 120 octets - S: - S: . - C: DELE 1 - S: +OK message 1 deleted + C (-->): RETR 1 + S (<--): +OK 120 octets + S (<--): + S (<--): . + C (-->): DELE 1 + S (<--): +OK message 1 deleted -What does POP3 look like? -------------------------- +.. nextslide:: POP3 (Say goodbye):: - C: QUIT - S: +OK dewey POP3 server signing off (maildrop empty) - C: + C (-->): QUIT + S (<--): +OK dewey POP3 server signing off (maildrop empty) + C (-->): -POP3 Characteristics --------------------- +.. nextslide:: POP3 Characteristics -.. class:: incremental +.. rst-class:: build +.. container:: -* Interaction consists of commands and replies -* Each command or reply is *one line* terminated by -* The exception is message payload, terminated by . -* Each command has a *verb* and one or more *arguments* -* Each reply has a formal *code* and an informal *explanation* + .. rst-class:: build -.. class:: incremental + * Interaction consists of commands and replies + * Each command or reply is *one line* terminated by + * The exception is message payload, terminated by . + * Each command has a *verb* and one or more *arguments* + * Each reply has a formal *code* and an informal *explanation* -The codes don't really look the same, though, do they? + The codes don't really look the same, though, do they? -One Other Difference --------------------- +.. nextslide:: One Other Difference The exception to the one-line-per-message rule is *payload* -.. class:: incremental +.. rst-class:: build +.. container:: -In both SMTP and POP3 this is terminated by . + In both SMTP and POP3 this is terminated by . -.. class:: incremental + In SMTP, the *client* has this ability -In SMTP, the *client* has this ability - -.. class:: incremental - -But in POP3, it belongs to the *server*. Why? + But in POP3, it belongs to the *server*. Why? +IMAP +---- What does IMAP look like? -------------------------- -IMAP (Say hello and identify yourself):: +.. rst-class:: build +.. container:: - C: - S: * OK example.com IMAP4rev1 v12.264 server ready - C: A0001 USER "frobozz" "xyzzy" - S: * OK User frobozz authenticated + IMAP (Say hello and identify yourself):: + C (-->): + S (<--): * OK example.com IMAP4rev1 v12.264 server ready + C (-->): A0001 USER "frobozz" "xyzzy" + S (<--): * OK User frobozz authenticated -What does IMAP look like? -------------------------- + +.. nextslide:: IMAP (Ask for information, provide answers [connect to an inbox]):: - C: A0002 SELECT INBOX - S: * 1 EXISTS - S: * 1 RECENT - S: * FLAGS (\Answered \Flagged \Deleted \Draft \Seen) - S: * OK [UNSEEN 1] first unseen message in /var/spool/mail/esr - S: A0002 OK [READ-WRITE] SELECT completed + C (-->): A0002 SELECT INBOX + S (<--): * 1 EXISTS + S (<--): * 1 RECENT + S (<--): * FLAGS (\Answered \Flagged \Deleted \Draft \Seen) + S (<--): * OK [UNSEEN 1] first unseen message in /var/spool/mail/esr + S (<--): A0002 OK [READ-WRITE] SELECT completed -What does IMAP look like? -------------------------- +.. nextslide:: IMAP (Ask for information, provide answers [Get message sizes]):: - C: A0003 FETCH 1 RFC822.SIZE - S: * 1 FETCH (RFC822.SIZE 2545) - S: A0003 OK FETCH completed + C (-->): A0003 FETCH 1 RFC822.SIZE + S (<--): * 1 FETCH (RFC822.SIZE 2545) + S (<--): A0003 OK FETCH completed -What does IMAP look like? -------------------------- +.. nextslide:: IMAP (Ask for information, provide answers [Get first message header]):: - C: A0004 FETCH 1 BODY[HEADER] - S: * 1 FETCH (RFC822.HEADER {1425} + C (-->): A0004 FETCH 1 BODY[HEADER] + S (<--): * 1 FETCH (RFC822.HEADER {1425} - S: ) - S: A0004 OK FETCH completed + S (<--): ) + S (<--): A0004 OK FETCH completed -What does IMAP look like? -------------------------- +.. nextslide:: IMAP (Ask for information, provide answers [Get first message body]):: - C: A0005 FETCH 1 BODY[TEXT] - S: * 1 FETCH (BODY[TEXT] {1120} + C (-->): A0005 FETCH 1 BODY[TEXT] + S (<--): * 1 FETCH (BODY[TEXT] {1120} - S: ) - S: * 1 FETCH (FLAGS (\Recent \Seen)) - S: A0005 OK FETCH completed + S (<--): ) + S (<--): * 1 FETCH (FLAGS (\Recent \Seen)) + S (<--): A0005 OK FETCH completed -What does IMAP look like? -------------------------- +.. nextslide:: IMAP (Say goodbye):: - C: A0006 LOGOUT - S: * BYE example.com IMAP4rev1 server terminating connection - S: A0006 OK LOGOUT completed - C: + C (-->): A0006 LOGOUT + S (<--): * BYE example.com IMAP4rev1 server terminating connection + S (<--): A0006 OK LOGOUT completed + C (-->): -IMAP Characteristics --------------------- +.. nextslide:: IMAP Characteristics -.. class:: incremental +.. rst-class:: build * Interaction consists of commands and replies * Each command or reply is *one line* terminated by * Each command has a *verb* and one or more *arguments* * Each reply has a formal *code* and an informal *explanation* -.. class:: incremental +.. nextslide:: IMAP Differences -IMAP Differences ----------------- +.. rst-class:: build +.. container:: -.. class:: incremental + .. rst-class:: build -* Commands and replies are prefixed by 'sequence identifier' -* Payloads are prefixed by message size, rather than terminated by reserved - sequence + * Commands and replies are prefixed by 'sequence identifier' + * Payloads are prefixed by message size, rather than terminated by reserved + sequence -.. class:: incremental + Compared with POP3, what do these differences suggest? -Compared with POP3, what do these differences suggest? +Using IMAP in Python +-------------------- -Protocols in Python -------------------- +Let's try this out for ourselves! -.. class:: big-centered +.. rst-class:: build +.. container:: -Let's try this out for ourselves! + .. container:: + Fire up your python interpreters and prepare to type. -Protocols in Python -------------------- -.. class:: big-centered +.. nextslide:: -Fire up your python interpreters and prepare to type. +Begin by importing the ``imaplib`` module from the Python Standard Library: +.. rst-class:: build +.. container:: -IMAP in Python --------------- + .. code-block:: pycon -Begin by importing the ``imaplib`` module from the Python Standard Library:: + >>> import imaplib + >>> dir(imaplib) + ['AllowedVersions', 'CRLF', 'Commands', + 'Continuation', 'Debug', 'Flags', 'IMAP4', + 'IMAP4_PORT', 'IMAP4_SSL', 'IMAP4_SSL_PORT', + ... + 'socket', 'ssl', 'sys', 'time'] + >>> imaplib.Debug = 4 - >>> import imaplib - >>> dir(imaplib) - ['AllowedVersions', 'CRLF', 'Commands', - 'Continuation', 'Debug', 'Flags', 'IMAP4', - 'IMAP4_PORT', 'IMAP4_SSL', 'IMAP4_SSL_PORT', - ... - 'socket', 'ssl', 'sys', 'time'] - >>> imaplib.Debug = 4 + Setting ``imap.Debug`` shows us what is sent and received -.. class:: incremental -Setting ``imap.Debug`` shows us what is sent and received +.. nextslide:: +I've prepared a server for us to use. -IMAP in Python --------------- +.. rst-class:: build +.. container:: -I've prepared a server for us to use, we'll need to set up a client to speak -to it. Our server requires SSL for connecting to IMAP servers, so let's -initialize an IMAP4_SSL client and authenticate:: + We'll need to set up a client to speak to it. - >>> conn = imaplib.IMAP4_SSL('mail.webfaction.com') - 57:04.83 imaplib version 2.58 - 57:04.83 new IMAP4 connection, tag=FNHG - ... - >>> conn.login(username, password) - 12:16.50 > IMAD1 LOGIN username password - 12:18.52 < IMAD1 OK Logged in. - ('OK', ['Logged in.']) + Our server requires SSL (Secure Socket Layer) for connecting to IMAP + servers, so let's initialize an IMAP4_SSL client and authenticate: + .. code-block:: pycon -IMAP in Python --------------- + >>> conn = imaplib.IMAP4_SSL('mail.webfaction.com') + 57:04.83 imaplib version 2.58 + 57:04.83 new IMAP4 connection, tag=FNHG + ... + >>> conn.login(username, password) + 12:16.50 > IMAD1 LOGIN username password + 12:18.52 < IMAD1 OK Logged in. + ('OK', ['Logged in.']) + +.. nextslide:: + +We can start by listing the mailboxes we have on the server: -We can start by listing the mailboxes we have on the server:: +.. code-block:: pycon >>> conn.list() 00:41.91 > FNHG3 LIST "" * @@ -418,11 +415,12 @@ We can start by listing the mailboxes we have on the server:: ('OK', ['(\\HasNoChildren) "." "INBOX"']) -IMAP in Python --------------- +.. nextslide:: To interact with our email, we must select a mailbox from the list we received -earlier:: +earlier: + +.. code-block:: pycon >>> conn.select('INBOX') 00:00.47 > FNHG2 SELECT INBOX @@ -437,108 +435,73 @@ earlier:: ('OK', ['2']) -IMAP in Python --------------- +.. nextslide:: We can search our selected mailbox for messages matching one or more criteria. -The return value is a string list of the UIDs of messages that match our -search:: - - >>> conn.search(None, '(FROM "cris")') - 18:25.41 > FNHG5 SEARCH (FROM "cris") - 18:25.54 < * SEARCH 1 - 18:25.54 < FNHG5 OK Search completed. - ('OK', ['1']) - >>> - - -IMAP in Python --------------- - -Once we've found a message we want to look at, we can use the ``fetch`` -command to read it from the server. IMAP allows fetching each part of -a message independently:: - - >>> conn.fetch('1', '(BODY[HEADER])') - ... - >>> conn.fetch('1', '(BODY[TEXT])') - ... - >>> conn.fetch('1', '(FLAGS)') - - -Python Means Batteries Included -------------------------------- - -So we can download an entire message and then make a Python email message -object - -.. class:: small - -:: - - >>> import email - >>> typ, data = conn.fetch('1', '(RFC822)') - 28:08.40 > FNHG8 FETCH 1 (RFC822) - ... -Parse the returned data to get to the actual message +.. rst-class:: build +.. container:: -.. class:: small + The return value is a string list of the UIDs of messages that match our + search: -:: + .. code-block:: pycon - >>> for part in data: - ... if isinstance(part, tuple): - ... msg = email.message_from_string(part[1]) - ... - >>> + >>> conn.search(None, '(FROM "cris")') + 18:25.41 > FNHG5 SEARCH (FROM "cris") + 18:25.54 < * SEARCH 1 + 18:25.54 < FNHG5 OK Search completed. + ('OK', ['1']) + >>> +.. nextslide:: -IMAP in Python --------------- +Once we've found a message we want to look at, we can use the ``fetch`` +command to read it from the server. -Once we have that, we can play with the resulting email object: +.. rst-class:: build +.. container:: -.. class:: small + IMAP allows fetching each part of a message independently: -:: + .. code-block:: pycon - >>> msg.keys() - ['Return-Path', 'X-Original-To', 'Delivered-To', 'Received', - ... - 'To', 'Mime-Version', 'X-Mailer'] - >>> msg['To'] - 'demo@crisewing.com' - >>> print msg.get_payload()[0] - If you are reading this email, ... + >>> conn.fetch('1', '(BODY[HEADER])') + ... + >>> conn.fetch('1', '(BODY[TEXT])') + ... + >>> conn.fetch('1', '(FLAGS)') -.. class:: incremental center + What does the message say? -**Neat, huh?** + Python even includes an *email* library that would allow us to interact + with this message in an *OO* style. + *Neat, Huh?* What Have We Learned? --------------------- -.. class:: incremental +.. rst-class:: build +.. container:: -* Protocols are just a set of rules for how to communicate + .. rst-class:: build -* Protocols tell us how to parse and delimit messages + * Protocols are just a set of rules for how to communicate -* Protocols tell us what messages are valid + * Protocols tell us how to parse and delimit messages -* If we properly format request messages to a server, we can get response - messages + * Protocols tell us what messages are valid -* Python supports a number of these protocols + * If we properly format request messages to a server, we can get response + messages -* So we don't have to remember how to format the commands ourselves + * Python supports a number of these protocols -.. class:: incremental + * So we don't have to remember how to format the commands ourselves -But in every case we've seen, we could do the same thing with a socket and -some strings + But in every case we've seen, we could do the same thing with a socket and + some strings Break Time @@ -546,42 +509,46 @@ Break Time Let's take a few minutes here to clear our heads. -.. class:: incremental - -See you back here in 10 minutes. - HTTP ----- - -.. class:: big-centered +==== -HTTP is no different +.. rst-class:: left +.. container:: + HTTP is no different -HTTP ----- + .. rst-class:: build + .. container:: -HTTP is also message-centered, with two-way communications: + HTTP is also message-centered, with two-way communications: -.. class:: incremental + .. rst-class:: build -* Requests (Asking for information) -* Responses (Providing answers) + * Requests (Asking for information) + * Responses (Providing answers) What does HTTP look like? ------------------------- -HTTP (Ask for information):: +HTTP (Ask for information): + +.. code-block:: http GET /index.html HTTP/1.1 Host: www.example.com -What does HTTP look like? -------------------------- +**note**: the ```` you see here is a visualization of an empty line. It's +really just the standard line terminator on an empty line. + +You don't need to type the ```` there. + +.. nextslide:: + +HTTP (Provide answers): -HTTP (Provide answers):: +.. code-block:: http HTTP/1.1 200 OK Date: Mon, 23 May 2005 22:38:34 GMT @@ -593,15 +560,16 @@ HTTP (Provide answers):: Connection: close Content-Type: text/html; charset=UTF-8 - <438 bytes of content> + \n\n \n This is a .... </html> +You don't need to type the ``<CRLF>`` here either. -HTTP Req/Resp Format --------------------- -Both share a common basic format: +.. nextslide:: HTTP Core Format + +In HTTP, both *request* and *response* share a common basic format: -.. class:: incremental +.. rst-class:: build * Line separators are <CRLF> (familiar, no?) * A required initial line (a command or a response code) @@ -610,172 +578,154 @@ Both share a common basic format: * An optional body -HTTP In Real Life +Implementing HTTP ----------------- -Let's investigate the HTTP protocol a bit in real life. +Let's investigate the HTTP protocol a bit in real life. -.. class:: incremental +.. rst-class:: build +.. container:: -We'll do so by building a simplified HTTP server, one step at a time. + We'll do so by building a simplified HTTP server, one step at a time. -.. class:: incremental + There is a copy of the echo server from last time in + ``resources/session05``. It's called ``http_server.py``. -There is a copy of the echo server from last time in ``resources/session02``. -It's called ``http_server.py``. + In a terminal, move into that directory. We'll be doing our work here for + the rest of the session -.. class:: incremental -In a terminal, move into that directory. We'll be doing our work here for the -rest of the session - - -TDD IRL (a quick aside) ------------------------ +.. nextslide:: TDD IRL (a quick aside) Test Driven Development (TDD) is all the rage these days. -.. class:: incremental - -It means that before you write code, you first write tests demonstrating what -you want your code to do. - -.. class:: incremental +.. rst-class:: build +.. container:: -When all your tests pass, you are finished. You did this for your last -assignment. + It means that before you write code, you first write tests demonstrating + what you want your code to do. -.. class:: incremental + When all your tests pass, you are finished. You did this for your last + assignment. -We'll be doing it again today. + We'll be doing it again today. -Run the Tests -------------- +.. nextslide:: Run the Tests -From inside ``resources/session02`` start a second python interpreter and run +From inside ``resources/session05`` start a second python interpreter and run ``$ python http_server.py`` -.. container:: incremental - +.. rst-class:: build +.. container:: + In your first interpreter run the tests. You should see similar output: - - .. class:: small - - :: - + + .. code-block:: bash + $ python tests.py [...] Ran 10 tests in 0.003s FAILED (failures=3, errors=7) + Let's take a few minutes here to look at these tests and understand them. -.. class:: incremental -Let's take a few minutes here to look at these tests and understand them. - - -Viewing an HTTP Request ------------------------ +.. nextslide:: Viewing an HTTP Request Our job is to make all those tests pass. -.. class:: incremental - -First, though, let's pretend this server really is a functional HTTP server. - -.. class:: incremental +.. rst-class:: build +.. container:: -This time, instead of using the echo client to make a connection to the -server, let's use a web browser! + First, though, let's pretend this server really is a functional HTTP + server. -.. class:: incremental + This time, instead of using the echo client to make a connection to the + server, let's use a web browser! -Point your favorite browser at ``http://localhost:10000`` + Point your favorite browser at ``http://localhost:10000`` -A Bad Interaction ------------------ +.. nextslide:: A Bad Interaction First, look at the printed output from your echo server. -.. class:: incremental - -Second, note that your browser is still waiting to finish loading the page +.. rst-class:: build +.. container:: -.. class:: incremental + Second, note that your browser is still waiting to finish loading the page -Moreover, your server should also be hung, waiting for more from the 'client' + Moreover, your server should also be hung, waiting for more from the + 'client' -.. class:: incremental + This is because the server is waiting for the browser to respond -This is because we are not yet following the right protocol. + And at the same time, the browser is waiting for the server to indicate it + is done. + Our server does not yet speak the HTTP protocol, but the browser is + expecting it. -Echoing A Request ------------------ +.. nextslide:: Echoing A Request Kill your server with ``ctrl-c`` (the keyboard interrupt) and you should see some printed content: -.. class:: small incremental +.. rst-class:: build +.. container:: -:: + .. code-block:: http - GET / HTTP/1.1 - Host: localhost:10000 - User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:22.0) Gecko/20100101 Firefox/22.0 - Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 - Accept-Language: en-US,en;q=0.5 - Accept-Encoding: gzip, deflate - DNT: 1 - Cookie: __utma=111872281.383966302.1364503233.1364503233.1364503233.1; __utmz=111872281.1364503233.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); csrftoken=uiqj579iGRbReBHmJQNTH8PFfAz2qRJS - Connection: keep-alive - Cache-Control: max-age=0 + GET / HTTP/1.1 + Host: localhost:10000 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:22.0) Gecko/20100101 Firefox/22.0 + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Language: en-US,en;q=0.5 + Accept-Encoding: gzip, deflate + DNT: 1 + Cookie: __utma=111872281.383966302.1364503233.1364503233.1364503233.1; __utmz=111872281.1364503233.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); csrftoken=uiqj579iGRbReBHmJQNTH8PFfAz2qRJS + Connection: keep-alive + Cache-Control: max-age=0 -.. class:: incremental + Your results will vary from mine. -Your results will vary from mine. +.. nextslide:: HTTP Debugging -HTTP Debugging --------------- When working on applications, it's nice to be able to see all this going back -and forth. +and forth. -.. container:: incremental +.. rst-class:: build +.. container:: Good browsers support this with a set of developer tools built-in. - .. class:: small incremental + .. rst-class:: build * firefox -> ctrl-shift-K or cmd-opt-K (os X) * safari -> enable in preferences:advanced then cmd-opt-i * chrome -> ctrl-shift-i or cmd-opt-i (os X) * IE (7.0+) -> F12 or tools menu -> developer tools -.. class:: incremental + The 'Net(work)' pane of these tools can show you both request and response, + headers and all. Very useful. -The 'Net(work)' pane of these tools can show you both request and response, -headers and all. Very useful. +.. nextslide:: Stop! Demo Time -Stop! Demo Time ---------------- +.. rst-class:: centered -.. class:: big-centered +**Let's take a quick look** -Let's take a quick look - -Other Debugging Options ------------------------ +.. nextslide:: Other Debugging Options Sometimes you need or want to debug http requests that are not going through your browser. -.. class:: incremental +.. rst-class:: build Or perhaps you need functionality that is not supported by in-browser tools (request munging, header mangling, decryption of https request/responses) @@ -784,82 +734,83 @@ Or perhaps you need functionality that is not supported by in-browser tools Then it might be time for an HTTP debugging proxy: + .. rst-class:: build + * windows: http://www.fiddler2.com/fiddler2/ * win/osx/linux: http://www.charlesproxy.com/ + We won't cover any of these tools here today. But you can check them out + when you have the time. -HTTP Requests -------------- -In HTTP 1.0, the only required line in an HTTP request is this:: +Step 1: Basic HTTP Protocol +--------------------------- - GET /path/to/index.html HTTP/1.0 - <CRLF> +In HTTP 1.0, the only required line in an HTTP request is this: -.. class:: incremental +.. code-block:: http -As virtual hosting grew more common, that was not enough, so HTTP 1.1 adds a -single required *header*, **Host**: + GET /path/to/index.html HTTP/1.0 + <CRLF> -.. class:: incremental +.. rst-class:: build +.. container:: -:: + As virtual hosting grew more common, that was not enough, so HTTP 1.1 adds + a single required *header*, **Host**: - GET /path/to/index.html HTTP/1.1 - Host: www.mysite1.com:80 - <CRLF> + .. code-block:: http + + GET /path/to/index.html HTTP/1.1 + Host: www.mysite1.com:80 + <CRLF> -HTTP Responses --------------- +.. nextslide:: HTTP Responses In both HTTP 1.0 and 1.1, a proper response consists of an intial line, followed by optional headers, a single blank line, and then optionally a -response body:: - - HTTP/1.1 200 OK - Content-Type: text/plain - <CRLF> - this is a pretty minimal response +response body: -.. class:: incremental +.. rst-class:: build +.. container:: -Let's update our server to return such a response. + .. code-block:: http + + HTTP/1.1 200 OK + Content-Type: text/plain + <CRLF> + this is a pretty minimal response + Let's update our server to return such a response. -Basic HTTP Protocol -------------------- +.. nextslide:: Returning a Canned HTTP Response Begin by implementing a new function in your ``http_server.py`` script called `response_ok`. -.. class:: incremental +.. rst-class:: build +.. container:: -It can be super-simple for now. We'll improve it later. + It can be super-simple for now. We'll improve it later. -.. container:: incremental + .. container:: - It needs to return our minimal response from above: + It needs to return our minimal response from above: - .. class:: small - - :: - - HTTP/1.1 200 OK - Content-Type: text/plain - <CRLF> - this is a pretty minimal response - -.. class:: incremental small + .. code-block:: http + + HTTP/1.1 200 OK + Content-Type: text/plain + <CRLF> + this is a pretty minimal response -**Remember, <CRLF> is a placeholder for an intentionally blank line** + **Remember, <CRLF> is a placeholder for an intentionally blank line** -My Solution ------------ +.. nextslide:: My Solution .. code-block:: python - :class: incremental def response_ok(): """returns a basic HTTP response""" @@ -871,53 +822,45 @@ My Solution return "\r\n".join(resp) -Run The Tests -------------- +.. nextslide:: Run The Tests We've now implemented a function that is tested by our tests. Let's run them again: -.. class:: incremental small +.. rst-class:: build +.. container:: -:: + .. code-block:: bash - $ python tests.py - [...] - ---------------------------------------------------------------------- - Ran 10 tests in 0.002s + $ python tests.py + [...] + ---------------------------------------------------------------------- + Ran 10 tests in 0.002s - FAILED (failures=3, errors=3) + FAILED (failures=3, errors=3) -.. class:: incremental + Great! We've now got 4 tests that pass. Good work. -Great! We've now got 4 tests that pass. Good work. - -Server Modifications --------------------- +.. nextslide:: Server Modifications Next, we need to rebuild the server loop from our echo server for it's new purpose: -.. class:: incremental - -It should now wait for an incoming request to be *finished*, *then* send a -response back to the client. - -.. class:: incremental +.. rst-class:: build +.. container:: -The response it sends can be the result of calling our new ``response_ok`` -function for now. + It should now wait for an incoming request to be *finished*, *then* send a + response back to the client. -.. class:: incremental + The response it sends can be the result of calling our new ``response_ok`` + function for now. -We could also bump up the ``recv`` buffer size to something more reasonable -for HTTP traffic, say 1024. + We could also bump up the ``recv`` buffer size to something more reasonable + for HTTP traffic, say 1024. -My Solution ------------ +.. nextslide:: My Solution .. code-block:: python - :class: incremental small # ... try: @@ -930,7 +873,7 @@ My Solution data = conn.recv(1024) if len(data) < 1024: break - + print >>log_buffer, 'sending response' response = response_ok() conn.sendall(response) @@ -939,20 +882,18 @@ My Solution # ... -Run The Tests -------------- +.. nextslide:: Run The Tests Once you've got that set, restart your server:: $ python http_server.py -.. container:: incremental +.. rst-class:: build +.. container:: Then you can re-run your tests: - .. class:: small - - :: + .. code-block:: bash $ python tests.py [...] @@ -961,56 +902,51 @@ Once you've got that set, restart your server:: FAILED (failures=2, errors=3) -.. class:: incremental + Five tests now pass! -Five tests now pass! - -Parts of a Request ------------------- +Step 2: Handling HTTP Methods +----------------------------- Every HTTP request **must** begin with a single line, broken by whitespace into -three parts:: +three parts: + +.. code-block:: http GET /path/to/index.html HTTP/1.1 -.. class:: incremental +.. rst-class:: build +.. container:: -The three parts are the *method*, the *URI*, and the *protocol* + The three parts are the *method*, the *URI*, and the *protocol* -.. class:: incremental + Let's look at each in turn. -Let's look at each in turn. - -HTTP Methods ------------- +.. nextslide:: HTTP Methods **GET** ``/path/to/index.html HTTP/1.1`` -.. class:: incremental +.. rst-class:: build * Every HTTP request must start with a *method* * There are four main HTTP methods: - .. class:: incremental - - * GET - * POST - * PUT - * DELETE + .. rst-class:: build -.. class:: incremental + * GET + * POST + * PUT + * DELETE * There are others, notably HEAD, but you won't see them too much -HTTP Methods ------------- +.. nextslide:: HTTP Methods These four methods are mapped to the four basic steps (*CRUD*) of persistent storage: -.. class:: incremental +.. rst-class:: build * POST = Create * GET = Read @@ -1018,71 +954,72 @@ storage: * DELETE = Delete -Methods: Safe <--> Unsafe -------------------------- +.. nextslide:: Methods: Safe <--> Unsafe HTTP methods can be categorized as **safe** or **unsafe**, based on whether they might change something on the server: -.. class:: incremental - -* Safe HTTP Methods - * GET -* Unsafe HTTP Methods - * POST - * PUT - * DELETE +.. rst-class:: build +.. container:: -.. class:: incremental + .. rst-class:: build -This is a *normative* distinction, which is to say **be careful** + * Safe HTTP Methods + + * GET + + * Unsafe HTTP Methods + + * POST + * PUT + * DELETE + This is a *normative* distinction, which is to say **be careful** -Methods: Idempotent <--> ??? ----------------------------- -HTTP methods can be categorized as **idempotent**, based on whether a given -request will always have the same result: +.. nextslide:: Methods: Idempotent <--> ??? -.. class:: incremental +HTTP methods can be categorized as **idempotent**. -* Idempotent HTTP Methods - * GET - * PUT - * DELETE -* Non-Idempotent HTTP Methods - * POST +.. rst-class:: build +.. container:: -.. class:: incremental + This means that a given request will always have the same result: -Again, *normative*. The developer is responsible for ensuring that it is true. + .. rst-class:: build + * Idempotent HTTP Methods + + * GET + * PUT + * DELETE + + * Non-Idempotent HTTP Methods + + * POST -HTTP Method Handling --------------------- + Again, *normative*. The developer is responsible for ensuring that it is true. -Let's keep things simple, our server will only respond to *GET* requests. -.. class:: incremental +.. nextslide:: HTTP Method Handling -We need to create a function that parses a request and determines if we can -respond to it: ``parse_request``. +Let's keep things simple, our server will only respond to *GET* requests. -.. class:: incremental +.. rst-class:: build +.. container:: -If the request method is not *GET*, our method should raise an error + We need to create a function that parses a request and determines if we can + respond to it: ``parse_request``. -.. class:: incremental + If the request method is not *GET*, our method should raise an error -Remember, although a request is more than one line long, all we care about -here is the first line + Remember, although a request is more than one line long, all we care about + here is the first line -My Solution ------------ +.. nextslide:: My Solution .. code-block:: python - :class: incremental def parse_request(request): first_line = request.split("\r\n", 1)[0] @@ -1092,23 +1029,20 @@ My Solution print >>sys.stderr, 'request is okay' -Update the Server ------------------ +.. nextslide:: Update the Server We'll also need to update the server code. It should -.. class:: incremental +.. rst-class:: build * save the request as it comes in * check the request using our new function * send an OK response if things go well -My Solution ------------ +.. nextslide:: My Solution .. code-block:: python - :class: incremental small # ... conn, addr = sock.accept() # blocking @@ -1130,133 +1064,121 @@ My Solution # ... -Run The Tests -------------- +.. nextslide:: Run The Tests Quit and restart your server now that you've updated the code:: $ python http_server.py -.. container:: incremental +.. rst-class:: build +.. container:: At this point, we should have seven tests passing: - - .. class:: small - - :: + + .. code-block:: bash $ python tests.py Ran 10 tests in 0.002s - + FAILED (failures=1, errors=2) -What About a Browser? ---------------------- +.. nextslide:: What About a Browser? Quit and restart your server, now that you've updated the code. -.. class:: incremental - -Reload your browser. It should work fine. +.. rst-class:: build +.. container:: -.. class:: incremental + Reload your browser. It should work fine. -We can use the ``simple_client.py`` script in our resources to test our error -condition. In a second terminal window run the script like so: + We can use the ``simple_client.py`` script in our resources to test our + error condition. In a second terminal window run the script like so:: -.. class:: incremental + $ python simple_client.py "POST / HTTP/1.0\r\n\r\n" -:: + You'll have to quit the client pretty quickly with ``ctrl-c`` - $ python simple_client.py "POST / HTTP/1.0\r\n\r\n" -.. class:: incremental - -You'll have to quit the client pretty quickly with ``ctrl-c`` - - -Error Responses ---------------- +Step 3: Error Responses +----------------------- Okay, so the outcome there was pretty ugly. The client went off the rails, and our server has terminated as well. -.. class:: incremental +.. rst-class:: build +.. container:: -The HTTP protocol allows us to handle errors like this more gracefully. + The HTTP protocol allows us to handle errors like this more gracefully. -.. class:: incremental center + .. rst-class:: centered -**Enter the Response Code** + **Enter the Response Code** -HTTP Response Codes -------------------- +.. nextslide:: HTTP Response Codes ``HTTP/1.1`` **200 OK** All HTTP responses must include a **response code** indicating the outcome of the request. -.. class:: incremental +.. rst-class:: build +.. container:: -* 1xx (HTTP 1.1 only) - Informational message -* 2xx - Success of some kind -* 3xx - Redirection of some kind -* 4xx - Client Error of some kind -* 5xx - Server Error of some kind + .. rst-class:: build -.. class:: incremental + * 1xx (HTTP 1.1 only) - Informational message + * 2xx - Success of some kind + * 3xx - Redirection of some kind + * 4xx - Client Error of some kind + * 5xx - Server Error of some kind -The text bit makes the code more human-readable + The text bit makes the code more human-readable -Common Response Codes ---------------------- +.. nextslide:: Common Response Codes There are certain HTTP response codes you are likely to see (and use) most often: -.. class:: incremental +.. rst-class:: build +.. container:: -* ``200 OK`` - Everything is good -* ``301 Moved Permanently`` - You should update your link -* ``304 Not Modified`` - You should load this from cache -* ``404 Not Found`` - You've asked for something that doesn't exist -* ``500 Internal Server Error`` - Something bad happened + .. rst-class:: build -.. class:: incremental + * ``200 OK`` - Everything is good + * ``301 Moved Permanently`` - You should update your link + * ``304 Not Modified`` - You should load this from cache + * ``404 Not Found`` - You've asked for something that doesn't exist + * ``500 Internal Server Error`` - Something bad happened -Do not be afraid to use other, less common codes in building good apps. There -are a lot of them for a reason. See -http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + Do not be afraid to use other, less common codes in building good apps. + There are a lot of them for a reason. See + http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html -Handling our Error ------------------- -Luckily, there's an error code that is tailor-made for this situation. +.. nextslide:: Handling our Error -.. class:: incremental +Luckily, there's an error code that is tailor-made for this situation. -The client has made a request using a method we do not support +.. rst-class:: build +.. container:: -.. class:: incremental + The client has made a request using a method we do not support -``405 Method Not Allowed`` + ``405 Method Not Allowed`` -.. class:: incremental + Let's add a new function that returns this error code. It should be called + ``response_method_not_allowed`` -Let's add a new function that returns this error code. It should be called -``response_method_not_allowed`` + Remember, it must be a complete HTTP Response with the correct *code* -My Solution ------------ +.. nextslide:: My Solution .. code-block:: python - :class: incremental def response_method_not_allowed(): """returns a 405 Method Not Allowed response""" @@ -1266,23 +1188,21 @@ My Solution return "\r\n".join(resp) -Server Updates --------------- +.. nextslide:: Server Updates Again, we'll need to update the server to handle this error condition correctly. It should -.. class:: incremental +.. rst-class:: build * catch the exception raised by the ``parse_request`` function -* return our new error response as a result -* if no exception is raised, then return the OK response +* create our new error response as a result +* if no exception is raised, then create the OK response +* return the generated response to the user -My Solution ------------ +.. nextslide:: My Solution .. code-block:: python - :class: incremental small # ... while True: @@ -1303,28 +1223,23 @@ My Solution # ... -Run The Tests -------------- +.. nextslide:: Run The Tests Start your server (or restart it if by some miracle it's still going). -.. container:: incremental +.. rst-class:: build +.. container:: + + Then run the tests again:: - Then run the tests again: - - .. class:: small - - :: - $ python tests.py [...] Ran 10 tests in 0.002s - - OK -.. class:: incremental + OK -Wahoo! All our tests are passing. That means we are done writing code for now. + Wahoo! All our tests are passing. That means we are done writing code for + now. HTTP - Resources @@ -1333,35 +1248,37 @@ HTTP - Resources We've got a very simple server that accepts a request and sends a response. But what happens if we make a different request? -.. container:: incremental +.. rst-class:: build +.. container:: - In your web browser, enter the following URL:: + .. container:: + + In your web browser, enter the following URL:: - http://localhost:10000/page + http://localhost:10000/page -.. container:: incremental + .. container:: - What happened? What happens if you use this URL:: + What happened? What happens if you use this URL:: - http://localhost:10000/section/page? + http://localhost:10000/section/page? -HTTP - Resources ----------------- +.. nextslide:: We expect different urls to result in different responses. -.. class:: incremental +.. rst-class:: build +.. container:: -But this isn't happening with our server, for obvious reasons. + But this isn't happening with our server, for obvious reasons. -.. class:: incremental + It brings us back to the second element of that first line of an HTTP + request. -It brings us back to the second element of that first line of an HTTP request. + .. rst-class:: centered -.. class:: incremental center - -**The Return of the URI** + **The Return of the URI** HTTP Requests: URI @@ -1369,7 +1286,7 @@ HTTP Requests: URI ``GET`` **/path/to/index.html** ``HTTP/1.1`` -.. class:: incremental +.. rst-class:: build * Every HTTP request must include a **URI** used to determine the **resource** to be returned @@ -1379,11 +1296,11 @@ HTTP Requests: URI * Resource? Files (html, img, .js, .css), but also: - .. class:: incremental + .. rst-class:: build - * Dynamic scripts - * Raw data - * API endpoints + * Dynamic scripts + * Raw data + * API endpoints Homework @@ -1392,16 +1309,15 @@ Homework For your homework this week you will expand your server's capabilities so that it can make different responses to different URIs. -.. class:: incremental - -You'll allow your server to serve up directories and files from your own -filesystem. +.. rst-class:: build +.. container:: -.. class:: incremental + You'll allow your server to serve up directories and files from your own + filesystem. -You'll be starting from the ``http_server.py`` script that is currently in the -``assignments/session02`` directory. It should be pretty much the same as what -you've created here. + You'll be starting from the ``http_server.py`` script that is currently in + the ``assignments/session02`` directory. It should be pretty much the same + as what you've created here. One Step At A Time @@ -1410,7 +1326,7 @@ One Step At A Time Take the following steps one at a time. Run the tests in ``assignments/session02`` between to ensure that you are getting it right. -.. class:: incremental +.. rst-class:: build * Update ``parse_request`` to return the URI it parses from the request. @@ -1431,14 +1347,13 @@ Along the way, you'll discover that simply returning as the body in response_ok is insufficient. Different *types* of content need to be identified to your browser -.. class:: incremental - -We can fix this by passing information about exactly what we are returning as -part of the response. +.. rst-class:: build +.. container:: -.. class:: incremental + We can fix this by passing information about exactly what we are returning + as part of the response. -HTTP provides for this type of thing with the generic idea of *Headers* + HTTP provides for this type of thing with the generic idea of *Headers* HTTP Headers @@ -1446,17 +1361,18 @@ HTTP Headers Both requests and responses can contain **headers** of the form ``Name: Value`` -.. class:: incremental +.. rst-class:: build +.. container:: -* HTTP 1.0 has 16 valid headers, 1.1 has 46 -* Any number of spaces or tabs may separate the *name* from the *value* -* If a header line starts with spaces or tabs, it is considered part of the - value for the previous header -* Header *names* are **not** case-sensitive, but *values* may be + .. rst-class:: build -.. class:: incremental + * HTTP 1.0 has 16 valid headers, 1.1 has 46 + * Any number of spaces or tabs may separate the *name* from the *value* + * If a header line starts with spaces or tabs, it is considered part of the + value for the previous header + * Header *names* are **not** case-sensitive, but *values* may be -read more about HTTP headers: http://www.cs.tut.fi/~jkorpela/http.html + read more about HTTP headers: http://www.cs.tut.fi/~jkorpela/http.html Content-Type Header @@ -1465,7 +1381,7 @@ Content-Type Header A very common header used in HTTP responses is ``Content-Type``. It tells the client what to expect. -.. class:: incremental +.. rst-class:: build * uses **mime-type** (Multi-purpose Internet Mail Extensions) * foo.jpeg - ``Content-Type: image/jpeg`` @@ -1473,7 +1389,7 @@ client what to expect. * bar.txt - ``Content-Type: text/plain`` * baz.html - ``Content-Type: text/html`` -.. class:: incremental +.. rst-class:: build There are *many* mime-type identifiers: http://www.webmaster-toolkit.com/mime-types.shtml @@ -1484,7 +1400,7 @@ Mapping Mime-types By mapping a given file to a mime-type, we can write a header. -.. class:: incremental +.. rst-class:: build The standard lib module ``mimetypes`` does just this. @@ -1493,8 +1409,7 @@ The standard lib module ``mimetypes`` does just this. We can guess the mime-type of a file based on the filename or map a file extension to a type: - .. code-block:: python - :class: small + .. code-block:: python >>> import mimetypes >>> mimetypes.guess_type('file.txt') @@ -1508,7 +1423,7 @@ Resolving a URI Your ``resolve_uri`` function will need to accomplish the following tasks: -.. class:: incremental +.. rst-class:: build * It should take a URI as the sole argument @@ -1532,12 +1447,12 @@ Use Your Tests One of the benefits of test-driven development is that the tests that are failing should tell you what code you need to write. -.. class:: incremental +.. rst-class:: build As you work your way through the steps outlined above, look at your tests. Write code that makes them pass. -.. class:: incremental +.. rst-class:: build If all the tests in ``assignments/session02/tests.py`` are passing, you've completed the assignment. @@ -1555,7 +1470,7 @@ To submit your homework: * Using the github web interface, send me a pull request. -.. class:: incremental +.. rst-class:: build I will review your work when I receive your pull requests, make comments on it there, and then close the pull request. @@ -1567,7 +1482,7 @@ A Few Steps Further If you are able to finish the above in less than 4-6 hours, consider taking on one or more of the following challenges: -.. class:: incremental +.. rst-class:: build * Format directory listings as HTML, so you can link to files. * Add a GMT ``Date:`` header in the proper format (RFC-1123) to responses. From 7a146bdbc02e47fe4e9efd467196f670411baea9 Mon Sep 17 00:00:00 2001 From: cewing <cris@crisewing.com> Date: Sat, 17 Jan 2015 11:41:02 -0800 Subject: [PATCH 030/173] remove irrelevant flask lectures --- source/presentations/session05.rst.norender | 1653 ------------------- source/presentations/session06.rst.norender | 179 -- 2 files changed, 1832 deletions(-) delete mode 100644 source/presentations/session05.rst.norender delete mode 100644 source/presentations/session06.rst.norender diff --git a/source/presentations/session05.rst.norender b/source/presentations/session05.rst.norender deleted file mode 100644 index efb1775d..00000000 --- a/source/presentations/session05.rst.norender +++ /dev/null @@ -1,1653 +0,0 @@ -Python Web Programming -====================== - -.. image:: img/bike.jpg - :align: left - :width: 50% - -Session 5: Frameworks and Flask - -.. class:: intro-blurb right - -| "Reinventing the wheel is great -| if your goal is to learn more about the wheel" -| -| -- James Tauber, PyCon 2007 - -.. class:: image-credit - -image: Britanglishman http://www.flickr.com/photos/britanglishman/5999131365/ - CC-BY - - -A Moment to Reflect -------------------- - -We've been at this for a couple of days now. We've learned a great deal: - -.. class:: incremental - -* Sockets, the TCP/IP Stack and Basic Mechanics -* Web Protocols and the Importance of Clear Communication -* APIs and Consuming Data from The Web -* CGI and WSGI and Getting Information to Your Dynamic Applications - -.. class:: incremental - -Everything we do from here out will be based on tools built using these -*foundational technologies*. - - -From Now On ------------ - -Think of everything we do as sitting on top of WSGI - -.. class:: incremental - -This may not *actually* be true - -.. class:: incremental - -But we will always be working at that level of abstraction. - - -Frameworks ----------- - -From Wikipedia: - -.. class:: center incremental - -A web application framework (WAF) is a software framework that is designed to -support the development of dynamic websites, web applications and web -services. The framework aims to alleviate the overhead associated with common -activities performed in Web development. For example, many frameworks provide -libraries for database access, templating frameworks and session management, -and they often promote code reuse - - -What Does That *Mean*? ----------------------- - -You use a framework to build an application. - -.. class:: incremental - -A framework allows you to build different kinds of applications. - -.. class:: incremental - -A framework abstracts what needs to be abstracted, and allows control of the -rest. - -.. class:: incremental - -Think back over the last four sessions. What were your pain points? Which bits -do you wish you didn't have to think about? - - -Level of Abstraction --------------------- - -This last part is important when it comes to choosing a framework - -.. class:: incremental - -* abstraction ∠1/freedom -* The more they choose, the less you can -* *Every* framework makes choices in what to abstract -* *Every* framework makes *different* choices - - -Impedance Mismatch ------------------- - -.. class:: big-centered - -Don't Fight the Framework - - -Python Web Frameworks ---------------------- - -There are scores of 'em (this is a partial list). - -.. class:: incremental invisible small center - -========= ======== ======== ========== ============== -Django Grok Pylons TurboGears web2py -Zope CubicWeb Enamel Gizmo(QP) Glashammer -Karrigell Nagare notmm Porcupine QP -SkunkWeb Spyce Tipfy Tornado WebCore -web.py Webware Werkzeug WHIFF XPRESS -AppWsgi Bobo Bo7le CherryPy circuits.web -Paste PyWebLib WebStack Albatross Aquarium -Divmod Nevow Flask JOTWeb2 Python Servlet -Engine Pyramid Quixote Spiked weblayer -========= ======== ======== ========== ============== - - -Choosing a Framework --------------------- - -Many folks will tell you "<XYZ> is the **best** framework". - -.. class:: incremental - -In most cases, what they really mean is "I know how to use <XYZ>" - -.. class:: incremental - -In some cases, what they really mean is "<XYZ> fits my brain the best" - -.. class:: incremental - -What they usually forget is that everyone's brain (and everyone's use-case) is -different. - - -Cris' First Law of Frameworks ------------------------------ - -.. class:: center - -**Pick the Right Tool for the Job** - -.. class:: incremental - -First Corollary - -.. class:: incremental center - -The right tool is the tool that allows you to finish the job quickly and -correctly. - -.. class:: incremental center - -But how do you know which that one is? - - -Cris' Second Law of Frameworks ------------------------------- - -.. class:: big-centered - -You can't know unless you try - -.. class:: incremental center - -so let's try - - -From Your Homework ------------------- - -During the week, you walked through an introduction to the *Flask* web -framework. You wrote a file that looked like this: - -.. code-block:: python - :class: small - - from flask import Flask - app = Flask(__name__) - - @app.route('/') - def hello_world(): - return 'Hello World!' - - if __name__ == '__main__': - app.run() - - -The outcome ------------ - -When you ran this file, you should have seen something like this in your -browser: - -.. image:: img/flask_hello.png - :align: center - :width: 80% - - -What's Happening Here? ----------------------- - -Flask the framework provides a Python class called `Flask`. This class -functions as a single *application* in the WSGI sense. - -.. class:: incremental - -We know a WSGI application must be a *callable* that takes the arguments -*environ* and *start_response*. - -.. class:: incremental - -It has to call the *start_response* method, providing status and headers. - -.. class:: incremental - -And it has to return an *iterable* that represents the HTTP response body. - - -Under the Covers ----------------- - -In Python, an object is a *callable* if it has a ``__call__`` method. - -.. container:: incremental - - Here's the ``__call__`` method of the ``Flask`` class: - - .. code-block:: python - - def __call__(self, environ, start_response): - """Shortcut for :attr:`wsgi_app`.""" - return self.wsgi_app(environ, start_response) - -.. class:: incremental - -As you can see, it calls another method, called ``wsgi_app``. Let's follow -this down... - - -Flask.wsgi_app --------------- - -.. code-block:: python - :class: small - - def wsgi_app(self, environ, start_response): - """The actual WSGI application. - ... - """ - ctx = self.request_context(environ) - ctx.push() - error = None - try: - try: - response = self.full_dispatch_request() - except Exception as e: - error = e - response = self.make_response(self.handle_exception(e)) - return response(environ, start_response) - #... - -.. class:: incremental - -``response`` is another WSGI app. ``Flask`` is actually *middleware* - - -Abstraction Layers ------------------- - -Finally, way down in a package called *werkzeug*, we find this response object -and it's ``__call__`` method: - -.. code-block:: python - :class: small - - def __call__(self, environ, start_response): - """Process this response as WSGI application. - - :param environ: the WSGI environment. - :param start_response: the response callable provided by the WSGI - server. - :return: an application iterator - """ - app_iter, status, headers = self.get_wsgi_response(environ) - start_response(status, headers) - return app_iter - - -Common Threads --------------- - -All Python web frameworks that operate under the WSGI spec will do this same -sort of thing. - -.. class:: incremental - -They have to do it. - -.. class:: incremental - -And these layers of abstraction allow you, the developer to focus only on the -thing that really matters to you. - -.. class:: incremental - -Getting input from a request, and returning a response. - - -A Quick Reminder ----------------- - -Over the week, in addition to walking through a Flask intro you did two other -tasks: - -.. class:: incremental - -You walked through a tutorial on the Python DB API2, and learned how -to use ``sqlite3`` to store and retrieve data. - -.. class:: incremental - -You also read a bit about ``Jinja2``, the templating language Flask -uses out of the box, and ran some code to explore its abilities. - - -Moving On ---------- - -Now it is time to put all that together. - -.. class:: incremental - -We'll spend this session building a "microblog" application. - -.. class:: incremental - -Let's dive right in. - -.. class:: incremental - -Start by activating your Flask virtualenv - - -Our Database ------------- - -We need first to define what an *entry* for our microblog might look like. - -.. class:: incremental - -Let's keep it a simple as possible for now. - -.. class:: incremental - -Create a new directory ``microblog``, and open a new file in it: -``schema.sql`` - -.. code-block:: sql - :class: incremental small - - drop table if exists entries; - create table entries ( - id integer primary key autoincrement, - title string not null, - text string not null - ); - - -App Configuration ------------------ - -For any but the most trivial applications, you'll need some configuration. - -.. class:: incremental - -Flask provides a number of ways of loading configuration. We'll be using a -config file - -.. class:: incremental - -Create a new file ``microblog.cfg`` in the same directory. - -.. code-block:: python - :class: small incremental - - # application configuration for a Flask microblog - DATABASE = 'microblog.db' - - -Our App Skeleton ----------------- - -Finally, we'll need a basic app skeleton to work from. - -.. class:: incremental - -Create one more file ``microblog.py`` in the same directory, and enter the -following: - -.. code-block:: python - :class: small incremental - - from flask import Flask - - app = Flask(__name__) - - app.config.from_pyfile('microblog.cfg') - - if __name__ == '__main__': - app.run(debug=True) - - -Test Your Work --------------- - -This is enough to get us off the ground. - -.. container:: incremental - - From a terminal in the ``microblog`` directory, run the app: - - .. class:: small - - :: - - (flaskenv)$ python microblog.py - * Running on http://127.0.0.1:5000/ - * Restarting with reloader - -.. class:: incremental - -Then point your browser at http://localhost:5000/ - -.. class:: incremental - -What do you see in your browser? In the terminal? Why? - - -Creating the Database ---------------------- - -Quit the app with ``^C``. Then return to ``microblog.py`` and add the -following: - -.. code-block:: python - :class: incremental small - - # add this up at the top - import sqlite3 - - # add the rest of this below the app.config statement - def connect_db(): - return sqlite3.connect(app.config['DATABASE']) - -.. class:: incremental - -This should look familiar. What will happen? - -.. class:: incremental - -This convenience method allows us to write our very first test. - - -Tests and TDD -------------- - -.. class:: center - -**If it isn't tested, it's broken** - -.. class:: incremental - -We are going to write tests at every step of this exercise using the -``unittest`` module. - -.. class:: incremental - -In your ``microblog`` folder create a ``microblog_tests.py`` file. - -.. class:: incremental - -Open it in your editor. - - -Testing Setup -------------- - -Add the following to provide minimal test setup. - -.. code-block:: python - :class: small - - import os - import tempfile - import unittest - - import microblog - - class MicroblogTestCase(unittest.TestCase): - - def setUp(self): - db_fd = tempfile.mkstemp() - self.db_fd, microblog.app.config['DATABASE'] = db_fd - microblog.app.config['TESTING'] = True - self.client = microblog.app.test_client() - self.app = microblog.app - - -Testing Teardown ----------------- - -**Add** this method to your existing test case class to tear down after each -test: - -.. code-block:: python - - class MicroblogTestCase(unittest.TestCase): - # ... - - def tearDown(self): - os.close(self.db_fd) - os.unlink(microblog.app.config['DATABASE']) - - -Make Tests Runnable -------------------- - -Finally, we make our tests runnable by adding a ``main`` block: - -.. container:: incremental - - Add the following at the end of ``microblog_tests.py``: - - .. code-block:: python - :class: small - - if __name__ == '__main__': - unittest.main() - -.. class:: incremental - -Now, we're ready to add our first actual test.. - - -Test Database Setup -------------------- - -We'd like to test that our database is correctly initialized. The schema has -one table with three columns. Let's test that. - -.. container:: incremental - - **Add** the following method to your test class in ``microblog_tests.py``: - - .. code-block:: python - :class: small - - def test_database_setup(self): - con = microblog.connect_db() - cur = con.execute('PRAGMA table_info(entries);') - rows = cur.fetchall() - self.assertEquals(len(rows), 3) - - -Run the Tests -------------- - -We can now run our test module: - -.. class:: small - -:: - - (flaskenv)$ python microblog_tests.py - F - ====================================================================== - FAIL: test_database_setup (__main__.MicroblogTestCase) - ---------------------------------------------------------------------- - Traceback (most recent call last): - File "microblog_tests.py", line 23, in test_database_setup - self.assertEquals(len(rows) == 3) - AssertionError: 0 != 3 - - ---------------------------------------------------------------------- - Ran 1 test in 0.011s - - FAILED (failures=1) - - -Make the Test Pass ------------------- - -This is an expected failure. Why? - -.. container:: incremental - - Let's add some code to ``microblog.py`` that will actually create our - database schema: - - .. code-block:: python - :class: small - - # add this import at the top - from contextlib import closing - - # add this function after the connect_db function - def init_db(): - with closing(connect_db()) as db: - with app.open_resource('schema.sql') as f: - db.cursor().executescript(f.read()) - db.commit() - - -Initialize the DB in Tests --------------------------- - -We also need to call that function in our ``microblog_tests.py`` to set up the -database schema for each test. - -.. container:: incremental - - Add the following line at the end of that ``setUp`` method: - - .. code-block:: python - :class: small - - def setUp(self): - # ... - microblog.init_db() # <- add this at the end - -.. class:: incremental - -:: - - (flaskenv)$ python microblog_tests.py - - -Success? --------- - -.. class:: big-centered incremental - - \\o/ Wahoooo! - - -Initialize the DB IRL ---------------------- - -Our test passed, so we have confidence that ``init_db`` does what it should - -.. class:: incremental - -We'll need to have a working database for our app, so let's go ahead and do -this "in real life" - -.. class:: incremental - - (flaskenv)$ python - -.. code-block:: python - :class: incremental - - >>> import microblog - >>> microblog.init_db() - >>> ^D - - -First Break ------------ - -After you quit the interpreter, you should see ``microblog.db`` in your -directory. - -.. class:: incremental - -Let's take a few minutes here to rest and consider what we've done. - -.. class:: incremental - -When we return, we'll start writing data to our database, and reading it back -out. - - -Reading and Writing Data ------------------------- - -Before the break, we created a function that would initialize our database. - -.. class:: incremental - -It's time now to think about writing and reading data for our blog. - -.. class:: incremental - -We'll start by writing tests. - -.. class:: incremental - -But first, a word or two about the circle of life. - - -The Request/Response Cycle --------------------------- - -Every interaction in HTTP is bounded by the interchange of one request and one -response. - -.. class:: incremental - -No HTTP application can do anything until some client makes a request. - -.. class:: incremental - -And no action by an application is complete until a response has been sent -back to the client. - -.. class:: incremental - -This is the lifecycle of an http web application. - - -Managing DB Connections ------------------------ - -It makes sense to bind the lifecycle of a database connection to this same -border. - -.. class:: incremental - -Flask does not dictate that we write an application that uses a database. - -.. class:: incremental - -Because of this, managing the lifecycle of database connection so that they -are connected to the request/response cycle is up to us. - -.. class:: incremental - -Happily, Flask *does* have a way to help us. - - -Request Boundary Decorators ---------------------------- - -The Flask *app* provides decorators we can use on our database lifecycle -functions: - -.. class:: incremental - -* ``@app.before_request``: any method decorated by this will be called before - the cycle begins - -* ``@app.after_request``: any method decorated by this will be called after - the cycle is complete. If an unhandled exception occurs, these functions are - skipped. - -* ``@app.teardown_request``: any method decorated by this will be called at - the end of the cycle, *even if* an unhandled exception occurs. - - -Managing our DB ---------------- - -Consider the following functions: - -.. code-block:: python - :class: small - - def get_database_connection(): - db = connect_db() - return db - - @app.teardown_request - def teardown_request(exception): - db.close() - -.. class:: incremental - -How does the ``db`` object get from one place to the other? - - -Global Context in Flask ------------------------ - -Our flask ``app`` is only really instantiated once - -.. class:: incremental - -This means that anything we tie to it will be shared across all requests. - -.. class:: incremental - -This is what we call ``global`` context. - -.. class:: incremental - -What happens if two clients make a request at the same time? - - -Local Context in Flask ----------------------- - -Flask provides something it calls a ``local global``: "g". - -.. class:: incremental - -This is an object that *looks* global (you can import it anywhere) - -.. class:: incremental - -But in reality, it is *local* to a single request. - -.. class:: incremental - -Resources tied to this object are *not* shared among requests. Perfect for -things like a database connection. - - -Working DB Functions --------------------- - -Add the following, working methods to ``microblog.py``: - -.. code-block:: python - :class: small - - # add this import at the top: - from flask import g - - # add these function after init_db - def get_database_connection(): - db = getattr(g, 'db', None) - if db is None: - g.db = db = connect_db() - return db - - @app.teardown_request - def teardown_request(exception): - db = getattr(g, 'db', None) - if db is not None: - db.close() - - -Writing Blog Entries --------------------- - -Our microblog will have *entries*. We've set up a simple database schema to -represent them. - -.. class:: incremental - -To write an entry, what would we need to do? - -.. class:: incremental - -* Provide a title -* Provide some body text -* Write them to a row in the database - -.. class:: incremental - -Let's write a test of a function that would do that. - - -Test Writing Entries --------------------- - -The database connection is bound by a request. We'll need to mock one (in -``microblog_tests.py``) - -.. container:: incremental - - Flask provides ``app.test_request_context`` to do just that - - .. code-block:: python - :class: small - - def test_write_entry(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - con = microblog.connect_db() - cur = con.execute("select * from entries;") - rows = cur.fetchall() - self.assertEquals(len(rows), 1) - for val in expected: - self.assertTrue(val in rows[0]) - - -Run Your Test -------------- - -.. class:: small - -:: - - (flaskenv)$ python microblog_tests.py - .E - ====================================================================== - ERROR: test_write_entry (__main__.MicroblogTestCase) - ---------------------------------------------------------------------- - Traceback (most recent call last): - File "microblog_tests.py", line 30, in test_write_entry - microblog.write_entry(*expected) - AttributeError: 'module' object has no attribute 'write_entry' - - ---------------------------------------------------------------------- - Ran 2 tests in 0.018s - - FAILED (errors=1) - -.. class:: incremental - -Great. Two tests, one passing. - - -Make It Pass ------------- - -Now we are ready to write an entry to our database. Add this function to -``microblog.py``: - -.. code-block:: python - :class: small incremental - - def write_entry(title, text): - con = get_database_connection() - con.execute('insert into entries (title, text) values (?, ?)', - [title, text]) - con.commit() - -.. class:: incremental small - -:: - - (flaskenv)$ python microblog_tests.py - .. - ---------------------------------------------------------------------- - Ran 2 tests in 0.146s - - OK - - -Reading Entries ---------------- - -We'd also like to be able to read the entries in our blog - -.. container:: incremental - - We need a method that returns all of them for a listing page - - .. class:: incremental - - * The return value should be a list of entries - * If there are none, it should return an empty list - * Each entry in the list should be a dictionary of 'title' and 'text' - -.. class:: incremental - -Let's begin by writing tests. - - -Test Reading Entries --------------------- - -In ``microblog_tests.py``: - -.. code-block:: python - :class: small - - def test_get_all_entries_empty(self): - with self.app.test_request_context('/'): - entries = microblog.get_all_entries() - self.assertEquals(len(entries), 0) - - def test_get_all_entries(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - entries = microblog.get_all_entries() - self.assertEquals(len(entries), 1) - for entry in entries: - self.assertEquals(expected[0], entry['title']) - self.assertEquals(expected[1], entry['text']) - - -Run Your Tests --------------- - -.. class:: small - -:: - - (flaskenv)$ python microblog_tests.py - .EE. - ====================================================================== - ERROR: test_get_all_entries (__main__.MicroblogTestCase) - ---------------------------------------------------------------------- - Traceback (most recent call last): - File "microblog_tests.py", line 47, in test_get_all_entries - entries = microblog.get_all_entries() - AttributeError: 'module' object has no attribute 'get_all_entries' - - ====================================================================== - ERROR: test_get_all_entries_empty (__main__.MicroblogTestCase) - ---------------------------------------------------------------------- - Traceback (most recent call last): - File "microblog_tests.py", line 40, in test_get_all_entries_empty - entries = microblog.get_all_entries() - AttributeError: 'module' object has no attribute 'get_all_entries' - - ---------------------------------------------------------------------- - Ran 4 tests in 0.021s - - FAILED (errors=2) - -Make Them Pass --------------- - -Now we have 4 tests, and two fail. - -.. class:: incremental - -add the ``get_all_entries`` function to ``microblog.py``: - -.. code-block:: python - :class: small incremental - - def get_all_entries(): - con = get_database_connection() - cur = con.execute('SELECT title, text FROM entries ORDER BY id DESC') - return [dict(title=row[0], text=row[1]) for row in cur.fetchall()] - -.. container:: incremental - - And back in your terminal: - - .. class:: small - - :: - - (flaskenv)$ python microblog_tests.py - .... - ---------------------------------------------------------------------- - Ran 4 tests in 0.021s - - OK - - -Where We Stand --------------- - -We've moved quite a ways in implementing our microblog: - -.. class:: incremental - -* We've created code to initialize our database schema -* We've added functions to manage the lifecycle of our database connection -* We've put in place functions to write and read blog entries -* And, since it's tested, we are reasonably sure our code does what we think - it does. - -.. class:: incremental - -We're ready now to put a face on it, so we can see what we're doing! - - -Second Break ------------- - -But first, let's take a quick break to clear our heads. - - -Templates In Flask ------------------- - -We'll start with a detour into templates as they work in Flask - -.. container:: incremental - - Jinja2 templates use the concept of an *Environment* to: - - .. class:: incremental - - * Figure out where to look for templates - * Set configuration for the templating system - * Add some commonly used functionality to the template *context* - -.. class:: incremental - -Flask sets up a proper Jinja2 Environment when you instantiate your ``app``. - - -Flask Environment ------------------ - -Flask uses the value you pass to the ``app`` constructor to calculate the root -of your application on the filesystem. - -.. class:: incremental - -From that root, it expects to find templates in a directory name ``templates`` - -.. container:: incremental - - This allows you to use the ``render_template`` command from ``flask`` like - so: - - .. code-block:: python - :class: small - - from flask import render_template - page_html = render_template('hello_world.html', name="Cris") - - -Flask Context -------------- - -Keyword arguments you pass to ``render_template`` become the *context* passed -to the template for rendering. - -.. class:: incremental - -Flask will add a few things to this context. - -.. class:: incremental - -* **config**: contains the current configuration object -* **request**: contains the current request object -* **session**: any session data that might be available -* **g**: the request-local object to which global variables are bound -* **url_for**: so you can easily *reverse* urls from within your templates -* **get_flashed_messages**: a function that returns messages you flash to your - users (more on this later). - - -Setting Up Our Templates ------------------------- - -In your ``microblog`` directory, add a new ``templates`` directory - -.. container:: incremental - - In this directory create a new file ``layout.html`` - - .. code-block:: jinja - :class: small - - <!DOCTYPE html> - <html> - <head> - <title>Microblog! - - -

My Microblog

-
- {% block body %}{% endblock %} -
- - - -Template Inheritance --------------------- - -You can combine templates in a number of different ways. - -.. class:: incremental - -* you can make replaceable blocks in templates with blocks - - * ``{% block foo %}{% endblock %}`` - -* you can build on a template in a second template by extending - - * ``{% extends "layout.html" %}`` - * this *must* be the first text in the template - -* you can re-use common structure with *include*: - - * ``{% include "footer.html" %}`` - - -Displaying an Entries List --------------------------- - -Create a new file, ``show_entries.html`` in ``templates``: - -.. code-block:: jinja - :class: small - - {% extends "layout.html" %} - {% block body %} -

Posts

-
    - {% for entry in entries %} -
  • -

    {{ entry.title }}

    -
    - {{ entry.text|safe }} -
    -
  • - {% else %} -
  • No entries here so far
  • - {% endfor %} -
- {% endblock %} - - -Viewing Entries ---------------- - -We just need a Python function that will: - -.. class:: incremental - -* build a list of entries -* pass the list to our template to be rendered -* return the result to a client's browser - -.. class:: incremental - -As usual, we'll start by writing tests for this new function - - -Test Viewing Entries --------------------- - -Add the following two tests to ``microblog_tests.py``: - -.. code-block:: python - :class: small - - def test_empty_listing(self): - actual = self.client.get('/').data - expected = 'No entries here so far' - self.assertTrue(expected in actual) - - def test_listing(self): - expected = ("My Title", "My Text") - with self.app.test_request_context('/'): - microblog.write_entry(*expected) - actual = self.client.get('/').data - for value in expected: - self.assertTrue(value in actual) - -.. class:: incremental - -``app.test_client()`` creates a mock http client for us. - - -Run Your Tests --------------- - -.. class:: small - -:: - - (flaskenv)$ python microblog_tests.py - .F..F. - ====================================================================== - FAIL: test_empty_listing (__main__.MicroblogTestCase) - ---------------------------------------------------------------------- - Traceback (most recent call last): - File "microblog_tests.py", line 55, in test_empty_listing - assert 'No entries here so far' in response.data - AssertionError - ====================================================================== - FAIL: test_listing (__main__.MicroblogTestCase) - ---------------------------------------------------------------------- - Traceback (most recent call last): - File "microblog_tests.py", line 63, in test_listing - assert value in response.data - AssertionError - ---------------------------------------------------------------------- - Ran 6 tests in 0.138s - - FAILED (failures=2) - - -Make Them Pass --------------- - -In ``microblog.py``: - -.. code-block:: python - :class: small - - # at the top, import - from flask import render_template - - # and after our last functions: - @app.route('/') - def show_entries(): - entries = get_all_entries() - return render_template('show_entries.html', entries=entries) - -.. class:: incremental small - -:: - - (flaskenv)$ python microblog_tests.py - ...... - ---------------------------------------------------------------------- - Ran 6 tests in 0.100s - - OK - - -Creating Entries ----------------- - -We still lack a way to add an entry. We need a view that will: - -.. class:: incremental - -* Accept incoming form data from a request -* Get the data for ``title`` and ``text`` -* Create a new entry in the database -* Throw an appropriate HTTP error if that fails -* Show the user the list of entries when done. - -.. class:: incremental - -Again, first come the tests. - - -Testing Add an Entry --------------------- - -Add this to ``microblog_tests.py``: - -.. code-block:: python - :class: small - - def test_add_entries(self): - actual = self.client.post('/add', data=dict( - title='Hello', - text='This is a post' - ), follow_redirects=True).data - self.assertFalse('No entries here so far' in actual) - self.assertTrue('Hello' in actual) - self.assertTrue('This is a post' in actual) - - -Run Your Tests --------------- - -Verify that our test fails as expected: - -.. class:: small - -:: - - (flaskenv)$ python microblog_tests.py - F...... - ====================================================================== - FAIL: test_add_entries (__main__.MicroblogTestCase) - ---------------------------------------------------------------------- - Traceback (most recent call last): - File "microblog_tests.py", line 72, in test_add_entries - self.assertTrue('Hello' in actual) - AssertionError: False is not true - - ---------------------------------------------------------------------- - Ran 7 tests in 0.050s - - FAILED (failures=1) - - -Make Them Pass --------------- - -We have all we need to write entries, all we lack is an endpoint (in -``microblog.py``): - -.. code-block:: python - :class: small - - # add imports - from flask import abort - from flask import request - from flask import url_for - from flask import redirect - - @app.route('/add', methods=['POST']) - def add_entry(): - try: - write_entry(request.form['title'], request.form['text']) - except sqlite3.Error: - abort(500) - return redirect(url_for('show_entries')) - - -And...? -------- - -.. class:: small - -:: - - (flaskenv)$ python microblog_tests.py - ....... - ---------------------------------------------------------------------- - Ran 7 tests in 0.047s - - OK - -.. class:: incremental center - -**Hooray!** - - -Where do Entries Come From --------------------------- - -Finally, we're almost done. We can add entries and view them. But look at that -last view. Do you see a call to ``render_template`` in there at all? - -.. class:: incremental - -There isn't one. That's because that view is never meant to be be visible. -Look carefully at the logic. What happens? - -.. class:: incremental - -So where do the form values come from? - -.. class:: incremental - -Let's add a form to the main view. Open ``show_entries.html`` - - -Provide a Form --------------- - -.. code-block:: jinja - :class: small - - {% block body %} -
-
- - -
-
- - -
-
- -
-
-

Posts

- - -All Done --------- - -Okay. That's it. We've got an app all written. - -.. class:: incremental - -So far, we haven't actually touched our browsers at all, but we have -reasonable certainty that this works because of our tests. Let's try it. - - -.. class:: incremental - -In the terminal where you've been running tests, run our microblog app: - -.. class:: incremental - -:: - - (flaskenv)$ python microblog.py - * Running on http://127.0.0.1:5000/ - * Restarting with reloader - - -The Big Payoff --------------- - -Now load ``http://localhost:5000/`` in your browser and enjoy your reward. - - -Making It Pretty ----------------- - -What we've got here is pretty ugly. - -.. class:: incremental - -If you've fallen behind, or want to start fresh, you can find the finished -``microblog`` directory in the class resources. - -.. class:: incremental - -In that directory inside the ``static`` directory you will find -``styles.css``. Open it in your editor. It contains basic CSS for this app. - -.. class:: incremental - -We'll need to include this file in our ``layout.html``. - - -Static Files ------------- - -Like page templates, Flask locates static resources like images, css and -javascript by looking for a ``static`` directory relative to the app root. - -.. class:: incremental - -You can use the special url endpoint ``static`` to build urls that point here. -Open ``layout.html`` and add the following: - -.. code-block:: jinja - :class: small incremental - - - Flaskr - - - - -Reap the Rewards ----------------- - -Make sure that your `microblog` folder has a `static` folder inside it, and -that the `styles.css` file is in it. - -.. class:: incremental - -Then, reload your web browser and see the difference a bit of style can make. - -Homework --------- - -We've built a simple microblog application in the *Flask* web framework. - -.. class:: incremental - -For your homework this week I'd like you to add two features to this app. - -.. class:: incremental - -1. Authentication -2. Flash messaging - - -Authentication Specifications ------------------------------ - -Writing new entries should be restricted to users who have logged in. This -means that: - -.. class:: incremental - -* The form to create a new entry should only be visible to logged in users -* There should be a visible link to allow a user to log in -* This link should display a login form that expects a username and password -* If the user provides incorrect login information, this form should tell her - so. -* If the user provides correct login information, she should end up at the - list page -* Once logged in, the user should see a link to log out. -* Upon clicking that link, the system should no longer show the entry form and - the log in link should re-appear. - - -Flash Messaging Specifications ------------------------------- - -A flask app provides a method called `flash` that allows passing messages from -a view function into a template context so that they can be viewed by a user. - -.. class:: incremental - -Use this method to provide the following messages to users: - -.. class:: incremental - -* Upon a successful login, display the message "You are logged in" -* Upon a successful logout, display the message "You have logged out" -* Upon posting a successful new entry, display the message "New entry posted" -* If adding an entry causes an error, instead of returning a 500 response, - alert the user to the error by displaying the error message to the user. - - -Resources to Use ----------------- - -The microblog we created today comes from the tutorial on the `flask` website. -I've modified that tutorial to omit authentication and flash messaging. You can -refer to the tutorial and to the flask api documentation to learn what you need -to accomplish these tasks. - -`The Flask Tutorial `_ - -`Flask API Documentation `_ - -Both features depend on *sessions*, so you will want to pay particular -attention to how a session is enabled and what you can do with it once it -exists. - - -Next Week ---------- - -Next week we are going to mix things up a little and do something quite -different. - -.. class:: incremental - -We'll be starting from the app you have just built (with the additional -features you complete over the week). - -.. class:: incremental - -We will divide into pairs and each pair will select one feature from a list I -will provide. - -.. class:: incremental - -We'll spend the entire class implementing this feature, and at 8:15, each pair -will show their work to the class. - - -Wrap-Up -------- - -For educational purposes you might try taking a look at the source code for -Flask and Werkzeug. Neither is too large a package. - -.. class:: incremental - -In particular seeing how Werkzeug sets up a Request and Response--and how -these relate to the WSGI specification--can be very enlightening. diff --git a/source/presentations/session06.rst.norender b/source/presentations/session06.rst.norender deleted file mode 100644 index 3bc546f1..00000000 --- a/source/presentations/session06.rst.norender +++ /dev/null @@ -1,179 +0,0 @@ -Python Web Programming -====================== - -.. image:: img/flask_cover.png - :align: left - :width: 50% - -Session 6: Extending Flask - -.. class:: intro-blurb right - -| "Web Development, -| one drop at a time" - -.. class:: image-credit - -image: Flask Logo (http://flask.pocoo.org/community/logos/) - - -Last Week ---------- - -Last week, we created a nice, simple flask microblog application. - -.. class:: incremental - -Over the week, as your homework, you added in authentication and flash -messaging. - -.. class:: incremental - -There's still quite a bit more we can do to improve this application. - -.. class:: incremental - -And today, that's what we are going to do. - - -Pair Programming ----------------- - -`Pair programming `_ is a -technique used in agile development. - -.. class:: incremental - -The basic idea is that two heads are better than one. - -.. class:: incremental - -A pair of developers work together at one computer. One *drives* and the other -*navigates* - -.. class:: incremental - -The driver can focus on the tactics of completing a function, while the -navigator can catch typos, think strategically, and find answers to questions -that arise. - - -Pair Up -------- - -We are going to employ this technique for todays class. - -.. class:: incremental - -So take the next few minutes to find a partner and pair up. You must end up -sitting next to your partner, so get up and move. - -.. class:: incremental - -One of you will start as the driver, the other as the observer. - -.. class:: incremental - -About every 20-30 minutes, we will switch, so that each of you can take a turn -driving. - - -Preparation ------------ - -In order for this to work properly, we'll need to have a few things in place. - -.. container:: incremental small - - First, we'll start from a canonical copy of the microblog. Make a fork of - the following repository to your github account: - - .. code-block:: - :class: small - - https://github.com/UWPCE-PythonCert/training.sample-flask-app - -.. container:: incremental small - - Then, clone that repository to your local machine: - - .. code-block:: bash - :class: small - - $ git clone https://github.com//training.sample-flask-app.git - or - $ git clone git@github.com:/training.sample-flask-app.git - -Connect to Your Partner ------------------------ - -Finally, you'll want to connect to your partner's repository, so that you can -each work on your own laptop and still share the changes you make. - -.. container:: incremental small - - First, add your partner's repository as ``upstream`` to yours: - - .. code-block:: bash - :class: small - - $ git remote add upstream https://github.com//training.sample-flask-app.git - or - $ git remote add upstream git@github.com:/training.sample-flask-app.git - -.. container:: incremental small - - Then, fetch their copy so that you can easily merge their changes later: - - .. code-block:: bash - :class: small - - $ git fetch upstream - -While You Work --------------- - -.. class:: small - -Now, when you switch roles during your work, here's the workflow you can use: - -.. class:: small - -1. The current driver commits all changes and pushes to their repository: - -.. code-block:: bash - :class: small - - $ git commit -a -m "Time to switch roles" - $ git push origin master - -.. class:: small - -2. The new driver fetches and merges changes made upstream: - -.. code-block:: bash - :class: small - - $ git fetch upstream master - $ git branch -a - * master - remotes/origin/master - remotes/upstream/master - $ git merge upstream/master - -.. class:: small - -3. The new driver continues working from where their partner left off. - - -Homework --------- - -For this week, please read and complete the Introduction to Django tutorial -linked from the class website and from the course outline. - -You will be expected to have successfully completed that tutorial upon arrival -in class for our next session. - -We will begin our work starting from where it leaves off. - From 5e02f6f84322145433c515c191679ccf976dcae4 Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 17 Jan 2015 11:41:41 -0800 Subject: [PATCH 031/173] completed homework assignments --- resources/session03/forms.py | 26 +++++++++++++ resources/session03/models.py | 70 +++++++++++++++++++++++++++++++++++ resources/session03/views.py | 54 +++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 resources/session03/forms.py create mode 100644 resources/session03/models.py create mode 100644 resources/session03/views.py diff --git a/resources/session03/forms.py b/resources/session03/forms.py new file mode 100644 index 00000000..fad71bd1 --- /dev/null +++ b/resources/session03/forms.py @@ -0,0 +1,26 @@ +from wtforms import ( + Form, + HiddenField, + TextField, + TextAreaField, + validators, +) + +strip_filter = lambda x: x.strip() if x else None + + +class EntryCreateForm(Form): + title = TextField( + 'Entry title', + [validators.Length(min=1, max=255)], + filters=[strip_filter] + ) + body = TextAreaField( + 'Entry body', + [validators.Length(min=1)], + filters=[strip_filter] + ) + + +class EntryEditForm(EntryCreateForm): + id = HiddenField() diff --git a/resources/session03/models.py b/resources/session03/models.py new file mode 100644 index 00000000..f80c7932 --- /dev/null +++ b/resources/session03/models.py @@ -0,0 +1,70 @@ +import datetime +from sqlalchemy import ( + Column, + DateTime, + Index, + Integer, + Text, + Unicode, + UnicodeText, + ) + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + +Index('my_index', MyModel.name, unique=True, mysql_length=255) + + +class Entry(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode(255), unique=True, nullable=False) + body = Column(UnicodeText, default=u'') + created = Column(DateTime, default=datetime.datetime.utcnow) + edited = Column(DateTime, default=datetime.datetime.utcnow) + + @classmethod + def all(cls, session=None): + """return a query with all entries, ordered by creation date reversed + """ + if session is None: + session = DBSession + return session.query(cls).order_by(sa.desc(cls.created)).all() + + @classmethod + def by_id(cls, id, session=None): + """return a single entry identified by id + + If no entry exists with the provided id, return None + """ + if session is None: + session = DBSession + return session.query(cls).get(id) + + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(Unicode(255), unique=True, nullable=False) + password = Column(Unicode(255), nullable=False) + + @classmethod + def by_name(cls, name): + return DBSession.query(User).filter(User.name == name).first() diff --git a/resources/session03/views.py b/resources/session03/views.py new file mode 100644 index 00000000..35e37963 --- /dev/null +++ b/resources/session03/views.py @@ -0,0 +1,54 @@ +from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid.view import view_config + +from .models import ( + DBSession, + MyModel, + Entry, + ) + +from .forms import ( + EntryCreateForm, + EntryEditForm, +) + + +@view_config(route_name='home', renderer='templates/list.jinja2') +def index_page(request): + entries = Entry.all() + return {'entries': entries} + + +@view_config(route_name='detail', renderer='templates/detail.jinja2') +def view(request): + this_id = request.matchdict.get('id', -1) + entry = Entry.by_id(this_id) + if not entry: + return HTTPNotFound() + return {'entry': entry} + + +@view_config(route_name='action', match_param='action=create', + renderer='templates/edit.jinja2') +def create(request): + entry = Entry() + form = EntryCreateForm(request.POST) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + DBSession.add(entry) + return HTTPFound(location=request.route_url('home')) + return {'form': form, 'action': request.matchdict.get('action')} + + +@view_config(route_name='action', match_param='action=edit', + renderer='templates/edit.jinja2') +def update(request): + id = int(request.params.get('id', -1)) + entry = Entry.by_id(id) + if not entry: + return HTTPNotFound() + form = EntryEditForm(request.POST, entry) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + return HTTPFound(location=request.route_url('detail', id=entry.id)) + return {'form': form, 'action': request.matchdict.get('action')} From 9e1c9db3a379d1d63371cffddaf8e63f862872c8 Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 17 Jan 2015 11:55:49 -0800 Subject: [PATCH 032/173] working link to edit form for a single entry --- resources/session03/detail.jinja2 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 resources/session03/detail.jinja2 diff --git a/resources/session03/detail.jinja2 b/resources/session03/detail.jinja2 new file mode 100644 index 00000000..f80810d3 --- /dev/null +++ b/resources/session03/detail.jinja2 @@ -0,0 +1,15 @@ +{% extends "layout.jinja2" %} +{% block body %} +
+

{{ entry.title }}

+
+

{{ entry.body }}

+
+

Created {{entry.created}}

+
+

+ Go Back :: + + Edit Entry +

+{% endblock %} From 064552bae21431b9c8ba42f1b8de77ac03da331c Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 17 Jan 2015 12:00:27 -0800 Subject: [PATCH 033/173] starting to work on session 5 --- source/presentations/session05.rst | 87 ++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/source/presentations/session05.rst b/source/presentations/session05.rst index f18ed3aa..7d8c7c71 100644 --- a/source/presentations/session05.rst +++ b/source/presentations/session05.rst @@ -1242,8 +1242,8 @@ Start your server (or restart it if by some miracle it's still going). now. -HTTP - Resources ----------------- +Step 4: Serving Resources +------------------------- We've got a very simple server that accepts a request and sends a response. But what happens if we make a different request? @@ -1264,13 +1264,15 @@ But what happens if we make a different request? http://localhost:10000/section/page? -.. nextslide:: +.. nextslide:: Determining a Resource We expect different urls to result in different responses. .. rst-class:: build .. container:: + Each separate *path* provided should map to a *resource* + But this isn't happening with our server, for obvious reasons. It brings us back to the second element of that first line of an HTTP @@ -1281,8 +1283,7 @@ We expect different urls to result in different responses. **The Return of the URI** -HTTP Requests: URI ------------------- +.. nextslide:: HTTP Requests: URI ``GET`` **/path/to/index.html** ``HTTP/1.1`` @@ -1302,22 +1303,75 @@ HTTP Requests: URI * Raw data * API endpoints +.. nextslide:: Parsing a Request + +Our ``parse_request`` method actually already finds the ``uri`` in the first +line of a request + +.. rst-class:: build +.. container:: + + All we need to do is update the method so that it *returns* that uri + + Then we can use it. + +.. nextslide:: My Solution + +.. code-block:: python + + 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' + # add the following line: + return uri + +.. nextslide:: Pass It Along + +Now we can update our server code so that it uses the return value of +``parse_request``. + +.. rst-class:: build +.. container:: + + That's a pretty simple change: + + .. code-block:: python + + try: + uri = parse_request(request) + except NotImplementedError: + response = response_method_not_allowed() + else: + content, type = resolve_uri(uri) # change this line + # and add this block + try: + response = response_ok(content, type) + except NameError: + response = response_not_found() Homework -------- -For your homework this week you will expand your server's capabilities so that -it can make different responses to different URIs. +You may have noticed that we just added a call to functions that don't exist .. rst-class:: build .. container:: - You'll allow your server to serve up directories and files from your own - filesystem. + This is a common method for building working software, called + ``pseudocode`` + + It's a program that shows you what you want to do, but won't actually run. + + For your homework this week you will create these functions, completing the + HTTP server. - You'll be starting from the ``http_server.py`` script that is currently in - the ``assignments/session02`` directory. It should be pretty much the same - as what you've created here. + Your starting point will be what we've made here in class. + + A working copy of which is in ``resources/session05`` as + ``http_server_at_home.py`` One Step At A Time @@ -1330,15 +1384,18 @@ Take the following steps one at a time. Run the tests in * Update ``parse_request`` to return the URI it parses from the request. -* Update ``response_ok`` so that it uses the resource and mimetype identified - by the URI. - * Write a new function ``resolve_uri`` that handles looking up resources on disk using the URI. * Write a new function ``response_not_found`` that returns a 404 response if the resource does not exist. +* Update ``response_ok`` so that it uses the values returned by ``resolve_uri`` + by the URI. + +* You'll plug those values into the response you generate in the way required + by the protocol + HTTP Headers ------------ From a683c76c3621eac63fba187c01debd1ec454a1d2 Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 17 Jan 2015 12:00:46 -0800 Subject: [PATCH 034/173] starting to work on session 3 --- source/presentations/session03.rst | 274 +++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 source/presentations/session03.rst diff --git a/source/presentations/session03.rst b/source/presentations/session03.rst new file mode 100644 index 00000000..e20d3e5a --- /dev/null +++ b/source/presentations/session03.rst @@ -0,0 +1,274 @@ +********** +Session 03 +********** + +.. figure:: /_static/no_entry.jpg + :align: center + :width: 60% + + By `Joel Kramer via Flickr`_ + +.. _Joel Kramer via Flickr: https://www.flickr.com/photos/75001512@N00/2707796203 + +Security And Deployment +======================= + +.. rst-class:: left +.. container:: + + By the end of this session we'll have deployed our learning journal to a + public server. + + So we will need to add a bit of security to it. + + We'll get started on that in a moment + +But First +--------- + +.. rst-class:: large center + +Questions About the Homework? + +.. nextslide:: A Working Edit Form + +.. code-block:: python + + class EntryEditForm(EntryCreateForm): + id = HiddenField() + +`View this online `_ + +.. nextslide:: A Working Edit View + +.. code-block:: python + + @view_config(route_name='action', match_param='action=edit', + renderer='templates/edit.jinja2') + def update(request): + id = int(request.params.get('id', -1)) + entry = Entry.by_id(id) + if not entry: + return HTTPNotFound() + form = EntryEditForm(request.POST, entry) + if request.method == 'POST' and form.validate(): + form.populate_obj(entry) + return HTTPFound(location=request.route_url('detail', id=entry.id)) + return {'form': form, 'action': request.matchdict.get('action')} + +`View this online `_ + +.. nextslide:: Linking to the Edit Form + +.. code-block:: jinja + + {% extends "layout.jinja2" %} + {% block body %} +
+ +
+

+ Go Back :: + + Edit Entry +

+ {% endblock %} + + +`View this online `_ + +.. nextslide:: A Working User Model + +.. code-block:: python + + class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(Unicode(255), unique=True, nullable=False) + password = Column(Unicode(255), nullable=False) + + @classmethod + def by_name(cls, name): + return DBSession.query(User).filter(User.name == name).first() + +`View this online `_ + +Securing An Application +======================= + +.. rst-class:: left +.. container:: + + We've got a solid start on our learning journal. + + .. rst-class:: build + .. container:: + + We can: + + .. rst-class:: build + + * view a list of entries + * view a single entry + * create a new entry + * edit existing entries + + But so can everyone who visits the journal. + + It's a recipe for **TOTAL CHAOS** + + Let's lock it down a bit. + + +AuthN and AuthZ +--------------- + +There are two aspects to the process of access control online. + +.. class:: incremental + +* **Authentication**: Verification of the identity of a *principal* +* **Authorization**: Enumeration of the rights of that *principal* in a + context. + +.. class:: incremental + +All systems with access control involve both of these aspects. + +.. class:: incremental + +AuthZ in our Flask and Django apps was minimal + + +Pyramid Security +---------------- + +In Pyramid these two aspects are handled by separate configuration settings: + +.. class:: incremental + +* ``config.set_authentication_policy(AuthnPolicy())`` +* ``config.set_authorization_policy(AuthzPolicy())`` + +.. class:: incremental + +If you set one, you must set the other. + +.. class:: incremental + +Pyramid comes with a few policy classes included. + +.. class:: incremental + +You can also roll your own, so long as they fulfill the contract. + + +Our Wiki Security +----------------- + +We'll be using two built-in policies today: + +.. class:: incremental + +* ``AuthTktAuthenticationPolicy``: sets an expirable authentication ticket + cookie. +* ``ACLAuthorizationPolicy``: uses an *Access Control List* to grant + permissions to *principals* + +.. class:: incremental + +Our access control system will have the following properties: + +.. class:: incremental + +* Everyone can view pages +* Users who log in may be added to an 'editors' group +* Editors can add and edit pages. + +Introduce authn/authz + + +Discuss authz + +Discuss ACLs + +Create a 'factory' for our action views + +prove that the edit/create buttons now return "403 Forbidden" + + +Introduce Authentication + +Discuss methods for proving who you are, username/password combination + +Passwords and encryption + +How Cryptacular works + +Adding encryption to our application + +Update initializedb so that it creates a user, stores it with an enrypted +password + +Add api instance method to user that will verify a password + +Add routes for login/logout actions + +Add login/logout views + + +Start app and login/logout + + +Deploying An Application +======================== + +A bit about how heroku works + +running the application + +Create a runapp.py (use it locally from python to demonstrate) + +add a shell script that will install and then run the app using the above script + +Create a Procfile + +set up heroku app for this application + +install postgresql plugin + +Show how you can get DB url from config and environment, + +Note how python has os.environ to allow us to access environment variables + +alter __init__.py to use this to set up the database url (and initializedb as well) + +Note how we can use the the environment for other special values too: + +* administrator password +* authentication policy secret + +Update app to use those as well + +git push heroku master + +git run initialize_learning_journal_db heroku.ini + +heroku logs + +Adding Polish +============= + +Markdown for posts so you can create a formatted entry + +add markdown package, pygments package + +pygmentize -f html -S colorful -a .syntax + +create jinja2 filter + +add filter to configuration (.ini file or in __init__.py) + + + + From 43e22ec92115013c94653dfb4b8c01cf92b8cb6a Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 17 Jan 2015 17:29:32 -0800 Subject: [PATCH 035/173] push forward with session 3 slides --- source/presentations/session03.rst | 627 ++++++++++++++++++++++++++--- 1 file changed, 574 insertions(+), 53 deletions(-) diff --git a/source/presentations/session03.rst b/source/presentations/session03.rst index e20d3e5a..3f825db8 100644 --- a/source/presentations/session03.rst +++ b/source/presentations/session03.rst @@ -60,7 +60,7 @@ Questions About the Homework? .. nextslide:: Linking to the Edit Form -.. code-block:: jinja +.. code-block:: html+jinja {% extends "layout.jinja2" %} {% block body %} @@ -103,7 +103,7 @@ Securing An Application .. rst-class:: build .. container:: - + We can: .. rst-class:: build @@ -112,7 +112,7 @@ Securing An Application * view a single entry * create a new entry * edit existing entries - + But so can everyone who visits the journal. It's a recipe for **TOTAL CHAOS** @@ -125,99 +125,614 @@ AuthN and AuthZ There are two aspects to the process of access control online. -.. class:: incremental +.. rst-class:: build +.. container:: -* **Authentication**: Verification of the identity of a *principal* -* **Authorization**: Enumeration of the rights of that *principal* in a - context. + .. rst-class:: build -.. class:: incremental + * **Authentication**: Verification of the identity of a *principal* + * **Authorization**: Enumeration of the rights of that *principal* in a + context. -All systems with access control involve both of these aspects. + Think of them as **Who Am I** and **What Can I Do** -.. class:: incremental + All systems with access control involve both of these aspects. -AuthZ in our Flask and Django apps was minimal + But many systems wire them together as one. -Pyramid Security ----------------- +.. nextslide:: Pyramid Security In Pyramid these two aspects are handled by separate configuration settings: -.. class:: incremental - -* ``config.set_authentication_policy(AuthnPolicy())`` -* ``config.set_authorization_policy(AuthzPolicy())`` +.. rst-class:: build +.. container:: -.. class:: incremental + .. rst-class:: build -If you set one, you must set the other. + * ``config.set_authentication_policy(AuthnPolicy())`` + * ``config.set_authorization_policy(AuthzPolicy())`` -.. class:: incremental + If you set one, you must set the other. -Pyramid comes with a few policy classes included. + Pyramid comes with a few policy classes included. -.. class:: incremental + You can also roll your own, so long as they fulfill the requried interface. -You can also roll your own, so long as they fulfill the contract. + You can learn about the interfaces for `authentication`_ and + `authorization`_ in the Pyramid documentation +.. _authentication: http://docs.pylonsproject.org/docs/pyramid/en/latest/api/interfaces.html#pyramid.interfaces.IAuthenticationPolicy +.. _authorization: http://docs.pylonsproject.org/docs/pyramid/en/latest/api/interfaces.html#pyramid.interfaces.IAuthorizationPolicy -Our Wiki Security ------------------ +.. nextslide:: Our Journal Security We'll be using two built-in policies today: -.. class:: incremental +.. rst-class:: build +.. container:: + + .. rst-class:: build + + * ``AuthTktAuthenticationPolicy``: sets an expirable + `authentication ticket`_ cookie. + * ``ACLAuthorizationPolicy``: uses an `Access Control List`_ to grant + permissions to *principals* + + Our access control system will have the following properties: + + .. rst-class:: build + + * Everyone can view entries, and the list of all entries + * Users who log in may edit entries or create new ones + +.. _authentication ticket: http://docs.pylonsproject.org/docs/pyramid/en/latest/api/authentication.html#pyramid.authentication.AuthTktAuthenticationPolicy +.. _Access Control List: http://docs.pylonsproject.org/docs/pyramid/en/latest/api/authorization.html#pyramid.authorization.ACLAuthorizationPolicy + +.. nextslide:: Engaging Security + +By default, Pyramid uses no security. We enable it through configuration. + +.. rst-class:: build +.. container:: + + Open ``learning_journal/__init__.py`` and update it as follows: + + .. code-block:: python + + # add these imports + from pyramid.authentication import AuthTktAuthenticationPolicy + from pyramid.authorization import ACLAuthorizationPolicy + # and add this configuration: + def main(global_config, **settings): + # ... + # update building the configurator to pass in our policies + config = Configurator( + settings=settings, + authentication_policy=AuthTktAuthenticationPolicy('somesecret'), + authorization_policy=ACLAuthorizationPolicy(), + default_permission='view' + ) + # ... + +.. nextslide:: Verify It Worked + +We've now informed our application that we want to use security. + +.. rst-class:: build +.. container:: + + We've told it that by default we want a principal to have the 'view' + permission to see anything. + + Let's verify that this worked. + + Start your application, and try to view any page (You should get 403 + Forbidden): + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 84467. + serving on http://0.0.0.0:6543 + + .. rst-class:: build + + * http://localhost:6543/ + * http://localhost:6543/journal/1 + * http://localhost:6543/journal/create + * http://localhost:6543/journal/edit + +Implementing Authz +------------------ + +Next we have to grant some permissions to principals. + +.. rst-class:: build +.. container:: + + Pyramid authorization relies on a concept it calls "context". + + A *principal* can be granted rights in a particular *context* + + Context can be made as specific as a single persistent object + + Or it can be generalized to a *route* or *view* + + To have a context, we need a Python object called a *factory* that must + have an ``__acl__`` special attribute. + + The framework will use this object to determine what permissions a + *principal* has + + Let's create one + +.. nextslide:: Add ``security.py`` + +In the same folder where you have ``models.py`` and ``views.py``, add a new +file ``security.py`` + +.. rst-class:: build +.. container:: + + .. code-block:: python + + from pyramid.security import Allow, Everyone, Authenticated + + class EntryFactory(object): + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, Authenticated, 'create'), + (Allow, Authenticated, 'edit'), + ] + def __init__(self, request): + pass + + The ``__acl__`` attribute of this object contains a list of *ACE*\ s + + An *ACE* combines an *action* (Allow, Deny), a *principal* and a *permission* + +.. nextslide:: Using Our Context Factory + +Now that we have a factory that will provide context for permissions to work, +we can tell our configuration to use it. + +.. rst-class:: build +.. container:: + + Open ``learning_journal/__init__.py`` and update the route configuration + for our routes: + + .. code-block:: python + + # add an import at the top: + from .security import EntryFactory + # update the route configurations: + def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + # ... Add the factory keyword argument to our route configurations: + config.add_route('home', '/', factory=EntryFactory) + config.add_route('detail', '/journal/{id:\d+}', factory=EntryFactory) + config.add_route('action', '/journal/{action}', factory=EntryFactory) + +.. nextslide:: What We've Done + +We've now told our application we want a principal to have the *view* +permission by default. + +.. rst-class:: build +.. container:: + + And we've provided a factory to supply context and an ACL for each route. + + Check our ACL. Who can view the home page? The detail page? The action + pages? + + Pyramid allows us to set a *default_permission* for *all views*\ . + + But view configuration allows us to require a different permission for *a view*\ . + + Let's make our action views require appropriate permissions next + +.. nextslide:: Requiring Permissions for a View + +Open ``learning_journal/views.py``, and edit the ``@view_config`` for +``create`` and ``update``: + +.. code-block:: python + + @view_config(route_name='action', match_param='action=create', + renderer='templates/edit.jinja2', + permission='create') # <-- ADD THIS + def create(request): + # ... + + @view_config(route_name='action', match_param='action=edit', + renderer='templates/edit.jinja2', + permission='edit') # <-- ADD THIS + def update(request): + # ... + +.. nextslide:: Verify It Worked + +At this point, our "action" views should require permissions other than the +default ``view``. + +.. rst-class:: build +.. container:: + + Start your application and verify that it is true: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 84467. + serving on http://0.0.0.0:6543 + + .. rst-class:: build + + * http://localhost:6543/ + * http://localhost:6543/journal/1 + * http://localhost:6543/journal/create + * http://localhost:6543/journal/edit + + You should get a ``403 Forbidden`` for the action pages only. + +Implement AuthN +--------------- + +Now that we have authorization implemented, we need to add authentication. + +.. rst-class:: build +.. container:: + + By providing the system with an *authenticated user*, our ACEs for + ``Authenticated`` will apply. + + We'll need to have a way for a user to prove who they are to the + satisfaction of the system. + + The most common way of handling this is through a *username* and + *password*. + + A person provides both in an html form. -* ``AuthTktAuthenticationPolicy``: sets an expirable authentication ticket - cookie. -* ``ACLAuthorizationPolicy``: uses an *Access Control List* to grant - permissions to *principals* + When the form is submitted, the system seeks a user with that name, and + compares the passwords. -.. class:: incremental + If there is no such user, or the password does not match, authentication + fails. -Our access control system will have the following properties: +.. nextslide:: An Example -.. class:: incremental +Let's imagine that Alice wants to authenticate with our website. -* Everyone can view pages -* Users who log in may be added to an 'editors' group -* Editors can add and edit pages. +.. rst-class:: build +.. container:: + + Her username is ``alice`` and her password is ``s3cr3t``. + + She fills these out in a form on our website and submits the form. + + Our website looks for a ``User`` object in the database with the username + ``alice``. + + Let's imagine that there is one, so our site next compares the value she + sent for her *password* to the value stored in the database. + + If her stored password is also ``s3cr3t``, then she is who she says she is. + + All set, right? + +.. nextslide:: Encryption + +The problem here is that the value we've stored for her password is in ``plain +text``. + +.. rst-class:: build +.. container:: + + This means that anyone could potentially steal our database and have access + to all our users' passwords. + + Instead, we should *encrypt* her password with a strong one-way hash. + + Then we can store the hashed value. + + When she provides the plain text password to us, we *encrypt* it the same + way, and compare the result to the stored value. + + If they match, then we know the value she provided is the same we used to + create the stored hash. + +.. nextslide:: Adding Encryption + +Python provides a number of libraries for implementing strong encryption. + +.. rst-class:: build +.. container:: + + You should always use a well-known library for encryption. + + We'll use a good one called `Cryptacular`_. + + This library provides a number of different algorithms and a *Manager* that + implements a simple interface for each. + + .. code-block:: python + + from cryptacular.bcrypt import BCRYPTPasswordManager + manager = BCRYPTPasswordManager() + hashed = manager.encode('password') + if manager.check(hashed, 'password'): + print "It matched" + +.. _Cryptacular: https://pypi.python.org/pypi/cryptacular/ + +.. nextslide:: Install Cryptactular + +To install a new package as a dependency, we add the package to our list in +``setup.py``: + +.. rst-class:: build +.. container:: + + .. code-block:: python + + requires = [ + ... + 'wtforms', + 'cryptacular', + ] + + Then, we re-install our package to pick up the new dependency: + + .. code-block:: bash + + (ljenv)$ python setup.py develop + + *note* if you have a c compiler installed but not the Python dev headers, + this may not work. Let me know if you get errors. + +.. nextslide:: Comparing Passwords + +The job of comparing passwords should belong to the ``User`` object. + +.. rst-class:: build +.. container:: + + Let's add an instance method that handles it for us. + + Open ``learning_journal/models.py`` and add the following to the ``User`` + class: + + .. code-block:: python + + # add this import at the top + # from cryptacular.pbkdf2 import PBKDF2PassordManager as Manager + from cryptacular.bcrypt import BCRYPTPasswordManager as Manager + + # add this method to the User class: + class User(Base): + # ... + def verify_password(self, password): + manager = Manager() + return manager.check(self.password, password) + +.. nextslide:: Create a User + +We'll also need to have a user for our system. -Introduce authn/authz +.. rst-class:: build +.. container:: + + We can leverage the database initialization script to handle this. + + Open ``learning_journal/scripts/initialzedb.py``: + + .. code-block:: python + + # add the import + # from cryptacular.pbkdf2 import PBKDF2PassordManager as Manager + from cryptacular.bcrypt import BCRYPTPasswordManager as Manager + from ..models import User + # and update the main function like so: + def main(argv=sys.argv): + # ... + with transaction.manager: + # replace the code to create a MyModel instance + manager = Manager() + password = manager.encode(u'admin') + admin = User(name=u'admin', password=password) + DBSession.add(admin) + +.. nextslide:: Rebuild the Database: + +In order to get our user created, we'll need to delete our database and +re-build it. + +.. rst-class:: build +.. container:: + + Make sure you are in the folder where ``setup.py`` appears. + + Then remove the sqlite database: + + .. code-block:: bash + + (ljenv)$ rm *.sqlite + + And re-initialize: + + .. code-block:: bash + + (ljenv)$ initialize_learning_journal_db development.ini + ... + 2015-01-17 16:43:55,237 INFO [sqlalchemy.engine.base.Engine][MainThread] + INSERT INTO users (name, password) VALUES (?, ?) + 2015-01-17 16:43:55,237 INFO [sqlalchemy.engine.base.Engine][MainThread] + (u'admin', '$2a$10$4Z6RVNhTE21mPLJW5VeiVe0EG57gN/HOb7V7GUwIr4n1vE.wTTTzy') + +Providing Login UI +------------------ + +We now have a user in our database with a strongly encrypted password. + +.. rst-class:: build +.. container:: + + We also have a method on our user model that will verify a supplied + password against this encrypted version. + We must now provide a view that lets us log in to our application. -Discuss authz + We start by adding a new *route* to our configuration in + ``learning_journal/__init__.py``: -Discuss ACLs + .. code-block:: python + + config.add_rount('action' ...) + # ADD THIS + config.add_route('auth', '/sign/{action}', factory=EntryFactory) + +.. nextslide:: A Login Form + +It would be nice to use the form library again to make a login form. -Create a 'factory' for our action views +.. rst-class:: build +.. container:: -prove that the edit/create buttons now return "403 Forbidden" + Open ``learning_journal/forms.py`` and add the following: + .. code-block:: python -Introduce Authentication + # add an import: + from wtforms import PasswordField + # and a new form class + class LoginForm(Form): + username = TextField( + 'Username', [validators.Length(min=1, max=255)] + ) + password = PasswordField( + 'Password', [validators.Length(min=1, max=255)] + ) -Discuss methods for proving who you are, username/password combination -Passwords and encryption +.. nextslide:: Login View -How Cryptacular works +We'll use that form in a view to log in (in ``learning_journal/views.py``): -Adding encryption to our application +.. rst-class:: build +.. container:: -Update initializedb so that it creates a user, stores it with an enrypted -password + .. code-block:: python + + # a new imports: + from pyramid.security import remember + from .forms import LoginForm + from .models import User + + # and a new view + @view_config(route_name='auth', match_param='action=in', renderer='string', + request_method='POST') + def sign_in(request): + login_form = None + if request.method == 'POST': + login_form = LoginForm(request.POST) + if login_form and login_form.validate(): + user = User.by_name(login_form.username.data) + if user and user.verify_password(login_form.password.data): + headers = remember(request, user.name) + return HTTPFound(location=request.route_url('home'), + headers=headers) + +.. nextslide:: Where's the form? + +Notice that this view doesn't render anything. No matter what, you end up +returning to the ``home`` route. + +.. rst-class:: build +.. container:: -Add api instance method to user that will verify a password + We have to incorporate our login form somewhere. -Add routes for login/logout actions + The home page seems like a good place. -Add login/logout views + But we don't want to show it all the time. + Only when we aren't logged in already. -Start app and login/logout + Let's give that a whirl. + +.. nextslide:: Updating ``index_page`` + +Pyramid security provides a method that returns the id of the user who is +logged in, if any. + +.. rst-class:: build +.. container:: + + We can use that to update our home page in ``learning_journal/views.py``: + + .. code-block:: python + + # add an import: + from pyramid.security import authenticated_userid + + # and update the index_page view: + @view_config(...) + def index_page(request): + # ... get all entries here + form = None + if not authenticated_userid(request): + form = LoginForm() + return {'entries': entries, 'login_form': form} + +.. nextslide:: Update ``list.jinja2`` + +Now we have to update our template to display the form, *if it is there* + +.. rst-class:: build +.. container:: + + .. code-block:: jinja + + {% block body %} + {% if login_form %} + + {% else %} + {% if entries %} + ... + +.. nextslide:: Try It Out + +We should be ready at this point. + +.. rst-class:: build +.. container:: + + Fire up your application and see it in action: + + .. code-block:: bash + + (ljenv)$ pserve development.ini + Starting server in PID 84467. + serving on http://0.0.0.0:6543 + + Load the home page and see your login form: + + * http://localhost:6543/ Deploying An Application @@ -237,7 +752,7 @@ set up heroku app for this application install postgresql plugin -Show how you can get DB url from config and environment, +Show how you can get DB url from config and environment, Note how python has os.environ to allow us to access environment variables @@ -256,6 +771,12 @@ git run initialize_learning_journal_db heroku.ini heroku logs + +outline +------- + +add logout?? + Adding Polish ============= From 0f9a9c1a6ba128cf473b0ea03d7ac748f6029721 Mon Sep 17 00:00:00 2001 From: cewing Date: Sat, 17 Jan 2015 17:29:52 -0800 Subject: [PATCH 036/173] add cover image for session 3 --- source/_static/no_entry.jpg | Bin 0 -> 639171 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 source/_static/no_entry.jpg diff --git a/source/_static/no_entry.jpg b/source/_static/no_entry.jpg new file mode 100644 index 0000000000000000000000000000000000000000..76c1826553234e59b7c235cb837f122c06bd651c GIT binary patch literal 639171 zcmb5VXH-+q7d9GtQL2FS-V%hL^xj(%dI$jm2uP7GEg&dGiWKQ2^iD!T7bJ8nsB{rX zDAEK0X##>E3Zk$7_kOtd>wV5yXRTRh_I~!voU_*KGJhBTt^rt~CJ++<1qB5F{O=>Xul4_el9Gz@f1swKqNJjsrlFysrlzK$rK6>xp`)dyre&a|qyHahXc-t8=o$al z_@9#hDfpk%znlJ_)c@`B|LgJh6@ZPN3P25@qF@71vQbd6QT%-i5CQ=H8T5ax1N=Xr zqo-w{reUO_qySv|*TI$*Ktc6CP|{FR(Na+ZZcF{c4N*FWaFp$Y$ZByK7h4suJ**#2DvT&JS=H`afmx`1Z?_(2aMqv~p zOhP_XKlfUrBKQ}K{ksE87SI^;Gs3I|&Qw2e^T+Ze*xuR+xNU4ePe!zjExDQ8Cm!5C zm*ziR1KTGng?=IL!_qymBN3V-AwY;wmsUl&e{W>Va_oxV-W4~ZKH{)=zLE1_upqJ> zQ*|S`o?B3t@E35WW0FyydT2J`(On65+1hh%ojmX2Y_mYpZp8bkUT2_mg%mKTFwbC& z*^1|<`o5X9Re(VcLMW>8bacLzDF5A6*=inRrN5A%Cp~HU$K?hT7Exu9N3XTxfwG2e zI-HdR+`B8QGjGj#X*xIw|l&#B8t0ju+syF>jmIZBhYQl)RI6Kg6~V$o?q| zGuV%d)Yt|6SY6BnlIZac&?EZlV@ zU~eFz+|s#j)hiCRoce9S^>A@1QGC1X-s9Y12-Ilg_bGlQ3dGuM*~`{*tSE0<>h~8w zhAQib%9#W{-0gakyJ9ADp=w||$cZ`b|Fw3S+)lhIc<+5^WneU%!Y!w+^|BGMr4B_j(<^doI7 z-%0KuUY2_NyFp3FjZOkYS3}wFYh%#GGY}$ho%w0l(%H?8H!dIojBrK+oPs6fuN&co zvmR{V+J|~D)zy)b_Qil5h6UfcOP@#eZq*m|&vv=a++ZJPOLc06z1gaq@2iyPNi7zX zI9fv=d{fYfvaE>zS`nGIRln(9_1A|l=0E0-E zPmh^80-%$H`>yz%`R7Ma`S-9JqZQ)^bhZNmLu4##684gxKf3~p%y9->W`7`xal8(k z%KOZ(F1%9@S#s~zIW_L>=tcEKO4)WCrn${>Jz-Ly)+oO<M5k^U=ns z%C=dIGim8qDYVCc3l*rQXV#pKYpUyJUnXG0#j$H^-=}UNf4!5nm`s|!L%-g4&yj^= zFi+Tje5SGB{h3HTqCEDgQ>OrSiAZlnw=tIf%8U>LVl*tBj2RUZ%igG}?}wR-?;~&i zelhl31c^PH-y+^o!Ceq$kE~ZSN*edQ>t~cL*9U^2f#opYbMCa@R}Yj@N@F!;mC${x zD$_o>gU~F~56CAxsT~{izI5jTM)|E8Z`zo7Zh26P>*>+zQ3&%;$m1*GPfEI7{yTM& z0V_Q<3n~{gQ<25n<4h}cj8$%a$-~iXxlHZlV{$64iEYXN&cdBClQ#7;`jg=yk?6~} z-3F-|-lHo3^jDpG3g|zzysS9WW$C;xP7m2jvufiNvT;S~Qzp$G*`>dK^N`rmIj-RT{<)kw zI5vj1Tvq+5Z7Tfb`JTsKfQqZ$xFyrmrGA+zxzBXE9{K-ei~Y9iB{4%hh|urxsHsHx zKRbfMe(pXceHzK?a`5F8`@34L_)ta5$jv06u*}u;y>2@qRMP;8rr?$uKj^ou`cd7b z=6AwL;O0N4TD0K%S_$X97w$(}?K2AtFNL0U6{l$LoS9sP>{)wO6-d6x$#Dw71PEi% zO(E#DV`<2des;)~9a(lpSpNszLiwZDr<{NRob4XsYTkdB_}%#KO1*xuQIvv*rX1C^ zB24Ft)d}e=Qr4vjdrXyJ^cw7@w3rnr?sOg~Vg`7f!ys6avHbhJT`M8(^@kbP2AM^H zzkmq)GhNGu)5{16+2KG!h?%ll8{0?s?Yt9{UA|zB!=q5v__oSOTRB$JAR=RUQfDY_ zjo&`%iP$rJh*dh2_{9h)btlJHoA?*-+xY+hFAc4Xibj*e2Xrh9D5dmj-x+Fenp_m& z{aXYEHYq9LPzW{dfxxxiH(v+!nDY18=9IP&=kU|{ zJ%9UfPufH)3zYa8kLs9T_1#V)fhzxl)v2Z=&5D zg;$^Ylbz_r;&=x>yh%BxnQ;*(+j8YHxc%^90pN@Mg`8GtxJV2CpQ z-=Wcc&tv}+R>)tOd#;%wcri@`u2DWZ75oeE+nKB0^EYi>iZ+M8!@lkIXS=~fAodzQ zvsbp{z{%#VaSQ*~U748Ju`O0p{xm+3`oYwf)%T1LMnT5Pf|qKwIB2F($!w3WEzsIu zsn^JBFqF}S0ggzN0D%Jpe$sS!#8*-awF3YNfQ&(whP7pda|!8!8^1C#`**7E&vIHDeCjc?1Gi4`mWU5N zsNOM3SXC9fyGWx))sZ2SahwyD(Bx@M$~T%+(+>h2TkiJ_VO zAd!H}sV^ZpP0j6(qCQlh&(`%OOv-jyI#ZMX0^-1zu$1OrB6bzCQ@Mupt~Pq!;13nR z+Hl=zG-Ouo3Uvqudf}vG37t1{C?QrnJ01(OJ?beVv5e5$Pv0dNFuPvWdAy2;n-(i2 z5B`oCpqjZ$Guro`G-oQ!$nzaYU5uykq|bE;e0ok@Q83PT6^6y4HXxWJ^S!#H)=GhP!^vor+u*aPkDSDd{F|?(E!8U{V z2A@1S{qTFN>w2aRN_&z!WS*AO&;L&5TEyH`- zNSltE4!%nJd$p$R;T6Z#WSpsYSSu})u_6-YV0csyNo`NzKd2&fV5M0@UTuB2ri7eK zqYf8BKJ|TfGkL}yL^Wjo05zX`ivQfK3r2yG&0hEv!ovi*!2zPknKX-k-V=@}6%iK# z+=5t3@$3~w1vyN=7AkA_3rP8VunOojue=}ZgYre5ncXEtp%H@J^$O00Q}Ew#%n#O$ zBB}c#Z-Z>_sSU}mWj&$(Uc2jvc%t;JiFeEsoUm77Xyd8&CKHoPoqXAem)4@b?62H7 zs_4GH12i;zfg$4JQp6KVs*;gdc>!5%flUcreer52eVi}jt;TCY+#tx$?Wb0k4F(8` zdFY*keakLSmvbhO^bA{ea}*qz@p(B|Y3vewH7-tgm}3zCElO}@MS(XHKqlu0V=^Q7 zztr83>sIO|!M8;0uSMyF)O~yfB|jL;%?M@>){6SMr#N>Xz-TN|NDY?XcS2I9V`8gz z>jvY+hdk3lykDUEVIOUf56#3vUJZS!kpE!Ok<9bHyWTHKUA>a+&ii>s+?`$*1;PY; z>M!6Oe7NCTnI=2gxqD{rW)>^^9*5rd4oaP9`J{fsS{EPGN~d?40C0nheB{UX&0N>wFD+h^`Wgxjs}O~3p%oZdKXEQ-7^_pC%(D86 z7Sd;6UoJ{{{{6VR@WG9yjs)y^zD8Er(17aJq=p^dHJbK~eb}Fx)58?@<#}I@wT66} z+F!BNIHvei(iBcYVk-WdH+V6UAmRnNV|%hZ@!1hE=yqDoNVE}iIm?Vi?mT)KF|9G< z{q$tN!4~!TFW?#QnNocwxXz?A=&5&~u~UhzcpcN-&7^b9_D@3`|}3h1C!nnb_U79YbnOHB%8o}$=!Pj7}c2xj^7(NJ*389vF zE1?;zM@^CRG)Bf(y^O+V)8u$#vQE>Jnk$vOmoMfjU)TRDoA*BIlc`g)-A?pI-Mm*% z$fsw4z8rhYT*#OtyWL54K)U$D#oVO;oUaw}E!Ve58cyS9-3A4nPtv4r%bzIu9mL>z z;T=0l8WRr=sBBWKL)*8 zSeN>}%x2~Xoe%(ZKVt<~#l#b-=PFp0=9C;lcNX{i@t6inW2w3h%dvxNB9flv zsBGD{3F>-U7xNxJ>N?^6b5D6ILw8-KJ<_obU0NkZK!%DATp(q~J12mo*)_A|w=zu4 z`i4LV_g=%h+Ej!ut?Bl9V}#sdnU4gW@2b+ie90q{;*jK(CWJIj>W=v}5 zi?Ugz`Kl3;*~pTIQPBmp{wtKjr+)#iCGWqIu0}pgd35aSpJRAFuP^8K-x{KUGTPPy z9`N)Ya);irnJ+#**+=?61p>RwyrBG56H#Anr!oQC4qOE_PK5@%`gU*f0xDU~kPrwCc`?O+bhejUS5v2WDf}2MOYQq|J2OIo2U_id z65NdOQ^#w3z$2eJkhQJa2bCGe`-C#a=53g5z0L3@b;I5|4pALionz$Ap-mB}Tpmk@ z*SlruAvdE?-FGTaP1$qlf_%vjQ*W?|vpt(BDm64OAswi-B2kRy{<8)p;&$fTyJq>4 z1#_oq3DMd;P`Ru*_d+f=TI$yR>b3J$`o5 z3Y(-BZo2;Ksm(c`?n~a=VyNrlo z{5JD2RKl+uS{&jEerf_fXx!Z_xz)wBZ%(+(fAS>km0b2qdpaQ)0Cb~{=;#%LMeiA1 zz8xk4&;J7OW;W5XE$4R#>3;$9s>s+L1YWPCz|nm%deJG=qaG+HEL)IwQUtP(T*U5BF##y3lZI#DOKvpUj z%?nfO19CZU2yvYrS~ZVFWSg<=Jk&s->`7?I-JY(%qrDD#dwGp$D~&3g93GaFS2PbS z5`zHpOo}}%mspKeRbt-wN55=zIg!TvP--9I-2{vEgfxx`dG>g1T z>?!d1Ow% z#9z66>+A}fv547!1MXNNuRve@-g_zWG!QqQZBqwd?_#s)aBwhIrRH$B?a>#thDX0>Yi)>OT#O>? zb$G_YXSI7LgzBQM_od7&ma5BDMRr_`PKUdjb0*&(vs^zK3L!rXPjO5XF(@{Juy6xjYi*ze^ta{%y95xLe6Grj8|_~v zvD(2t0lg)Q`H-06FLPRmYdFV zc86Jpq)|%%Gs1UNxoYgDVH!Pt5)%Re&_vP91l4dIn3*g9_l#UB0d zE%SoV*Wn!KJcf_H);40U9L3Z}gw4B^+CsAf!Ji^;5-Op}Om2Sx0lhc$62=2xH;Yuh z7s3sQVBaNMG^k26hV%E43@Z8&$74jO1WGW7WgHG5(C+zqW z3K1zaZ8*Cg(xac*`S<3Y^g6Wn$ko!`iPyaeWoLbqOHp)-F+FESCU`2DT5G9M0zZf4 zNYR95eYM}v{Ne7 zd2&m9CuzwEa>dzrEqG&;8*IzAI)Kx`~P zeZJZ@eekAPzFlnBeV8jk!(^BbcJtDbLyPrM_(p@18zxhf;!Ngv#vDgcPSBiqbYm>RDa>iW$Bg5WMYv;O-1yslcWf zV5KRwVyDfK-PGMP=`1$9{u8Vwu71o57pg5Kstp$VBy@CEuSq@0OEIhtNZW}Xfr&HR zl^#Yx7NL1%>>O%8cb`om<1N$qD5Jv2e*sE=#DW(LhQCr3vF8{NbExAzC;kOUniYBP z)t3mgbje(ldYEIE&+R13B^L`Oo|Ik?+61hf+B}DX>0mi}V_aonR~NzcK8LLOyhCrJ zSOjByJPthreyjnR0Swb%Yu?92>`I#E(w4c+LID=4bx!CZKcp%=8Gi#mm*G142C6jE z@+sUVpwYe9hJ*iJm9WZq#1EH-VR z^!>(X|0wHZk39kPi$KeAa03aO3<*rjz(H2Eaxg5ulUiZ(q1~@0l(v2{p8@eRL~1Rm zqHo&z)I0+E*gLI3nPi$a-cOr(bDgCWnSBe4kSH5 zLcm#<=U$@wcyrub6}dR4{Q%@=h0^qB1R^QZ`0Oh4Nj?VTde_^JvBhPCOF?Ok-Hxq1!!iK?# z7?A0MM^`-;wq8k+FigbVaK^mMXc_RgN?S82Lc_b_u;?J{vYqm9;cCNBGP5m9^{`Xc z(o?6j;a{p!Vk;Ze;g_r~Bz)T?JZ~L((yy$;7f=7mqn>Tu(AQr3T;HS=Z*drH`OJyf z=hbVSqFhciixpnRbUInFNH9Q_e$ZWpe-R3BjuN3xV37Z97j`40v)LILpK#+7Wa_Kt zpptc}RQU~MF<1cK<6Xq{bn|<6L5jH*wn0zCVPxAU_J08y&dZ@JLyEJzvPrDylVw3n ziF!ydhf4$uq7GM{i{kk++gtOYCq`=+Z&~B=oJdQbu%2tx+H3+XHHfnT=Wj}Th>203 zDldy%V@*Wof6?llP0kXA4L*G(^G*u%q?bDb%j{vL;T0o)kv`*c*Ulv0C58QC_q9(@ zAS&WTN%)VsxwWsx!FawQ82A@mRuUgw&S|Dp(34E(&%t>(KpR0qgCqAZ039l3qoDCl zG4Ga2fTqe9i=;Z|Lz36C)aMEVnaxB1S~^sZaZRYp#QGO(hV({H|3_7FXJw#2q-<1d zDnYAfiaqH&WQyd2*yXG5fby{wB&!lXG6X)uhzQE(up~~SZ|Ah`7D>jHT)+DKZxVu` z`hvk?3z^%|QjWb5LzOnZsQ;|&&YTf%Pl}J0P`*-+IDwjI!iNgmvTTw)e@Irm==va0 za?Lbb?3^}R?Ts0QMKLgkKE8S_N>(ZsDyV4#vbzk@4kY88&C-~#SVdXrU3=a&;qI}~AV9e<|v9&`$0KJ&anum=(QVc#|^iWcGvis1d<_(-1P6Y&1iUe@Z(M~{4ee4DM8Mf(&GcQ>O z|GWSbAvKk3OyhbM?uZTNeUk7H>X6tJ^|e=$UABc3|>HS>BnIZ47QHN z`f;;e;x+Zw#p+x{Ol+%gPR}+VX(d7fP){v&al+%=*J5>1a7^dFs38bq)nzX`VMyaB zI9(Puk9TyiJjJtv8#hm{8JFu#Utk@7;J1T4!Mew<9wg8z{zf$gC(M}iQaP{;UAs%n z3f5|5|22PLJR>s@daxqU(^ASd>RwV(BTo#8rR+JW-1xb#-_zsHo7n78WXZfgA|3vE z>(@`|E3atcj8ply_eOI~i+S2D^7if_gNlCvshQZgSTt!D_OPx>H}OR=!;z`QRXzHt zS=DOC7pW;Zka^LbSSMc$NIdtO{*Xq+%4lk}Jr|2gm|Fh64V|wYlSrN2k@34yQJJ8^ z0``GsVwP&X`tVXN$t>X3m&LlyQ>;p&_%1Hmu>f~>j|{aV!J7Lliw{n~U33+Pp zrC#o4N!T?8Aon-8Nvz)G>3*RjhK(3=FOWepE}UK16vOUe+9PegWYsN0ou!uP zKIz8RbbkbwM0V21m&$*i-=*4i@+JE~#I6EAFaqD^oLah|LF=H`TI@Vf_1vBO(E>0%xDdVrq)^X*%|RERM9 zzt8PjE@O$Q8=k|O7AsTH$FB69Dq3YE8p%Zk#qJLW{f=X80qZV`hmu+kDpFs%noj5-4{4BEd?vfk4GOh}lNL za_7Sm&jpm~C+D9h>?eW)uW%H^^(kxr&KtD+wXoqV2in_IOeAalytuK?0Q1<=|Qv6iZHt)DJ_*FUi;QyS6Kj4T1{N^2f{c&nZ17c!ge_xt$I-6w)Y~H=c*q zpDf{_nfkuq@Q#}oOL@NxGr)oFzPI`pCP~TH6FhG-=GQ&nJ=2Ar_EW1riyhGA{)O^a~QC;XIsFuBCtLg+kn zGdaR*I5J7LNsuEV@ztL4k~ETj!fq2Fm{g+Z>lI`KR0wA$-Z(^KS&GxYu#mR#PJ`34 z<NbUJf>(rQjBz96TV)u>_RNI2kgjE<799 z@Tkfv+s3)xY>E2|xOu}2wa5Q-pEOBuK?n{I>3PWGL)a~oT;~T8AX!B<`~ZuuoP>ls znXOAgs}ZM@(<33u{Lwo`Uou8)EZ-wTBoX7G5V=WzA`F0Rm;MFx_I&M$1KfXG^E#-@ zzHoPFAj}3>9n>TG*e1o+Y{UwqRNw-e9~P$0Gaa_EPizN|JcAtn@~3TT{@f{{erce^ zkxc(xm`zZTle1%h{$eTmynWm9ba)Sx;5&XWEo4!7E9G7pHO}1!40!x_GIoXg%{O}_ z0wqm<%<1x0*We^WXId)ucz#UgxzHQLpO=AwY$>%bZ~>RuW+=hv6_<%VYWvy3TR`4N^}m7GdPW$TfmK<2=XyCrQp#b`??VunKv-6e;;6s9 zKI3F24Ud4CnNzQA`2yDJHM6EaFbH3L^g-7_>RVKB2gCqd@RLtdOD9-UrBMb^Kl`Kq z^ld2g*cG@aAFt}L>#uFJCa+r#R;#d6S zz&g^LLW=PCvL8IJXE{6iFNiV=wXCyk3iQM;Yd8WeyT_;ZZ`(5)vWOTn`o8U^fF{gX zTg6wIL}N36*EMg7#rZ7-wMD!y?(MJ+f4~~Eu@g1B6MY+l>lKrOFG-w9{_w)2l!wd} z>@29N<+N~Mu5*fGGai4ROHOui!o7IkEW`LYz~FG*mxU_7pz_m!OX7lZ%kSKz?m8`%~XiX=appiay@`BhU9JV!6r49mg zOBuN3%4@K#slTr0Jg7sL3{1QSkqAAImKI8(B=YKhu6yqk5X>T;;%HdS$1mLIOMi#? zvqlashY_rqk14r$`H4-!8d_-Y5q%j5m}@gl(>h zIz4O@DWTLV!Xl7D4>W(ZK;=e9hI2IKGl*wl83bu{uxWJ`KW}m}M}to8j`XB|nATOf zHqaxuDNV;QGrO426U(u%i2aX|hpMjZ{$GGl<##KIdW{ctFE;S0`Rnk@y~5epPRGbh zMRU;2r>K3X(LH3LF&=jtbX!8im*u%E<6ppHj#H=%A;c*(rC$J>zN+f?Oc7i3%Z`+M zt0cUTquliJNpH9!;WV8}v%c5YaYpka3cVEFZ0Mc>vUF0FP?uFVD}kA;K`}%baTQEe zj{0l$&mDNPhxE!1LbOXXYhzPfzjTkC-K5QT^Z(P1>;yK>92BW?bgr4A`v=%C(rVn{ ziOIkh>-Tj<02PqEm*H!+*i0J7Z?iwS|nOv%bfx`1Mbx;G|AF~)szjq29-$C`D%#DA~hcy&yTTc)Z0x&`vdNWf55 zSe@+@a4n#MYl-&9v`%hc#??FKckR>g6|Ksmub$E>7RcJEhAY)LUohF}nn^G~;Q|q2 zziyfNno(z_ls7=*L&Q(5mk8{pcqsoN?f>5)z&LUmz;| z;}q}o2Wa#|x#rrNvP+RL(7a#s(7`Vhl_(8Gp)=}-OA;rMti+mw!^==D=U~yfQvtTu zji<)gDPO=1uYblpTPr%={|i{w`CJK-drms!HAbpnpg0oIuYk-TQ!{do+u47hhU