comparison piecrust/serving/server.py @ 852:4850f8c21b6e

core: Start of the big refactor for PieCrust 3.0. * Everything is a `ContentSource`, including assets directories. * Most content sources are subclasses of the base file-system source. * A source is processed by a "pipeline", and there are 2 built-in pipelines, one for assets and one for pages. The asset pipeline is vaguely functional, but the page pipeline is completely broken right now. * Rewrite the baking process as just running appropriate pipelines on each content item. This should allow for better parallelization.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 17 May 2017 00:11:48 -0700
parents ab5c6a8ae90a
children f070a4fc033c
comparison
equal deleted inserted replaced
851:2c7e57d80bba 852:4850f8c21b6e
4 import time 4 import time
5 import os.path 5 import os.path
6 import hashlib 6 import hashlib
7 import logging 7 import logging
8 from werkzeug.exceptions import ( 8 from werkzeug.exceptions import (
9 NotFound, MethodNotAllowed, InternalServerError, HTTPException) 9 NotFound, MethodNotAllowed, InternalServerError, HTTPException)
10 from werkzeug.wrappers import Request, Response 10 from werkzeug.wrappers import Request, Response
11 from jinja2 import FileSystemLoader, Environment 11 from jinja2 import FileSystemLoader, Environment
12 from piecrust import CACHE_DIR, RESOURCES_DIR 12 from piecrust import CACHE_DIR, RESOURCES_DIR
13 from piecrust.rendering import PageRenderingContext, render_page 13 from piecrust.rendering import RenderingContext, render_page
14 from piecrust.routing import RouteNotFoundError 14 from piecrust.routing import RouteNotFoundError
15 from piecrust.serving.util import ( 15 from piecrust.serving.util import (
16 content_type_map, make_wrapped_file_response, get_requested_page, 16 content_type_map, make_wrapped_file_response, get_requested_page,
17 get_app_for_server) 17 get_app_for_server)
18 from piecrust.sources.base import SourceNotFoundError 18 from piecrust.sources.base import SourceNotFoundError
19 19
20 20
21 logger = logging.getLogger(__name__) 21 logger = logging.getLogger(__name__)
22 22
23 23
24 class WsgiServer(object): 24 class WsgiServer(object):
25 """ A WSGI application that serves a PieCrust website.
26 """
25 def __init__(self, appfactory, **kwargs): 27 def __init__(self, appfactory, **kwargs):
26 self.server = Server(appfactory, **kwargs) 28 self.server = Server(appfactory, **kwargs)
27 29
28 def __call__(self, environ, start_response): 30 def __call__(self, environ, start_response):
29 return self.server._run_request(environ, start_response) 31 return self.server._run_request(environ, start_response)
30 32
31 33
32 class ServeRecord(object):
33 def __init__(self):
34 self.entries = {}
35
36 def addEntry(self, entry):
37 key = self._makeKey(entry.uri, entry.sub_num)
38 self.entries[key] = entry
39
40 def getEntry(self, uri, sub_num):
41 key = self._makeKey(uri, sub_num)
42 return self.entries.get(key)
43
44 def _makeKey(self, uri, sub_num):
45 return "%s:%s" % (uri, sub_num)
46
47
48 class ServeRecordPageEntry(object):
49 def __init__(self, uri, sub_num):
50 self.uri = uri
51 self.sub_num = sub_num
52 self.used_source_names = set()
53
54
55 class MultipleNotFound(HTTPException): 34 class MultipleNotFound(HTTPException):
35 """ Represents a 404 (not found) error that tried to serve one or
36 more pages. It will report which pages it tried to serve
37 before failing.
38 """
56 code = 404 39 code = 404
57 40
58 def __init__(self, description, nfes): 41 def __init__(self, description, nfes):
59 super(MultipleNotFound, self).__init__(description) 42 super(MultipleNotFound, self).__init__(description)
60 self._nfes = nfes 43 self._nfes = nfes
68 desc += '</p>' 51 desc += '</p>'
69 return desc 52 return desc
70 53
71 54
72 class Server(object): 55 class Server(object):
56 """ The PieCrust server.
57 """
73 def __init__(self, appfactory, 58 def __init__(self, appfactory,
74 enable_debug_info=True, 59 enable_debug_info=True,
75 root_url='/', 60 root_url='/',
76 static_preview=True): 61 static_preview=True):
77 self.appfactory = appfactory 62 self.appfactory = appfactory
78 self.enable_debug_info = enable_debug_info 63 self.enable_debug_info = enable_debug_info
79 self.root_url = root_url 64 self.root_url = root_url
80 self.static_preview = static_preview 65 self.static_preview = static_preview
81 self._page_record = ServeRecord()
82 self._out_dir = os.path.join( 66 self._out_dir = os.path.join(
83 appfactory.root_dir, 67 appfactory.root_dir,
84 CACHE_DIR, 68 CACHE_DIR,
85 (appfactory.cache_key or 'default'), 69 (appfactory.cache_key or 'default'),
86 'server') 70 'server')
87 71
88 def _run_request(self, environ, start_response): 72 def _run_request(self, environ, start_response):
89 try: 73 try:
90 response = self._try_run_request(environ) 74 response = self._try_run_request(environ)
91 return response(environ, start_response) 75 return response(environ, start_response)
102 if self.static_preview and request.method != 'GET': 86 if self.static_preview and request.method != 'GET':
103 logger.error("Only GET requests are allowed, got %s" % 87 logger.error("Only GET requests are allowed, got %s" %
104 request.method) 88 request.method)
105 raise MethodNotAllowed() 89 raise MethodNotAllowed()
106 90
107 # Also handle requests to a pipeline-built asset right away. 91 # Handle requests to a pipeline-built asset right away.
108 response = self._try_serve_asset(environ, request) 92 response = self._try_serve_asset(environ, request)
93 if response is not None:
94 return response
95
96 # Same for page assets.
97 response = self._try_serve_page_asset(
98 self.appfactory.root_dir, environ, request)
109 if response is not None: 99 if response is not None:
110 return response 100 return response
111 101
112 # Create the app for this request. 102 # Create the app for this request.
113 app = get_app_for_server(self.appfactory, 103 app = get_app_for_server(self.appfactory,
116 self.enable_debug_info and 106 self.enable_debug_info and
117 '!debug' in request.args): 107 '!debug' in request.args):
118 app.config.set('site/show_debug_info', True) 108 app.config.set('site/show_debug_info', True)
119 109
120 # We'll serve page assets directly from where they are. 110 # We'll serve page assets directly from where they are.
121 app.env.base_asset_url_format = self.root_url + '_asset/%path%' 111 app.config.set('site/asset_url_format',
122 112 self.root_url + '_asset/%path%')
123 # Let's see if it can be a page asset. 113
124 response = self._try_serve_page_asset(app, environ, request) 114 # Let's try to serve a page.
125 if response is not None:
126 return response
127
128 # Nope. Let's see if it's an actual page.
129 try: 115 try:
130 response = self._try_serve_page(app, environ, request) 116 response = self._try_serve_page(app, environ, request)
131 return response 117 return response
132 except (RouteNotFoundError, SourceNotFoundError) as ex: 118 except (RouteNotFoundError, SourceNotFoundError) as ex:
133 raise NotFound() from ex 119 raise NotFound() from ex
150 full_path = os.path.join(self.root_dir, rel_req_path) 136 full_path = os.path.join(self.root_dir, rel_req_path)
151 else: 137 else:
152 full_path = os.path.join(self._out_dir, rel_req_path) 138 full_path = os.path.join(self._out_dir, rel_req_path)
153 139
154 try: 140 try:
155 response = make_wrapped_file_response(environ, request, full_path) 141 return make_wrapped_file_response(environ, request, full_path)
156 return response
157 except OSError: 142 except OSError:
158 pass 143 return None
159 return None 144
160 145 def _try_serve_page_asset(self, app_root_dir, environ, request):
161 def _try_serve_page_asset(self, app, environ, request):
162 if not request.path.startswith(self.root_url + '_asset/'): 146 if not request.path.startswith(self.root_url + '_asset/'):
163 return None 147 return None
164 148
165 offset = len(self.root_url + '_asset/') 149 offset = len(self.root_url + '_asset/')
166 full_path = os.path.join(app.root_dir, request.path[offset:]) 150 full_path = os.path.join(app_root_dir, request.path[offset:])
167 if not os.path.isfile(full_path): 151
152 try:
153 return make_wrapped_file_response(environ, request, full_path)
154 except OSError:
168 return None 155 return None
169
170 return make_wrapped_file_response(environ, request, full_path)
171 156
172 def _try_serve_page(self, app, environ, request): 157 def _try_serve_page(self, app, environ, request):
173 # Find a matching page. 158 # Find a matching page.
174 req_page = get_requested_page(app, request.path) 159 req_page = get_requested_page(app, request.path)
175 160
179 if qp is None: 164 if qp is None:
180 msg = "Can't find path for '%s':" % request.path 165 msg = "Can't find path for '%s':" % request.path
181 raise MultipleNotFound(msg, req_page.not_found_errors) 166 raise MultipleNotFound(msg, req_page.not_found_errors)
182 167
183 # We have a page, let's try to render it. 168 # We have a page, let's try to render it.
184 render_ctx = PageRenderingContext(qp, 169 render_ctx = RenderingContext(qp, force_render=True)
185 page_num=req_page.page_num, 170 qp.page.source.prepareRenderContext(render_ctx)
186 force_render=True,
187 is_from_request=True)
188 if qp.route.is_generator_route:
189 qp.route.generator.prepareRenderContext(render_ctx)
190
191 # See if this page is known to use sources. If that's the case,
192 # just don't use cached rendered segments for that page (but still
193 # use them for pages that are included in it).
194 uri = qp.getUri()
195 entry = self._page_record.getEntry(uri, req_page.page_num)
196 if (qp.route.is_generator_route or entry is None or
197 entry.used_source_names):
198 cache_key = '%s:%s' % (uri, req_page.page_num)
199 app.env.rendered_segments_repository.invalidate(cache_key)
200 171
201 # Render the page. 172 # Render the page.
202 rendered_page = render_page(render_ctx) 173 rendered_page = render_page(render_ctx)
203
204 # Remember stuff for next time.
205 if entry is None:
206 entry = ServeRecordPageEntry(req_page.req_path, req_page.page_num)
207 self._page_record.addEntry(entry)
208 for pinfo in render_ctx.render_passes:
209 entry.used_source_names |= pinfo.used_source_names
210 174
211 # Start doing stuff. 175 # Start doing stuff.
212 page = rendered_page.page 176 page = rendered_page.page
213 rp_content = rendered_page.content 177 rp_content = rendered_page.content
214 178
215 # Profiling. 179 # Profiling.
216 if app.config.get('site/show_debug_info'): 180 if app.config.get('site/show_debug_info'):
217 now_time = time.perf_counter() 181 now_time = time.perf_counter()
218 timing_info = ( 182 timing_info = (
219 '%8.1f ms' % 183 '%8.1f ms' %
220 ((now_time - app.env.start_time) * 1000.0)) 184 ((now_time - app.env.start_time) * 1000.0))
221 rp_content = rp_content.replace( 185 rp_content = rp_content.replace(
222 '__PIECRUST_TIMING_INFORMATION__', timing_info) 186 '__PIECRUST_TIMING_INFORMATION__', timing_info)
223 187
224 # Build the response. 188 # Build the response.
225 response = Response() 189 response = Response()
226 190
227 etag = hashlib.md5(rp_content.encode('utf8')).hexdigest() 191 etag = hashlib.md5(rp_content.encode('utf8')).hexdigest()
309 273
310 def get_source(self, env, template): 274 def get_source(self, env, template):
311 template += '.html' 275 template += '.html'
312 return super(ErrorMessageLoader, self).get_source(env, template) 276 return super(ErrorMessageLoader, self).get_source(env, template)
313 277
314