changeset 621:8f9c0bdb3724

publish: Polish/refactor the publishing workflows. * Add a `--preview` option. * The `--list` option gives a nicer output, and prints warnings/errors for incorrect configuration. * Moved most of the `shell` code into a base class that's reusable. * Simplified the code to log publishing to a file. * Nicer output overall, with times.
author Ludovic Chabant <ludovic@chabant.com>
date Mon, 08 Feb 2016 20:44:26 -0800
parents c2708f20a87b
children 5d8e0c8cdb5f
files piecrust/commands/builtin/publishing.py piecrust/publishing/base.py piecrust/publishing/publisher.py piecrust/publishing/shell.py
diffstat 4 files changed, 214 insertions(+), 90 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/commands/builtin/publishing.py	Sat Feb 06 21:49:50 2016 -0800
+++ b/piecrust/commands/builtin/publishing.py	Mon Feb 08 20:44:26 2016 -0800
@@ -1,6 +1,7 @@
 import logging
+import urllib.parse
 from piecrust.commands.base import ChefCommand
-from piecrust.publishing.publisher import Publisher
+from piecrust.publishing.publisher import Publisher, find_publisher_name
 
 
 logger = logging.getLogger(__name__)
@@ -24,6 +25,10 @@
                 metavar='LOG_FILE',
                 help="Log the publisher's output to a given file.")
         parser.add_argument(
+                '--preview',
+                action='store_true',
+                help="Only preview what the publisher would do.")
+        parser.add_argument(
                 'target',
                 nargs='?',
                 default='default',
@@ -37,13 +42,37 @@
                 return
 
             for name, cfg in pub_cfg.items():
-                desc = cfg.get('description')
-                if not desc:
-                    logger.info(name)
+                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.info("%s: %s" % (name, desc))
+                    logger.error(
+                            "%s (incorrect configuration)" % name)
             return
 
         pub = Publisher(ctx.app)
-        pub.run(ctx.args.target, log_file=ctx.args.log_publisher)
+        pub.run(
+                ctx.args.target,
+                preview=ctx.args.preview,
+                log_file=ctx.args.log_publisher)
 
--- a/piecrust/publishing/base.py	Sat Feb 06 21:49:50 2016 -0800
+++ b/piecrust/publishing/base.py	Mon Feb 08 20:44:26 2016 -0800
@@ -1,23 +1,103 @@
+import os.path
+import shlex
+import logging
+import threading
+import subprocess
+
+
+logger = logging.getLogger(__name__)
 
 
 class PublishingContext(object):
     def __init__(self):
-        self.custom_logging_file = None
+        self.bake_out_dir = None
+        self.preview = False
 
 
 class Publisher(object):
     def __init__(self, app, target):
         self.app = app
         self.target = target
-        self.is_using_custom_logging = False
+        self.parsed_url = None
         self.log_file_path = None
 
+    @property
+    def has_url_config(self):
+        return self.parsed_url is not None
+
+    @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):
+        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))
 
     def run(self, ctx):
         raise NotImplementedError()
 
+
+class ShellCommandPublisherBase(Publisher):
+    def __init__(self, app, target):
+        super(ShellCommandPublisherBase, self).__init__(app, target)
+        self.expand_user_args = True
+
+    def run(self, ctx):
+        args = self._getCommandArgs(ctx)
+        if self.expand_user_args:
+            args = [os.path.expanduser(i) for i in args]
+
+        if ctx.preview:
+            preview_args = ' '.join([shlex.quote(i) for i in args])
+            logger.info(
+                    "Would run shell command: %s" % preview_args)
+            return True
+
+        logger.debug(
+                "Running shell command: %s" % args)
+
+        proc = subprocess.Popen(
+                args, cwd=self.app.root_dir, bufsize=0,
+                stdout=subprocess.PIPE)
+
+        logger.debug("Running publishing monitor for PID %d" % proc.pid)
+        thread = _PublishThread(proc)
+        thread.start()
+        proc.wait()
+        thread.join()
+
+        if proc.returncode != 0:
+            logger.error(
+                    "Publish process returned code %d" % proc.returncode)
+        else:
+            logger.debug("Publish process returned successfully.")
+
+        return proc.returncode == 0
+
+    def _getCommandArgs(self, ctx):
+        raise NotImplementedError()
+
+
+class _PublishThread(threading.Thread):
+    def __init__(self, proc):
+        super(_PublishThread, self).__init__(
+                name='publish_monitor', daemon=True)
+        self.proc = proc
+        self.root_logger = logging.getLogger()
+
+    def run(self):
+        for line in iter(self.proc.stdout.readline, b''):
+            line_str = line.decode('utf8')
+            logger.info(line_str.rstrip('\r\n'))
+            for h in self.root_logger.handlers:
+                h.flush()
+
+        self.proc.communicate()
+        logger.debug("Publish monitor exiting.")
+
--- a/piecrust/publishing/publisher.py	Sat Feb 06 21:49:50 2016 -0800
+++ b/piecrust/publishing/publisher.py	Mon Feb 08 20:44:26 2016 -0800
@@ -1,4 +1,8 @@
+import os.path
+import time
 import logging
+import urllib.parse
+from piecrust.chefutil import format_timed
 from piecrust.publishing.base import PublishingContext
 
 
@@ -17,17 +21,72 @@
     def __init__(self, app):
         self.app = app
 
