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