Mercurial > piecrust2
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 |