From 2df88e70d2a0759ff17856ed8f49f7dd2cbc75dc Mon Sep 17 00:00:00 2001 From: cewing Date: Tue, 6 Jan 2015 17:53:12 -0800 Subject: [PATCH 001/171] 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 002/171] 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 003/171] 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 004/171] 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 005/171] 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 006/171] 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 007/171] 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 008/171] 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 009/171] 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 010/171] 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 011/171] 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 012/171] 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 013/171] 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 014/171] 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 015/171] 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 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
?{@LHp)IryChMu3R7cN=JfF8De`Vr7L?= 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 Date: Tue, 13 Jan 2015 10:40:56 -0800 Subject: [PATCH 016/171] 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 017/171] * 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 018/171] 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 019/171] 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 020/171] 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 021/171] 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 022/171] 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 023/171] 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 024/171] 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 025/171] 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 026/171] 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 027/171] 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 028/171] 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 029/171] 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 030/171] 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 031/171] 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 032/171] 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 033/171] 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 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