From 1572c7c413aee9cd44d49bb550552dce2567d709 Mon Sep 17 00:00:00 2001 From: cewing Date: Wed, 14 Jan 2015 19:00:25 -0800 Subject: [PATCH 001/150] 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 002/150] 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 003/150] 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 004/150] 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 005/150] 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 006/150] 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 007/150] 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 008/150] 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 009/150] 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 010/150] 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 011/150] 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 012/150] 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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