Mercurial > piecrust2
changeset 758:6abb436fea5b
publish: Make publisher more powerful and better exposed on the command line.
* Make the `chef publish` command have one sub-command per publish target.
* Add custom argument parsing per publisher to have strong extra arguments
available per publish target.
* Make publish targets a first class citizen of the `PieCrust` app class.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sat, 25 Jun 2016 17:03:29 -0700 |
parents | 7147b06670fd |
children | dd03385adb62 |
files | piecrust/app.py piecrust/commands/builtin/publishing.py piecrust/plugins/builtin.py piecrust/publishing/base.py piecrust/publishing/publisher.py piecrust/publishing/rsync.py |
diffstat | 6 files changed, 141 insertions(+), 109 deletions(-) [+] |
line wrap: on
line diff
--- a/piecrust/app.py Sat Jun 25 17:01:08 2016 -0700 +++ b/piecrust/app.py Sat Jun 25 17:03:29 2016 -0700 @@ -2,6 +2,7 @@ import os.path import hashlib import logging +import urllib.parse from werkzeug.utils import cached_property from piecrust import ( RESOURCES_DIR, @@ -10,9 +11,9 @@ CONFIG_PATH, THEME_CONFIG_PATH) from piecrust.appconfig import PieCrustConfiguration from piecrust.cache import ExtensibleCache, NullExtensibleCache -from piecrust.plugins.base import PluginLoader +from piecrust.configuration import ConfigurationError, merge_dicts from piecrust.environment import StandardEnvironment -from piecrust.configuration import ConfigurationError, merge_dicts +from piecrust.plugins.base import PluginLoader from piecrust.routing import Route from piecrust.sources.base import REALM_THEME @@ -165,6 +166,36 @@ gens.append(gen) return gens + @cached_property + def publishers(self): + defs_by_name = {} + defs_by_scheme = {} + for cls in self.plugin_loader.getPublishers(): + defs_by_name[cls.PUBLISHER_NAME] = cls + if cls.PUBLISHER_SCHEME: + defs_by_scheme[cls.PUBLISHER_SCHEME] = cls + + tgts = [] + publish_config = self.config.get('publish') + if publish_config is None: + return tgts + for n, t in publish_config.items(): + pub_type = None + is_scheme = False + if isinstance(t, dict): + pub_type = t.get('type') + elif isinstance(t, str): + comps = urllib.parse.urlparse(t) + pub_type = comps.scheme + is_scheme = True + cls = (defs_by_scheme.get(pub_type) if is_scheme + else defs_by_name.get(pub_type)) + if cls is None: + raise ConfigurationError("No such publisher: %s" % pub_type) + tgt = cls(self, n, t) + tgts.append(tgt) + return tgts + def getSource(self, source_name): for source in self.sources: if source.name == source_name: @@ -195,6 +226,12 @@ return route return None + def getPublisher(self, target_name): + for pub in self.publishers: + if pub.target == target_name: + return pub + return None + def _get_dir(self, default_rel_dir): abs_dir = os.path.join(self.root_dir, default_rel_dir) if os.path.isdir(abs_dir):
--- a/piecrust/commands/builtin/publishing.py Sat Jun 25 17:01:08 2016 -0700 +++ b/piecrust/commands/builtin/publishing.py Sat Jun 25 17:03:29 2016 -0700 @@ -1,6 +1,7 @@ import logging import urllib.parse from piecrust.commands.base import ChefCommand +from piecrust.pathutil import SiteNotFoundError from piecrust.publishing.publisher import Publisher, find_publisher_name @@ -17,10 +18,6 @@ def setupParser(self, parser, app): parser.add_argument( - '-l', '--list', - action='store_true', - help="List available publish targets for the current site.") - parser.add_argument( '--log-publisher', metavar='LOG_FILE', help="Log the publisher's output to a given file.") @@ -28,51 +25,39 @@ '--preview', action='store_true', help="Only preview what the publisher would do.") - parser.add_argument( - 'target', - nargs='?', - default='default', - help="The publish target to use.") - def run(self, ctx): - if ctx.args.list: - pub_cfg = ctx.app.config.get('publish') - if not pub_cfg: - logger.info("No available publish targets.") - return + subparsers = parser.add_subparsers() + for pub in app.publishers: + p = subparsers.add_parser( + pub.target, + help="Publish using target '%s'." % pub.target) + pub.setupPublishParser(p, app) + p.set_defaults(sub_func=self._doPublish) + p.set_defaults(target=pub.target) - for name, cfg in pub_cfg.items(): - if isinstance(cfg, dict): - pub_type = cfg.get('type') - if pub_type: - desc = cfg.get('description') - bake_first = cfg.get('bake', True) - msg = '%s (%s)' % (name, pub_type) - if not bake_first: - msg += ' (no local baking)' - if desc: - msg += ': ' + desc - logger.info(msg) - else: - logger.error( - "%s (unknown type '%s')" % (name, pub_type)) - elif isinstance(cfg, str): - comps = urllib.parse.urlparse(str(cfg)) - pub_name = find_publisher_name(ctx.app, comps.scheme) - if pub_name: - logger.info("%s (%s)" % (name, pub_name)) - else: - logger.error( - "%s (unknown scheme '%s')" % - (name, comps.scheme)) - else: - logger.error( - "%s (incorrect configuration)" % name) + if not app.publishers: + parser.epilog = ( + "No publishers have been defined. You can define publishers " + "through the `publish` configuration settings. " + "For more information see: " + "https://bolt80.com/piecrust/en/latest/docs/publishing/") + + def checkedRun(self, ctx): + if ctx.app.root_dir is None: + raise SiteNotFoundError(theme=ctx.app.theme_site) + + if not hasattr(ctx.args, 'sub_func'): + ctx.parser.parse_args(['publish', '--help']) return + ctx.args.sub_func(ctx) + def _doPublish(self, ctx): pub = Publisher(ctx.app) pub.run( ctx.args.target, preview=ctx.args.preview, - log_file=ctx.args.log_publisher) + extra_args=ctx.args, + log_file=ctx.args.log_publisher, + applied_config_variant=ctx.config_variant, + applied_config_values=ctx.config_values)
--- a/piecrust/plugins/builtin.py Sat Jun 25 17:01:08 2016 -0700 +++ b/piecrust/plugins/builtin.py Sat Jun 25 17:03:29 2016 -0700 @@ -37,6 +37,7 @@ from piecrust.processing.sass import SassProcessor from piecrust.processing.sitemap import SitemapProcessor from piecrust.processing.util import ConcatProcessor +from piecrust.publishing.sftp import SftpPublisher from piecrust.publishing.shell import ShellCommandPublisher from piecrust.publishing.rsync import RsyncPublisher from piecrust.sources.default import DefaultPageSource @@ -133,5 +134,6 @@ def getPublishers(self): return [ ShellCommandPublisher, + SftpPublisher, RsyncPublisher]
--- a/piecrust/publishing/base.py Sat Jun 25 17:01:08 2016 -0700 +++ b/piecrust/publishing/base.py Sat Jun 25 17:03:29 2016 -0700 @@ -1,47 +1,69 @@ import os.path import shlex +import urllib.parse import logging import threading import subprocess +from piecrust.configuration import try_get_dict_value logger = logging.getLogger(__name__) +FILE_MODIFIED = 1 +FILE_DELETED = 2 + + +class PublisherConfigurationError(Exception): + pass + + class PublishingContext(object): def __init__(self): self.bake_out_dir = None + self.bake_record = None + self.processing_record = None + self.was_baked = False self.preview = False + self.args = None class Publisher(object): - def __init__(self, app, target): + PUBLISHER_NAME = 'undefined' + PUBLISHER_SCHEME = None + + def __init__(self, app, target, config): self.app = app self.target = target - self.parsed_url = None + self.config = config + self.has_url_config = isinstance(config, urllib.parse.ParseResult) self.log_file_path = None - @property - def has_url_config(self): - return self.parsed_url is not None + def setupPublishParser(self, parser, app): + return - @property - def url_config(self): - if self.parsed_url is not None: - return self.getConfig() - raise Exception("This publisher has a full configuration.") - - def getConfig(self): - return self.app.config.get('publish/%s' % self.target) - - def getConfigValue(self, name): + def getConfigValue(self, name, default_value=None): if self.has_url_config: raise Exception("This publisher only has a URL configuration.") - return self.app.config.get('publish/%s/%s' % (self.target, name)) + return try_get_dict_value(self.config, name, default_value) def run(self, ctx): raise NotImplementedError() + def getBakedFiles(self, ctx): + for e in ctx.bake_record.entries: + for sub in e.subs: + if sub.was_baked: + yield sub.out_path + for e in ctx.processing_record.entries: + if e.was_processed: + yield from [os.path.join(ctx.processing_record.out_dir, p) + for p in e.rel_outputs] + + def getDeletedFiles(self, ctx): + yield from ctx.bake_record.deleted + yield from ctx.processing_record.deleted + class ShellCommandPublisherBase(Publisher): def __init__(self, app, target):
--- a/piecrust/publishing/publisher.py Sat Jun 25 17:01:08 2016 -0700 +++ b/piecrust/publishing/publisher.py Sat Jun 25 17:03:29 2016 -0700 @@ -21,35 +21,21 @@ def __init__(self, app): self.app = app - def run(self, target, preview=False, log_file=None): + def run(self, target, + force=False, preview=False, extra_args=None, log_file=None, + applied_config_variant=None, applied_config_values=None): start_time = time.perf_counter() - # Get the configuration for this target. - target_cfg = self.app.config.get('publish/%s' % target) - if not target_cfg: + # Get publisher for this target. + pub = self.app.getPublisher(target) + if pub is None: raise InvalidPublishTargetError( "No such publish target: %s" % target) - target_type = None + # Will we need to bake first? bake_first = True - parsed_url = None - if isinstance(target_cfg, dict): - target_type = target_cfg.get('type') - if not target_type: - raise InvalidPublishTargetError( - "Publish target '%s' doesn't specify a type." % target) - bake_first = target_cfg.get('bake', True) - elif isinstance(target_cfg, str): - comps = urllib.parse.urlparse(target_cfg) - if not comps.scheme: - raise InvalidPublishTargetError( - "Publish target '%s' has an invalid target URL." % - target) - parsed_url = comps - target_type = find_publisher_name(self.app, comps.scheme) - if target_type is None: - raise InvalidPublishTargetError( - "No such publish target scheme: %s" % comps.scheme) + if not pub.has_url_config: + bake_first = pub.getConfigValue('bake', True) # Setup logging stuff. hdlr = None @@ -64,21 +50,31 @@ logger.info("Previewing deployment to %s" % target) # Bake first is necessary. - bake_out_dir = None + rec1 = None + rec2 = None + was_baked = False + bake_out_dir = os.path.join(self.app.root_dir, '_pub', target) if bake_first: - bake_out_dir = os.path.join(self.app.cache_dir, 'pub', target) if not preview: bake_start_time = time.perf_counter() logger.debug("Baking first to: %s" % bake_out_dir) from piecrust.baking.baker import Baker - baker = Baker(self.app, bake_out_dir) + baker = Baker( + self.app, bake_out_dir, + applied_config_variant=applied_config_variant, + applied_config_values=applied_config_values) rec1 = baker.bake() from piecrust.processing.pipeline import ProcessorPipeline - proc = ProcessorPipeline(self.app, bake_out_dir) + proc = ProcessorPipeline( + self.app, bake_out_dir, + applied_config_variant=applied_config_variant, + applied_config_values=applied_config_values) rec2 = proc.run() + was_baked = True + if not rec1.success or not rec2.success: raise Exception( "Error during baking, aborting publishing.") @@ -86,18 +82,6 @@ else: logger.info("Would bake to: %s" % bake_out_dir) - # Create the appropriate publisher. - pub = None - for pub_cls in self.app.plugin_loader.getPublishers(): - if pub_cls.PUBLISHER_NAME == target_type: - pub = pub_cls(self.app, target) - break - if pub is None: - raise InvalidPublishTargetError( - "Publish target '%s' has invalid type: %s" % - (target, target_type)) - pub.parsed_url = parsed_url - # Publish! logger.debug( "Running publish target '%s' with publisher: %s" % @@ -106,9 +90,13 @@ ctx = PublishingContext() ctx.bake_out_dir = bake_out_dir + ctx.bake_record = rec1 + ctx.processing_record = rec2 + ctx.was_baked = was_baked ctx.preview = preview + ctx.args = extra_args try: - success = pub.run(ctx) + pub.run(ctx) except Exception as ex: raise PublishingError( "Error publishing to target: %s" % target) from ex @@ -117,25 +105,23 @@ root_logger.removeHandler(hdlr) hdlr.close() - if not success: - raise PublishingError( - "Unknown error publishing to target: %s" % target) logger.info(format_timed( pub_start_time, "Ran publisher %s" % pub.PUBLISHER_NAME)) logger.info(format_timed(start_time, 'Deployed to %s' % target)) -def find_publisher_class(app, scheme): +def find_publisher_class(app, name, is_scheme=False): + attr_name = 'PUBLISHER_SCHEME' if is_scheme else 'PUBLISHER_NAME' for pub_cls in app.plugin_loader.getPublishers(): - pub_sch = getattr(pub_cls, 'PUBLISHER_SCHEME', None) - if ('bake+%s' % pub_sch) == scheme: + pub_sch = getattr(pub_cls, attr_name, None) + if pub_sch == name: return pub_cls return None def find_publisher_name(app, scheme): - pub_cls = find_publisher_class(app, scheme) + pub_cls = find_publisher_class(app, scheme, True) if pub_cls: return pub_cls.PUBLISHER_NAME return None
--- a/piecrust/publishing/rsync.py Sat Jun 25 17:01:08 2016 -0700 +++ b/piecrust/publishing/rsync.py Sat Jun 25 17:03:29 2016 -0700 @@ -8,7 +8,7 @@ def _getCommandArgs(self, ctx): if self.has_url_config: orig = ctx.bake_out_dir - dest = self.parsed_url.netloc + self.parsed_url.path + dest = self.config.netloc + self.config.path else: orig = self.getConfigValue('source', ctx.bake_our_dir) dest = self.getConfigValue('destination')