comparison piecrust/sources/base.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 7f3043f9f26f
children f070a4fc033c
comparison
equal deleted inserted replaced
851:2c7e57d80bba 852:4850f8c21b6e
1 import copy
2 import logging 1 import logging
3 from werkzeug.utils import cached_property 2 import collections
4 from piecrust.page import Page
5 from piecrust.data.assetor import Assetor
6 3
7 4
5 # Source realms, to differentiate sources in the site itself ('User')
6 # and sources in the site's theme ('Theme').
8 REALM_USER = 0 7 REALM_USER = 0
9 REALM_THEME = 1 8 REALM_THEME = 1
10 REALM_NAMES = { 9 REALM_NAMES = {
11 REALM_USER: 'User', 10 REALM_USER: 'User',
12 REALM_THEME: 'Theme'} 11 REALM_THEME: 'Theme'}
13 12
14 13
15 MODE_PARSING = 0 14 # Types of relationships a content source can be asked for.
16 MODE_CREATING = 1 15 REL_ASSETS = 1
17 16
18 17
19 logger = logging.getLogger(__name__) 18 logger = logging.getLogger(__name__)
20
21
22 def build_pages(app, factories):
23 for f in factories:
24 yield f.buildPage()
25 19
26 20
27 class SourceNotFoundError(Exception): 21 class SourceNotFoundError(Exception):
28 pass 22 pass
29 23
30 24
31 class InvalidFileSystemEndpointError(Exception): 25 class InsufficientRouteParameters(Exception):
32 def __init__(self, source_name, fs_endpoint): 26 pass
33 super(InvalidFileSystemEndpointError, self).__init__(
34 "Invalid file-system endpoint for source '%s': %s" %
35 (source_name, fs_endpoint))
36 27
37 28
38 class PageFactory(object): 29 class AbortedSourceUseError(Exception):
39 """ A class responsible for creating a page. 30 pass
31
32
33 class GeneratedContentException(Exception):
34 pass
35
36
37 CONTENT_TYPE_PAGE = 0
38 CONTENT_TYPE_ASSET = 1
39
40
41 class ContentItem:
42 """ Describes a piece of content.
40 """ 43 """
41 def __init__(self, source, rel_path, metadata): 44 def __init__(self, spec, metadata):
42 self.source = source 45 self.spec = spec
43 self.rel_path = rel_path
44 self.metadata = metadata 46 self.metadata = metadata
45 47
46 @cached_property 48 @property
47 def ref_spec(self): 49 def is_group(self):
48 return '%s:%s' % (self.source.name, self.rel_path) 50 return False
49
50 @cached_property
51 def path(self):
52 path, _ = self.source.resolveRef(self.rel_path)
53 return path
54
55 def buildPage(self):
56 repo = self.source.app.env.page_repository
57 cache_key = '%s:%s' % (self.source.name, self.rel_path)
58 return repo.get(cache_key, self._doBuildPage)
59
60 def _doBuildPage(self):
61 logger.debug("Building page: %s" % self.path)
62 page = Page(self.source, copy.deepcopy(self.metadata), self.rel_path)
63 return page
64 51
65 52
66 class PageSource(object): 53 class ContentGroup:
67 """ A source for pages, e.g. a directory with one file per page. 54 """ Describes a group of `ContentItem`s.
55 """
56 def __init__(self, spec, metadata):
57 self.spec = spec
58 self.metadata = metadata
59
60 @property
61 def is_group(self):
62 return True
63
64
65 class ContentSource:
66 """ A source for content.
68 """ 67 """
69 def __init__(self, app, name, config): 68 def __init__(self, app, name, config):
70 self.app = app 69 self.app = app
71 self.name = name 70 self.name = name
72 self.config = config or {} 71 self.config = config or {}
73 self.config.setdefault('realm', REALM_USER)
74 self._factories = None
75 self._provider_type = None
76
77 def __getattr__(self, name):
78 try:
79 return self.config[name]
80 except KeyError:
81 raise AttributeError()
82 72
83 @property 73 @property
84 def is_theme_source(self): 74 def is_theme_source(self):
85 return self.realm == REALM_THEME 75 return self.config['realm'] == REALM_THEME
86 76
87 @property 77 @property
88 def root_dir(self): 78 def root_dir(self):
89 if self.is_theme_source: 79 if self.is_theme_source:
90 return self.app.theme_dir 80 return self.app.theme_dir
91 return self.app.root_dir 81 return self.app.root_dir
92 82
93 def getPages(self): 83 def openItem(self, item, mode='r'):
94 return build_pages(self.app, self.getPageFactories()) 84 raise NotImplementedError()
95 85
96 def getPage(self, metadata): 86 def getItemMtime(self, item):
97 factory = self.findPageFactory(metadata, MODE_PARSING) 87 raise NotImplementedError()
98 if factory is None:
99 return None
100 return factory.buildPage()
101 88
102 def getPageFactories(self): 89 def getAllContents(self):
103 if self._factories is None: 90 stack = collections.deque()
104 self._factories = list(self.buildPageFactories()) 91 stack.append(None)
105 return self._factories 92 while len(stack) > 0:
93 cur = stack.popleft()
94 try:
95 contents = self.getContents(cur)
96 except GeneratedContentException:
97 continue
98 if contents is not None:
99 for c in contents:
100 if c.is_group:
101 stack.append(c)
102 else:
103 yield c
104
105 def getContents(self, group):
106 raise NotImplementedError("'%s' doesn't implement 'getContents'." %
107 self.__class__)
108
109 def getRelatedContents(self, item, relationship):
110 raise NotImplementedError()
111
112 def findContent(self, route_params):
113 raise NotImplementedError()
106 114
107 def getSupportedRouteParameters(self): 115 def getSupportedRouteParameters(self):
108 raise NotImplementedError() 116 raise NotImplementedError()
109 117
110 def buildPageFactories(self): 118 def prepareRenderContext(self, ctx):
111 raise NotImplementedError()
112
113 def buildPageFactory(self, path):
114 raise NotImplementedError()
115
116 def resolveRef(self, ref_path):
117 """ Returns the full path and source metadata given a source
118 (relative) path, like a ref-spec.
119 """
120 raise NotImplementedError()
121
122 def findPageFactory(self, metadata, mode):
123 raise NotImplementedError()
124
125 def buildDataProvider(self, page, override):
126 if not self._provider_type:
127 from piecrust.data.provider import get_data_provider_class
128 self._provider_type = get_data_provider_class(self.app,
129 self.data_type)
130 return self._provider_type(self, page, override)
131
132 def finalizeConfig(self, page):
133 pass 119 pass
134 120
135 def buildAssetor(self, page, uri): 121 def onRouteFunctionUsed(self, route_params):
136 return Assetor(page, uri) 122 pass
137 123
124 def describe(self):
125 return None
126