comparison piecrust/publishing/base.py @ 885:13e8b50a2113

publish: Fix publishers API and add a simple "copy" publisher.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 20 Jun 2017 21:12:35 -0700
parents fd694f1297c7
children d709429f02eb
comparison
equal deleted inserted replaced
884:18b3e2acd069 885:13e8b50a2113
1 import os.path 1 import os.path
2 import shlex 2 import time
3 import urllib.parse
4 import logging 3 import logging
5 import threading 4 from piecrust.chefutil import format_timed
6 import subprocess
7 from piecrust.configuration import try_get_dict_value
8 5
9 6
10 logger = logging.getLogger(__name__) 7 logger = logging.getLogger(__name__)
11 8
12 9
16 13
17 class PublisherConfigurationError(Exception): 14 class PublisherConfigurationError(Exception):
18 pass 15 pass
19 16
20 17
21 class PublishingContext(object): 18 class PublishingContext:
22 def __init__(self): 19 def __init__(self):
23 self.bake_out_dir = None 20 self.bake_out_dir = None
24 self.bake_record = None 21 self.bake_records = None
25 self.processing_record = None 22 self.processing_record = None
26 self.was_baked = False 23 self.was_baked = False
27 self.preview = False 24 self.preview = False
28 self.args = None 25 self.args = None
29 26
30 27
31 class Publisher(object): 28 class Publisher:
32 PUBLISHER_NAME = 'undefined' 29 PUBLISHER_NAME = 'undefined'
33 PUBLISHER_SCHEME = None 30 PUBLISHER_SCHEME = None
34 31
35 def __init__(self, app, target, config): 32 def __init__(self, app, target, config):
36 self.app = app 33 self.app = app
37 self.target = target 34 self.target = target
38 self.config = config 35 self.config = config
39 self.has_url_config = isinstance(config, urllib.parse.ParseResult)
40 self.log_file_path = None 36 self.log_file_path = None
41 37
42 def setupPublishParser(self, parser, app): 38 def setupPublishParser(self, parser, app):
43 return 39 return
44 40
45 def getConfigValue(self, name, default_value=None): 41 def parseUrlTarget(self, url):
46 if self.has_url_config: 42 raise NotImplementedError()
47 raise Exception("This publisher only has a URL configuration.")
48 return try_get_dict_value(self.config, name, default=default_value)
49 43
50 def run(self, ctx): 44 def run(self, ctx):
51 raise NotImplementedError() 45 raise NotImplementedError()
52 46
53 def getBakedFiles(self, ctx): 47 def getBakedFiles(self, ctx):
54 for e in ctx.bake_record.entries: 48 for rec in ctx.bake_records.records:
55 for sub in e.subs: 49 for e in rec.getEntries():
56 if sub.was_baked: 50 paths = e.getAllOutputPaths()
57 yield sub.out_path 51 if paths is not None:
58 for e in ctx.processing_record.entries: 52 yield from paths
59 if e.was_processed:
60 yield from [os.path.join(ctx.processing_record.out_dir, p)
61 for p in e.rel_outputs]
62 53
63 def getDeletedFiles(self, ctx): 54 def getDeletedFiles(self, ctx):
64 yield from ctx.bake_record.deleted 55 for rec in ctx.bake_records.records:
65 yield from ctx.processing_record.deleted 56 yield from rec.deleted_out_paths
66 57
67 58
68 class ShellCommandPublisherBase(Publisher): 59 class InvalidPublishTargetError(Exception):
69 def __init__(self, app, target, config): 60 pass
70 super(ShellCommandPublisherBase, self).__init__(app, target, config)
71 self.expand_user_args = True
72
73 def run(self, ctx):
74 args = self._getCommandArgs(ctx)
75 if self.expand_user_args:
76 args = [os.path.expanduser(i) for i in args]
77
78 if ctx.preview:
79 preview_args = ' '.join([shlex.quote(i) for i in args])
80 logger.info(
81 "Would run shell command: %s" % preview_args)
82 return True
83
84 logger.debug(
85 "Running shell command: %s" % args)
86
87 proc = subprocess.Popen(
88 args, cwd=self.app.root_dir, bufsize=0,
89 stdout=subprocess.PIPE)
90
91 logger.debug("Running publishing monitor for PID %d" % proc.pid)
92 thread = _PublishThread(proc)
93 thread.start()
94 proc.wait()
95 thread.join()
96
97 if proc.returncode != 0:
98 logger.error(
99 "Publish process returned code %d" % proc.returncode)
100 else:
101 logger.debug("Publish process returned successfully.")
102
103 return proc.returncode == 0
104
105 def _getCommandArgs(self, ctx):
106 raise NotImplementedError()
107 61
108 62
109 class _PublishThread(threading.Thread): 63 class PublishingError(Exception):
110 def __init__(self, proc): 64 pass
111 super(_PublishThread, self).__init__(
112 name='publish_monitor', daemon=True)
113 self.proc = proc
114 self.root_logger = logging.getLogger()
115 65
116 def run(self):
117 for line in iter(self.proc.stdout.readline, b''):
118 line_str = line.decode('utf8')
119 logger.info(line_str.rstrip('\r\n'))
120 for h in self.root_logger.handlers:
121 h.flush()
122 66
123 self.proc.communicate() 67 class PublishingManager:
124 logger.debug("Publish monitor exiting.") 68 def __init__(self, appfactory, app):
69 self.appfactory = appfactory
70 self.app = app
125 71
72 def run(self, target,
73 force=False, preview=False, extra_args=None, log_file=None):
74 start_time = time.perf_counter()
75
76 # Get publisher for this target.
77 pub = self.app.getPublisher(target)
78 if pub is None:
79 raise InvalidPublishTargetError(
80 "No such publish target: %s" % target)
81
82 # Will we need to bake first?
83 bake_first = pub.config.get('bake', True)
84
85 # Setup logging stuff.
86 hdlr = None
87 root_logger = logging.getLogger()
88 if log_file and not preview:
89 logger.debug("Adding file handler for: %s" % log_file)
90 hdlr = logging.FileHandler(log_file, mode='w', encoding='utf8')
91 root_logger.addHandler(hdlr)
92 if not preview:
93 logger.info("Deploying to %s" % target)
94 else:
95 logger.info("Previewing deployment to %s" % target)
96
97 # Bake first is necessary.
98 records = None
99 was_baked = False
100 bake_out_dir = os.path.join(self.app.root_dir, '_pub', target)
101 if bake_first:
102 if not preview:
103 bake_start_time = time.perf_counter()
104 logger.debug("Baking first to: %s" % bake_out_dir)
105
106 from piecrust.baking.baker import Baker
107 baker = Baker(
108 self.appfactory, self.app, bake_out_dir, force=force)
109 records = baker.bake()
110 was_baked = True
111
112 if not records.success:
113 raise Exception(
114 "Error during baking, aborting publishing.")
115 logger.info(format_timed(bake_start_time, "Baked website."))
116 else:
117 logger.info("Would bake to: %s" % bake_out_dir)
118
119 # Publish!
120 logger.debug(
121 "Running publish target '%s' with publisher: %s" %
122 (target, pub.PUBLISHER_NAME))
123 pub_start_time = time.perf_counter()
124
125 ctx = PublishingContext()
126 ctx.bake_out_dir = bake_out_dir
127 ctx.bake_records = records
128 ctx.was_baked = was_baked
129 ctx.preview = preview
130 ctx.args = extra_args
131 try:
132 pub.run(ctx)
133 except Exception as ex:
134 raise PublishingError(
135 "Error publishing to target: %s" % target) from ex
136 finally:
137 if hdlr:
138 root_logger.removeHandler(hdlr)
139 hdlr.close()
140
141 logger.info(format_timed(
142 pub_start_time, "Ran publisher %s" % pub.PUBLISHER_NAME))
143
144 logger.info(format_timed(start_time, 'Deployed to %s' % target))
145
146
147 def find_publisher_class(app, name, is_scheme=False):
148 attr_name = 'PUBLISHER_SCHEME' if is_scheme else 'PUBLISHER_NAME'
149 for pub_cls in app.plugin_loader.getPublishers():
150 pub_sch = getattr(pub_cls, attr_name, None)
151 if pub_sch == name:
152 return pub_cls
153 return None
154
155
156 def find_publisher_name(app, scheme):
157 pub_cls = find_publisher_class(app, scheme, True)
158 if pub_cls:
159 return pub_cls.PUBLISHER_NAME
160 return None
161