changeset 374:fa3ee8a8ee2d

serve: Split the server code in a couple modules inside a `serving` package. This makes the `serve` command's code a bit more removed from implementation details, and paves the way for the CMS mode.
author Ludovic Chabant <ludovic@chabant.com>
date Thu, 07 May 2015 21:37:38 -0700
parents 9fb7c4921d75
children aade4ea57e7f
files piecrust/commands/builtin/serving.py piecrust/mime.types piecrust/serving.py piecrust/serving/__init__.py piecrust/serving/mime.types piecrust/serving/procloop.py piecrust/serving/server.py piecrust/serving/wrappers.py
diffstat 7 files changed, 1323 insertions(+), 1276 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/commands/builtin/serving.py	Thu May 07 21:36:17 2015 -0700
+++ b/piecrust/commands/builtin/serving.py	Thu May 07 21:37:38 2015 -0700
@@ -1,6 +1,6 @@
 import logging
-from piecrust.serving import Server, _sse_abort
 from piecrust.commands.base import ChefCommand
+from piecrust.serving.wrappers import run_werkzeug_server, run_gunicorn_server
 
 
 logger = logging.getLogger(__name__)
@@ -37,54 +37,31 @@
                 default='werkzeug')
 
     def run(self, ctx):
+        root_dir = ctx.app.root_dir
         host = ctx.args.address
         port = int(ctx.args.port)
         debug = ctx.args.debug or ctx.args.use_debugger
 
-        server = Server(
-                ctx.app.root_dir,
-                debug=debug,
-                sub_cache_dir=ctx.app.sub_cache_dir,
-                use_reloader=ctx.args.use_reloader)
-        app = server.getWsgiApp()
-
         if ctx.args.wsgi == 'werkzeug':
-            from werkzeug.serving import run_simple
-            try:
-                run_simple(host, port, app,
-                           threaded=True,
-                           use_debugger=debug,
-                           use_reloader=ctx.args.use_reloader)
-            finally:
-                _sse_abort.set()
+            run_werkzeug_server(
+                    root_dir, host, port,
+                    debug_piecrust=debug,
+                    sub_cache_dir=ctx.app.sub_cache_dir,
+                    use_debugger=debug,
+                    use_reloader=ctx.args.use_reloader)
 
         elif ctx.args.wsgi == 'gunicorn':
-            from gunicorn.app.base import BaseApplication
-
-            class PieCrustGunicornApplication(BaseApplication):
-                def __init__(self, app, options):
-                    self.app = app
-                    self.options = options
-                    super(PieCrustGunicornApplication, self).__init__()
-
-                def load_config(self):
-                    for k, v in self.options.items():
-                        if k in self.cfg.settings and v is not None:
-                            self.cfg.set(k, v)
-
-                def load(self):
-                    return self.app
-
             options = {
                     'bind': '%s:%s' % (host, port),
-                    'accesslog': '-',
-                    'worker_class': 'gaiohttp',
-                    'workers': 2,
-                    'timeout': 999999}
+                    'accesslog': '-',  # print access log to stderr
+                    }
             if debug:
                 options['loglevel'] = 'debug'
             if ctx.args.use_reloader:
                 options['reload'] = True
-            app_wrapper = PieCrustGunicornApplication(app, options)
-            app_wrapper.run()
+            run_gunicorn_server(
+                    root_dir,
+                    debug_piecrust=debug,
+                    sub_cache_dir=ctx.app.sub_cache_dir,
+                    gunicorn_options=options)
 
