Mercurial > piecrust2
comparison piecrust/commands/builtin/baking.py @ 853:f070a4fc033c
core: Continue PieCrust3 refactor, simplify pages.
The asset pipeline is still the only function pipeline at this point.
* No more `QualifiedPage`, and several other pieces of code deleted.
* Data providers are simpler and more focused. For instance, the page iterator
doesn't try to support other types of items.
* Route parameters are proper known source metadata to remove the confusion
between the two.
* Make the baker and pipeline more correctly manage records and record
histories.
* Add support for record collapsing and deleting stale outputs in the asset
pipeline.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sun, 21 May 2017 00:06:59 -0700 |
parents | 4850f8c21b6e |
children | 08e02c2a2a1a |
comparison
equal
deleted
inserted
replaced
852:4850f8c21b6e | 853:f070a4fc033c |
---|---|
1 import time | 1 import time |
2 import os.path | 2 import os.path |
3 import logging | 3 import logging |
4 import hashlib | |
5 import fnmatch | 4 import fnmatch |
6 import datetime | 5 import datetime |
7 from colorama import Fore | 6 from colorama import Fore |
8 from piecrust.commands.base import ChefCommand | 7 from piecrust.commands.base import ChefCommand |
9 | 8 |
101 records = baker.bake() | 100 records = baker.bake() |
102 | 101 |
103 return records | 102 return records |
104 | 103 |
105 | 104 |
105 class ShowRecordCommand(ChefCommand): | |
106 def __init__(self): | |
107 super(ShowRecordCommand, self).__init__() | |
108 self.name = 'showrecord' | |
109 self.description = ("Shows the bake record for a given output " | |
110 "directory.") | |
111 | |
112 def setupParser(self, parser, app): | |
113 parser.add_argument( | |
114 '-o', '--output', | |
115 help="The output directory for which to show the bake record " | |
116 "(defaults to `_counter`)", | |
117 nargs='?') | |
118 parser.add_argument( | |
119 '-i', '--in-path', | |
120 help="A pattern that will be used to filter the relative path " | |
121 "of entries to show.") | |
122 parser.add_argument( | |
123 '-t', '--out-path', | |
124 help="A pattern that will be used to filter the output path " | |
125 "of entries to show.") | |
126 parser.add_argument( | |
127 '--fails', | |
128 action='store_true', | |
129 help="Only show record entries for failures.") | |
130 parser.add_argument( | |
131 '--last', | |
132 type=int, | |
133 default=0, | |
134 help="Show the last Nth bake record.") | |
135 parser.add_argument( | |
136 '--html-only', | |
137 action='store_true', | |
138 help="Only show records for pages (not from the asset " | |
139 "pipeline).") | |
140 parser.add_argument( | |
141 '--assets-only', | |
142 action='store_true', | |
143 help="Only show records for assets (not from pages).") | |
144 parser.add_argument( | |
145 '-p', '--pipelines', | |
146 nargs='*', | |
147 help="Only show records for the given pipeline(s).") | |
148 parser.add_argument( | |
149 '--show-stats', | |
150 action='store_true', | |
151 help="Show stats from the record.") | |
152 parser.add_argument( | |
153 '--show-manifest', | |
154 help="Show manifest entries from the record.") | |
155 | |
156 def run(self, ctx): | |
157 from piecrust.baking.baker import get_bake_records_path | |
158 from piecrust.pipelines.records import load_records | |
159 | |
160 out_dir = ctx.args.output or os.path.join(ctx.app.root_dir, '_counter') | |
161 suffix = '' if ctx.args.last == 0 else '.%d' % ctx.args.last | |
162 records_path = get_bake_records_path(ctx.app, out_dir, suffix=suffix) | |
163 records = load_records(records_path) | |
164 if records.invalidated: | |
165 raise Exception( | |
166 "The bake record was saved by a previous version of " | |
167 "PieCrust and can't be shown.") | |
168 | |
169 in_pattern = None | |
170 if ctx.args.in_path: | |
171 in_pattern = '*%s*' % ctx.args.in_path.strip('*') | |
172 | |
173 out_pattern = None | |
174 if ctx.args.out_path: | |
175 out_pattern = '*%s*' % ctx.args.out.strip('*') | |
176 | |
177 pipelines = ctx.args.pipelines | |
178 if not pipelines: | |
179 pipelines = [p.PIPELINE_NAME | |
180 for p in ctx.app.plugin_loader.getPipelines()] | |
181 if ctx.args.assets_only: | |
182 pipelines = ['asset'] | |
183 if ctx.args.html_only: | |
184 pipelines = ['page'] | |
185 | |
186 logger.info("Bake record for: %s" % out_dir) | |
187 logger.info("Status: %s" % ('SUCCESS' if records.success | |
188 else 'FAILURE')) | |
189 logger.info("Date/time: %s" % | |
190 datetime.datetime.fromtimestamp(records.bake_time)) | |
191 logger.info("Incremental count: %d" % records.incremental_count) | |
192 logger.info("Versions: %s/%s" % (records._app_version, | |
193 records._record_version)) | |
194 logger.info("") | |
195 | |
196 for rec in records.records: | |
197 if ctx.args.fails and rec.success: | |
198 continue | |
199 | |
200 logger.info("Record: %s" % rec.name) | |
201 logger.info("Status: %s" % ('SUCCESS' if rec.success | |
202 else 'FAILURE')) | |
203 for e in rec.entries: | |
204 if ctx.args.fails and e.success: | |
205 continue | |
206 if in_pattern and not fnmatch.fnmatch(e.item_spec, in_pattern): | |
207 continue | |
208 if out_pattern and not any( | |
209 [fnmatch.fnmatch(op, out_pattern) | |
210 for op in e.out_paths]): | |
211 continue | |
212 _print_record_entry(e) | |
213 | |
214 logger.info("") | |
215 | |
216 stats = records.stats | |
217 if ctx.args.show_stats: | |
218 _show_stats(stats) | |
219 | |
220 if ctx.args.show_manifest: | |
221 for name in sorted(stats.manifests.keys()): | |
222 if ctx.args.show_manifest.lower() in name.lower(): | |
223 val = stats.manifests[name] | |
224 logger.info( | |
225 " [%s%s%s] [%d entries]" % | |
226 (Fore.CYAN, name, Fore.RESET, len(val))) | |
227 for v in val: | |
228 logger.info(" - %s" % v) | |
229 | |
230 | |
106 def _show_stats(stats, *, full=False): | 231 def _show_stats(stats, *, full=False): |
107 indent = ' ' | 232 indent = ' ' |
108 | 233 |
109 logger.info(' Timers:') | 234 logger.info(' Timers:') |
110 for name, val in sorted(stats.timers.items(), key=lambda i: i[1], | 235 for name, val in sorted(stats.timers.items(), key=lambda i: i[1], |
130 if full: | 255 if full: |
131 for v in val: | 256 for v in val: |
132 logger.info("%s - %s" % (indent, v)) | 257 logger.info("%s - %s" % (indent, v)) |
133 | 258 |
134 | 259 |
135 class ShowRecordCommand(ChefCommand): | 260 def _print_record_entry(e): |
136 def __init__(self): | 261 logger.info(" - %s" % e.item_spec) |
137 super(ShowRecordCommand, self).__init__() | 262 logger.info(" Outputs:") |
138 self.name = 'showrecord' | 263 if e.out_paths: |
139 self.description = ("Shows the bake record for a given output " | 264 for op in e.out_paths: |
140 "directory.") | 265 logger.info(" - %s" % op) |
141 | 266 else: |
142 def setupParser(self, parser, app): | 267 logger.info(" <none>") |
143 parser.add_argument( | 268 |
144 '-o', '--output', | 269 e_desc = e.describe() |
145 help="The output directory for which to show the bake record " | 270 for k in sorted(e_desc.keys()): |
146 "(defaults to `_counter`)", | 271 logger.info(" %s: %s" % (k, e_desc[k])) |
147 nargs='?') | 272 |
148 parser.add_argument( | 273 if e.errors: |
149 '-p', '--path', | 274 logger.error(" Errors:") |
150 help="A pattern that will be used to filter the relative path " | 275 for err in e.errors: |
151 "of entries to show.") | 276 logger.error(" - %s" % err) |
152 parser.add_argument( | |
153 '-t', '--out', | |
154 help="A pattern that will be used to filter the output path " | |
155 "of entries to show.") | |
156 parser.add_argument( | |
157 '--last', | |
158 type=int, | |
159 default=0, | |
160 help="Show the last Nth bake record.") | |
161 parser.add_argument( | |
162 '--html-only', | |
163 action='store_true', | |
164 help="Only show records for pages (not from the asset " | |
165 "pipeline).") | |
166 parser.add_argument( | |
167 '--assets-only', | |
168 action='store_true', | |
169 help="Only show records for assets (not from pages).") | |
170 parser.add_argument( | |
171 '--show-stats', | |
172 action='store_true', | |
173 help="Show stats from the record.") | |
174 parser.add_argument( | |
175 '--show-manifest', | |
176 help="Show manifest entries from the record.") | |
177 | |
178 def run(self, ctx): | |
179 from piecrust.processing.records import ( | |
180 FLAG_PREPARED, FLAG_PROCESSED, FLAG_BYPASSED_STRUCTURED_PROCESSING, | |
181 FLAG_COLLAPSED_FROM_LAST_RUN) | |
182 from piecrust.rendering import ( | |
183 PASS_FORMATTING, PASS_RENDERING) | |
184 | |
185 out_dir = ctx.args.output or os.path.join(ctx.app.root_dir, '_counter') | |
186 record_id = hashlib.md5(out_dir.encode('utf8')).hexdigest() | |
187 suffix = '' if ctx.args.last == 0 else '.%d' % ctx.args.last | |
188 record_name = '%s%s.record' % (record_id, suffix) | |
189 | |
190 pattern = None | |
191 if ctx.args.path: | |
192 pattern = '*%s*' % ctx.args.path.strip('*') | |
193 | |
194 out_pattern = None | |
195 if ctx.args.out: | |
196 out_pattern = '*%s*' % ctx.args.out.strip('*') | |
197 | |
198 if not ctx.args.show_stats and not ctx.args.show_manifest: | |
199 if not ctx.args.assets_only: | |
200 self._showBakeRecord( | |
201 ctx, record_name, pattern, out_pattern) | |
202 if not ctx.args.html_only: | |
203 self._showProcessingRecord( | |
204 ctx, record_name, pattern, out_pattern) | |
205 return | |
206 | |
207 stats = {} | |
208 bake_rec = self._getBakeRecord(ctx, record_name) | |
209 if bake_rec: | |
210 _merge_stats(bake_rec.stats, stats) | |
211 proc_rec = self._getProcessingRecord(ctx, record_name) | |
212 if proc_rec: | |
213 _merge_stats(proc_rec.stats, stats) | |
214 | |
215 if ctx.args.show_stats: | |
216 _show_stats(stats, full=False) | |
217 | |
218 if ctx.args.show_manifest: | |
219 for name in sorted(stats.keys()): | |
220 logger.info('%s:' % name) | |
221 s = stats[name] | |
222 for name in sorted(s.manifests.keys()): | |
223 if ctx.args.show_manifest.lower() in name.lower(): | |
224 val = s.manifests[name] | |
225 logger.info( | |
226 " [%s%s%s] [%d entries]" % | |
227 (Fore.CYAN, name, Fore.RESET, len(val))) | |
228 for v in val: | |
229 logger.info(" - %s" % v) | |
230 | |
231 def _getBakeRecord(self, ctx, record_name): | |
232 record_cache = ctx.app.cache.getCache('baker') | |
233 if not record_cache.has(record_name): | |
234 logger.warning( | |
235 "No page bake record has been created for this output " | |
236 "path.") | |
237 return None | |
238 | |
239 record = BakeRecord.load(record_cache.getCachePath(record_name)) | |
240 return record | |
241 | |
242 def _showBakeRecord(self, ctx, record_name, pattern, out_pattern): | |
243 record = self._getBakeRecord(ctx, record_name) | |
244 if record is None: | |
245 return | |
246 | |
247 logging.info("Bake record for: %s" % record.out_dir) | |
248 logging.info("From: %s" % record_name) | |
249 logging.info("Last baked: %s" % | |
250 datetime.datetime.fromtimestamp(record.bake_time)) | |
251 if record.success: | |
252 logging.info("Status: success") | |
253 else: | |
254 logging.error("Status: failed") | |
255 logging.info("Entries:") | |
256 for entry in record.entries: | |
257 if pattern and not fnmatch.fnmatch(entry.path, pattern): | |
258 continue | |
259 if out_pattern and not ( | |
260 any([o for o in entry.all_out_paths | |
261 if fnmatch.fnmatch(o, out_pattern)])): | |
262 continue | |
263 | |
264 flags = _get_flag_descriptions( | |
265 entry.flags, | |
266 { | |
267 BakeRecordEntry.FLAG_NEW: 'new', | |
268 BakeRecordEntry.FLAG_SOURCE_MODIFIED: 'modified', | |
269 BakeRecordEntry.FLAG_OVERRIDEN: 'overriden'}) | |
270 | |
271 logging.info(" - ") | |
272 | |
273 rel_path = os.path.relpath(entry.path, ctx.app.root_dir) | |
274 logging.info(" path: %s" % rel_path) | |
275 logging.info(" source: %s" % entry.source_name) | |
276 if entry.extra_key: | |
277 logging.info(" extra key: %s" % entry.extra_key) | |
278 logging.info(" flags: %s" % _join(flags)) | |
279 logging.info(" config: %s" % entry.config) | |
280 | |
281 if entry.errors: | |
282 logging.error(" errors: %s" % entry.errors) | |
283 | |
284 logging.info(" %d sub-pages:" % len(entry.subs)) | |
285 for sub in entry.subs: | |
286 sub_flags = _get_flag_descriptions( | |
287 sub.flags, | |
288 { | |
289 SubPageBakeInfo.FLAG_BAKED: 'baked', | |
290 SubPageBakeInfo.FLAG_FORCED_BY_SOURCE: | |
291 'forced by source', | |
292 SubPageBakeInfo.FLAG_FORCED_BY_NO_PREVIOUS: | |
293 'forced by missing previous record entry', | |
294 SubPageBakeInfo.FLAG_FORCED_BY_PREVIOUS_ERRORS: | |
295 'forced by previous errors', | |
296 SubPageBakeInfo.FLAG_FORMATTING_INVALIDATED: | |
297 'formatting invalidated'}) | |
298 | |
299 logging.info(" - ") | |
300 logging.info(" URL: %s" % sub.out_uri) | |
301 logging.info(" path: %s" % os.path.relpath( | |
302 sub.out_path, record.out_dir)) | |
303 logging.info(" flags: %s" % _join(sub_flags)) | |
304 | |
305 pass_names = { | |
306 PASS_FORMATTING: 'formatting pass', | |
307 PASS_RENDERING: 'rendering pass'} | |
308 for p, ri in enumerate(sub.render_info): | |
309 logging.info(" - %s" % pass_names[p]) | |
310 if not ri: | |
311 logging.info(" no info") | |
312 continue | |
313 | |
314 logging.info(" used sources: %s" % | |
315 _join(ri.used_source_names)) | |
316 pgn_info = 'no' | |
317 if ri.used_pagination: | |
318 pgn_info = 'yes' | |
319 if ri.pagination_has_more: | |
320 pgn_info += ', has more' | |
321 logging.info(" used pagination: %s", pgn_info) | |
322 logging.info(" used assets: %s", | |
323 'yes' if ri.used_assets else 'no') | |
324 logging.info(" other info:") | |
325 for k, v in ri._custom_info.items(): | |
326 logging.info(" - %s: %s" % (k, v)) | |
327 | |
328 if sub.errors: | |
329 logging.error(" errors: %s" % sub.errors) | |
330 | |
331 def _getProcessingRecord(self, ctx, record_name): | |
332 record_cache = ctx.app.cache.getCache('proc') | |
333 if not record_cache.has(record_name): | |
334 logger.warning( | |
335 "No asset processing record has been created for this " | |
336 "output path.") | |
337 return None | |
338 | |
339 record = ProcessorPipelineRecord.load( | |
340 record_cache.getCachePath(record_name)) | |
341 return record | |
342 | |
343 def _showProcessingRecord(self, ctx, record_name, pattern, out_pattern): | |
344 record = self._getProcessingRecord(ctx, record_name) | |
345 if record is None: | |
346 return | |
347 | |
348 logging.info("") | |
349 logging.info("Processing record for: %s" % record.out_dir) | |
350 logging.info("Last baked: %s" % | |
351 datetime.datetime.fromtimestamp(record.process_time)) | |
352 if record.success: | |
353 logging.info("Status: success") | |
354 else: | |
355 logging.error("Status: failed") | |
356 logging.info("Entries:") | |
357 for entry in record.entries: | |
358 rel_path = os.path.relpath(entry.path, ctx.app.root_dir) | |
359 if pattern and not fnmatch.fnmatch(rel_path, pattern): | |
360 continue | |
361 if out_pattern and not ( | |
362 any([o for o in entry.rel_outputs | |
363 if fnmatch.fnmatch(o, out_pattern)])): | |
364 continue | |
365 | |
366 flags = _get_flag_descriptions( | |
367 entry.flags, | |
368 { | |
369 FLAG_PREPARED: 'prepared', | |
370 FLAG_PROCESSED: 'processed', | |
371 FLAG_BYPASSED_STRUCTURED_PROCESSING: 'external', | |
372 FLAG_COLLAPSED_FROM_LAST_RUN: 'from last run'}) | |
373 | |
374 logger.info(" - ") | |
375 logger.info(" path: %s" % rel_path) | |
376 logger.info(" out paths: %s" % entry.rel_outputs) | |
377 logger.info(" flags: %s" % _join(flags)) | |
378 logger.info(" proc tree: %s" % _format_proc_tree( | |
379 entry.proc_tree, 14*' ')) | |
380 | |
381 if entry.errors: | |
382 logger.error(" errors: %s" % entry.errors) | |
383 | |
384 | |
385 def _join(items, sep=', ', text_if_none='none'): | |
386 if items: | |
387 return sep.join(items) | |
388 return text_if_none | |
389 | |
390 | |
391 def _get_flag_descriptions(flags, descriptions): | |
392 res = [] | |
393 for k, v in descriptions.items(): | |
394 if flags & k: | |
395 res.append(v) | |
396 return res | |
397 | |
398 | |
399 def _format_proc_tree(tree, margin='', level=0): | |
400 name, children = tree | |
401 res = '%s%s+ %s\n' % (margin if level > 0 else '', level * ' ', name) | |
402 if children: | |
403 for c in children: | |
404 res += _format_proc_tree(c, margin, level + 1) | |
405 return res | |
406 |