Check-in [fdaa20eb53]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Renamed the org file, and finally got the GET requests to work
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256:fdaa20eb53d56df5caa46493d2af2a9ee3b7b23b682170428567effe753d4d88
User & Date: sehqlr 2017-12-21 02:00:04
Context
2017-12-26
17:37
Removing Static.org, forgot to do that last commit Leaf check-in: 5054b266bc user: sehqlr tags: trunk
2017-12-21
02:00
Renamed the org file, and finally got the GET requests to work check-in: fdaa20eb53 user: sehqlr tags: trunk
2017-12-17
21:56
Delete Idris source, add Literate Programming document check-in: 06ec941e60 user: sehqlr tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Deleted Project.org.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#+TITLE: Static: a semi-dynamic documents-as-database web content management system

# Local Variables:
# org-src-preserve-indentation: t
# End:

This is a literate programming document for this project. Version control will
be Fossil SCM.

* Overview
** What does the tagline mean?
   The tagline for this project is, "a database-as-document web content
   management system". Since these are pre-buzzwords, no one can guess the meaning.
  
*** "semi-dynamic" means blending the best parts of static and dynamic sites
    Web sites and applications are either static files only or
    static template files injected with data, AKA "dynamic web
    pages". This system will be a hybrid of the two. The first
    iterations of this project will only support the static site
    half of the equation first, since the dynamic site features
    depend on them.
*** "database-as-documents" means whole documents are stored in a database
    HTTP revolves around documents, which are typically stored within a web
    servers' file system. Dynamic sites combine static template files with
    queries to a data store. I want to create is a system where all of the
    documents on a server are wholly within a database system, and HTTP
    requests are mapped to SQL statements. There are a lot of technical
    implications with this kind of system, especially when it comes to
    security, which we'll explore.
*** Web Content Management System means an integrated tool
    I want an integrated tool that handles the web server, document
    versioning, static site generation, and everything else that developers
    know how to do today, but can also be used by normal folks. I also want
    a tool that can run locally and remotely the same, like Fossil SCM.

** What this document is
   This is going to be a Literate Programming document, where I sketch out the
   system from a high level and then drill down on each part. Each part should
   include any testing that verifies the code. Because =org-mode= makes it really
   easy to tangle one document into multiple files, the code base will conform to
   a conventional shape for whatever language it is in.

   I plan on using Python as the language of the first prototype, since I've
   worked with Python the longest. However, once I understand that
   implementation, I plan on rewriting it in Haskell.

* The Code
  In this section, I specify and then implement the different components of this
  application, and define some tests for them as well. Each component will have
  its own section.

** Document schema
   All of the code snippets below assume this schema for the =documents= table.
   #+NAME: documents schema
   #+BEGIN_SRC sql
     DROP TABLE IF EXISTS documents;

     CREATE TABLE documents (
     id PRIMARY KEY,
     fpath TEXT,
     content BLOB
     );
   #+END_SRC
