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