--- a/piecrust/mime.types	Thu May 07 21:36:17 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,617 +0,0 @@
-application/activemessage
-application/andrew-inset			ez
-application/applefile
-application/atomicmail
-application/batch-SMTP
-application/beep+xml
-application/cals-1840
-application/commonground
-application/cu-seeme				csm cu
-application/cybercash
-application/dca-rft
-application/dec-dx
-application/dsptype				tsp
-application/dvcs
-application/edi-consent
-application/edifact
-application/edi-x12
-application/eshop
-application/font-tdpfr
-application/futuresplash			spl
-application/ghostview
-application/hta					hta
-application/http
-application/hyperstudio
-application/iges
-application/index
-application/index.cmd
-application/index.obj
-application/index.response
-application/index.vnd
-application/iotp
-application/ipp
-application/isup
-application/mac-compactpro			cpt
-application/marc
-application/mac-binhex40			hqx
-application/macwriteii
-application/mathematica				nb
-application/mathematica-old
-application/msaccess				mdb
-application/msword				doc dot
-application/news-message-id
-application/news-transmission
-application/octet-stream			bin
-application/ocsp-request
-application/ocsp-response
-application/oda					oda
-application/ogg					ogg
-application/parityfec
-application/pics-rules				prf
-application/pgp-encrypted
-application/pgp-keys				key
-application/pdf					pdf
-application/pgp-signature			pgp
-application/pkcs10
-application/pkcs7-mime
-application/pkcs7-signature
-application/pkix-cert
-application/pkixcmp
-application/pkix-crl
-application/postscript				ps ai eps
-application/prs.alvestrand.titrax-sheet
-application/prs.cww
-application/prs.nprend
-application/qsig
-application/riscos
-application/remote-printing
-application/rss+xml				rss
-application/rtf					rtf
-application/sdp
-application/set-payment
-application/set-payment-initiation
-application/set-registration
-application/set-registration-initiation
-application/sgml
-application/sgml-open-catalog
-application/sieve
-application/slate
-application/smil				smi smil
-application/timestamp-query
-application/timestamp-reply
-application/vemmi
-application/whoispp-query
-application/whoispp-response
-application/wita
-application/wordperfect5.1			wp5
-application/x400-bp
-application/xhtml+xml				xht xhtml
-application/xml
-application/xml-dtd
-application/xml-external-parsed-entity
-application/zip					zip
-application/vnd.3M.Post-it-Notes
-application/vnd.accpac.simply.aso
-application/vnd.accpac.simply.imp
-application/vnd.acucobol
-application/vnd.aether.imp
-application/vnd.anser-web-certificate-issue-initiation
-application/vnd.anser-web-funds-transfer-initiation
-application/vnd.audiograph
-application/vnd.bmi
-application/vnd.businessobjects
-application/vnd.canon-cpdl
-application/vnd.canon-lips
-application/vnd.cinderella			cdy
-application/vnd.claymore
-application/vnd.commerce-battelle
-application/vnd.commonspace
-application/vnd.comsocaller
-application/vnd.contact.cmsg
-application/vnd.cosmocaller
-application/vnd.ctc-posml
-application/vnd.cups-postscript
-application/vnd.cups-raster
-application/vnd.cups-raw
-application/vnd.cybank
-application/vnd.dna
-application/vnd.dpgraph
-application/vnd.dxr
-application/vnd.ecdis-update
-application/vnd.ecowin.chart
-application/vnd.ecowin.filerequest
-application/vnd.ecowin.fileupdate
-application/vnd.ecowin.series
-application/vnd.ecowin.seriesrequest
-application/vnd.ecowin.seriesupdate
-application/vnd.enliven
-application/vnd.epson.esf
-application/vnd.epson.msf
-application/vnd.epson.quickanime
-application/vnd.epson.salt
-application/vnd.epson.ssf
-application/vnd.ericsson.quickcall
-application/vnd.eudora.data
-application/vnd.fdf
-application/vnd.ffsns
-application/vnd.flographit
-application/vnd.framemaker
-application/vnd.fsc.weblaunch
-application/vnd.fujitsu.oasys
-application/vnd.fujitsu.oasys2
-application/vnd.fujitsu.oasys3
-application/vnd.fujitsu.oasysgp
-application/vnd.fujitsu.oasysprs
-application/vnd.fujixerox.ddd
-application/vnd.fujixerox.docuworks
-application/vnd.fujixerox.docuworks.binder
-application/vnd.fut-misnet
-application/vnd.grafeq
-application/vnd.groove-account
-application/vnd.groove-identity-message
-application/vnd.groove-injector
-application/vnd.groove-tool-message
-application/vnd.groove-tool-template
-application/vnd.groove-vcard
-application/vnd.hhe.lesson-player
-application/vnd.hp-HPGL
-application/vnd.hp-PCL
-application/vnd.hp-PCLXL
-application/vnd.hp-hpid
-application/vnd.hp-hps
-application/vnd.httphone
-application/vnd.hzn-3d-crossword
-application/vnd.ibm.MiniPay
-application/vnd.ibm.afplinedata
-application/vnd.ibm.modcap
-application/vnd.informix-visionary
-application/vnd.intercon.formnet
-application/vnd.intertrust.digibox
-application/vnd.intertrust.nncp
-application/vnd.intu.qbo
-application/vnd.intu.qfx
-application/vnd.irepository.package+xml
-application/vnd.is-xpr
-application/vnd.japannet-directory-service
-application/vnd.japannet-jpnstore-wakeup
-application/vnd.japannet-payment-wakeup
-application/vnd.japannet-registration
-application/vnd.japannet-registration-wakeup
-application/vnd.japannet-setstore-wakeup
-application/vnd.japannet-verification
-application/vnd.japannet-verification-wakeup
-application/vnd.koan
-application/vnd.lotus-1-2-3
-application/vnd.lotus-approach
-application/vnd.lotus-freelance
-application/vnd.lotus-notes
-application/vnd.lotus-organizer
-application/vnd.lotus-screencam
-application/vnd.lotus-wordpro
-application/vnd.mcd
-application/vnd.mediastation.cdkey
-application/vnd.meridian-slingshot
-application/vnd.mif	      mif
-application/vnd.minisoft-hp3000-save
-application/vnd.mitsubishi.misty-guard.trustweb
-application/vnd.mobius.daf
-application/vnd.mobius.dis
-application/vnd.mobius.msl
-application/vnd.mobius.plc
-application/vnd.mobius.txf
-application/vnd.motorola.flexsuite
-application/vnd.motorola.flexsuite.adsi
-application/vnd.motorola.flexsuite.fis
-application/vnd.motorola.flexsuite.gotap
-application/vnd.motorola.flexsuite.kmr
-application/vnd.motorola.flexsuite.ttc
-application/vnd.motorola.flexsuite.wem
-application/vnd.mozilla.xul+xml
-application/vnd.ms-artgalry
-application/vnd.ms-asf
-application/vnd.ms-excel			xls xlb
-application/vnd.ms-lrm
-application/vnd.ms-pki.seccat			cat
-application/vnd.ms-pki.stl			stl
-application/vnd.ms-powerpoint			ppt pps pot
-application/vnd.ms-project
-application/vnd.ms-tnef
-application/vnd.ms-works
-application/vnd.mseq
-application/vnd.msign
-application/vnd.music-niff
-application/vnd.musician
-application/vnd.netfpx
-application/vnd.noblenet-directory
-application/vnd.noblenet-sealer
-application/vnd.noblenet-web
-application/vnd.novadigm.EDM
-application/vnd.novadigm.EDX
-application/vnd.novadigm.EXT
-application/vnd.osa.netdeploy
-application/vnd.palm
-application/vnd.pg.format
-application/vnd.pg.osasli
-application/vnd.powerbuilder6
-application/vnd.powerbuilder6-s
-application/vnd.powerbuilder7
-application/vnd.powerbuilder7-s
-application/vnd.powerbuilder75
-application/vnd.powerbuilder75-s
-application/vnd.previewsystems.box
-application/vnd.publishare-delta-tree
-application/vnd.pvi.ptid1
-application/vnd.pwg-xhtml-print+xml
-application/vnd.rapid
-application/vnd.s3sms
-application/vnd.seemail
-application/vnd.shana.informed.formdata
-application/vnd.shana.informed.formtemplate
-application/vnd.shana.informed.interchange
-application/vnd.shana.informed.package
-application/vnd.sss-cod
-application/vnd.sss-dtf
-application/vnd.sss-ntf
-application/vnd.stardivision.calc		sdc
-application/vnd.stardivision.draw		sda
-application/vnd.stardivision.impress		sdd sdp
-application/vnd.stardivision.math		smf
-application/vnd.stardivision.writer		sdw vor
-application/vnd.stardivision.writer-global	sgl
-application/vnd.street-stream
-application/vnd.sun.xml.calc			sxc
-application/vnd.sun.xml.calc.template		stc
-application/vnd.sun.xml.draw			sxd
-application/vnd.sun.xml.draw.template		std
-application/vnd.sun.xml.impress			sxi
-application/vnd.sun.xml.impress.template	sti
-application/vnd.sun.xml.math			sxm
-application/vnd.sun.xml.writer			sxw
-application/vnd.sun.xml.writer.global		sxg
-application/vnd.sun.xml.writer.template		stw
-application/vnd.svd
-application/vnd.swiftview-ics
-application/vnd.triscape.mxs
-application/vnd.trueapp
-application/vnd.truedoc
-application/vnd.tve-trigger
-application/vnd.ufdl
-application/vnd.uplanet.alert
-application/vnd.uplanet.alert-wbxml
-application/vnd.uplanet.bearer-choice
-application/vnd.uplanet.bearer-choice-wbxml
-application/vnd.uplanet.cacheop
-application/vnd.uplanet.cacheop-wbxml
-application/vnd.uplanet.channel
-application/vnd.uplanet.channel-wbxml
-application/vnd.uplanet.list
-application/vnd.uplanet.list-wbxml
-application/vnd.uplanet.listcmd
-application/vnd.uplanet.listcmd-wbxml
-application/vnd.uplanet.signal
-application/vnd.vcx
-application/vnd.vectorworks
-application/vnd.vidsoft.vidconference
-application/vnd.visio
-application/vnd.vividence.scriptfile
-application/vnd.wap.sic
-application/vnd.wap.slc
-application/vnd.wap.wbxml			wbxml
-application/vnd.wap.wmlc			wmlc
-application/vnd.wap.wmlscriptc			wmlsc
-application/vnd.webturbo
-application/vnd.wrq-hp3000-labelled
-application/vnd.wt.stf
-application/vnd.xara
-application/vnd.xfdl
-application/vnd.yellowriver-custom-menu
-application/x-123				wk
-application/x-apple-diskimage			dmg
-application/x-bcpio				bcpio
-application/x-cdf				cdf
-application/x-cdlink				vcd
-application/x-chess-pgn				pgn
-application/x-core
-application/x-cpio				cpio
-application/x-csh				csh
-application/x-debian-package			deb
-application/x-director				dcr dir dxr
-application/x-doom				wad
-application/x-dms				dms
-application/x-dvi				dvi
-application/x-executable
-application/x-font				pfa pfb gsf pcf pcf.Z
-application/x-futuresplash			spl
-application/x-gnumeric				gnumeric
-application/x-go-sgf				sgf
-application/x-graphing-calculator		gcf
-application/x-gtar				gtar tgz taz
-application/x-hdf				hdf
-application/x-httpd-php				phtml pht php
-application/x-httpd-php-source			phps
-application/x-httpd-php3			php3
-application/x-httpd-php3-preprocessed		php3p
-application/x-httpd-php4			php4
-application/x-ica				ica
-application/x-internet-signup			ins isp
-application/x-iphone				iii
-application/x-java-applet
-application/x-java-archive			jar
-application/x-java-bean
-application/x-java-jnlp-file			jnlp
-application/x-java-serialized-object		ser
-application/x-java-vm				class
-application/x-javascript			js
-application/x-kdelnk
-application/x-kchart				chrt
-application/x-killustrator			kil
-application/x-kpresenter			kpr kpt
-application/x-koan				skp skd skt skm
-application/x-kspread				ksp
-application/x-kword				kwd kwt
-application/x-latex				latex
-application/x-lha				lha
-application/x-lzh				lzh
-application/x-lzx				lzx
-application/x-maker				frm maker frame fm fb book fbdoc
-application/x-mif				mif
-application/x-ms-wmz				wmz
-application/x-ms-wmd				wmd
-application/x-msdos-program			com exe bat dll
-application/x-msi				msi
-application/x-netcdf				nc
-application/x-ns-proxy-autoconfig		pac
-application/x-object				o
-application/x-oz-application			oza
-application/x-perl				pl pm
-application/x-pkcs7-certreqresp			p7r
-application/x-pkcs7-crl				crl
-application/x-quicktimeplayer			qtl
-application/x-redhat-package-manager		rpm
-application/x-rx
-application/x-sh
-application/x-shar				shar
-application/x-shellscript
-application/x-shockwave-flash			swf swfl
-application/x-sh				sh
-application/x-stuffit				sit
-application/x-sv4cpio				sv4cpio
-application/x-sv4crc				sv4crc
-application/x-tar				tar
-application/x-tcl				tcl
-application/x-tex				tex
-application/x-tex-gf				gf
-application/x-tex-pk				pk
-application/x-texinfo				texinfo texi
-application/x-trash				~ % bak old sik
-application/x-troff				t tr roff
-application/x-troff-man				man
-application/x-troff-me				me
-application/x-troff-ms				ms
-application/x-ustar				ustar
-application/x-wais-source			src
-application/x-wingz				wz
-application/x-x509-ca-cert			crt
-application/x-xfig				fig
-
-audio/32kadpcm
-#audio/aiff					aif aifc aiff
-audio/basic					au snd
-audio/g.722.1
-audio/l16
-audio/midi					mid midi kar
-audio/mp4a-latm
-audio/mpa-robust
-audio/mpeg					mpga mpega mp2 mp3
-audio/mpegurl					m3u
-audio/parityfec
-audio/prs.sid					sid
-audio/telephone-event
-audio/tone
-#audio/wav					wav
-audio/vnd.cisco.nse
-audio/vnd.cns.anp1
-audio/vnd.cns.inf1
-audio/vnd.digital-winds
-audio/vnd.everad.plj
-audio/vnd.lucent.voice
-audio/vnd.nortel.vbk
-audio/vnd.nuera.ecelp4800
-audio/vnd.nuera.ecelp7470
-audio/vnd.nuera.ecelp9600
-audio/vnd.octel.sbc
-audio/vnd.qcelp
-audio/vnd.rhetorex.32kadpcm
-audio/vnd.vmx.cvsd
-audio/x-aiff					aif aiff aifc
-audio/x-gsm					gsm
-audio/x-mpegurl					m3u
-audio/x-ms-wma					wma
-audio/x-ms-wax					wax
-audio/x-pn-realaudio-plugin			rpm
-audio/x-pn-realaudio				ra rm ram
-audio/x-realaudio				ra
-audio/x-scpls					pls
-audio/x-sd2					sd2
-audio/x-wav					wav
-
-chemical/x-pdb					pdb
-chemical/x-xyz					xyz
-
-image/bmp					bmp
-image/cgm
-image/g3fax
-image/gif					gif
-image/ief					ief
-image/jpeg					jpeg jpg jpe
-image/naplps
-image/pcx					pcx
-image/png					png
-image/prs.btif
-image/prs.pti
-image/svg+xml					svg svgz
-image/tiff					tiff tif
-image/vnd.cns.inf2
-image/vnd.dwg
-image/vnd.dxf
-image/vnd.fastbidsheet
-image/vnd.fpx
-image/vnd.fst
-image/vnd.fujixerox.edmics-mmr
-image/vnd.fujixerox.edmics-rlc
-image/vnd.mix
-image/vnd.net-fpx
-image/vnd.svf
-image/vnd.wap.wbmp				wbmp
-image/vnd.xiff
-image/x-cmu-raster				ras
-image/x-coreldraw				cdr
-image/x-coreldrawpattern			pat
-image/x-coreldrawtemplate			cdt
-image/x-corelphotopaint				cpt
-image/x-djvu					djvu djv
-image/x-icon					ico
-image/x-jg					art
-image/x-jng					jng
-image/x-ms-bmp					bmp
-image/x-photoshop				psd
-image/x-portable-anymap				pnm
-image/x-portable-bitmap				pbm
-image/x-portable-graymap			pgm
-image/x-portable-pixmap				ppm
-image/x-rgb					rgb
-image/x-xbitmap					xbm
-image/x-xpixmap					xpm
-image/x-xwindowdump				xwd
-
-inode/chardevice
-inode/blockdevice
-inode/directory-locked
-inode/directory
-inode/fifo
-inode/socket
-
-message/delivery-status
-message/disposition-notification
-message/external-body
-message/http
-message/s-http
-message/news
-message/partial
-message/rfc822
-
-model/iges					igs iges
-model/mesh					msh mesh silo
-model/vnd.dwf
-model/vnd.flatland.3dml
-model/vnd.gdl
-model/vnd.gs-gdl
-model/vnd.gtw
-model/vnd.mts
-model/vnd.vtu
-model/vrml					wrl vrml
-
-multipart/alternative
-multipart/appledouble
-multipart/byteranges
-multipart/digest
-multipart/encrypted
-multipart/form-data
-multipart/header-set
-multipart/mixed
-multipart/parallel
-multipart/related
-multipart/report
-multipart/signed
-multipart/voice-message
-
-text/calendar
-text/comma-separated-values			csv
-text/css					css
-text/directory
-text/english
-text/enriched
-text/h323					323
-text/html					htm html
-text/iuls					uls
-text/mathml					mml
-text/parityfec
-text/plain					asc txt text diff
-text/prs.lines.tag
-text/rfc822-headers
-text/richtext					rtx
-text/rtf					rtf
-text/scriptlet					sct wsc
-text/t140
-text/texmacs					tm ts
-text/tab-separated-values			tsv
-text/uri-list
-text/vnd.abc
-text/vnd.curl
-text/vnd.DMClientScript
-text/vnd.flatland.3dml
-text/vnd.fly
-text/vnd.fmi.flexstor
-text/vnd.in3d.3dml
-text/vnd.in3d.spot
-text/vnd.IPTC.NewsML
-text/vnd.IPTC.NITF
-text/vnd.latex-z
-text/vnd.motorola.reflex
-text/vnd.ms-mediapackage
-text/vnd.wap.si
-text/vnd.wap.sl
-text/vnd.wap.wml				wml
-text/vnd.wap.wmlscript				wmls
-text/xml					xml xsl
-text/x-c++hdr					h++ hpp hxx hh
-text/x-c++src					c++ cpp cxx cc
-text/x-chdr					h
-text/x-crontab
-text/x-csh					csh
-text/x-csrc					c
-text/x-java					java
-text/x-makefile
-text/xml-external-parsed-entity
-text/x-moc					moc
-text/x-pascal					p pas
-text/x-pcs-gcd					gcd
-text/x-server-parsed-html			shtml
-text/x-setext					etx
-text/x-sh					sh
-text/x-tcl					tcl tk
-text/x-tex					tex ltx sty cls
-text/x-vcalendar				vcs
-text/x-vcard					vcf
-
-#video/avi					avi
-video/dl					dl
-video/fli					fli
-video/gl					gl
-video/mpeg					mpeg mpg mpe
-video/quicktime					qt mov
-video/mp4v-es
-video/parityfec
-video/pointer
-video/vnd.fvt
-video/vnd.motorola.video
-video/vnd.motorola.videop
-video/vnd.mpegurl				mxu
-video/vnd.mts
-video/vnd.nokia.interleaved-multimedia
-video/vnd.vivo
-video/x-dv					dif dv
-video/x-la-asf					lsf lsx
-video/x-mng					mng
-video/x-ms-asf					asf asx
-video/x-ms-wm					wm
-video/x-ms-wmv					wmv
-video/x-ms-wmx					wmx
-video/x-ms-wvx					wvx
-video/x-msvideo					avi
-video/x-sgi-movie				movie
-
-x-conference/x-cooltalk				ice
-
-x-world/x-vrml					vrm vrml wrl
-
--- a/piecrust/serving.py	Thu May 07 21:36:17 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,621 +0,0 @@
-import io
-import os
-import re
-import json
-import gzip
-import time
-import queue
-import os.path
-import hashlib
-import logging
-import datetime
-import threading
-from werkzeug.exceptions import (
-        NotFound, MethodNotAllowed, InternalServerError, HTTPException)
-from werkzeug.wrappers import Request, Response
-from werkzeug.wsgi import ClosingIterator, wrap_file
-from jinja2 import FileSystemLoader, Environment
-from piecrust.app import PieCrust
-from piecrust.environment import StandardEnvironment
-from piecrust.processing.base import ProcessorPipeline
-from piecrust.rendering import QualifiedPage, PageRenderingContext, render_page
-from piecrust.sources.base import PageFactory, MODE_PARSING
-from piecrust.uriutil import split_sub_uri
-
-
-logger = logging.getLogger(__name__)
-
-
-_sse_abort = threading.Event()
-
-
-class ServingEnvironment(StandardEnvironment):
-    pass
-
-
-class ServeRecord(object):
-    def __init__(self):
-        self.entries = {}
-
-    def addEntry(self, entry):
-        key = self._makeKey(entry.uri, entry.sub_num)
-        self.entries[key] = entry
-
-    def getEntry(self, uri, sub_num):
-        key = self._makeKey(uri, sub_num)
-        return self.entries.get(key)
-
-    def _makeKey(self, uri, sub_num):
-        return "%s:%s" % (uri, sub_num)
-
-
-class ServeRecordPageEntry(object):
-    def __init__(self, uri, sub_num):
-        self.uri = uri
-        self.sub_num = sub_num
-        self.used_source_names = set()
-
-
-class WsgiServerWrapper(object):
-    def __init__(self, server):
-        self.server = server
-
-    def __call__(self, environ, start_response):
-        return self.server._run_request(environ, start_response)
-
-
-class Server(object):
-    def __init__(self, root_dir,
-                 debug=False, sub_cache_dir=None,
-                 use_reloader=False, static_preview=True):
-        self.root_dir = root_dir
-        self.debug = debug
-        self.sub_cache_dir = sub_cache_dir
-        self.use_reloader = use_reloader
-        self.static_preview = static_preview
-        self._out_dir = None
-        self._page_record = None
-        self._proc_loop = None
-        self._mimetype_map = load_mimetype_map()
-
-    def getWsgiApp(self):
-        # Bake all the assets so we know what we have, and so we can serve
-        # them to the client. We need a temp app for this.
-        app = PieCrust(root_dir=self.root_dir, debug=self.debug)
-        app._useSubCacheDir(self.sub_cache_dir)
-        self._out_dir = os.path.join(app.sub_cache_dir, 'server')
-        self._page_record = ServeRecord()
-
-        if (not self.use_reloader or
-                os.environ.get('WERKZEUG_RUN_MAIN') == 'true'):
-            # We don't want to run the processing loop here if this isn't
-            # the actual process that does the serving. In most cases it is,
-            # but if we're using Werkzeug's reloader, then it won't be the
-            # first time we get there... it will only be the correct process
-            # the second time, when the reloading process is spawned, with the
-            # `WERKZEUG_RUN_MAIN` variable set.
-            pipeline = ProcessorPipeline(app, self._out_dir)
-            self._proc_loop = ProcessingLoop(pipeline)
-            self._proc_loop.start()
-
-        # Run the WSGI app.
-        wsgi_wrapper = WsgiServerWrapper(self)
-        return wsgi_wrapper
-
-    def _run_request(self, environ, start_response):
-        try:
-            return self._try_run_request(environ, start_response)
-        except Exception as ex:
-            if self.debug:
-                raise
-            return self._handle_error(ex, environ, start_response)
-
-    def _try_run_request(self, environ, start_response):
-        request = Request(environ)
-
-        # We don't support anything else than GET requests since we're
-        # previewing something that will be static later.
-        if self.static_preview and request.method != 'GET':
-            logger.error("Only GET requests are allowed, got %s" %
-                         request.method)
-            raise MethodNotAllowed()
-
-        # Handle special requests right away.
-        response = self._try_special_request(environ, request)
-        if response is not None:
-            return response(environ, start_response)
-
-        # Also handle requests to a pipeline-built asset right away.
-        response = self._try_serve_asset(environ, request)
-        if response is not None:
-            return response(environ, start_response)
-
-        # Create the app for this request.
-        app = PieCrust(root_dir=self.root_dir, debug=self.debug)
-        app._useSubCacheDir(self.sub_cache_dir)
-        app.config.set('site/root', '/')
-        app.config.set('server/is_serving', True)
-        if (app.config.get('site/enable_debug_info') and
-                '!debug' in request.args):
-            app.config.set('site/show_debug_info', True)
-
-        # We'll serve page assets directly from where they are.
-        app.env.base_asset_url_format = '/_asset/%path%'
-
-        # Let's see if it can be a page asset.
-        response = self._try_serve_page_asset(app, environ, request)
-        if response is not None:
-            return response(environ, start_response)
-
-        # Nope. Let's see if it's an actual page.
-        try:
-            response = self._try_serve_page(app, environ, request)
-            return response(environ, start_response)
-        except (RouteNotFoundError, SourceNotFoundError) as ex:
-            raise NotFound(str(ex)) from ex
-        except HTTPException:
-            raise
-        except Exception as ex:
-            if app.debug:
-                logger.exception(ex)
-                raise
-            msg = str(ex)
-            logger.error(msg)
-            raise InternalServerError(msg) from ex
-
-    def _try_special_request(self, environ, request):
-        static_mount = '/__piecrust_static/'
-        if request.path.startswith(static_mount):
-            rel_req_path = request.path[len(static_mount):]
-            mount = os.path.join(
-                    os.path.dirname(__file__),
-                    'resources', 'server')
-            full_path = os.path.join(mount, rel_req_path)
-            try:
-                response = self._make_wrapped_file_response(
-                        environ, request, full_path)
-                return response
-            except OSError:
-                pass
-
-        debug_mount = '/__piecrust_debug/'
-        if request.path.startswith(debug_mount):
-            rel_req_path = request.path[len(debug_mount):]
-            if rel_req_path == 'pipeline_status':
-                provider = PipelineStatusServerSideEventProducer(
-                        self._proc_loop.status_queue)
-                it = ClosingIterator(provider.run(), [provider.close])
-                response = Response(it)
-                response.headers['Cache-Control'] = 'no-cache'
-                if 'text/event-stream' in request.accept_mimetypes:
-                    response.mimetype = 'text/event-stream'
-                response.direct_passthrough = True
-                response.implicit_sequence_conversion = False
-                return response
-
-        return None
-
-    def _try_serve_asset(self, environ, request):
-        rel_req_path = request.path.lstrip('/').replace('/', os.sep)
-        if request.path.startswith('/_cache/'):
-            # Some stuff needs to be served directly from the cache directory,
-            # like LESS CSS map files.
-            full_path = os.path.join(self.root_dir, rel_req_path)
-        else:
-            full_path = os.path.join(self._out_dir, rel_req_path)
-
-        try:
-            response = self._make_wrapped_file_response(
-                    environ, request, full_path)
-            return response
-        except OSError:
-            pass
-        return None
-
-    def _try_serve_page_asset(self, app, environ, request):
-        if not request.path.startswith('/_asset/'):
-            return None
-
-        full_path = os.path.join(app.root_dir, request.path[len('/_asset/'):])
-        if not os.path.isfile(full_path):
-            return None
-
-        return self._make_wrapped_file_response(environ, request, full_path)
-
-    def _try_serve_page(self, app, environ, request):
-        # Try to find what matches the requested URL.
-        req_path, page_num = split_sub_uri(app, request.path)
-
-        routes = find_routes(app.routes, req_path)
-        if len(routes) == 0:
-            raise RouteNotFoundError("Can't find route for: %s" % req_path)
-
-        rendered_page = None
-        first_not_found = None
-        for route, route_metadata in routes:
-            try:
-                logger.debug("Trying to render match from source '%s'." %
-                             route.source_name)
-                rendered_page = self._try_render_page(
-                        app, route, route_metadata, page_num, req_path)
-                if rendered_page is not None:
-                    break
-            except NotFound as nfe:
-                if first_not_found is None:
-                    first_not_found = nfe
-        else:
-            raise SourceNotFoundError(
-                    "Can't find path for: %s (looked in: %s)" %
-                    (req_path, [r.source_name for r, _ in routes]))
-
-        # If we haven't found any good match, raise whatever exception we
-        # first got. Otherwise, raise a generic exception.
-        if rendered_page is None:
-            first_not_found = first_not_found or NotFound(
-                    "This page couldn't be found.")
-            raise first_not_found
-
-        # Start doing stuff.
-        page = rendered_page.page
-        rp_content = rendered_page.content
-
-        # Profiling.
-        if app.config.get('site/show_debug_info'):
-            now_time = time.clock()
-            timing_info = ('%8.1f ms' %
-                    ((now_time - app.env.start_time) * 1000.0))
-            rp_content = rp_content.replace('__PIECRUST_TIMING_INFORMATION__',
-                    timing_info)
-
-        # Build the response.
-        response = Response()
-
-        etag = hashlib.md5(rp_content.encode('utf8')).hexdigest()
-        if not app.debug and etag in request.if_none_match:
-            response.status_code = 304
-            return response
-
-        response.set_etag(etag)
-        response.content_md5 = etag
-
-        cache_control = response.cache_control
-        if app.debug:
-            cache_control.no_cache = True
-            cache_control.must_revalidate = True
-        else:
-            cache_time = (page.config.get('cache_time') or
-                    app.config.get('site/cache_time'))
-            if cache_time:
-                cache_control.public = True
-                cache_control.max_age = cache_time
-
-        content_type = page.config.get('content_type')
-        if content_type and '/' not in content_type:
-            mimetype = content_type_map.get(content_type, content_type)
-        else:
-            mimetype = content_type
-        if mimetype:
-            response.mimetype = mimetype
-
-        if ('gzip' in request.accept_encodings and
-                app.config.get('site/enable_gzip')):
-            try:
-                with io.BytesIO() as gzip_buffer:
-                    with gzip.open(gzip_buffer, mode='wt',
-                                   encoding='utf8') as gzip_file:
-                        gzip_file.write(rp_content)
-                    rp_content = gzip_buffer.getvalue()
-                    response.content_encoding = 'gzip'
-            except Exception:
-                logger.exception("Error compressing response, "
-                                 "falling back to uncompressed.")
-        response.set_data(rp_content)
-
-        return response
-
-    def _try_render_page(self, app, route, route_metadata, page_num, req_path):
-        # Match the route to an actual factory.
-        taxonomy_info = None
-        source = app.getSource(route.source_name)
-        if route.taxonomy_name is None:
-            factory = source.findPageFactory(route_metadata, MODE_PARSING)
-            if factory is None:
-                return None
-        else:
-            taxonomy = app.getTaxonomy(route.taxonomy_name)
-            route_terms = route_metadata.get(taxonomy.term_name)
-            if route_terms is None:
-                return None
-
-            tax_page_ref = taxonomy.getPageRef(source.name)
-            factory = tax_page_ref.getFactory()
-            tax_terms = route.unslugifyTaxonomyTerm(route_terms)
-            route_metadata[taxonomy.term_name] = tax_terms
-            taxonomy_info = (taxonomy, tax_terms)
-
-        # Build the page.
-        page = factory.buildPage()
-        # We force the rendering of the page because it could not have
-        # changed, but include pages that did change.
-        qp = QualifiedPage(page, route, route_metadata)
-        render_ctx = PageRenderingContext(qp,
-                                          page_num=page_num,
-                                          force_render=True)
-        if taxonomy_info is not None:
-            taxonomy, tax_terms = taxonomy_info
-            render_ctx.setTaxonomyFilter(taxonomy, tax_terms)
-
-        # See if this page is known to use sources. If that's the case,
-        # just don't use cached rendered segments for that page (but still
-        # use them for pages that are included in it).
-        uri = qp.getUri()
-        assert uri == req_path
-        entry = self._page_record.getEntry(uri, page_num)
-        if (taxonomy_info is not None or entry is None or
-                entry.used_source_names):
-            cache_key = '%s:%s' % (uri, page_num)
-            app.env.rendered_segments_repository.invalidate(cache_key)
-
-        # Render the page.
-        rendered_page = render_page(render_ctx)
-
-        # Check if this page is a taxonomy page that actually doesn't match
-        # anything.
-        if taxonomy_info is not None:
-            paginator = rendered_page.data.get('pagination')
-            if (paginator and paginator.is_loaded and
-                    len(paginator.items) == 0):
-                taxonomy = taxonomy_info[0]
-                message = ("This URL matched a route for taxonomy '%s' but "
-                           "no pages have been found to have it. This page "
-                           "won't be generated by a bake." % taxonomy.name)
-                raise NotFound(message)
-
-        # Remember stuff for next time.
-        if entry is None:
-            entry = ServeRecordPageEntry(req_path, page_num)
-            self._page_record.addEntry(entry)
-        for p, pinfo in render_ctx.render_passes.items():
-            entry.used_source_names |= pinfo.used_source_names
-
-        # Ok all good.
-        return rendered_page
-
-    def _make_wrapped_file_response(self, environ, request, path):
-        logger.debug("Serving %s" % path)
-
-        # Check if we can return a 304 status code.
-        mtime = os.path.getmtime(path)
-        etag_str = '%s$$%s' % (path, mtime)
-        etag = hashlib.md5(etag_str.encode('utf8')).hexdigest()
-        if etag in request.if_none_match:
-            response = Response()
-            response.status_code = 304
-            return response
-
-        wrapper = wrap_file(environ, open(path, 'rb'))
-        response = Response(wrapper)
-        _, ext = os.path.splitext(path)
-        response.set_etag(etag)
-        response.last_modified = datetime.datetime.fromtimestamp(mtime)
-        response.mimetype = self._mimetype_map.get(
-                ext.lstrip('.'), 'text/plain')
-        return response
-
-    def _handle_error(self, exception, environ, start_response):
-        code = 500
-        if isinstance(exception, HTTPException):
-            code = exception.code
-
-        path = 'error'
-        if isinstance(exception, NotFound):
-            path += '404'
-
-        descriptions = self._get_exception_descriptions(exception)
-
-        env = Environment(loader=ErrorMessageLoader())
-        template = env.get_template(path)
-        context = {'details': descriptions}
-        response = Response(template.render(context), mimetype='text/html')
-        response.status_code = code
-        return response(environ, start_response)
-
-    def _get_exception_descriptions(self, exception):
-        desc = []
-        while exception is not None:
-            if isinstance(exception, HTTPException):
-                desc.append(exception.description)
-            else:
-                desc.append(str(exception))
-
-            inner_ex = exception.__cause__
-            if inner_ex is None:
-                inner_ex = exception.__context__
-            exception = inner_ex
-        return desc
-
-
-class RouteNotFoundError(Exception):
-    pass
-
-
-class SourceNotFoundError(Exception):
-    pass
-
-
-content_type_map = {
-        'html': 'text/html',
-        'xml': 'text/xml',
-        'txt': 'text/plain',
-        'text': 'text/plain',
-        'css': 'text/css',
-        'xhtml': 'application/xhtml+xml',
-        'atom': 'application/atom+xml',  # or 'text/xml'?
-        'rss': 'application/rss+xml',    # or 'text/xml'?
-        'json': 'application/json'}
-
-
-def find_routes(routes, uri):
-    res = []
-    for route in routes:
-        metadata = route.matchUri(uri)
-        if metadata is not None:
-            res.append((route, metadata))
-    return res
-
-
-class ErrorMessageLoader(FileSystemLoader):
-    def __init__(self):
-        base_dir = os.path.join(os.path.dirname(__file__), 'resources',
-                                'messages')
-        super(ErrorMessageLoader, self).__init__(base_dir)
-
-    def get_source(self, env, template):
-        template += '.html'
-        return super(ErrorMessageLoader, self).get_source(env, template)
-
-
-def load_mimetype_map():
-    mimetype_map = {}
-    sep_re = re.compile(r'\s+')
-    path = os.path.join(os.path.dirname(__file__), 'mime.types')
-    with open(path, 'r') as f:
-        for line in f:
-            tokens = sep_re.split(line)
-            if len(tokens) > 1:
-                for t in tokens[1:]:
-                    mimetype_map[t] = tokens[0]
-    return mimetype_map
-
-
-class PipelineStatusServerSideEventProducer(object):
-    def __init__(self, status_queue):
-        self.status_queue = status_queue
-        self.interval = 2
-        self.timeout = 60*10
-        self._start_time = 0
-
-    def run(self):
-        logger.debug("Starting pipeline status SSE.")
-        self._start_time = time.time()
-
-        outstr = 'event: ping\ndata: started\n\n'
-        yield bytes(outstr, 'utf8')
-
-        count = 0
-        while True:
-            if time.time() > self.timeout + self._start_time:
-                logger.debug("Closing pipeline status SSE, timeout reached.")
-                outstr = 'event: pipeline_timeout\ndata: bye\n\n'
-                yield bytes(outstr, 'utf8')
-                break
-
-            if _sse_abort.is_set():
-                break
-
-            try:
-                logger.debug("Polling pipeline status queue...")
-                count += 1
-                data = self.status_queue.get(True, self.interval)
-            except queue.Empty:
-                if count < 3:
-                    continue
-                data = {'type': 'ping', 'message': 'ping'}
-                count = 0
-
-            event_type = data['type']
-            outstr = 'event: %s\ndata: %s\n\n' % (
-                    event_type, json.dumps(data))
-            logger.debug("Sending pipeline status SSE.")
-            yield bytes(outstr, 'utf8')
-
-    def close(self):
-        logger.debug("Closing pipeline status SSE.")
-
-
-class ProcessingLoop(threading.Thread):
-    def __init__(self, pipeline):
-        super(ProcessingLoop, self).__init__(
-                name='pipeline-reloader', daemon=True)
-        self.pipeline = pipeline
-        self.status_queue = queue.Queue()
-        self.interval = 1
-        self._paths = set()
-        self._record = None
-        self._last_bake = 0
-
-    def run(self):
-        # Build the first list of known files and run the pipeline once.
-        app = self.pipeline.app
-        roots = [os.path.join(app.root_dir, r)
-                 for r in self.pipeline.mounts.keys()]
-        for root in roots:
-            for dirpath, dirnames, filenames in os.walk(root):
-                self._paths |= set([os.path.join(dirpath, f)
-                                    for f in filenames])
-        self._last_bake = time.time()
-        self._record = self.pipeline.run(save_record=False)
-
-        while True:
-            for root in roots:
-                # For each mount root we try to find the first new or
-                # modified file. If any, we just run the pipeline on
-                # that mount.
-                found_new_or_modified = False
-                for dirpath, dirnames, filenames in os.walk(root):
-                    for filename in filenames:
-                        path = os.path.join(dirpath, filename)
-                        if path not in self._paths:
-                            logger.debug("Found new asset: %s" % path)
-                            self._paths.add(path)
-                            found_new_or_modified = True
-                            break
-                        if os.path.getmtime(path) > self._last_bake:
-                            logger.debug("Found modified asset: %s" % path)
-                            found_new_or_modified = True
-                            break
-
-                    if found_new_or_modified:
-                        break
-
-                if found_new_or_modified:
-                    self._runPipeline(root)
-
-            time.sleep(self.interval)
-
-    def _runPipeline(self, root):
-        self._last_bake = time.time()
-        try:
-            self._record = self.pipeline.run(
-                    root,
-                    previous_record=self._record,
-                    save_record=False)
-
-            # Update the status queue.
-            # (we need to clear it because there may not be a consumer
-            #  on the other side, if the user isn't running with the
-            #  debug window active)
-            while True:
-                try:
-                    self.status_queue.get_nowait()
-                except queue.Empty:
-                    break
-
-            if self._record.success:
-                item = {
-                        'type': 'pipeline_success'}
-                self.status_queue.put_nowait(item)
-            else:
-                item = {
-                        'type': 'pipeline_error',
-                        'assets': []}
-                for entry in self._record.entries:
-                    if entry.errors:
-                        asset_item = {
-                                'path': entry.rel_input,
-                                'errors': list(entry.errors)}
-                        item['assets'].append(asset_item)
-                self.status_queue.put_nowait(item)
-        except:
-            pass
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/serving/mime.types	Thu May 07 21:37:38 2015 -0700
@@ -0,0 +1,617 @@
+application/activemessage
+application/andrew-inset			ez
+application/applefile
+application/atomicmail
+application/batch-SMTP
+application/beep+xml
+application/cals-1840
+application/commonground
+application/cu-seeme				csm cu
+application/cybercash
+application/dca-rft
+application/dec-dx
+application/dsptype				tsp
+application/dvcs
+application/edi-consent
+application/edifact
+application/edi-x12
+application/eshop
+application/font-tdpfr
+application/futuresplash			spl
+application/ghostview
+application/hta					hta
+application/http
+application/hyperstudio
+application/iges
+application/index
+application/index.cmd
+application/index.obj
+application/index.response
+application/index.vnd
+application/iotp
+application/ipp
+application/isup
+application/mac-compactpro			cpt
+application/marc
+application/mac-binhex40			hqx
+application/macwriteii
+application/mathematica				nb
+application/mathematica-old
+application/msaccess				mdb
+application/msword				doc dot
+application/news-message-id
+application/news-transmission
+application/octet-stream			bin
+application/ocsp-request
+application/ocsp-response
+application/oda					oda
+application/ogg					ogg
+application/parityfec
+application/pics-rules				prf
+application/pgp-encrypted
+application/pgp-keys				key
+application/pdf					pdf
+application/pgp-signature			pgp
+application/pkcs10
+application/pkcs7-mime
+application/pkcs7-signature
+application/pkix-cert
+application/pkixcmp
+application/pkix-crl
+application/postscript				ps ai eps
+application/prs.alvestrand.titrax-sheet
+application/prs.cww
+application/prs.nprend
+application/qsig
+application/riscos
+application/remote-printing
+application/rss+xml				rss
+application/rtf					rtf
+application/sdp
+application/set-payment
+application/set-payment-initiation
+application/set-registration
+application/set-registration-initiation
+application/sgml
+application/sgml-open-catalog
+application/sieve
+application/slate
+application/smil				smi smil
+application/timestamp-query
+application/timestamp-reply
+application/vemmi
+application/whoispp-query
+application/whoispp-response
+application/wita
+application/wordperfect5.1			wp5
+application/x400-bp
+application/xhtml+xml				xht xhtml
+application/xml
+application/xml-dtd
+application/xml-external-parsed-entity
+application/zip					zip
+application/vnd.3M.Post-it-Notes
+application/vnd.accpac.simply.aso
+application/vnd.accpac.simply.imp
+application/vnd.acucobol
+application/vnd.aether.imp
+application/vnd.anser-web-certificate-issue-initiation
+application/vnd.anser-web-funds-transfer-initiation
+application/vnd.audiograph
+application/vnd.bmi
+application/vnd.businessobjects
+application/vnd.canon-cpdl
+application/vnd.canon-lips
+application/vnd.cinderella			cdy
+application/vnd.claymore
+application/vnd.commerce-battelle
+application/vnd.commonspace
+application/vnd.comsocaller
+application/vnd.contact.cmsg
+application/vnd.cosmocaller
+application/vnd.ctc-posml
+application/vnd.cups-postscript
+application/vnd.cups-raster
+application/vnd.cups-raw
+application/vnd.cybank
+application/vnd.dna
+application/vnd.dpgraph
+application/vnd.dxr
+application/vnd.ecdis-update
+application/vnd.ecowin.chart
+application/vnd.ecowin.filerequest
+application/vnd.ecowin.fileupdate
+application/vnd.ecowin.series
+application/vnd.ecowin.seriesrequest
+application/vnd.ecowin.seriesupdate
+application/vnd.enliven
+application/vnd.epson.esf
+application/vnd.epson.msf
+application/vnd.epson.quickanime
+application/vnd.epson.salt
+application/vnd.epson.ssf
+application/vnd.ericsson.quickcall
+application/vnd.eudora.data
+application/vnd.fdf
+application/vnd.ffsns
+application/vnd.flographit
+application/vnd.framemaker
+application/vnd.fsc.weblaunch
+application/vnd.fujitsu.oasys
+application/vnd.fujitsu.oasys2
+application/vnd.fujitsu.oasys3
+application/vnd.fujitsu.oasysgp
+application/vnd.fujitsu.oasysprs
+application/vnd.fujixerox.ddd
+application/vnd.fujixerox.docuworks
+application/vnd.fujixerox.docuworks.binder
+application/vnd.fut-misnet
+application/vnd.grafeq
+application/vnd.groove-account
+application/vnd.groove-identity-message
+application/vnd.groove-injector
+application/vnd.groove-tool-message
+application/vnd.groove-tool-template
+application/vnd.groove-vcard
+application/vnd.hhe.lesson-player
+application/vnd.hp-HPGL
+application/vnd.hp-PCL
+application/vnd.hp-PCLXL
+application/vnd.hp-hpid
+application/vnd.hp-hps
+application/vnd.httphone
+application/vnd.hzn-3d-crossword
+application/vnd.ibm.MiniPay
+application/vnd.ibm.afplinedata
+application/vnd.ibm.modcap
+application/vnd.informix-visionary
+application/vnd.intercon.formnet
+application/vnd.intertrust.digibox
+application/vnd.intertrust.nncp
+application/vnd.intu.qbo
+application/vnd.intu.qfx
+application/vnd.irepository.package+xml
+application/vnd.is-xpr
+application/vnd.japannet-directory-service
+application/vnd.japannet-jpnstore-wakeup
+application/vnd.japannet-payment-wakeup
+application/vnd.japannet-registration
+application/vnd.japannet-registration-wakeup
+application/vnd.japannet-setstore-wakeup
+application/vnd.japannet-verification
+application/vnd.japannet-verification-wakeup
+application/vnd.koan
+application/vnd.lotus-1-2-3
+application/vnd.lotus-approach
+application/vnd.lotus-freelance
+application/vnd.lotus-notes
+application/vnd.lotus-organizer
+application/vnd.lotus-screencam
+application/vnd.lotus-wordpro
+application/vnd.mcd
+application/vnd.mediastation.cdkey
+application/vnd.meridian-slingshot
+application/vnd.mif	      mif
+application/vnd.minisoft-hp3000-save
+application/vnd.mitsubishi.misty-guard.trustweb
+application/vnd.mobius.daf
+application/vnd.mobius.dis
+application/vnd.mobius.msl
+application/vnd.mobius.plc
+application/vnd.mobius.txf
+application/vnd.motorola.flexsuite
+application/vnd.motorola.flexsuite.adsi
+application/vnd.motorola.flexsuite.fis
+application/vnd.motorola.flexsuite.gotap
+application/vnd.motorola.flexsuite.kmr
+application/vnd.motorola.flexsuite.ttc
+application/vnd.motorola.flexsuite.wem
+application/vnd.mozilla.xul+xml
+application/vnd.ms-artgalry
+application/vnd.ms-asf
+application/vnd.ms-excel			xls xlb
+application/vnd.ms-lrm
+application/vnd.ms-pki.seccat			cat
+application/vnd.ms-pki.stl			stl
+application/vnd.ms-powerpoint			ppt pps pot
+application/vnd.ms-project
+application/vnd.ms-tnef
+application/vnd.ms-works
+application/vnd.mseq
+application/vnd.msign
+application/vnd.music-niff
+application/vnd.musician
+application/vnd.netfpx
+application/vnd.noblenet-directory
+application/vnd.noblenet-sealer
+application/vnd.noblenet-web
+application/vnd.novadigm.EDM
+application/vnd.novadigm.EDX
+application/vnd.novadigm.EXT
+application/vnd.osa.netdeploy
+application/vnd.palm
+application/vnd.pg.format
+application/vnd.pg.osasli
+application/vnd.powerbuilder6
+application/vnd.powerbuilder6-s
+application/vnd.powerbuilder7
+application/vnd.powerbuilder7-s
+application/vnd.powerbuilder75
+application/vnd.powerbuilder75-s
+application/vnd.previewsystems.box
+application/vnd.publishare-delta-tree
+application/vnd.pvi.ptid1
+application/vnd.pwg-xhtml-print+xml
+application/vnd.rapid
+application/vnd.s3sms
+application/vnd.seemail
+application/vnd.shana.informed.formdata
+application/vnd.shana.informed.formtemplate
+application/vnd.shana.informed.interchange
+application/vnd.shana.informed.package
+application/vnd.sss-cod
+application/vnd.sss-dtf
+application/vnd.sss-ntf
+application/vnd.stardivision.calc		sdc
+application/vnd.stardivision.draw		sda
+application/vnd.stardivision.impress		sdd sdp
+application/vnd.stardivision.math		smf
+application/vnd.stardivision.writer		sdw vor
+application/vnd.stardivision.writer-global	sgl
+application/vnd.street-stream
+application/vnd.sun.xml.calc			sxc
+application/vnd.sun.xml.calc.template		stc
+application/vnd.sun.xml.draw			sxd
+application/vnd.sun.xml.draw.template		std
+application/vnd.sun.xml.impress			sxi
+application/vnd.sun.xml.impress.template	sti
+application/vnd.sun.xml.math			sxm
+application/vnd.sun.xml.writer			sxw
+application/vnd.sun.xml.writer.global		sxg
+application/vnd.sun.xml.writer.template		stw
+application/vnd.svd
+application/vnd.swiftview-ics
+application/vnd.triscape.mxs
+application/vnd.trueapp
+application/vnd.truedoc
+application/vnd.tve-trigger
+application/vnd.ufdl
+application/vnd.uplanet.alert
+application/vnd.uplanet.alert-wbxml
+application/vnd.uplanet.bearer-choice
+application/vnd.uplanet.bearer-choice-wbxml
+application/vnd.uplanet.cacheop
+application/vnd.uplanet.cacheop-wbxml
+application/vnd.uplanet.channel
+application/vnd.uplanet.channel-wbxml
+application/vnd.uplanet.list
+application/vnd.uplanet.list-wbxml
+application/vnd.uplanet.listcmd
+application/vnd.uplanet.listcmd-wbxml
+application/vnd.uplanet.signal
+application/vnd.vcx
+application/vnd.vectorworks
+application/vnd.vidsoft.vidconference
+application/vnd.visio
+application/vnd.vividence.scriptfile
+application/vnd.wap.sic
+application/vnd.wap.slc
+application/vnd.wap.wbxml			wbxml
+application/vnd.wap.wmlc			wmlc
+application/vnd.wap.wmlscriptc			wmlsc
+application/vnd.webturbo
+application/vnd.wrq-hp3000-labelled
+application/vnd.wt.stf
+application/vnd.xara
+application/vnd.xfdl
+application/vnd.yellowriver-custom-menu
+application/x-123				wk
+application/x-apple-diskimage			dmg
+application/x-bcpio				bcpio
+application/x-cdf				cdf
+application/x-cdlink				vcd
+application/x-chess-pgn				pgn
+application/x-core
+application/x-cpio				cpio
+application/x-csh				csh
+application/x-debian-package			deb
+application/x-director				dcr dir dxr
+application/x-doom				wad
+application/x-dms				dms
+application/x-dvi				dvi
+application/x-executable
+application/x-font				pfa pfb gsf pcf pcf.Z
+application/x-futuresplash			spl
+application/x-gnumeric				gnumeric
+application/x-go-sgf				sgf
+application/x-graphing-calculator		gcf
+application/x-gtar				gtar tgz taz
+application/x-hdf				hdf
+application/x-httpd-php				phtml pht php
+application/x-httpd-php-source			phps
+application/x-httpd-php3			php3
+application/x-httpd-php3-preprocessed		php3p
+application/x-httpd-php4			php4
+application/x-ica				ica
+application/x-internet-signup			ins isp
+application/x-iphone				iii
+application/x-java-applet
+application/x-java-archive			jar
+application/x-java-bean
+application/x-java-jnlp-file			jnlp
+application/x-java-serialized-object		ser
+application/x-java-vm				class
+application/x-javascript			js
+application/x-kdelnk
+application/x-kchart				chrt
+application/x-killustrator			kil
+application/x-kpresenter			kpr kpt
+application/x-koan				skp skd skt skm
+application/x-kspread				ksp
+application/x-kword				kwd kwt
+application/x-latex				latex
+application/x-lha				lha
+application/x-lzh				lzh
+application/x-lzx				lzx
+application/x-maker				frm maker frame fm fb book fbdoc
+application/x-mif				mif
+application/x-ms-wmz				wmz
+application/x-ms-wmd				wmd
+application/x-msdos-program			com exe bat dll
+application/x-msi				msi
+application/x-netcdf				nc
+application/x-ns-proxy-autoconfig		pac
+application/x-object				o
+application/x-oz-application			oza
+application/x-perl				pl pm
+application/x-pkcs7-certreqresp			p7r
+application/x-pkcs7-crl				crl
+application/x-quicktimeplayer			qtl
+application/x-redhat-package-manager		rpm
+application/x-rx
+application/x-sh
+application/x-shar				shar
+application/x-shellscript
+application/x-shockwave-flash			swf swfl
+application/x-sh				sh
+application/x-stuffit				sit
+application/x-sv4cpio				sv4cpio
+application/x-sv4crc				sv4crc
+application/x-tar				tar
+application/x-tcl				tcl
+application/x-tex				tex
+application/x-tex-gf				gf
+application/x-tex-pk				pk
+application/x-texinfo				texinfo texi
+application/x-trash				~ % bak old sik
+application/x-troff				t tr roff
+application/x-troff-man				man
+application/x-troff-me				me
+application/x-troff-ms				ms
+application/x-ustar				ustar
+application/x-wais-source			src
+application/x-wingz				wz
+application/x-x509-ca-cert			crt
+application/x-xfig				fig
+
+audio/32kadpcm
+#audio/aiff					aif aifc aiff
+audio/basic					au snd
+audio/g.722.1
+audio/l16
+audio/midi					mid midi kar
+audio/mp4a-latm
+audio/mpa-robust
+audio/mpeg					mpga mpega mp2 mp3
+audio/mpegurl					m3u
+audio/parityfec
+audio/prs.sid					sid
+audio/telephone-event
+audio/tone
+#audio/wav					wav
+audio/vnd.cisco.nse
+audio/vnd.cns.anp1
+audio/vnd.cns.inf1
+audio/vnd.digital-winds
+audio/vnd.everad.plj
+audio/vnd.lucent.voice
+audio/vnd.nortel.vbk
+audio/vnd.nuera.ecelp4800
+audio/vnd.nuera.ecelp7470
+audio/vnd.nuera.ecelp9600
+audio/vnd.octel.sbc
+audio/vnd.qcelp
+audio/vnd.rhetorex.32kadpcm
+audio/vnd.vmx.cvsd
+audio/x-aiff					aif aiff aifc
+audio/x-gsm					gsm
+audio/x-mpegurl					m3u
+audio/x-ms-wma					wma
+audio/x-ms-wax					wax
+audio/x-pn-realaudio-plugin			rpm
+audio/x-pn-realaudio				ra rm ram
+audio/x-realaudio				ra
+audio/x-scpls					pls
+audio/x-sd2					sd2
+audio/x-wav					wav
+
+chemical/x-pdb					pdb
+chemical/x-xyz					xyz
+
+image/bmp					bmp
+image/cgm
+image/g3fax
+image/gif					gif
+image/ief					ief
+image/jpeg					jpeg jpg jpe
+image/naplps
+image/pcx					pcx
+image/png					png
+image/prs.btif
+image/prs.pti
+image/svg+xml					svg svgz
+image/tiff					tiff tif
+image/vnd.cns.inf2
+image/vnd.dwg
+image/vnd.dxf
+image/vnd.fastbidsheet
+image/vnd.fpx
+image/vnd.fst
+image/vnd.fujixerox.edmics-mmr
+image/vnd.fujixerox.edmics-rlc
+image/vnd.mix
+image/vnd.net-fpx
+image/vnd.svf
+image/vnd.wap.wbmp				wbmp
+image/vnd.xiff
+image/x-cmu-raster				ras
+image/x-coreldraw				cdr
+image/x-coreldrawpattern			pat
+image/x-coreldrawtemplate			cdt
+image/x-corelphotopaint				cpt
+image/x-djvu					djvu djv
+image/x-icon					ico
+image/x-jg					art
+image/x-jng					jng
+image/x-ms-bmp					bmp
+image/x-photoshop				psd
+image/x-portable-anymap				pnm
+image/x-portable-bitmap				pbm
+image/x-portable-graymap			pgm
+image/x-portable-pixmap				ppm
+image/x-rgb					rgb
+image/x-xbitmap					xbm
+image/x-xpixmap					xpm
+image/x-xwindowdump				xwd
+
+inode/chardevice
+inode/blockdevice
+inode/directory-locked
+inode/directory
+inode/fifo
+inode/socket
+
+message/delivery-status
+message/disposition-notification
+message/external-body
+message/http
+message/s-http
+message/news
+message/partial
+message/rfc822
+
+model/iges					igs iges
+model/mesh					msh mesh silo
+model/vnd.dwf
+model/vnd.flatland.3dml
+model/vnd.gdl
+model/vnd.gs-gdl
+model/vnd.gtw
+model/vnd.mts
+model/vnd.vtu
+model/vrml					wrl vrml
+
+multipart/alternative
+multipart/appledouble
+multipart/byteranges
+multipart/digest
+multipart/encrypted
+multipart/form-data
+multipart/header-set
+multipart/mixed
+multipart/parallel
+multipart/related
+multipart/report
+multipart/signed
+multipart/voice-message
+
+text/calendar
+text/comma-separated-values			csv
+text/css					css
+text/directory
+text/english
+text/enriched
+text/h323					323
+text/html					htm html
+text/iuls					uls
+text/mathml					mml
+text/parityfec
+text/plain					asc txt text diff
+text/prs.lines.tag
+text/rfc822-headers
+text/richtext					rtx
+text/rtf					rtf
+text/scriptlet					sct wsc
+text/t140
+text/texmacs					tm ts
+text/tab-separated-values			tsv
+text/uri-list
+text/vnd.abc
+text/vnd.curl
+text/vnd.DMClientScript
+text/vnd.flatland.3dml
+text/vnd.fly
+text/vnd.fmi.flexstor
+text/vnd.in3d.3dml
+text/vnd.in3d.spot
+text/vnd.IPTC.NewsML
+text/vnd.IPTC.NITF
+text/vnd.latex-z
+text/vnd.motorola.reflex
+text/vnd.ms-mediapackage
+text/vnd.wap.si
+text/vnd.wap.sl
+text/vnd.wap.wml				wml
+text/vnd.wap.wmlscript				wmls
+text/xml					xml xsl
+text/x-c++hdr					h++ hpp hxx hh
+text/x-c++src					c++ cpp cxx cc
+text/x-chdr					h
+text/x-crontab
+text/x-csh					csh
+text/x-csrc					c
+text/x-java					java
+text/x-makefile
+text/xml-external-parsed-entity
+text/x-moc					moc
+text/x-pascal					p pas
+text/x-pcs-gcd					gcd
+text/x-server-parsed-html			shtml
+text/x-setext					etx
+text/x-sh					sh
+text/x-tcl					tcl tk
+text/x-tex					tex ltx sty cls
+text/x-vcalendar				vcs
+text/x-vcard					vcf
+
+#video/avi					avi
+video/dl					dl
+video/fli					fli
+video/gl					gl
+video/mpeg					mpeg mpg mpe
+video/quicktime					qt mov
+video/mp4v-es
+video/parityfec
+video/pointer
+video/vnd.fvt
+video/vnd.motorola.video
+video/vnd.motorola.videop
+video/vnd.mpegurl				mxu
+video/vnd.mts
+video/vnd.nokia.interleaved-multimedia
+video/vnd.vivo
+video/x-dv					dif dv
+video/x-la-asf					lsf lsx
+video/x-mng					mng
+video/x-ms-asf					asf asx
+video/x-ms-wm					wm
+video/x-ms-wmv					wmv
+video/x-ms-wmx					wmx
+video/x-ms-wvx					wvx
+video/x-msvideo					avi
+video/x-sgi-movie				movie
+
+x-conference/x-cooltalk				ice
+
+x-world/x-vrml					vrm vrml wrl
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/serving/procloop.py	Thu May 07 21:37:38 2015 -0700
@@ -0,0 +1,146 @@
+import os
+import os.path
+import time
+import json
+import queue
+import logging
+import threading
+
+
+logger = logging.getLogger(__name__)
+
+
+_sse_abort = threading.Event()
+
+
+class PipelineStatusServerSideEventProducer(object):
+    def __init__(self, status_queue):
+        self.status_queue = status_queue
+        self.interval = 2
+        self.timeout = 60*10
+        self._start_time = 0
+
+    def run(self):
+        logger.debug("Starting pipeline status SSE.")
+        self._start_time = time.time()
+
+        outstr = 'event: ping\ndata: started\n\n'
+        yield bytes(outstr, 'utf8')
+
+        count = 0
+        while True:
+            if time.time() > self.timeout + self._start_time:
+                logger.debug("Closing pipeline status SSE, timeout reached.")
+                outstr = 'event: pipeline_timeout\ndata: bye\n\n'
+                yield bytes(outstr, 'utf8')
+                break
+
+            if _sse_abort.is_set():
+                break
+
+            try:
+                logger.debug("Polling pipeline status queue...")
+                count += 1
+                data = self.status_queue.get(True, self.interval)
+            except queue.Empty:
+                if count < 3:
+                    continue
+                data = {'type': 'ping', 'message': 'ping'}
+                count = 0
+
+            event_type = data['type']
+            outstr = 'event: %s\ndata: %s\n\n' % (
+                    event_type, json.dumps(data))
+            logger.debug("Sending pipeline status SSE.")
+            yield bytes(outstr, 'utf8')
+
+    def close(self):
+        logger.debug("Closing pipeline status SSE.")
+
+
+class ProcessingLoop(threading.Thread):
+    def __init__(self, pipeline):
+        super(ProcessingLoop, self).__init__(
+                name='pipeline-reloader', daemon=True)
+        self.pipeline = pipeline
+        self.status_queue = queue.Queue()
+        self.interval = 1
+        self._paths = set()
+        self._record = None
+        self._last_bake = 0
+
+    def run(self):
+        # Build the first list of known files and run the pipeline once.
+        app = self.pipeline.app
+        roots = [os.path.join(app.root_dir, r)
+                 for r in self.pipeline.mounts.keys()]
+        for root in roots:
+            for dirpath, dirnames, filenames in os.walk(root):
+                self._paths |= set([os.path.join(dirpath, f)
+                                    for f in filenames])
+        self._last_bake = time.time()
+        self._record = self.pipeline.run()
+
+        while True:
+            for root in roots:
+                # For each mount root we try to find the first new or
+                # modified file. If any, we just run the pipeline on
+                # that mount.
+                found_new_or_modified = False
+                for dirpath, dirnames, filenames in os.walk(root):
+                    for filename in filenames:
+                        path = os.path.join(dirpath, filename)
+                        if path not in self._paths:
+                            logger.debug("Found new asset: %s" % path)
+                            self._paths.add(path)
+                            found_new_or_modified = True
+                            break
+                        if os.path.getmtime(path) > self._last_bake:
+                            logger.debug("Found modified asset: %s" % path)
+                            found_new_or_modified = True
+                            break
+
+                    if found_new_or_modified:
+                        break
+
+                if found_new_or_modified:
+                    self._runPipeline(root)
+
+            time.sleep(self.interval)
+
+    def _runPipeline(self, root):
+        self._last_bake = time.time()
+        try:
+            self._record = self.pipeline.run(
+                    root,
+                    previous_record=self._record,
+                    save_record=False)
+
+            # Update the status queue.
+            # (we need to clear it because there may not be a consumer
+            #  on the other side, if the user isn't running with the
+            #  debug window active)
+            while True:
+                try:
+                    self.status_queue.get_nowait()
+                except queue.Empty:
+                    break
+
+            if self._record.success:
+                item = {
+                        'type': 'pipeline_success'}
+                self.status_queue.put_nowait(item)
+            else:
+                item = {
+                        'type': 'pipeline_error',
+                        'assets': []}
+                for entry in self._record.entries:
+                    if entry.errors:
+                        asset_item = {
+                                'path': entry.rel_input,
+                                'errors': list(entry.errors)}
+                        item['assets'].append(asset_item)
+                self.status_queue.put_nowait(item)
+        except:
+            pass
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/serving/server.py	Thu May 07 21:37:38 2015 -0700
@@ -0,0 +1,479 @@
+import io
+import os
+import re
+import gzip
+import time
+import os.path
+import hashlib
+import logging
+import datetime
+from werkzeug.exceptions import (
+        NotFound, MethodNotAllowed, InternalServerError, HTTPException)
+from werkzeug.wrappers import Request, Response
+from werkzeug.wsgi import ClosingIterator, wrap_file
+from jinja2 import FileSystemLoader, Environment
+from piecrust.app import PieCrust
+from piecrust.rendering import QualifiedPage, PageRenderingContext, render_page
+from piecrust.sources.base import MODE_PARSING
+from piecrust.uriutil import split_sub_uri
+
+
+logger = logging.getLogger(__name__)
+
+
+class ServeRecord(object):
+    def __init__(self):
+        self.entries = {}
+
+    def addEntry(self, entry):
+        key = self._makeKey(entry.uri, entry.sub_num)
+        self.entries[key] = entry
+
+    def getEntry(self, uri, sub_num):
+        key = self._makeKey(uri, sub_num)
+        return self.entries.get(key)
+
+    def _makeKey(self, uri, sub_num):
+        return "%s:%s" % (uri, sub_num)
+
+
+class ServeRecordPageEntry(object):
+    def __init__(self, uri, sub_num):
+        self.uri = uri
+        self.sub_num = sub_num
+        self.used_source_names = set()
+
+
+class WsgiServerWrapper(object):
+    def __init__(self, server):
+        self.server = server
+
+    def __call__(self, environ, start_response):
+        return self.server._run_request(environ, start_response)
+
+
+class Server(object):
+    def __init__(self, root_dir,
+                 debug=False, sub_cache_dir=None,
+                 static_preview=True, run_sse_check=None):
+        self.root_dir = root_dir
+        self.debug = debug
+        self.sub_cache_dir = sub_cache_dir
+        self.run_sse_check = run_sse_check
+        self.static_preview = static_preview
+        self._out_dir = None
+        self._page_record = None
+        self._proc_loop = None
+        self._mimetype_map = load_mimetype_map()
+
+    def getWsgiApp(self):
+        # Bake all the assets so we know what we have, and so we can serve
+        # them to the client. We need a temp app for this.
+        app = PieCrust(root_dir=self.root_dir, debug=self.debug)
+        app._useSubCacheDir(self.sub_cache_dir)
+        self._out_dir = os.path.join(app.sub_cache_dir, 'server')
+        self._page_record = ServeRecord()
+
+        if not self.run_sse_check or self.run_sse_check():
+            # When using a server with code reloading, some implementations
+            # use process forking and we end up going here twice. We only want
+            # to start the pipeline loop in the inner process most of the
+            # time so we let the implementation tell us if this is OK.
+            from piecrust.processing.base import ProcessorPipeline
+            from piecrust.serving.procloop import ProcessingLoop
+            pipeline = ProcessorPipeline(app, self._out_dir)
+            self._proc_loop = ProcessingLoop(pipeline)
+            self._proc_loop.start()
+
+        # Run the WSGI app.
+        wsgi_wrapper = WsgiServerWrapper(self)
+        return wsgi_wrapper
+
+    def _run_request(self, environ, start_response):
+        try:
+            return self._try_run_request(environ, start_response)
+        except Exception as ex:
+            if self.debug:
+                raise
+            return self._handle_error(ex, environ, start_response)
+
+    def _try_run_request(self, environ, start_response):
+        request = Request(environ)
+
+        # We don't support anything else than GET requests since we're
+        # previewing something that will be static later.
+        if self.static_preview and request.method != 'GET':
+            logger.error("Only GET requests are allowed, got %s" %
+                         request.method)
+            raise MethodNotAllowed()
+
+        # Handle special requests right away.
+        response = self._try_special_request(environ, request)
+        if response is not None:
+            return response(environ, start_response)
+
+        # Also handle requests to a pipeline-built asset right away.
+        response = self._try_serve_asset(environ, request)
+        if response is not None:
+            return response(environ, start_response)
+
+        # Create the app for this request.
+        app = PieCrust(root_dir=self.root_dir, debug=self.debug)
+        app._useSubCacheDir(self.sub_cache_dir)
+        app.config.set('site/root', '/')
+        app.config.set('server/is_serving', True)
+        if (app.config.get('site/enable_debug_info') and
+                '!debug' in request.args):
+            app.config.set('site/show_debug_info', True)
+
+        # We'll serve page assets directly from where they are.
+        app.env.base_asset_url_format = '/_asset/%path%'
+
+        # Let's see if it can be a page asset.
+        response = self._try_serve_page_asset(app, environ, request)
+        if response is not None:
+            return response(environ, start_response)
+
+        # Nope. Let's see if it's an actual page.
+        try:
+            response = self._try_serve_page(app, environ, request)
+            return response(environ, start_response)
+        except (RouteNotFoundError, SourceNotFoundError) as ex:
+            raise NotFound(str(ex)) from ex
+        except HTTPException:
+            raise
+        except Exception as ex:
+            if app.debug:
+                logger.exception(ex)
+                raise
+            msg = str(ex)
+            logger.error(msg)
+            raise InternalServerError(msg) from ex
+
+    def _try_special_request(self, environ, request):
+        static_mount = '/__piecrust_static/'
+        if request.path.startswith(static_mount):
+            rel_req_path = request.path[len(static_mount):]
+            mount = os.path.join(
+                    os.path.dirname(__file__),
+                    'resources', 'server')
+            full_path = os.path.join(mount, rel_req_path)
+            try:
+                response = self._make_wrapped_file_response(
+                        environ, request, full_path)
+                return response
+            except OSError:
+                pass
+
+        debug_mount = '/__piecrust_debug/'
+        if request.path.startswith(debug_mount):
+            rel_req_path = request.path[len(debug_mount):]
+            if rel_req_path == 'pipeline_status':
+                from piecrust.server.procloop import (
+                        PipelineStatusServerSideEventProducer)
+                provider = PipelineStatusServerSideEventProducer(
+                        self._proc_loop.status_queue)
+                it = ClosingIterator(provider.run(), [provider.close])
+                response = Response(it)
+                response.headers['Cache-Control'] = 'no-cache'
+                if 'text/event-stream' in request.accept_mimetypes:
+                    response.mimetype = 'text/event-stream'
+                response.direct_passthrough = True
+                response.implicit_sequence_conversion = False
+                return response
+
+        return None
+
+    def _try_serve_asset(self, environ, request):
+        rel_req_path = request.path.lstrip('/').replace('/', os.sep)
+        if request.path.startswith('/_cache/'):
+            # Some stuff needs to be served directly from the cache directory,
+            # like LESS CSS map files.
+            full_path = os.path.join(self.root_dir, rel_req_path)
+        else:
+            full_path = os.path.join(self._out_dir, rel_req_path)
+
+        try:
+            response = self._make_wrapped_file_response(
+                    environ, request, full_path)
+            return response
+        except OSError:
+            pass
+        return None
+
+    def _try_serve_page_asset(self, app, environ, request):
+        if not request.path.startswith('/_asset/'):
+            return None
+
+        full_path = os.path.join(app.root_dir, request.path[len('/_asset/'):])
+        if not os.path.isfile(full_path):
+            return None
+
+        return self._make_wrapped_file_response(environ, request, full_path)
+
+    def _try_serve_page(self, app, environ, request):
+        # Try to find what matches the requested URL.
+        req_path, page_num = split_sub_uri(app, request.path)
+
+        routes = find_routes(app.routes, req_path)
+        if len(routes) == 0:
+            raise RouteNotFoundError("Can't find route for: %s" % req_path)
+
+        rendered_page = None
+        first_not_found = None
+        for route, route_metadata in routes:
+            try:
+                logger.debug("Trying to render match from source '%s'." %
+                             route.source_name)
+                rendered_page = self._try_render_page(
+                        app, route, route_metadata, page_num, req_path)
+                if rendered_page is not None:
+                    break
+            except NotFound as nfe:
+                if first_not_found is None:
+                    first_not_found = nfe
+        else:
+            raise SourceNotFoundError(
+                    "Can't find path for: %s (looked in: %s)" %
+                    (req_path, [r.source_name for r, _ in routes]))
+
+        # If we haven't found any good match, raise whatever exception we
+        # first got. Otherwise, raise a generic exception.
+        if rendered_page is None:
+            first_not_found = first_not_found or NotFound(
+                    "This page couldn't be found.")
+            raise first_not_found
+
+        # Start doing stuff.
+        page = rendered_page.page
+        rp_content = rendered_page.content
+
+        # Profiling.
+        if app.config.get('site/show_debug_info'):
+            now_time = time.clock()
+            timing_info = (
+                    '%8.1f ms' %
+                    ((now_time - app.env.start_time) * 1000.0))
+            rp_content = rp_content.replace(
+                    '__PIECRUST_TIMING_INFORMATION__', timing_info)
+
+        # Build the response.
+        response = Response()
+
+        etag = hashlib.md5(rp_content.encode('utf8')).hexdigest()
+        if not app.debug and etag in request.if_none_match:
+            response.status_code = 304
+            return response
+
+        response.set_etag(etag)
+        response.content_md5 = etag
+
+        cache_control = response.cache_control
+        if app.debug:
+            cache_control.no_cache = True
+            cache_control.must_revalidate = True
+        else:
+            cache_time = (page.config.get('cache_time') or
+                          app.config.get('site/cache_time'))
+            if cache_time:
+                cache_control.public = True
+                cache_control.max_age = cache_time
+
+        content_type = page.config.get('content_type')
+        if content_type and '/' not in content_type:
+            mimetype = content_type_map.get(content_type, content_type)
+        else:
+            mimetype = content_type
+        if mimetype:
+            response.mimetype = mimetype
+
+        if ('gzip' in request.accept_encodings and
+                app.config.get('site/enable_gzip')):
+            try:
+                with io.BytesIO() as gzip_buffer:
+                    with gzip.open(gzip_buffer, mode='wt',
+                                   encoding='utf8') as gzip_file:
+                        gzip_file.write(rp_content)
+                    rp_content = gzip_buffer.getvalue()
+                    response.content_encoding = 'gzip'
+            except Exception:
+                logger.exception("Error compressing response, "
+                                 "falling back to uncompressed.")
+        response.set_data(rp_content)
+
+        return response
+
+    def _try_render_page(self, app, route, route_metadata, page_num, req_path):
+        # Match the route to an actual factory.
+        taxonomy_info = None
+        source = app.getSource(route.source_name)
+        if route.taxonomy_name is None:
+            factory = source.findPageFactory(route_metadata, MODE_PARSING)
+            if factory is None:
+                return None
+        else:
+            taxonomy = app.getTaxonomy(route.taxonomy_name)
+            route_terms = route_metadata.get(taxonomy.term_name)
+            if route_terms is None:
+                return None
+
+            tax_page_ref = taxonomy.getPageRef(source.name)
+            factory = tax_page_ref.getFactory()
+            tax_terms = route.unslugifyTaxonomyTerm(route_terms)
+            route_metadata[taxonomy.term_name] = tax_terms
+            taxonomy_info = (taxonomy, tax_terms)
+
+        # Build the page.
+        page = factory.buildPage()
+        # We force the rendering of the page because it could not have
+        # changed, but include pages that did change.
+        qp = QualifiedPage(page, route, route_metadata)
+        render_ctx = PageRenderingContext(qp,
+                                          page_num=page_num,
+                                          force_render=True)
+        if taxonomy_info is not None:
+            taxonomy, tax_terms = taxonomy_info
+            render_ctx.setTaxonomyFilter(taxonomy, tax_terms)
+
+        # See if this page is known to use sources. If that's the case,
+        # just don't use cached rendered segments for that page (but still
+        # use them for pages that are included in it).
+        uri = qp.getUri()
+        assert uri == req_path
+        entry = self._page_record.getEntry(uri, page_num)
+        if (taxonomy_info is not None or entry is None or
+                entry.used_source_names):
+            cache_key = '%s:%s' % (uri, page_num)
+            app.env.rendered_segments_repository.invalidate(cache_key)
+
+        # Render the page.
+        rendered_page = render_page(render_ctx)
+
+        # Check if this page is a taxonomy page that actually doesn't match
+        # anything.
+        if taxonomy_info is not None:
+            paginator = rendered_page.data.get('pagination')
+            if (paginator and paginator.is_loaded and
+                    len(paginator.items) == 0):
+                taxonomy = taxonomy_info[0]
+                message = ("This URL matched a route for taxonomy '%s' but "
+                           "no pages have been found to have it. This page "
+                           "won't be generated by a bake." % taxonomy.name)
+                raise NotFound(message)
+
+        # Remember stuff for next time.
+        if entry is None:
+            entry = ServeRecordPageEntry(req_path, page_num)
+            self._page_record.addEntry(entry)
+        for p, pinfo in render_ctx.render_passes.items():
+            entry.used_source_names |= pinfo.used_source_names
+
+        # Ok all good.
+        return rendered_page
+
+    def _make_wrapped_file_response(self, environ, request, path):
+        logger.debug("Serving %s" % path)
+
+        # Check if we can return a 304 status code.
+        mtime = os.path.getmtime(path)
+        etag_str = '%s$$%s' % (path, mtime)
+        etag = hashlib.md5(etag_str.encode('utf8')).hexdigest()
+        if etag in request.if_none_match:
+            response = Response()
+            response.status_code = 304
+            return response
+
+        wrapper = wrap_file(environ, open(path, 'rb'))
+        response = Response(wrapper)
+        _, ext = os.path.splitext(path)
+        response.set_etag(etag)
+        response.last_modified = datetime.datetime.fromtimestamp(mtime)
+        response.mimetype = self._mimetype_map.get(
+                ext.lstrip('.'), 'text/plain')
+        return response
+
+    def _handle_error(self, exception, environ, start_response):
+        code = 500
+        if isinstance(exception, HTTPException):
+            code = exception.code
+
+        path = 'error'
+        if isinstance(exception, NotFound):
+            path += '404'
+
+        descriptions = self._get_exception_descriptions(exception)
+
+        env = Environment(loader=ErrorMessageLoader())
+        template = env.get_template(path)
+        context = {'details': descriptions}
+        response = Response(template.render(context), mimetype='text/html')
+        response.status_code = code
+        return response(environ, start_response)
+
+    def _get_exception_descriptions(self, exception):
+        desc = []
+        while exception is not None:
+            if isinstance(exception, HTTPException):
+                desc.append(exception.description)
+            else:
+                desc.append(str(exception))
+
+            inner_ex = exception.__cause__
+            if inner_ex is None:
+                inner_ex = exception.__context__
+            exception = inner_ex
+        return desc
+
+
+class RouteNotFoundError(Exception):
+    pass
+
+
+class SourceNotFoundError(Exception):
+    pass
+
+
+content_type_map = {
+        'html': 'text/html',
+        'xml': 'text/xml',
+        'txt': 'text/plain',
+        'text': 'text/plain',
+        'css': 'text/css',
+        'xhtml': 'application/xhtml+xml',
+        'atom': 'application/atom+xml',  # or 'text/xml'?
+        'rss': 'application/rss+xml',    # or 'text/xml'?
+        'json': 'application/json'}
+
+
+def find_routes(routes, uri):
+    res = []
+    for route in routes:
+        metadata = route.matchUri(uri)
+        if metadata is not None:
+            res.append((route, metadata))
+    return res
+
+
+class ErrorMessageLoader(FileSystemLoader):
+    def __init__(self):
+        base_dir = os.path.join(os.path.dirname(__file__), 'resources',
+                                'messages')
+        super(ErrorMessageLoader, self).__init__(base_dir)
+
+    def get_source(self, env, template):
+        template += '.html'
+        return super(ErrorMessageLoader, self).get_source(env, template)
+
+
+def load_mimetype_map():
+    mimetype_map = {}
+    sep_re = re.compile(r'\s+')
+    path = os.path.join(os.path.dirname(__file__), 'mime.types')
+    with open(path, 'r') as f:
+        for line in f:
+            tokens = sep_re.split(line)
+            if len(tokens) > 1:
+                for t in tokens[1:]:
+                    mimetype_map[t] = tokens[0]
+    return mimetype_map
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/serving/wrappers.py	Thu May 07 21:37:38 2015 -0700
@@ -0,0 +1,66 @@
+import os
+from piecrust.serving.server import Server
+from piecrust.serving.procloop import _sse_abort
+
+
+def run_werkzeug_server(root_dir, host, port,
+                        debug_piecrust=False, sub_cache_dir=None,
+                        use_debugger=False, use_reloader=False):
+    from werkzeug.serving import run_simple
+
+    def _run_sse_check():
+        # We don't want to run the processing loop here if this isn't
+        # the actual process that does the serving. In most cases it is,
+        # but if we're using Werkzeug's reloader, then it won't be the
+        # first time we get there... it will only be the correct process
+        # the second time, when the reloading process is spawned, with the
+        # `WERKZEUG_RUN_MAIN` variable set.
+        return (not use_reloader or
+                os.environ.get('WERKZEUG_RUN_MAIN') == 'true')
+
+    app = _get_piecrust_server(root_dir,
+                               debug=debug_piecrust,
+                               sub_cache_dir=sub_cache_dir,
+                               run_sse_check=_run_sse_check)
+    try:
+        run_simple(host, port, app,
+                   threaded=True,
+                   use_debugger=use_debugger,
+                   use_reloader=use_reloader)
+    finally:
+        _sse_abort.set()
+
+
+def run_gunicorn_server(root_dir,
+                        debug_piecrust=False, sub_cache_dir=None,
+                        gunicorn_options=None):
+    from gunicorn.app.base import BaseApplication
+
+    class PieCrustGunicornApplication(BaseApplication):
+        def __init__(self, app, options):
+            self.app = app
+            self.options = options
+            super(PieCrustGunicornApplication, self).__init__()
+
+        def load_config(self):
+            for k, v in self.options.items():
+                if k in self.cfg.settings and v is not None:
+                    self.cfg.set(k, v)
+
+        def load(self):
+            return self.app
+
+    app = _get_piecrust_server(root_dir,
+                               debug=debug_piecrust,
+                               sub_cache_dir=sub_cache_dir)
+
+    gunicorn_options = gunicorn_options or {}
+    app_wrapper = PieCrustGunicornApplication(app, gunicorn_options)
+    app_wrapper.run()
+
+
+def _get_piecrust_server(root_dir, **kwargs):
+    server = Server(root_dir, **kwargs)
+    app = server.getWsgiApp()
+    return app
+