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 |
