comparison sat_frontends/jp/cmd_blog.py @ 2624:56f94936df1e

code style reformatting using black
author Goffi <goffi@goffi.org>
date Wed, 27 Jun 2018 20:14:46 +0200
parents 0b6adc2672d9
children 266f871fcb4b
comparison
equal deleted inserted replaced
2623:49533de4540b 2624:56f94936df1e
38 import codecs 38 import codecs
39 from sat.tools.common import data_format 39 from sat.tools.common import data_format
40 40
41 __commands__ = ["Blog"] 41 __commands__ = ["Blog"]
42 42
43 SYNTAX_XHTML = u'xhtml' 43 SYNTAX_XHTML = u"xhtml"
44 # extensions to use with known syntaxes 44 # extensions to use with known syntaxes
45 SYNTAX_EXT = { 45 SYNTAX_EXT = {
46 '': 'txt', # used when the syntax is not found 46 "": "txt", # used when the syntax is not found
47 SYNTAX_XHTML: "xhtml", 47 SYNTAX_XHTML: "xhtml",
48 "markdown": "md" 48 "markdown": "md",
49 } 49 }
50 50
51 51
52 CONF_SYNTAX_EXT = u'syntax_ext_dict' 52 CONF_SYNTAX_EXT = u"syntax_ext_dict"
53 BLOG_TMP_DIR = u"blog" 53 BLOG_TMP_DIR = u"blog"
54 # key to remove from metadata tmp file if they exist 54 # key to remove from metadata tmp file if they exist
55 KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'comments_service', 'updated') 55 KEY_TO_REMOVE_METADATA = (
56 56 "id",
57 URL_REDIRECT_PREFIX = 'url_redirect_' 57 "content",
58 "content_xhtml",
59 "comments_node",
60 "comments_service",
61 "updated",
62 )
63
64 URL_REDIRECT_PREFIX = "url_redirect_"
58 INOTIFY_INSTALL = '"pip install inotify"' 65 INOTIFY_INSTALL = '"pip install inotify"'
59 MB_KEYS = (u"id", 66 MB_KEYS = (
60 u"url", 67 u"id",
61 u"atom_id", 68 u"url",
62 u"updated", 69 u"atom_id",
63 u"published", 70 u"updated",
64 u"language", 71 u"published",
65 u"comments", # this key is used for all comments* keys 72 u"language",
66 u"tags", # this key is used for all tag* keys 73 u"comments", # this key is used for all comments* keys
67 u"author", 74 u"tags", # this key is used for all tag* keys
68 u"author_jid", 75 u"author",
69 u"author_email", 76 u"author_jid",
70 u"author_jid_verified", 77 u"author_email",
71 u"content", 78 u"author_jid_verified",
72 u"content_xhtml", 79 u"content",
73 u"title", 80 u"content_xhtml",
74 u"title_xhtml", 81 u"title",
75 ) 82 u"title_xhtml",
76 OUTPUT_OPT_NO_HEADER = u'no-header' 83 )
84 OUTPUT_OPT_NO_HEADER = u"no-header"
77 85
78 86
79 def guessSyntaxFromPath(host, sat_conf, path): 87 def guessSyntaxFromPath(host, sat_conf, path):
80 """Return syntax guessed according to filename extension 88 """Return syntax guessed according to filename extension
81 89
82 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration 90 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
83 @param path(str): path to the content file 91 @param path(str): path to the content file
84 @return(unicode): syntax to use 92 @return(unicode): syntax to use
85 """ 93 """
86 # we first try to guess syntax with extension 94 # we first try to guess syntax with extension
87 ext = os.path.splitext(path)[1][1:] # we get extension without the '.' 95 ext = os.path.splitext(path)[1][1:] # we get extension without the '.'
88 if ext: 96 if ext:
89 for k,v in SYNTAX_EXT.iteritems(): 97 for k, v in SYNTAX_EXT.iteritems():
90 if k and ext == v: 98 if k and ext == v:
91 return k 99 return k
92 100
93 # if not found, we use current syntax 101 # if not found, we use current syntax
94 return host.bridge.getParamA("Syntax", "Composition", "value", host.profile) 102 return host.bridge.getParamA("Syntax", "Composition", "value", host.profile)
96 104
97 class BlogPublishCommon(object): 105 class BlogPublishCommon(object):
98 """handle common option for publising commands (Set and Edit)""" 106 """handle common option for publising commands (Set and Edit)"""
99 107
100 def add_parser_options(self): 108 def add_parser_options(self):
101 self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"title of the item")) 109 self.parser.add_argument(
102 self.parser.add_argument("-t", '--tag', type=base.unicode_decoder, action='append', help=_(u"tag (category) of your item")) 110 "-T", "--title", type=base.unicode_decoder, help=_(u"title of the item")
103 self.parser.add_argument("-C", "--comments", action='store_true', help=_(u"enable comments")) 111 )
104 self.parser.add_argument("-S", '--syntax', type=base.unicode_decoder, help=_(u"syntax to use (default: get profile's default syntax)")) 112 self.parser.add_argument(
113 "-t",
114 "--tag",
115 type=base.unicode_decoder,
116 action="append",
117 help=_(u"tag (category) of your item"),
118 )
119 self.parser.add_argument(
120 "-C", "--comments", action="store_true", help=_(u"enable comments")
121 )
122 self.parser.add_argument(
123 "-S",
124 "--syntax",
125 type=base.unicode_decoder,
126 help=_(u"syntax to use (default: get profile's default syntax)"),
127 )
105 128
106 def setMbDataContent(self, content, mb_data): 129 def setMbDataContent(self, content, mb_data):
107 if self.args.syntax is None: 130 if self.args.syntax is None:
108 # default syntax has been used 131 # default syntax has been used
109 mb_data['content_rich'] = content 132 mb_data["content_rich"] = content
110 elif self.current_syntax == SYNTAX_XHTML: 133 elif self.current_syntax == SYNTAX_XHTML:
111 mb_data['content_xhtml'] = content 134 mb_data["content_xhtml"] = content
112 else: 135 else:
113 mb_data['content_xhtml'] = self.host.bridge.syntaxConvert(content, self.current_syntax, SYNTAX_XHTML, False, self.profile) 136 mb_data["content_xhtml"] = self.host.bridge.syntaxConvert(
137 content, self.current_syntax, SYNTAX_XHTML, False, self.profile
138 )
114 139
115 def setMbDataFromArgs(self, mb_data): 140 def setMbDataFromArgs(self, mb_data):
116 """set microblog metadata according to command line options 141 """set microblog metadata according to command line options
117 142
118 if metadata already exist, it will be overwritten 143 if metadata already exist, it will be overwritten
119 """ 144 """
120 mb_data['allow_comments'] = C.boolConst(self.args.comments) 145 mb_data["allow_comments"] = C.boolConst(self.args.comments)
121 if self.args.tag: 146 if self.args.tag:
122 data_format.iter2dict('tag', self.args.tag, mb_data, check_conflict=False) 147 data_format.iter2dict("tag", self.args.tag, mb_data, check_conflict=False)
123 if self.args.title is not None: 148 if self.args.title is not None:
124 mb_data['title'] = self.args.title 149 mb_data["title"] = self.args.title
125 150
126 151
127 class Set(base.CommandBase, BlogPublishCommon): 152 class Set(base.CommandBase, BlogPublishCommon):
128
129 def __init__(self, host): 153 def __init__(self, host):
130 base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM}, 154 base.CommandBase.__init__(
131 help=_(u'publish a new blog item or update an existing one')) 155 self,
156 host,
157 "set",
158 use_pubsub=True,
159 pubsub_flags={C.SINGLE_ITEM},
160 help=_(u"publish a new blog item or update an existing one"),
161 )
132 BlogPublishCommon.__init__(self) 162 BlogPublishCommon.__init__(self)
133 self.need_loop=True 163 self.need_loop = True
134 164
135 def add_parser_options(self): 165 def add_parser_options(self):
136 BlogPublishCommon.add_parser_options(self) 166 BlogPublishCommon.add_parser_options(self)
137 167
138 def mbSendCb(self): 168 def mbSendCb(self):
141 171
142 def start(self): 172 def start(self):
143 self.pubsub_item = self.args.item 173 self.pubsub_item = self.args.item
144 mb_data = {} 174 mb_data = {}
145 self.setMbDataFromArgs(mb_data) 175 self.setMbDataFromArgs(mb_data)
146 content = codecs.getreader('utf-8')(sys.stdin).read() 176 content = codecs.getreader("utf-8")(sys.stdin).read()
147 self.setMbDataContent(content, mb_data) 177 self.setMbDataContent(content, mb_data)
148 178
149 self.host.bridge.mbSend( 179 self.host.bridge.mbSend(
150 self.args.service, 180 self.args.service,
151 self.args.node, 181 self.args.node,
152 mb_data, 182 mb_data,
153 self.profile, 183 self.profile,
154 callback=self.exitCb, 184 callback=self.exitCb,
155 errback=partial(self.errback, 185 errback=partial(
156 msg=_(u"can't send item: {}"), 186 self.errback,
157 exit_code=C.EXIT_BRIDGE_ERRBACK)) 187 msg=_(u"can't send item: {}"),
188 exit_code=C.EXIT_BRIDGE_ERRBACK,
189 ),
190 )
158 191
159 192
160 class Get(base.CommandBase): 193 class Get(base.CommandBase):
161 TEMPLATE = u"blog/articles.html" 194 TEMPLATE = u"blog/articles.html"
162 195
163 def __init__(self, host): 196 def __init__(self, host):
164 extra_outputs = {'default': self.default_output, 197 extra_outputs = {"default": self.default_output, "fancy": self.fancy_output}
165 'fancy': self.fancy_output} 198 base.CommandBase.__init__(
166 base.CommandBase.__init__(self, host, 'get', use_verbose=True, use_pubsub=True, pubsub_flags={C.MULTI_ITEMS}, 199 self,
167 use_output=C.OUTPUT_COMPLEX, extra_outputs=extra_outputs, help=_(u'get blog item(s)')) 200 host,
168 self.need_loop=True 201 "get",
202 use_verbose=True,
203 use_pubsub=True,
204 pubsub_flags={C.MULTI_ITEMS},
205 use_output=C.OUTPUT_COMPLEX,
206 extra_outputs=extra_outputs,
207 help=_(u"get blog item(s)"),
208 )
209 self.need_loop = True
169 210
170 def add_parser_options(self): 211 def add_parser_options(self):
171 # TODO: a key(s) argument to select keys to display 212 #  TODO: a key(s) argument to select keys to display
172 self.parser.add_argument("-k", "--key", type=base.unicode_decoder, action='append', dest='keys', 213 self.parser.add_argument(
173 help=_(u"microblog data key(s) to display (default: depend of verbosity)")) 214 "-k",
215 "--key",
216 type=base.unicode_decoder,
217 action="append",
218 dest="keys",
219 help=_(u"microblog data key(s) to display (default: depend of verbosity)"),
220 )
174 # TODO: add MAM filters 221 # TODO: add MAM filters
175 222
176 def template_data_mapping(self, data): 223 def template_data_mapping(self, data):
177 return {u'items': data_objects.BlogItems(data)} 224 return {u"items": data_objects.BlogItems(data)}
178 225
179 def format_comments(self, item, keys): 226 def format_comments(self, item, keys):
180 comments_data = data_format.dict2iterdict(u'comments', item, (u'node', u'service'), pop=True) 227 comments_data = data_format.dict2iterdict(
228 u"comments", item, (u"node", u"service"), pop=True
229 )
181 lines = [] 230 lines = []
182 for data in comments_data: 231 for data in comments_data:
183 lines.append(data[u'comments']) 232 lines.append(data[u"comments"])
184 for k in (u'node', u'service'): 233 for k in (u"node", u"service"):
185 if OUTPUT_OPT_NO_HEADER in self.args.output_opts: 234 if OUTPUT_OPT_NO_HEADER in self.args.output_opts:
186 header = u'' 235 header = u""
187 else: 236 else:
188 header = C.A_HEADER + k + u': ' + A.RESET 237 header = C.A_HEADER + k + u": " + A.RESET
189 lines.append(header + data[k]) 238 lines.append(header + data[k])
190 return u'\n'.join(lines) 239 return u"\n".join(lines)
191 240
192 def format_tags(self, item, keys): 241 def format_tags(self, item, keys):
193 tags = data_format.dict2iter('tag', item, pop=True) 242 tags = data_format.dict2iter("tag", item, pop=True)
194 return u', '.join(tags) 243 return u", ".join(tags)
195 244
196 def format_updated(self, item, keys): 245 def format_updated(self, item, keys):
197 return self.format_time(item['updated']) 246 return self.format_time(item["updated"])
198 247
199 def format_published(self, item, keys): 248 def format_published(self, item, keys):
200 return self.format_time(item['published']) 249 return self.format_time(item["published"])
201 250
202 def format_url(self, item, keys): 251 def format_url(self, item, keys):
203 return uri.buildXMPPUri(u'pubsub', 252 return uri.buildXMPPUri(
204 subtype=u'microblog', 253 u"pubsub",
205 path=self.metadata[u'service'], 254 subtype=u"microblog",
206 node=self.metadata[u'node'], 255 path=self.metadata[u"service"],
207 item=item[u'id']) 256 node=self.metadata[u"node"],
257 item=item[u"id"],
258 )
208 259
209 def get_keys(self): 260 def get_keys(self):
210 """return keys to display according to verbosity or explicit key request""" 261 """return keys to display according to verbosity or explicit key request"""
211 verbosity = self.args.verbose 262 verbosity = self.args.verbose
212 if self.args.keys: 263 if self.args.keys:
213 if not set(MB_KEYS).issuperset(self.args.keys): 264 if not set(MB_KEYS).issuperset(self.args.keys):
214 self.disp(u"following keys are invalid: {invalid}.\n" 265 self.disp(
215 u"Valid keys are: {valid}.".format( 266 u"following keys are invalid: {invalid}.\n"
216 invalid = u', '.join(set(self.args.keys).difference(MB_KEYS)), 267 u"Valid keys are: {valid}.".format(
217 valid = u', '.join(sorted(MB_KEYS))), 268 invalid=u", ".join(set(self.args.keys).difference(MB_KEYS)),
218 error=True) 269 valid=u", ".join(sorted(MB_KEYS)),
270 ),
271 error=True,
272 )
219 self.host.quit(C.EXIT_BAD_ARG) 273 self.host.quit(C.EXIT_BAD_ARG)
220 return self.args.keys 274 return self.args.keys
221 else: 275 else:
222 if verbosity == 0: 276 if verbosity == 0:
223 return (u'title', u'content') 277 return (u"title", u"content")
224 elif verbosity == 1: 278 elif verbosity == 1:
225 return (u"title", u"tags", u"author", u"author_jid", u"author_email", u"author_jid_verified", u"published", u"updated", u"content") 279 return (
280 u"title",
281 u"tags",
282 u"author",
283 u"author_jid",
284 u"author_email",
285 u"author_jid_verified",
286 u"published",
287 u"updated",
288 u"content",
289 )
226 else: 290 else:
227 return MB_KEYS 291 return MB_KEYS
228 292
229 def default_output(self, data): 293 def default_output(self, data):
230 """simple key/value output""" 294 """simple key/value output"""
231 items, self.metadata = data 295 items, self.metadata = data
232 keys = self.get_keys() 296 keys = self.get_keys()
233 297
234 # k_cb use format_[key] methods for complex formattings 298 #  k_cb use format_[key] methods for complex formattings
235 k_cb = {} 299 k_cb = {}
236 for k in keys: 300 for k in keys:
237 try: 301 try:
238 callback = getattr(self, "format_" + k) 302 callback = getattr(self, "format_" + k)
239 except AttributeError: 303 except AttributeError:
243 for idx, item in enumerate(items): 307 for idx, item in enumerate(items):
244 for k in keys: 308 for k in keys:
245 if k not in item and k not in k_cb: 309 if k not in item and k not in k_cb:
246 continue 310 continue
247 if OUTPUT_OPT_NO_HEADER in self.args.output_opts: 311 if OUTPUT_OPT_NO_HEADER in self.args.output_opts:
248 header = '' 312 header = ""
249 else: 313 else:
250 header = u"{k_fmt}{key}:{k_fmt_e} {sep}".format( 314 header = u"{k_fmt}{key}:{k_fmt_e} {sep}".format(
251 k_fmt = C.A_HEADER, 315 k_fmt=C.A_HEADER,
252 key = k, 316 key=k,
253 k_fmt_e = A.RESET, 317 k_fmt_e=A.RESET,
254 sep = u'\n' if 'content' in k else u'') 318 sep=u"\n" if "content" in k else u"",
319 )
255 value = k_cb[k](item, keys) if k in k_cb else item[k] 320 value = k_cb[k](item, keys) if k in k_cb else item[k]
256 self.disp(header + value) 321 self.disp(header + value)
257 # we want a separation line after each item but the last one 322 # we want a separation line after each item but the last one
258 if idx < len(items)-1: 323 if idx < len(items) - 1:
259 print(u'') 324 print(u"")
260 325
261 def format_time(self, timestamp): 326 def format_time(self, timestamp):
262 """return formatted date for timestamp 327 """return formatted date for timestamp
263 328
264 @param timestamp(str,int,float): unix timestamp 329 @param timestamp(str,int,float): unix timestamp
271 """display blog is a nice to read way 336 """display blog is a nice to read way
272 337
273 this output doesn't use keys filter 338 this output doesn't use keys filter
274 """ 339 """
275 # thanks to http://stackoverflow.com/a/943921 340 # thanks to http://stackoverflow.com/a/943921
276 rows, columns = map(int, os.popen('stty size', 'r').read().split()) 341 rows, columns = map(int, os.popen("stty size", "r").read().split())
277 items, metadata = data 342 items, metadata = data
278 verbosity = self.args.verbose 343 verbosity = self.args.verbose
279 sep = A.color(A.FG_BLUE, columns * u'▬') 344 sep = A.color(A.FG_BLUE, columns * u"▬")
280 if items: 345 if items:
281 print(u'\n' + sep + '\n') 346 print(u"\n" + sep + "\n")
282 347
283 for idx, item in enumerate(items): 348 for idx, item in enumerate(items):
284 title = item.get(u'title') 349 title = item.get(u"title")
285 if verbosity > 0: 350 if verbosity > 0:
286 author = item[u'author'] 351 author = item[u"author"]
287 published, updated = item[u'published'], item.get('updated') 352 published, updated = item[u"published"], item.get("updated")
288 else: 353 else:
289 author = published = updated = None 354 author = published = updated = None
290 if verbosity > 1: 355 if verbosity > 1:
291 tags = list(data_format.dict2iter('tag', item, pop=True)) 356 tags = list(data_format.dict2iter("tag", item, pop=True))
292 else: 357 else:
293 tags = None 358 tags = None
294 content = item.get(u'content') 359 content = item.get(u"content")
295 360
296 if title: 361 if title:
297 print(A.color(A.BOLD, A.FG_CYAN, item[u'title'])) 362 print(A.color(A.BOLD, A.FG_CYAN, item[u"title"]))
298 meta = [] 363 meta = []
299 if author: 364 if author:
300 meta.append(A.color(A.FG_YELLOW, author)) 365 meta.append(A.color(A.FG_YELLOW, author))
301 if published: 366 if published:
302 meta.append(A.color(A.FG_YELLOW, u'on ', self.format_time(published))) 367 meta.append(A.color(A.FG_YELLOW, u"on ", self.format_time(published)))
303 if updated != published: 368 if updated != published:
304 meta.append(A.color(A.FG_YELLOW, u'(updated on ', self.format_time(updated), u')')) 369 meta.append(
305 print(u' '.join(meta)) 370 A.color(A.FG_YELLOW, u"(updated on ", self.format_time(updated), u")")
371 )
372 print(u" ".join(meta))
306 if tags: 373 if tags:
307 print(A.color(A.FG_MAGENTA, u', '.join(tags))) 374 print(A.color(A.FG_MAGENTA, u", ".join(tags)))
308 if (title or tags) and content: 375 if (title or tags) and content:
309 print("") 376 print("")
310 if content: 377 if content:
311 self.disp(content) 378 self.disp(content)
312 379
313 print(u'\n' + sep + '\n') 380 print(u"\n" + sep + "\n")
314
315 381
316 def mbGetCb(self, mb_result): 382 def mbGetCb(self, mb_result):
317 self.output(mb_result) 383 self.output(mb_result)
318 self.host.quit(C.EXIT_OK) 384 self.host.quit(C.EXIT_OK)
319 385
320 def mbGetEb(self, failure_): 386 def mbGetEb(self, failure_):
321 self.disp(u"can't get blog items: {reason}".format( 387 self.disp(u"can't get blog items: {reason}".format(reason=failure_), error=True)
322 reason=failure_), error=True)
323 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 388 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
324 389
325 def start(self): 390 def start(self):
326 self.host.bridge.mbGet( 391 self.host.bridge.mbGet(
327 self.args.service, 392 self.args.service,
329 self.args.max, 394 self.args.max,
330 self.args.items, 395 self.args.items,
331 {}, 396 {},
332 self.profile, 397 self.profile,
333 callback=self.mbGetCb, 398 callback=self.mbGetCb,
334 errback=self.mbGetEb) 399 errback=self.mbGetEb,
400 )
335 401
336 402
337 class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit): 403 class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
338
339 def __init__(self, host): 404 def __init__(self, host):
340 base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM}, 405 base.CommandBase.__init__(
341 use_draft=True, use_verbose=True, help=_(u'edit an existing or new blog post')) 406 self,
407 host,
408 "edit",
409 use_pubsub=True,
410 pubsub_flags={C.SINGLE_ITEM},
411 use_draft=True,
412 use_verbose=True,
413 help=_(u"edit an existing or new blog post"),
414 )
342 BlogPublishCommon.__init__(self) 415 BlogPublishCommon.__init__(self)
343 common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True) 416 common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True)
344 417
345 @property 418 @property
346 def current_syntax(self): 419 def current_syntax(self):
347 if self._current_syntax is None: 420 if self._current_syntax is None:
348 self._current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile) 421 self._current_syntax = self.host.bridge.getParamA(
422 "Syntax", "Composition", "value", self.profile
423 )
349 return self._current_syntax 424 return self._current_syntax
350 425
351 def add_parser_options(self): 426 def add_parser_options(self):
352 BlogPublishCommon.add_parser_options(self) 427 BlogPublishCommon.add_parser_options(self)
353 self.parser.add_argument("-P", "--preview", action="store_true", help=_(u"launch a blog preview in parallel")) 428 self.parser.add_argument(
429 "-P",
430 "--preview",
431 action="store_true",
432 help=_(u"launch a blog preview in parallel"),
433 )
354 434
355 def buildMetadataFile(self, content_file_path, mb_data=None): 435 def buildMetadataFile(self, content_file_path, mb_data=None):
356 """Build a metadata file using json 436 """Build a metadata file using json
357 437
358 The file is named after content_file_path, with extension replaced by _metadata.json 438 The file is named after content_file_path, with extension replaced by _metadata.json
365 # or re-use the existing one if it exists 445 # or re-use the existing one if it exists
366 meta_file_path = os.path.splitext(content_file_path)[0] + common.METADATA_SUFF 446 meta_file_path = os.path.splitext(content_file_path)[0] + common.METADATA_SUFF
367 if os.path.exists(meta_file_path): 447 if os.path.exists(meta_file_path):
368 self.disp(u"Metadata file already exists, we re-use it") 448 self.disp(u"Metadata file already exists, we re-use it")
369 try: 449 try:
370 with open(meta_file_path, 'rb') as f: 450 with open(meta_file_path, "rb") as f:
371 mb_data = json.load(f) 451 mb_data = json.load(f)
372 except (OSError, IOError, ValueError) as e: 452 except (OSError, IOError, ValueError) as e:
373 self.disp(u"Can't read existing metadata file at {path}, aborting: {reason}".format( 453 self.disp(
374 path=meta_file_path, reason=e), error=True) 454 u"Can't read existing metadata file at {path}, aborting: {reason}".format(
455 path=meta_file_path, reason=e
456 ),
457 error=True,
458 )
375 self.host.quit(1) 459 self.host.quit(1)
376 else: 460 else:
377 mb_data = {} if mb_data is None else mb_data.copy() 461 mb_data = {} if mb_data is None else mb_data.copy()
378 462
379 # in all cases, we want to remove unwanted keys 463 # in all cases, we want to remove unwanted keys
385 # and override metadata with command-line arguments 469 # and override metadata with command-line arguments
386 self.setMbDataFromArgs(mb_data) 470 self.setMbDataFromArgs(mb_data)
387 471
388 # then we create the file and write metadata there, as JSON dict 472 # then we create the file and write metadata there, as JSON dict
389 # XXX: if we port jp one day on Windows, O_BINARY may need to be added here 473 # XXX: if we port jp one day on Windows, O_BINARY may need to be added here
390 with os.fdopen(os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC,0o600), 'w+b') as f: 474 with os.fdopen(
475 os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w+b"
476 ) as f:
391 # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters 477 # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters
392 unicode_dump = json.dumps(mb_data, ensure_ascii=False, indent=4, separators=(',', ': '), sort_keys=True) 478 unicode_dump = json.dumps(
393 f.write(unicode_dump.encode('utf-8')) 479 mb_data,
480 ensure_ascii=False,
481 indent=4,
482 separators=(",", ": "),
483 sort_keys=True,
484 )
485 f.write(unicode_dump.encode("utf-8"))
394 486
395 return mb_data, meta_file_path 487 return mb_data, meta_file_path
396 488
397 def edit(self, content_file_path, content_file_obj, 489 def edit(self, content_file_path, content_file_obj, mb_data=None):
398 mb_data=None):
399 """Edit the file contening the content using editor, and publish it""" 490 """Edit the file contening the content using editor, and publish it"""
400 # we first create metadata file 491 # we first create metadata file
401 meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, mb_data) 492 meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, mb_data)
402 493
403 # do we need a preview ? 494 # do we need a preview ?
404 if self.args.preview: 495 if self.args.preview:
405 self.disp(u"Preview requested, launching it", 1) 496 self.disp(u"Preview requested, launching it", 1)
406 # we redirect outputs to /dev/null to avoid console pollution in editor 497 # we redirect outputs to /dev/null to avoid console pollution in editor
407 # if user wants to see messages, (s)he can call "blog preview" directly 498 # if user wants to see messages, (s)he can call "blog preview" directly
408 DEVNULL = open(os.devnull, 'wb') 499 DEVNULL = open(os.devnull, "wb")
409 subprocess.Popen([sys.argv[0], "blog", "preview", "--inotify", "true", "-p", self.profile, content_file_path], stdout=DEVNULL, stderr=subprocess.STDOUT) 500 subprocess.Popen(
501 [
502 sys.argv[0],
503 "blog",
504 "preview",
505 "--inotify",
506 "true",
507 "-p",
508 self.profile,
509 content_file_path,
510 ],
511 stdout=DEVNULL,
512 stderr=subprocess.STDOUT,
513 )
410 514
411 # we launch editor 515 # we launch editor
412 self.runEditor("blog_editor_args", content_file_path, content_file_obj, meta_file_path=meta_file_path, meta_ori=meta_ori) 516 self.runEditor(
517 "blog_editor_args",
518 content_file_path,
519 content_file_obj,
520 meta_file_path=meta_file_path,
521 meta_ori=meta_ori,
522 )
413 523
414 def publish(self, content, mb_data): 524 def publish(self, content, mb_data):
415 self.setMbDataContent(content, mb_data) 525 self.setMbDataContent(content, mb_data)
416 526
417 if self.pubsub_item is not None: 527 if self.pubsub_item is not None:
418 mb_data['id'] = self.pubsub_item 528 mb_data["id"] = self.pubsub_item
419 529
420 self.host.bridge.mbSend(self.pubsub_service, self.pubsub_node, mb_data, self.profile) 530 self.host.bridge.mbSend(
531 self.pubsub_service, self.pubsub_node, mb_data, self.profile
532 )
421 self.disp(u"Blog item published") 533 self.disp(u"Blog item published")
422
423 534
424 def getTmpSuff(self): 535 def getTmpSuff(self):
425 # we get current syntax to determine file extension 536 # we get current syntax to determine file extension
426 return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT['']) 537 return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT[""])
427 538
428 def getItemData(self, service, node, item): 539 def getItemData(self, service, node, item):
429 items = [item] if item is not None else [] 540 items = [item] if item is not None else []
430 mb_data = self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)[0][0] 541 mb_data = self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)[0][0]
431 try: 542 try:
432 content = mb_data['content_xhtml'] 543 content = mb_data["content_xhtml"]
433 except KeyError: 544 except KeyError:
434 content = mb_data['content'] 545 content = mb_data["content"]
435 if content: 546 if content:
436 content = self.host.bridge.syntaxConvert(content, 'text', SYNTAX_XHTML, False, self.profile) 547 content = self.host.bridge.syntaxConvert(
548 content, "text", SYNTAX_XHTML, False, self.profile
549 )
437 if content and self.current_syntax != SYNTAX_XHTML: 550 if content and self.current_syntax != SYNTAX_XHTML:
438 content = self.host.bridge.syntaxConvert(content, SYNTAX_XHTML, self.current_syntax, False, self.profile) 551 content = self.host.bridge.syntaxConvert(
552 content, SYNTAX_XHTML, self.current_syntax, False, self.profile
553 )
439 if content and self.current_syntax == SYNTAX_XHTML: 554 if content and self.current_syntax == SYNTAX_XHTML:
440 try: 555 try:
441 from lxml import etree 556 from lxml import etree
442 except ImportError: 557 except ImportError:
443 self.disp(_(u"You need lxml to edit pretty XHTML")) 558 self.disp(_(u"You need lxml to edit pretty XHTML"))
444 else: 559 else:
445 parser = etree.XMLParser(remove_blank_text=True) 560 parser = etree.XMLParser(remove_blank_text=True)
446 root = etree.fromstring(content, parser) 561 root = etree.fromstring(content, parser)
447 content = etree.tostring(root, encoding=unicode, pretty_print=True) 562 content = etree.tostring(root, encoding=unicode, pretty_print=True)
448 563
449 return content, mb_data, mb_data['id'] 564 return content, mb_data, mb_data["id"]
450 565
451 def start(self): 566 def start(self):
452 # if there are user defined extension, we use them 567 # if there are user defined extension, we use them
453 SYNTAX_EXT.update(config.getConfig(self.sat_conf, 'jp', CONF_SYNTAX_EXT, {})) 568 SYNTAX_EXT.update(config.getConfig(self.sat_conf, "jp", CONF_SYNTAX_EXT, {}))
454 self._current_syntax = self.args.syntax 569 self._current_syntax = self.args.syntax
455 if self._current_syntax is not None: 570 if self._current_syntax is not None:
456 try: 571 try:
457 self._current_syntax = self.args.syntax = self.host.bridge.syntaxGet(self.current_syntax) 572 self._current_syntax = self.args.syntax = self.host.bridge.syntaxGet(
573 self.current_syntax
574 )
458 except Exception as e: 575 except Exception as e:
459 if "NotFound" in unicode(e): # FIXME: there is not good way to check bridge errors 576 if "NotFound" in unicode(
460 self.parser.error(_(u"unknown syntax requested ({syntax})").format(syntax=self.args.syntax)) 577 e
578 ): #  FIXME: there is not good way to check bridge errors
579 self.parser.error(
580 _(u"unknown syntax requested ({syntax})").format(
581 syntax=self.args.syntax
582 )
583 )
461 else: 584 else:
462 raise e 585 raise e
463 586
464 self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj, mb_data = self.getItemPath() 587 (
588 self.pubsub_service,
589 self.pubsub_node,
590 self.pubsub_item,
591 content_file_path,
592 content_file_obj,
593 mb_data,
594 ) = self.getItemPath()
465 595
466 self.edit(content_file_path, content_file_obj, mb_data=mb_data) 596 self.edit(content_file_path, content_file_obj, mb_data=mb_data)
467 597
468 598
469 class Preview(base.CommandBase): 599 class Preview(base.CommandBase):
470 # TODO: need to be rewritten with template output 600 # TODO: need to be rewritten with template output
471 601
472 def __init__(self, host): 602 def __init__(self, host):
473 base.CommandBase.__init__(self, host, 'preview', use_verbose=True, help=_(u'preview a blog content')) 603 base.CommandBase.__init__(
604 self, host, "preview", use_verbose=True, help=_(u"preview a blog content")
605 )
474 606
475 def add_parser_options(self): 607 def add_parser_options(self):
476 self.parser.add_argument("--inotify", type=str, choices=('auto', 'true', 'false'), default=u'auto', help=_(u"use inotify to handle preview")) 608 self.parser.add_argument(
477 self.parser.add_argument("file", type=base.unicode_decoder, nargs='?', default=u'current', help=_(u"path to the content file")) 609 "--inotify",
610 type=str,
611 choices=("auto", "true", "false"),
612 default=u"auto",
613 help=_(u"use inotify to handle preview"),
614 )
615 self.parser.add_argument(
616 "file",
617 type=base.unicode_decoder,
618 nargs="?",
619 default=u"current",
620 help=_(u"path to the content file"),
621 )
478 622
479 def showPreview(self): 623 def showPreview(self):
480 # we implement showPreview here so we don't have to import webbrowser and urllib 624 # we implement showPreview here so we don't have to import webbrowser and urllib
481 # when preview is not used 625 # when preview is not used
482 url = 'file:{}'.format(self.urllib.quote(self.preview_file_path)) 626 url = "file:{}".format(self.urllib.quote(self.preview_file_path))
483 self.webbrowser.open_new_tab(url) 627 self.webbrowser.open_new_tab(url)
484 628
485 def _launchPreviewExt(self, cmd_line, opt_name): 629 def _launchPreviewExt(self, cmd_line, opt_name):
486 url = 'file:{}'.format(self.urllib.quote(self.preview_file_path)) 630 url = "file:{}".format(self.urllib.quote(self.preview_file_path))
487 args = common.parse_args(self.host, cmd_line, url=url, preview_file=self.preview_file_path) 631 args = common.parse_args(
632 self.host, cmd_line, url=url, preview_file=self.preview_file_path
633 )
488 if not args: 634 if not args:
489 self.disp(u"Couln't find command in \"{name}\", abording".format(name=opt_name), error=True) 635 self.disp(
636 u'Couln\'t find command in "{name}", abording'.format(name=opt_name),
637 error=True,
638 )
490 self.host.quit(1) 639 self.host.quit(1)
491 subprocess.Popen(args) 640 subprocess.Popen(args)
492 641
493 def openPreviewExt(self): 642 def openPreviewExt(self):
494 self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd") 643 self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd")
495 644
496 def updatePreviewExt(self): 645 def updatePreviewExt(self):
497 self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd") 646 self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd")
498 647
499 def updateContent(self): 648 def updateContent(self):
500 with open(self.content_file_path, 'rb') as f: 649 with open(self.content_file_path, "rb") as f:
501 content = f.read().decode('utf-8-sig') 650 content = f.read().decode("utf-8-sig")
502 if content and self.syntax != SYNTAX_XHTML: 651 if content and self.syntax != SYNTAX_XHTML:
503 # we use safe=True because we want to have a preview as close as possible to what the 652 # we use safe=True because we want to have a preview as close as possible
504 # people will see 653 # to what the people will see
505 content = self.host.bridge.syntaxConvert(content, self.syntax, SYNTAX_XHTML, True, self.profile) 654 content = self.host.bridge.syntaxConvert(
506 655 content, self.syntax, SYNTAX_XHTML, True, self.profile
507 xhtml = (u'<html xmlns="http://www.w3.org/1999/xhtml">' + 656 )
508 u'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" /></head>'+ 657
509 '<body>{}</body>' + 658 xhtml = (
510 u'</html>').format(content) 659 u'<html xmlns="http://www.w3.org/1999/xhtml">'
511 660 u'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'
512 with open(self.preview_file_path, 'wb') as f: 661 u"</head>"
513 f.write(xhtml.encode('utf-8')) 662 u"<body>{}</body>"
663 u"</html>"
664 ).format(content)
665
666 with open(self.preview_file_path, "wb") as f:
667 f.write(xhtml.encode("utf-8"))
514 668
515 def start(self): 669 def start(self):
516 import webbrowser 670 import webbrowser
517 import urllib 671 import urllib
672
518 self.webbrowser, self.urllib = webbrowser, urllib 673 self.webbrowser, self.urllib = webbrowser, urllib
519 674
520 if self.args.inotify != 'false': 675 if self.args.inotify != "false":
521 try: 676 try:
522 import inotify.adapters 677 import inotify.adapters
523 import inotify.constants 678 import inotify.constants
524 from inotify.calls import InotifyError 679 from inotify.calls import InotifyError
525 except ImportError: 680 except ImportError:
526 if self.args.inotify == 'auto': 681 if self.args.inotify == "auto":
527 inotify = None 682 inotify = None
528 self.disp(u'inotify module not found, deactivating feature. You can install it with {install}'.format(install=INOTIFY_INSTALL)) 683 self.disp(
684 u"inotify module not found, deactivating feature. You can install"
685 u" it with {install}".format(install=INOTIFY_INSTALL)
686 )
529 else: 687 else:
530 self.disp(u"inotify not found, can't activate the feature! Please install it with {install}".format(install=INOTIFY_INSTALL), error=True) 688 self.disp(
689 u"inotify not found, can't activate the feature! Please install "
690 u"it with {install}".format(install=INOTIFY_INSTALL),
691 error=True,
692 )
531 self.host.quit(1) 693 self.host.quit(1)
532 else: 694 else:
533 # we deactivate logging in inotify, which is quite annoying 695 # we deactivate logging in inotify, which is quite annoying
534 try: 696 try:
535 inotify.adapters._LOGGER.setLevel(40) 697 inotify.adapters._LOGGER.setLevel(40)
536 except AttributeError: 698 except AttributeError:
537 self.disp(u"Logger doesn't exists, inotify may have chanded", error=True) 699 self.disp(
700 u"Logger doesn't exists, inotify may have chanded", error=True
701 )
538 else: 702 else:
539 inotify=None 703 inotify = None
540 704
541 sat_conf = config.parseMainConf() 705 sat_conf = config.parseMainConf()
542 SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {})) 706 SYNTAX_EXT.update(config.getConfig(sat_conf, "jp", CONF_SYNTAX_EXT, {}))
543 707
544 try: 708 try:
545 self.open_cb_cmd = config.getConfig(sat_conf, 'jp', "blog_preview_open_cmd", Exception) 709 self.open_cb_cmd = config.getConfig(
710 sat_conf, "jp", "blog_preview_open_cmd", Exception
711 )
546 except (NoOptionError, NoSectionError): 712 except (NoOptionError, NoSectionError):
547 self.open_cb_cmd = None 713 self.open_cb_cmd = None
548 open_cb = self.showPreview 714 open_cb = self.showPreview
549 else: 715 else:
550 open_cb = self.openPreviewExt 716 open_cb = self.openPreviewExt
551 717
552 self.update_cb_cmd = config.getConfig(sat_conf, 'jp', "blog_preview_update_cmd", self.open_cb_cmd) 718 self.update_cb_cmd = config.getConfig(
719 sat_conf, "jp", "blog_preview_update_cmd", self.open_cb_cmd
720 )
553 if self.update_cb_cmd is None: 721 if self.update_cb_cmd is None:
554 update_cb = self.showPreview 722 update_cb = self.showPreview
555 else: 723 else:
556 update_cb = self.updatePreviewExt 724 update_cb = self.updatePreviewExt
557 725
558 # which file do we need to edit? 726 # which file do we need to edit?
559 if self.args.file == 'current': 727 if self.args.file == "current":
560 self.content_file_path = self.getCurrentFile(sat_conf, self.profile) 728 self.content_file_path = self.getCurrentFile(sat_conf, self.profile)
561 else: 729 else:
562 self.content_file_path = os.path.abspath(self.args.file) 730 self.content_file_path = os.path.abspath(self.args.file)
563 731
564 self.syntax = guessSyntaxFromPath(self.host, sat_conf, self.content_file_path) 732 self.syntax = guessSyntaxFromPath(self.host, sat_conf, self.content_file_path)
565 733
566
567 # at this point the syntax is converted, we can display the preview 734 # at this point the syntax is converted, we can display the preview
568 preview_file = tempfile.NamedTemporaryFile(suffix='.xhtml', delete=False) 735 preview_file = tempfile.NamedTemporaryFile(suffix=".xhtml", delete=False)
569 self.preview_file_path = preview_file.name 736 self.preview_file_path = preview_file.name
570 preview_file.close() 737 preview_file.close()
571 self.updateContent() 738 self.updateContent()
572 739
573 if inotify is None: 740 if inotify is None:
574 # XXX: we don't delete file automatically because browser need it (and webbrowser.open can return before it is read) 741 # XXX: we don't delete file automatically because browser need it
575 self.disp(u'temporary file created at {}\nthis file will NOT BE DELETED AUTOMATICALLY, please delete it yourself when you have finished'.format(self.preview_file_path)) 742 # (and webbrowser.open can return before it is read)
743 self.disp(
744 u"temporary file created at {}\nthis file will NOT BE DELETED "
745 u"AUTOMATICALLY, please delete it yourself when you have finished".format(
746 self.preview_file_path
747 )
748 )
576 open_cb() 749 open_cb()
577 else: 750 else:
578 open_cb() 751 open_cb()
579 i = inotify.adapters.Inotify(block_duration_s=60) # no need for 1 s duraction, inotify drive actions here 752 i = inotify.adapters.Inotify(
753 block_duration_s=60
754 ) # no need for 1 s duraction, inotify drive actions here
580 755
581 def add_watch(): 756 def add_watch():
582 i.add_watch(self.content_file_path, mask=inotify.constants.IN_CLOSE_WRITE | 757 i.add_watch(
583 inotify.constants.IN_DELETE_SELF | 758 self.content_file_path,
584 inotify.constants.IN_MOVE_SELF) 759 mask=inotify.constants.IN_CLOSE_WRITE
760 | inotify.constants.IN_DELETE_SELF
761 | inotify.constants.IN_MOVE_SELF,
762 )
763
585 add_watch() 764 add_watch()
586 765
587 try: 766 try:
588 for event in i.event_gen(): 767 for event in i.event_gen():
589 if event is not None: 768 if event is not None:
590 self.disp(u"Content updated", 1) 769 self.disp(u"Content updated", 1)
591 if {"IN_DELETE_SELF", "IN_MOVE_SELF"}.intersection(event[1]): 770 if {"IN_DELETE_SELF", "IN_MOVE_SELF"}.intersection(event[1]):
592 self.disp(u"{} event catched, changing the watch".format(", ".join(event[1])), 2) 771 self.disp(
772 u"{} event catched, changing the watch".format(
773 ", ".join(event[1])
774 ),
775 2,
776 )
593 try: 777 try:
594 add_watch() 778 add_watch()
595 except InotifyError: 779 except InotifyError:
596 # if the new file is not here yet we can have an error 780 # if the new file is not here yet we can have an error
597 # as a workaround, we do a little rest 781 # as a workaround, we do a little rest
598 time.sleep(1) 782 time.sleep(1)
599 add_watch() 783 add_watch()
600 self.updateContent() 784 self.updateContent()
601 update_cb() 785 update_cb()
602 except InotifyError: 786 except InotifyError:
603 self.disp(u"Can't catch inotify events, as the file been deleted?", error=True) 787 self.disp(
788 u"Can't catch inotify events, as the file been deleted?", error=True
789 )
604 finally: 790 finally:
605 os.unlink(self.preview_file_path) 791 os.unlink(self.preview_file_path)
606 try: 792 try:
607 i.remove_watch(self.content_file_path) 793 i.remove_watch(self.content_file_path)
608 except InotifyError: 794 except InotifyError:
609 pass 795 pass
610 796
611 797
612 class Import(base.CommandAnswering): 798 class Import(base.CommandAnswering):
613 def __init__(self, host): 799 def __init__(self, host):
614 super(Import, self).__init__(host, 'import', use_pubsub=True, use_progress=True, help=_(u'import an external blog')) 800 super(Import, self).__init__(
615 self.need_loop=True 801 host,
802 "import",
803 use_pubsub=True,
804 use_progress=True,
805 help=_(u"import an external blog"),
806 )
807 self.need_loop = True
616 808
617 def add_parser_options(self): 809 def add_parser_options(self):
618 self.parser.add_argument("importer", type=base.unicode_decoder, nargs='?', help=_(u"importer name, nothing to display importers list")) 810 self.parser.add_argument(
619 self.parser.add_argument('--host', type=base.unicode_decoder, help=_(u"original blog host")) 811 "importer",
620 self.parser.add_argument('--no-images-upload', action='store_true', help=_(u"do *NOT* upload images (default: do upload images)")) 812 type=base.unicode_decoder,
621 self.parser.add_argument('--upload-ignore-host', help=_(u"do not upload images from this host (default: upload all images)")) 813 nargs="?",
622 self.parser.add_argument("--ignore-tls-errors", action="store_true", help=_("ignore invalide TLS certificate for uploads")) 814 help=_(u"importer name, nothing to display importers list"),
623 self.parser.add_argument('-o', '--option', action='append', nargs=2, default=[], metavar=(u'NAME', u'VALUE'), 815 )
624 help=_(u"importer specific options (see importer description)")) 816 self.parser.add_argument(
625 self.parser.add_argument("location", type=base.unicode_decoder, nargs='?', 817 "--host", type=base.unicode_decoder, help=_(u"original blog host")
626 help=_(u"importer data location (see importer description), nothing to show importer description")) 818 )
819 self.parser.add_argument(
820 "--no-images-upload",
821 action="store_true",
822 help=_(u"do *NOT* upload images (default: do upload images)"),
823 )
824 self.parser.add_argument(
825 "--upload-ignore-host",
826 help=_(u"do not upload images from this host (default: upload all images)"),
827 )
828 self.parser.add_argument(
829 "--ignore-tls-errors",
830 action="store_true",
831 help=_("ignore invalide TLS certificate for uploads"),
832 )
833 self.parser.add_argument(
834 "-o",
835 "--option",
836 action="append",
837 nargs=2,
838 default=[],
839 metavar=(u"NAME", u"VALUE"),
840 help=_(u"importer specific options (see importer description)"),
841 )
842 self.parser.add_argument(
843 "location",
844 type=base.unicode_decoder,
845 nargs="?",
846 help=_(
847 u"importer data location (see importer description), nothing to show "
848 u"importer description"
849 ),
850 )
627 851
628 def onProgressStarted(self, metadata): 852 def onProgressStarted(self, metadata):
629 self.disp(_(u'Blog upload started'),2) 853 self.disp(_(u"Blog upload started"), 2)
630 854
631 def onProgressFinished(self, metadata): 855 def onProgressFinished(self, metadata):
632 self.disp(_(u'Blog uploaded successfully'),2) 856 self.disp(_(u"Blog uploaded successfully"), 2)
633 redirections = {k[len(URL_REDIRECT_PREFIX):]:v for k,v in metadata.iteritems() 857 redirections = {
634 if k.startswith(URL_REDIRECT_PREFIX)} 858 k[len(URL_REDIRECT_PREFIX) :]: v
859 for k, v in metadata.iteritems()
860 if k.startswith(URL_REDIRECT_PREFIX)
861 }
635 if redirections: 862 if redirections:
636 conf = u'\n'.join([ 863 conf = u"\n".join(
637 u'url_redirections_profile = {}'.format(self.profile), 864 [
638 u"url_redirections_dict = {}".format( 865 u"url_redirections_profile = {}".format(self.profile),
639 # we need to add ' ' before each new line and to double each '%' for ConfigParser 866 u"url_redirections_dict = {}".format(
640 u'\n '.join(json.dumps(redirections, indent=1, separators=(',',': ')).replace(u'%', u'%%').split(u'\n'))), 867 # we need to add ' ' before each new line
641 ]) 868 # and to double each '%' for ConfigParser
642 self.disp(_(u'\nTo redirect old URLs to new ones, put the following lines in your sat.conf file, in [libervia] section:\n\n{conf}'.format(conf=conf))) 869 u"\n ".join(
870 json.dumps(redirections, indent=1, separators=(",", ": "))
871 .replace(u"%", u"%%")
872 .split(u"\n")
873 )
874 ),
875 ]
876 )
877 self.disp(
878 _(
879 u"\nTo redirect old URLs to new ones, put the following lines in your"
880 u" sat.conf file, in [libervia] section:\n\n{conf}".format(conf=conf)
881 )
882 )
643 883
644 def onProgressError(self, error_msg): 884 def onProgressError(self, error_msg):
645 self.disp(_(u'Error while uploading blog: {}').format(error_msg),error=True) 885 self.disp(_(u"Error while uploading blog: {}").format(error_msg), error=True)
646 886
647 def error(self, failure): 887 def error(self, failure):
648 self.disp(_("Error while trying to upload a blog: {reason}").format(reason=failure), error=True) 888 self.disp(
889 _("Error while trying to upload a blog: {reason}").format(reason=failure),
890 error=True,
891 )
649 self.host.quit(1) 892 self.host.quit(1)
650 893
651 def start(self): 894 def start(self):
652 if self.args.location is None: 895 if self.args.location is None:
653 for name in ('option', 'service', 'no_images_upload'): 896 for name in ("option", "service", "no_images_upload"):
654 if getattr(self.args, name): 897 if getattr(self.args, name):
655 self.parser.error(_(u"{name} argument can't be used without location argument").format(name=name)) 898 self.parser.error(
899 _(
900 u"{name} argument can't be used without location argument"
901 ).format(name=name)
902 )
656 if self.args.importer is None: 903 if self.args.importer is None:
657 self.disp(u'\n'.join([u'{}: {}'.format(name, desc) for name, desc in self.host.bridge.blogImportList()])) 904 self.disp(
905 u"\n".join(
906 [
907 u"{}: {}".format(name, desc)
908 for name, desc in self.host.bridge.blogImportList()
909 ]
910 )
911 )
658 else: 912 else:
659 try: 913 try:
660 short_desc, long_desc = self.host.bridge.blogImportDesc(self.args.importer) 914 short_desc, long_desc = self.host.bridge.blogImportDesc(
915 self.args.importer
916 )
661 except Exception as e: 917 except Exception as e:
662 msg = [l for l in unicode(e).split('\n') if l][-1] # we only keep the last line 918 msg = [l for l in unicode(e).split("\n") if l][
919 -1
920 ] # we only keep the last line
663 self.disp(msg) 921 self.disp(msg)
664 self.host.quit(1) 922 self.host.quit(1)
665 else: 923 else:
666 self.disp(u"{name}: {short_desc}\n\n{long_desc}".format(name=self.args.importer, short_desc=short_desc, long_desc=long_desc)) 924 self.disp(
925 u"{name}: {short_desc}\n\n{long_desc}".format(
926 name=self.args.importer,
927 short_desc=short_desc,
928 long_desc=long_desc,
929 )
930 )
667 self.host.quit() 931 self.host.quit()
668 else: 932 else:
669 # we have a location, an import is requested 933 # we have a location, an import is requested
670 options = {key: value for key, value in self.args.option} 934 options = {key: value for key, value in self.args.option}
671 if self.args.host: 935 if self.args.host:
672 options['host'] = self.args.host 936 options["host"] = self.args.host
673 if self.args.ignore_tls_errors: 937 if self.args.ignore_tls_errors:
674 options['ignore_tls_errors'] = C.BOOL_TRUE 938 options["ignore_tls_errors"] = C.BOOL_TRUE
675 if self.args.no_images_upload: 939 if self.args.no_images_upload:
676 options['upload_images'] = C.BOOL_FALSE 940 options["upload_images"] = C.BOOL_FALSE
677 if self.args.upload_ignore_host: 941 if self.args.upload_ignore_host:
678 self.parser.error(u"upload-ignore-host option can't be used when no-images-upload is set") 942 self.parser.error(
943 u"upload-ignore-host option can't be used when no-images-upload "
944 u"is set"
945 )
679 elif self.args.upload_ignore_host: 946 elif self.args.upload_ignore_host:
680 options['upload_ignore_host'] = self.args.upload_ignore_host 947 options["upload_ignore_host"] = self.args.upload_ignore_host
948
681 def gotId(id_): 949 def gotId(id_):
682 self.progress_id = id_ 950 self.progress_id = id_
683 self.host.bridge.blogImport(self.args.importer, self.args.location, options, self.args.service, self.args.node, self.profile, 951
684 callback=gotId, errback=self.error) 952 self.host.bridge.blogImport(
953 self.args.importer,
954 self.args.location,
955 options,
956 self.args.service,
957 self.args.node,
958 self.profile,
959 callback=gotId,
960 errback=self.error,
961 )
685 962
686 963
687 class Blog(base.CommandBase): 964 class Blog(base.CommandBase):
688 subcommands = (Set, Get, Edit, Preview, Import) 965 subcommands = (Set, Get, Edit, Preview, Import)
689 966
690 def __init__(self, host): 967 def __init__(self, host):
691 super(Blog, self).__init__(host, 'blog', use_profile=False, help=_('blog/microblog management')) 968 super(Blog, self).__init__(
969 host, "blog", use_profile=False, help=_("blog/microblog management")
970 )