** Running the server
   This is lifted directly from the [[https://docs.python.org/3/library/http.server.html][=http.server= documentation on python.org]].
   #+NAME: run the server
   #+BEGIN_SRC python 
     def run(server_class=HTTPServer, handler_class=BaseHTTPRequestHandler):
         server_address = ('', 8000)
         httpd = server_class(server_address, handler_class)
         httpd.serve_forever()
   #+END_SRC
** Make file
   Since I'm going to be dealing with a server and a database, I'm going to
   write a =Makefile= to manage it.
*** =website.db= target
    Create a fresh DB file when we change the schema
    #+NAME: website make target
    #+BEGIN_SRC makefile
      website.db: schema.sql
        $(RM) website.db
        cat schema.sql | sqlite3 website.db
    #+END_SRC
*** =all= target
    #+NAME: all make target
    #+BEGIN_SRC makefile
    .PHONY: all
    all: database
    #+END_SRC
** =StaticRequestHandler= class
   The next thing I need to do is define =StaticRequestHandler=, the class that
   I'll pass into the =run= function. I'll start by defining a new class that
   inherits from =BaseHTTPRequestHandler=.

   #+BEGIN_SRC python
     class StaticRequestHandler(BaseHTTPRequestHandler):
         pass
   #+END_SRC

   #+NAME: StaticRequestHandler class
   #+BEGIN_SRC python :export none :noweb yes
     class StaticRequestHandler(BaseHTTPRequestHandler):
         <<GET method>>
   #+END_SRC

   The =StaticRequestHandler= class will match the URL of the request to a SQL
   query, send that query, get the document, and return the document to the
   HTTP client.

*** Get a document by ID
    If you know what document you need by ID, fetch that from the DB directly.
    I believe that this will be the easiest to implement, and it'll be what is
    used for accessing a page long term. In this way, the URL for these will be
    like a citation link.

    According the docs, I my Handler needs to define a method named =do_GET= to
    process =GET= requests. I'll add that method first.

    #+BEGIN_SRC python
      def do_GET(self):
          pass
    #+END_SRC

    #+NAME: GET method
    #+BEGIN_SRC python
      def do_GET(self):
          self.wfile = self.path
    #+END_SRC

    According to the docs, the path of the request is stored in =self.path=. At
    first, I was going to write a test/REPL command to take a look at the type
    interactively, but I forgot that Python has cOOPling making it really hard
    to test in the small. I guess I'll have to write a make target to run the
    server and test it in the shell.

    #+NAME: server make target
    #+BEGIN_SRC makefile
      server:
        chmod 755 static.py && ./static.py
    #+END_SRC

*** Get a document by path
    This is the original vision for this project. The =static= web server
    should behave exactly like a regular web server, where paths correspond
    with documents.
*** Error pages, especially 404
    If the document doesn't exist in the database, the class should be able to
    return a 404 page, preferably from the database as well. That leads me to
    conclude that the database should have error pages in a separate table
    where they are indexed solely by HTTP status code. 

    #+NAME: error pages schema
    #+BEGIN_SRC sql
      DROP TABLE IF EXISTS error_pages;

      CREATE TABLE error_pages (
      status_code INT PRIMARY KEY,
      page BLOB
      );
    #+END_SRC

* Files
** =static.py=
   #+BEGIN_SRC python :tangle static.py :noweb yes
     #!/usr/bin/env python3
     from http.server import HTTPServer, BaseHTTPRequestHandler
     import sqlite3

     <<run the server>>

     <<StaticRequestHandler class>>

     def main():
         run(handler_class=StaticRequestHandler)

     if __name__ == "__main__":
         main()
   #+END_SRC
** =test.py=
   #+BEGIN_SRC python :tangle test.py :noweb yes
   import unittest
   import .static
   #+END_SRC
** =Makefile=
   #+BEGIN_SRC makefile :tangle Makefile :noweb yes
     <<all make target>>

     <<website make target>>

     <<server make target>>
   #+END_SRC
** =schema.sql=
   #+BEGIN_SRC sql :tangle schema.sql :noweb yes
     <<docuemnts schema>>

     <<error pages schema>>
   #+END_SRC
   

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































































































































































































































































































Added Static.org.









































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#+TITLE: Static: a semi-dynamic documents-as-database web content management system

This is a literate programming document for this project. Version control will
be Fossil SCM.

* Overview
** What does the tagline mean?
   The tagline for this project is, "a database-as-document web content
   management system". Since these are pre-buzzwords, no one can guess the meaning.
  
*** "semi-dynamic" means blending the best parts of static and dynamic sites
    Web sites and applications are either static files only or
    static template files injected with data, AKA "dynamic web
    pages". This system will be a hybrid of the two. The first
    iterations of this project will only support the static site
    half of the equation first, since the dynamic site features
    depend on them.
*** "database-as-documents" means whole documents are stored in a database
    HTTP revolves around documents, which are typically stored within a web
    servers' file system. Dynamic sites combine static template files with
    queries to a data store. I want to create is a system where all of the
    documents on a server are wholly within a database system, and HTTP
    requests are mapped to SQL statements. There are a lot of technical
    implications with this kind of system, especially when it comes to
    security, which we'll explore.
*** Web Content Management System means an integrated tool
    I want an integrated tool that handles the web server, document
    versioning, static site generation, and everything else that developers
    know how to do today, but can also be used by normal folks. I also want
    a tool that can run locally and remotely the same, like Fossil SCM.

** What this document is
   This is going to be a Literate Programming document, where I sketch out the
   system from a high level and then drill down on each part. Each part should
   include any testing that verifies the code. Because =org-mode= makes it really
   easy to tangle one document into multiple files, the code base will conform to
   a conventional shape for whatever language it is in.

   I plan on using Python as the language of the first prototype, since I've
   worked with Python the longest. However, once I understand that
   implementation, I plan on rewriting it in Haskell.

* The Code
  In this section, I specify and then implement the different components of this
  application, and define some tests for them as well. Each component will have
  its own section.
  
** Document schema
   #+BEGIN_SRC sql :export none :tangle fresh.sql :noweb yes
     <<documents schema>>
     <<dummy values>>
   #+END_SRC
   When I think about a database of documents, the main things that I think
   about are the URI, which must be unique, and the content of the document.

   #+NAME: documents schema
   #+BEGIN_SRC sql
     DROP TABLE IF EXISTS documents;

     CREATE TABLE documents (
     uri TEXT PRIMARY KEY,
     content TEXT
     );
   #+END_SRC

   An empty database isn't that great for demo/testing, so I'll add some dummy
   values into the DB.

   #+NAME: dummy values
   #+BEGIN_SRC sql :export none
     INSERT INTO documents (uri, content)
     VALUES
       ("/hello", "<h1>Hello World</h1>");
   #+END_SRC

   #+BEGIN_SRC sh :result output :noweb yes 
     rm website.db
     cat fresh.sql | sqlite3 website.db
     echo 'SELECT * FROM documents;' | sqlite3 website.db
   #+END_SRC

   #+RESULTS:
   | /hello | <h1>Hello World</h1> |
   
   Nice! Obviously, the document stored in the DB needs to be a full document,
   not a fragment, but I'm pretty sure I'm going to in the right direction here.

** Running the server
   This is lifted directly from the [[https://docs.python.org/3/library/http.server.html][=http.server= documentation on python.org]].
   #+NAME: run the server
   #+BEGIN_SRC python 
     def run(server_class=HTTPServer, handler_class=BaseHTTPRequestHandler):
         server_address = ('', 8000)
         httpd = server_class(server_address, handler_class)
         httpd.serve_forever()
   #+END_SRC
    
** =StaticRequestHandler= class
   The next thing I need to do is define =StaticRequestHandler=, the class that
   I'll pass into the =run= function. I'll start by defining a new class that
   inherits from =BaseHTTPRequestHandler=.

   #+BEGIN_SRC python
     class StaticRequestHandler(BaseHTTPRequestHandler):
         pass
   #+END_SRC

   #+NAME: StaticRequestHandler class
   #+BEGIN_SRC python :export none :noweb yes
     class StaticRequestHandler(BaseHTTPRequestHandler):
         <<GET method>>
   #+END_SRC

   The =StaticRequestHandler= class will match the URI of the request to a SQL
   query, send that query, get the document, and return the document to the
   HTTP client.

*** Connect to the database
    In order to get documents from the database, we'll need to connect to it. I
    think that the approach that I'll take is that the connection itself is
    a top-level binding, and that each request gets a cursor to that connection.
    
    #+NAME: Connect to database
    #+BEGIN_SRC python 
      con = sqlite3.connect("website.db")
    #+END_SRC

*** Get a document by =uri=
    The =static= web server should behave exactly like a regular web server,
    where paths correspond with documents.

    According the documentation, the path of the request is stored in
    =self.path=, and the content body is in =self.wfile=, and I my Handler needs
    to define a method named =do_GET= to process =GET= requests. I'll add that
    method first.

    #+BEGIN_SRC python :noweb yes
      def do_GET(self):
          c = con.cursor()
          c.execute("SELECT content FROM documents WHERE uri = ?", self.path)
          doc = c.fetchone()
          if doc:
              self.wfile.write(doc)
          else:
              self.send_error(404, message="Try again with a different URI")
          c.close()
    #+END_SRC

*** Get a document by =rowid=
    Because my tables are Rowid tables, and I did not define an integer primary
    key, each document in the database has a =rowid= column that I can query.
    If you know what document you need by ID, fetch that from the DB directly.
    In this way, this will act like a built-in URL shortener.

    This won't go into this prototype, but I wanted to make an observation to
    remind myself of this for later.

** Preliminary test
   We have enough of a basic test. 

   #+BEGIN_SRC python :export none :tangle server.py :noweb yes
     import sqlite3
     from http.server import HTTPServer, HTTPStatus, BaseHTTPRequestHandler


     <<Connect to database>>


     <<StaticRequestHandler class>>


     <<run the server>>


     if __name__ == "__main__":
         run(handler_class=StaticRequestHandler)
   #+END_SRC
   
   When I ran the script, and brought up the page in my browser, I got an
   exception.

   #+BEGIN_EXAMPLE
     Exception happened during processing of request from ('127.0.0.1', 60878)
     Traceback (most recent call last):
       File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
      317, in _handle_request_noblock                                                                                    
         self.process_request(request, client_address)
       File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
      348, in process_request                                                                                            
         self.finish_request(request, client_address)
       File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
      361, in finish_request                                                                                             
         self.RequestHandlerClass(request, client_address, self)
       File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
      696, in __init__                                                                                                   
         self.handle()
       File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/server.py", line 
     418, in handle                                                                                                      
         self.handle_one_request()
       File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/server.py", line 
     406, in handle_one_request                                                                                          
         method()
       File "server.py", line 10, in do_GET
         c.execute("SELECT content FROM documents WHERE uri = ?", self.path)
     sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 21 suppl
     ied.                                                                                                                
   #+END_EXAMPLE
   
   So, apparently, the =self.path= variable isn't just a string like I supposed.
   Let's dig into it a bit more, by doing a personal pet peeve of mine,
   overloading a 500 code!

    #+BEGIN_SRC python :noweb yes
      def do_GET(self):
          self.send_error(500, message=repr(self.path))
    #+END_SRC

    When I ran this, I was getting a single string back, like I expected. Hm.
    Maybe it was because I didn't surround the =?= with quotes?

    #+BEGIN_SRC python
      def do_GET(self):
          c = con.cursor()
          c.execute('SELECT content FROM documents WHERE uri = "?"', self.path)
          doc = c.fetchone()
          if doc:
              self.wfile.write(doc)
          else:
              self.send_error(404, message="Try again with a different URI")
          c.close()
    #+END_SRC

    That didn't work either, the truncated error message is 
    #+BEGIN_EXAMPLE
          File "server.py", line 11, in do_GET
          c.execute('SELECT content FROM documents WHERE uri = "?"', self.path)
      sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 0, and there are 20 suppl
      ied.
    #+END_EXAMPLE

    After some searching, I figured out that there is a different style for
    substitution, and for some reason, it works better that the "qmark" style I
    used above.

    #+NAME: GET method
    #+BEGIN_SRC python
      def do_GET(self):
          c = con.cursor()
          c.execute('SELECT content FROM documents WHERE uri=:uri', {"uri": self.path})
          doc = c.fetchone()
          if doc:
              self.wfile.write(doc)
          else:
              self.send_error(404, message="Try again with a different URI")
          c.close()
    #+END_SRC

    When I run this code, I get 404s like I expect. However, when I pass in the
    only URI in the database, =/hello=, I got this error:
    #+BEGIN_EXAMPLE
      Exception happened during processing of request from ('127.0.0.1', 61211)
      Traceback (most recent call last):
        File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
       317, in _handle_request_noblock                                                                                    
          self.process_request(request, client_address)
        File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
       348, in process_request                                                                                            
          self.finish_request(request, client_address)
        File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
       361, in finish_request                                                                                             
          self.RequestHandlerClass(request, client_address, self)
        File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
       696, in __init__                                                                                                   
          self.handle()
        File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/server.py", line 
      418, in handle                                                                                                      
          self.handle_one_request()
        File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/http/server.py", line 
      406, in handle_one_request                                                                                          
          method()
        File "server.py", line 15, in do_GET
          self.wfile.write(doc)
        File "/usr/local/Cellar/python3/3.6.3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socketserver.py", line
       775, in write                                                                                                      
          self._sock.sendall(b)
      TypeError: a bytes-like object is required, not 'tuple'
    #+END_EXAMPLE

    I realized then that the =sqlite3.Row= object is *not* a bytes-like object,
    it's duck-typed as a tuple! D'oh! However, since I know that I'm only
    fetching one column, I can access that member of the tuple directly and
    convert the string to bytes.
    
    #+BEGIN_SRC python
      def do_GET(self):
          c = con.cursor()
          c.execute('SELECT content FROM documents WHERE uri=:uri', {"uri": self.path})
          row = c.fetchone()
          if row is not None:
              doc = row[0].encode()
              self.wfile.write(doc)
          else:
              self.send_error(404, message="Try again with a different URI")
          c.close()
    #+END_SRC

    When I got those changes in, I tried it out in the browser, and although I
    didn't get any errors, the server dropped the connection so I didn't get any
    actual responses. Then, I looked at the source for the default web server,
    and so I'm going to incorporate those changes here.

    #+BEGIN_SRC python :noweb yes
      def do_GET(self):
          c = con.cursor()
          c.execute('SELECT content FROM documents WHERE uri=:uri', {"uri": self.path})
          row = c.fetchone()
          if row is not None:
              doc = row[0].encode()
              self.send_response(200, message=doc)
              self.end_headers()
          else:
              self.send_error(404, message="Try again with a different URI")
          c.close()
    #+END_SRC

    #+NAME: GET method
    #+BEGIN_SRC python :export none :noweb yes
      def do_GET(self):
          c = con.cursor()
          c.execute('SELECT content FROM documents WHERE uri=:uri', {"uri": self.path})
          row = c.fetchone()
          if row is not None:
              doc = row[0].encode()
              <<send HTTP response>>
          else:
              self.send_error(404, message="Try again with a different URI")
          c.close()
    #+END_SRC

    #+BEGIN_SRC python
      self.send_response(200, message=doc)
      self.end_headers()
    #+END_SRC

    #+NAME: send HTTP response
    #+BEGIN_SRC python
      self.send_response(200)
      self.send_header("Content-type", "text/html")
      self.end_headers()
      self.wfile.write(doc)
      return
    #+END_SRC

    AND it finally worked! I was able to get the HTML from the database into my
    browser. It was a good feeling, but I know that it's not actually that
    impressive.