-    def run(self, target, log_file=None):
+    def run(self, target, preview=False, log_file=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:
             raise InvalidPublishTargetError(
                     "No such publish target: %s" % target)
 
-        target_type = target_cfg.get('type')
-        if not target_type:
-            raise InvalidPublishTargetError(
-                    "Publish target '%s' doesn't specify a type." % target)
+        target_type = None
+        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)
 
+        # Setup logging stuff.
+        hdlr = None
+        root_logger = logging.getLogger()
+        if log_file and not preview:
+            logger.debug("Adding file handler for: %s" % log_file)
+            hdlr = logging.FileHandler(log_file, mode='w', encoding='utf8')
+            root_logger.addHandler(hdlr)
+        if not preview:
+            logger.info("Deploying to %s" % target)
+        else:
+            logger.info("Previewing deployment to %s" % target)
+
+        # Bake first is necessary.
+        bake_out_dir = None
+        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)
+                rec1 = baker.bake()
+
+                from piecrust.processing.pipeline import ProcessorPipeline
+                proc = ProcessorPipeline(self.app, bake_out_dir)
+                rec2 = proc.run()
+
+                if not rec1.success or not rec2.success:
+                    raise Exception(
+                            "Error during baking, aborting publishing.")
+                logger.info(format_timed(bake_start_time, "Baked website."))
+            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:
@@ -37,39 +96,47 @@
             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" %
+                (target, pub.PUBLISHER_NAME))
+        pub_start_time = time.perf_counter()
 
         ctx = PublishingContext()
-
-        hdlr = None
-        if log_file:
-            if not pub.is_using_custom_logging:
-                logger.debug("Adding file handler for: %s" % log_file)
-                hdlr = logging.FileHandler(log_file, mode='w', encoding='utf8')
-                logger.addHandler(hdlr)
-            else:
-                logger.debug("Creating custom log file: %s" % log_file)
-                ctx.custom_logging_file = open(
-                        log_file, mode='w', encoding='utf8')
-
-        intro_msg = ("Running publish target '%s' with publisher: %s" %
-                     (target, pub.PUBLISHER_NAME))
-        logger.debug(intro_msg)
-        if ctx.custom_logging_file:
-            ctx.custom_logging_file.write(intro_msg + "\n")
-
+        ctx.bake_out_dir = bake_out_dir
+        ctx.preview = preview
         try:
             success = pub.run(ctx)
         except Exception as ex:
             raise PublishingError(
                     "Error publishing to target: %s" % target) from ex
         finally:
-            if ctx.custom_logging_file:
-                ctx.custom_logging_file.close()
             if hdlr:
-                logger.removeHandler(hdlr)
+                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):
+    for pub_cls in app.plugin_loader.getPublishers():
+        pub_sch = getattr(pub_cls, 'PUBLISHER_SCHEME', None)
+        if ('bake+%s' % pub_sch) == scheme:
+            return pub_cls
+    return None
+
+
+def find_publisher_name(app, scheme):
+    pub_cls = find_publisher_class(app, scheme)
+    if pub_cls:
+        return pub_cls.PUBLISHER_NAME
+    return None
+
--- a/piecrust/publishing/shell.py	Sat Feb 06 21:49:50 2016 -0800
+++ b/piecrust/publishing/shell.py	Mon Feb 08 20:44:26 2016 -0800
@@ -1,67 +1,15 @@
-import sys
 import shlex
-import logging
-import threading
-import subprocess
-from piecrust.publishing.base import Publisher
+from piecrust.publishing.base import ShellCommandPublisherBase
 
 
-logger = logging.getLogger(__name__)
-
-
-class ShellCommandPublisher(Publisher):
+class ShellCommandPublisher(ShellCommandPublisherBase):
     PUBLISHER_NAME = 'shell'
 
-    def __init__(self, app, target):
-        super(ShellCommandPublisher, self).__init__(app, target)
-        self.is_using_custom_logging = True
-
-    def run(self, ctx):
+    def _getCommandArgs(self, ctx):
         target_cmd = self.getConfigValue('cmd')
         if not target_cmd:
             raise Exception("No command specified for publish target: %s" %
                             self.target)
         args = shlex.split(target_cmd)
-
-        logger.debug(
-                "Running shell command: %s" % args)
-
-        proc = subprocess.Popen(
-                args, cwd=self.app.root_dir, bufsize=0,
-                stdout=subprocess.PIPE,
-                universal_newlines=False)
-
-        logger.debug("Running publishing monitor for PID %d" % proc.pid)
-        thread = _PublishThread(proc, ctx.custom_logging_file)
-        thread.start()
-        proc.wait()
-        thread.join()
-
-        if proc.returncode != 0:
-            logger.error(
-                    "Publish process returned code %d" % proc.returncode)
-        else:
-            logger.debug("Publish process returned successfully.")
+        return args
 
-        return proc.returncode == 0
-
-
-class _PublishThread(threading.Thread):
-    def __init__(self, proc, log_fp):
-        super(_PublishThread, self).__init__(
-                name='publish_monitor', daemon=True)
-        self.proc = proc
-        self.log_fp = log_fp
-
-    def run(self):
-        for line in iter(self.proc.stdout.readline, b''):
-            line_str = line.decode('utf8')
-            sys.stdout.write(line_str)
-            sys.stdout.flush()
-            if self.log_fp:
-                self.log_fp.write(line_str)
-                self.log_fp.flush()
-
-        self.proc.communicate()
-        logger.debug("Publish monitor exiting.")
-