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

code style reformatting using black
author Goffi <goffi@goffi.org>
date Wed, 27 Jun 2018 20:14:46 +0200
parents c9dddf691d7b
children 003b8b4b56a7
comparison
equal deleted inserted replaced
2623:49533de4540b 2624:56f94936df1e
38 38
39 # defaut arguments used for some known editors (editing with metadata) 39 # defaut arguments used for some known editors (editing with metadata)
40 VIM_SPLIT_ARGS = "-c 'set nospr|vsplit|wincmd w|next|wincmd w'" 40 VIM_SPLIT_ARGS = "-c 'set nospr|vsplit|wincmd w|next|wincmd w'"
41 EMACS_SPLIT_ARGS = '--eval "(split-window-horizontally)"' 41 EMACS_SPLIT_ARGS = '--eval "(split-window-horizontally)"'
42 EDITOR_ARGS_MAGIC = { 42 EDITOR_ARGS_MAGIC = {
43 'vim': VIM_SPLIT_ARGS + ' {content_file} {metadata_file}', 43 "vim": VIM_SPLIT_ARGS + " {content_file} {metadata_file}",
44 'gvim': VIM_SPLIT_ARGS + ' --nofork {content_file} {metadata_file}', 44 "gvim": VIM_SPLIT_ARGS + " --nofork {content_file} {metadata_file}",
45 'emacs': EMACS_SPLIT_ARGS + ' {content_file} {metadata_file}', 45 "emacs": EMACS_SPLIT_ARGS + " {content_file} {metadata_file}",
46 'xemacs': EMACS_SPLIT_ARGS + ' {content_file} {metadata_file}', 46 "xemacs": EMACS_SPLIT_ARGS + " {content_file} {metadata_file}",
47 'nano': ' -F {content_file} {metadata_file}', 47 "nano": " -F {content_file} {metadata_file}",
48 } 48 }
49 49
50 SECURE_UNLINK_MAX = 10 50 SECURE_UNLINK_MAX = 10
51 SECURE_UNLINK_DIR = ".backup" 51 SECURE_UNLINK_DIR = ".backup"
52 METADATA_SUFF = '_metadata.json' 52 METADATA_SUFF = "_metadata.json"
53 53
54 54
55 def ansi_ljust(s, width): 55 def ansi_ljust(s, width):
56 """ljust method handling ANSI escape codes""" 56 """ljust method handling ANSI escape codes"""
57 cleaned = regex.ansiRemove(s) 57 cleaned = regex.ansiRemove(s)
58 return s + u' ' * (width - len(cleaned)) 58 return s + u" " * (width - len(cleaned))
59 59
60 60
61 def ansi_center(s, width): 61 def ansi_center(s, width):
62 """ljust method handling ANSI escape codes""" 62 """ljust method handling ANSI escape codes"""
63 cleaned = regex.ansiRemove(s) 63 cleaned = regex.ansiRemove(s)
64 diff = width - len(cleaned) 64 diff = width - len(cleaned)
65 half = diff/2 65 half = diff / 2
66 return half * u' ' + s + (half + diff % 2) * u' ' 66 return half * u" " + s + (half + diff % 2) * u" "
67 67
68 68
69 def ansi_rjust(s, width): 69 def ansi_rjust(s, width):
70 """ljust method handling ANSI escape codes""" 70 """ljust method handling ANSI escape codes"""
71 cleaned = regex.ansiRemove(s) 71 cleaned = regex.ansiRemove(s)
72 return u' ' * (width - len(cleaned)) + s 72 return u" " * (width - len(cleaned)) + s
73 73
74 74
75 def getTmpDir(sat_conf, cat_dir, sub_dir=None): 75 def getTmpDir(sat_conf, cat_dir, sub_dir=None):
76 """Return directory used to store temporary files 76 """Return directory used to store temporary files
77 77
81 profile can be used here, or special directory name 81 profile can be used here, or special directory name
82 sub_dir will be escaped to be usable in path (use regex.pathUnescape to find 82 sub_dir will be escaped to be usable in path (use regex.pathUnescape to find
83 initial str) 83 initial str)
84 @return (str): path to the dir 84 @return (str): path to the dir
85 """ 85 """
86 local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception) 86 local_dir = config.getConfig(sat_conf, "", "local_dir", Exception)
87 path = [local_dir.encode('utf-8'), cat_dir.encode('utf-8')] 87 path = [local_dir.encode("utf-8"), cat_dir.encode("utf-8")]
88 if sub_dir is not None: 88 if sub_dir is not None:
89 path.append(regex.pathEscape(sub_dir)) 89 path.append(regex.pathEscape(sub_dir))
90 return os.path.join(*path) 90 return os.path.join(*path)
91 91
92 92
100 try: 100 try:
101 # we split the arguments and add the known fields 101 # we split the arguments and add the known fields
102 # we split arguments first to avoid escaping issues in file names 102 # we split arguments first to avoid escaping issues in file names
103 return [a.format(**format_kw) for a in shlex.split(cmd_line)] 103 return [a.format(**format_kw) for a in shlex.split(cmd_line)]
104 except ValueError as e: 104 except ValueError as e:
105 host.disp(u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=cmd_line, reason=e)) 105 host.disp(
106 u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=cmd_line, reason=e)
107 )
106 return [] 108 return []
107 109
108 110
109 class BaseEdit(object): 111 class BaseEdit(object):
110 u"""base class for editing commands 112 u"""base class for editing commands
122 most of signature change with use_metadata with an additional metadata argument. 124 most of signature change with use_metadata with an additional metadata argument.
123 This is done to raise error if a command needs metadata but forget the flag, and vice versa 125 This is done to raise error if a command needs metadata but forget the flag, and vice versa
124 """ 126 """
125 self.host = host 127 self.host = host
126 self.sat_conf = config.parseMainConf() 128 self.sat_conf = config.parseMainConf()
127 self.cat_dir_str = cat_dir.encode('utf-8') 129 self.cat_dir_str = cat_dir.encode("utf-8")
128 self.use_metadata = use_metadata 130 self.use_metadata = use_metadata
129 131
130 def secureUnlink(self, path): 132 def secureUnlink(self, path):
131 """Unlink given path after keeping it for a while 133 """Unlink given path after keeping it for a while
132 134
136 @param path(str): file to unlink 138 @param path(str): file to unlink
137 """ 139 """
138 if not os.path.isfile(path): 140 if not os.path.isfile(path):
139 raise OSError(u"path must link to a regular file") 141 raise OSError(u"path must link to a regular file")
140 if not path.startswith(getTmpDir(self.sat_conf, self.cat_dir_str)): 142 if not path.startswith(getTmpDir(self.sat_conf, self.cat_dir_str)):
141 self.disp(u"File {} is not in SàT temporary hierarchy, we do not remove it".format(path.decode('utf-8')), 2) 143 self.disp(
144 u"File {} is not in SàT temporary hierarchy, we do not remove it".format(
145 path.decode("utf-8")
146 ),
147 2,
148 )
142 return 149 return
143 # we have 2 files per draft with use_metadata, so we double max 150 # we have 2 files per draft with use_metadata, so we double max
144 unlink_max = SECURE_UNLINK_MAX * 2 if self.use_metadata else SECURE_UNLINK_MAX 151 unlink_max = SECURE_UNLINK_MAX * 2 if self.use_metadata else SECURE_UNLINK_MAX
145 backup_dir = getTmpDir(self.sat_conf, self.cat_dir_str, SECURE_UNLINK_DIR) 152 backup_dir = getTmpDir(self.sat_conf, self.cat_dir_str, SECURE_UNLINK_DIR)
146 if not os.path.exists(backup_dir): 153 if not os.path.exists(backup_dir):
147 os.makedirs(backup_dir) 154 os.makedirs(backup_dir)
148 filename = os.path.basename(path) 155 filename = os.path.basename(path)
149 backup_path = os.path.join(backup_dir, filename) 156 backup_path = os.path.join(backup_dir, filename)
150 # we move file to backup dir 157 # we move file to backup dir
151 self.host.disp(u"Backuping file {src} to {dst}".format( 158 self.host.disp(
152 src=path.decode('utf-8'), dst=backup_path.decode('utf-8')), 1) 159 u"Backuping file {src} to {dst}".format(
160 src=path.decode("utf-8"), dst=backup_path.decode("utf-8")
161 ),
162 1,
163 )
153 os.rename(path, backup_path) 164 os.rename(path, backup_path)
154 # and if we exceeded the limit, we remove older file 165 # and if we exceeded the limit, we remove older file
155 backup_files = [os.path.join(backup_dir, f) for f in os.listdir(backup_dir)] 166 backup_files = [os.path.join(backup_dir, f) for f in os.listdir(backup_dir)]
156 if len(backup_files) > unlink_max: 167 if len(backup_files) > unlink_max:
157 backup_files.sort(key=lambda path: os.stat(path).st_mtime) 168 backup_files.sort(key=lambda path: os.stat(path).st_mtime)
158 for path in backup_files[:len(backup_files) - unlink_max]: 169 for path in backup_files[: len(backup_files) - unlink_max]:
159 self.host.disp(u"Purging backup file {}".format(path.decode('utf-8')), 2) 170 self.host.disp(u"Purging backup file {}".format(path.decode("utf-8")), 2)
160 os.unlink(path) 171 os.unlink(path)
161 172
162 def runEditor(self, editor_args_opt, content_file_path, 173 def runEditor(
163 content_file_obj, meta_file_path=None, meta_ori=None): 174 self,
175 editor_args_opt,
176 content_file_path,
177 content_file_obj,
178 meta_file_path=None,
179 meta_ori=None,
180 ):
164 """run editor to edit content and metadata 181 """run editor to edit content and metadata
165 182
166 @param editor_args_opt(unicode): option in [jp] section in configuration for 183 @param editor_args_opt(unicode): option in [jp] section in configuration for
167 specific args 184 specific args
168 @param content_file_path(str): path to the content file 185 @param content_file_path(str): path to the content file
176 assert meta_file_path is None 193 assert meta_file_path is None
177 assert meta_ori is None 194 assert meta_ori is None
178 195
179 # we calculate hashes to check for modifications 196 # we calculate hashes to check for modifications
180 import hashlib 197 import hashlib
198
181 content_file_obj.seek(0) 199 content_file_obj.seek(0)
182 tmp_ori_hash = hashlib.sha1(content_file_obj.read()).digest() 200 tmp_ori_hash = hashlib.sha1(content_file_obj.read()).digest()
183 content_file_obj.close() 201 content_file_obj.close()
184 202
185 # we prepare arguments 203 # we prepare arguments
186 editor = config.getConfig(self.sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi') 204 editor = config.getConfig(self.sat_conf, "jp", "editor") or os.getenv(
205 "EDITOR", "vi"
206 )
187 try: 207 try:
188 # is there custom arguments in sat.conf ? 208 # is there custom arguments in sat.conf ?
189 editor_args = config.getConfig(self.sat_conf, 'jp', editor_args_opt, Exception) 209 editor_args = config.getConfig(
210 self.sat_conf, "jp", editor_args_opt, Exception
211 )
190 except (NoOptionError, NoSectionError): 212 except (NoOptionError, NoSectionError):
191 # no, we check if we know the editor and have special arguments 213 # no, we check if we know the editor and have special arguments
192 if self.use_metadata: 214 if self.use_metadata:
193 editor_args = EDITOR_ARGS_MAGIC.get(os.path.basename(editor), '') 215 editor_args = EDITOR_ARGS_MAGIC.get(os.path.basename(editor), "")
194 else: 216 else:
195 editor_args = '' 217 editor_args = ""
196 parse_kwargs = {'content_file': content_file_path} 218 parse_kwargs = {"content_file": content_file_path}
197 if self.use_metadata: 219 if self.use_metadata:
198 parse_kwargs['metadata_file'] = meta_file_path 220 parse_kwargs["metadata_file"] = meta_file_path
199 args = parse_args(self.host, editor_args, **parse_kwargs) 221 args = parse_args(self.host, editor_args, **parse_kwargs)
200 if not args: 222 if not args:
201 args = [content_file_path] 223 args = [content_file_path]
202 224
203 # actual editing 225 # actual editing
204 editor_exit = subprocess.call([editor] + args) 226 editor_exit = subprocess.call([editor] + args)
205 227
206 # edition will now be checked, and data will be sent if it was a success 228 # edition will now be checked, and data will be sent if it was a success
207 if editor_exit != 0: 229 if editor_exit != 0:
208 self.disp(u"Editor exited with an error code, so temporary file has not be deleted, and item is not published.\nYou can find temporary file at {path}".format( 230 self.disp(
209 path=content_file_path), error=True) 231 u"Editor exited with an error code, so temporary file has not be deleted, and item is not published.\nYou can find temporary file at {path}".format(
232 path=content_file_path
233 ),
234 error=True,
235 )
210 else: 236 else:
211 # main content 237 # main content
212 try: 238 try:
213 with open(content_file_path, 'rb') as f: 239 with open(content_file_path, "rb") as f:
214 content = f.read() 240 content = f.read()
215 except (OSError, IOError): 241 except (OSError, IOError):
216 self.disp(u"Can read file at {content_path}, have it been deleted?\nCancelling edition".format( 242 self.disp(
217 content_path=content_file_path), error=True) 243 u"Can read file at {content_path}, have it been deleted?\nCancelling edition".format(
244 content_path=content_file_path
245 ),
246 error=True,
247 )
218 self.host.quit(C.EXIT_NOT_FOUND) 248 self.host.quit(C.EXIT_NOT_FOUND)
219 249
220 # metadata 250 # metadata
221 if self.use_metadata: 251 if self.use_metadata:
222 try: 252 try:
223 with open(meta_file_path, 'rb') as f: 253 with open(meta_file_path, "rb") as f:
224 metadata = json.load(f) 254 metadata = json.load(f)
225 except (OSError, IOError): 255 except (OSError, IOError):
226 self.disp(u"Can read file at {meta_file_path}, have it been deleted?\nCancelling edition".format( 256 self.disp(
227 content_path=content_file_path, meta_path=meta_file_path), error=True) 257 u"Can read file at {meta_file_path}, have it been deleted?\nCancelling edition".format(
258 content_path=content_file_path, meta_path=meta_file_path
259 ),
260 error=True,
261 )
228 self.host.quit(C.EXIT_NOT_FOUND) 262 self.host.quit(C.EXIT_NOT_FOUND)
229 except ValueError: 263 except ValueError:
230 self.disp(u"Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n" + 264 self.disp(
231 "You can find tmp file at {content_path} and temporary meta file at {meta_path}.".format( 265 u"Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n"
232 content_path=content_file_path, 266 + "You can find tmp file at {content_path} and temporary meta file at {meta_path}.".format(
233 meta_path=meta_file_path), error=True) 267 content_path=content_file_path, meta_path=meta_file_path
268 ),
269 error=True,
270 )
234 self.host.quit(C.EXIT_DATA_ERROR) 271 self.host.quit(C.EXIT_DATA_ERROR)
235 272
236 if self.use_metadata and not C.bool(metadata.get('publish', "true")): 273 if self.use_metadata and not C.bool(metadata.get("publish", "true")):
237 self.disp(u'Publication blocked by "publish" key in metadata, cancelling edition.\n\n' + 274 self.disp(
238 "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format( 275 u'Publication blocked by "publish" key in metadata, cancelling edition.\n\n'
239 content_path=content_file_path, meta_path=meta_file_path), error=True) 276 + "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format(
277 content_path=content_file_path, meta_path=meta_file_path
278 ),
279 error=True,
280 )
240 self.host.quit() 281 self.host.quit()
241 282
242 if len(content) == 0: 283 if len(content) == 0:
243 self.disp(u"Content is empty, cancelling the edition") 284 self.disp(u"Content is empty, cancelling the edition")
244 if not content_file_path.startswith(getTmpDir(self.sat_conf, self.cat_dir_str)): 285 if not content_file_path.startswith(
245 self.disp(u"File are not in SàT temporary hierarchy, we do not remove them", 2) 286 getTmpDir(self.sat_conf, self.cat_dir_str)
287 ):
288 self.disp(
289 u"File are not in SàT temporary hierarchy, we do not remove them",
290 2,
291 )
246 self.host.quit() 292 self.host.quit()
247 self.disp(u"Deletion of {}".format(content_file_path.decode('utf-8')), 2) 293 self.disp(u"Deletion of {}".format(content_file_path.decode("utf-8")), 2)
248 os.unlink(content_file_path) 294 os.unlink(content_file_path)
249 if self.use_metadata: 295 if self.use_metadata:
250 self.disp(u"Deletion of {}".format(meta_file_path.decode('utf-8')), 2) 296 self.disp(u"Deletion of {}".format(meta_file_path.decode("utf-8")), 2)
251 os.unlink(meta_file_path) 297 os.unlink(meta_file_path)
252 self.host.quit() 298 self.host.quit()
253 299
254 # time to re-check the hash 300 # time to re-check the hash
255 elif (tmp_ori_hash == hashlib.sha1(content).digest() and 301 elif tmp_ori_hash == hashlib.sha1(content).digest() and (
256 (not self.use_metadata or meta_ori == metadata)): 302 not self.use_metadata or meta_ori == metadata
303 ):
257 self.disp(u"The content has not been modified, cancelling the edition") 304 self.disp(u"The content has not been modified, cancelling the edition")
258 self.host.quit() 305 self.host.quit()
259 306
260 else: 307 else:
261 # we can now send the item 308 # we can now send the item
262 content = content.decode('utf-8-sig') # we use utf-8-sig to avoid BOM 309 content = content.decode("utf-8-sig") # we use utf-8-sig to avoid BOM
263 try: 310 try:
264 if self.use_metadata: 311 if self.use_metadata:
265 self.publish(content, metadata) 312 self.publish(content, metadata)
266 else: 313 else:
267 self.publish(content) 314 self.publish(content)
268 except Exception as e: 315 except Exception as e:
269 if self.use_metadata: 316 if self.use_metadata:
270 self.disp(u"Error while sending your item, the temporary files have been kept at {content_path} and {meta_path}: {reason}".format( 317 self.disp(
271 content_path=content_file_path, meta_path=meta_file_path, reason=e), error=True) 318 u"Error while sending your item, the temporary files have been kept at {content_path} and {meta_path}: {reason}".format(
319 content_path=content_file_path,
320 meta_path=meta_file_path,
321 reason=e,
322 ),
323 error=True,
324 )
272 else: 325 else:
273 self.disp(u"Error while sending your item, the temporary file has been kept at {content_path}: {reason}".format( 326 self.disp(
274 content_path=content_file_path, reason=e), error=True) 327 u"Error while sending your item, the temporary file has been kept at {content_path}: {reason}".format(
328 content_path=content_file_path, reason=e
329 ),
330 error=True,
331 )
275 self.host.quit(1) 332 self.host.quit(1)
276 333
277 self.secureUnlink(content_file_path) 334 self.secureUnlink(content_file_path)
278 if self.use_metadata: 335 if self.use_metadata:
279 self.secureUnlink(meta_file_path) 336 self.secureUnlink(meta_file_path)
286 """Create a temporary file 343 """Create a temporary file
287 344
288 @param suff (str): suffix to use for the filename 345 @param suff (str): suffix to use for the filename
289 @return (tuple(file, str)): opened (w+b) file object and file path 346 @return (tuple(file, str)): opened (w+b) file object and file path
290 """ 347 """
291 suff = '.' + self.getTmpSuff() 348 suff = "." + self.getTmpSuff()
292 cat_dir_str = self.cat_dir_str 349 cat_dir_str = self.cat_dir_str
293 tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, self.profile.encode('utf-8')) 350 tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, self.profile.encode("utf-8"))
294 if not os.path.exists(tmp_dir): 351 if not os.path.exists(tmp_dir):
295 try: 352 try:
296 os.makedirs(tmp_dir) 353 os.makedirs(tmp_dir)
297 except OSError as e: 354 except OSError as e:
298 self.disp(u"Can't create {path} directory: {reason}".format( 355 self.disp(
299 path=tmp_dir, reason=e), error=True) 356 u"Can't create {path} directory: {reason}".format(
357 path=tmp_dir, reason=e
358 ),
359 error=True,
360 )
300 self.host.quit(1) 361 self.host.quit(1)
301 try: 362 try:
302 fd, path = tempfile.mkstemp(suffix=suff.encode('utf-8'), 363 fd, path = tempfile.mkstemp(
303 prefix=time.strftime(cat_dir_str + '_%Y-%m-%d_%H:%M:%S_'), 364 suffix=suff.encode("utf-8"),
304 dir=tmp_dir, text=True) 365 prefix=time.strftime(cat_dir_str + "_%Y-%m-%d_%H:%M:%S_"),
305 return os.fdopen(fd, 'w+b'), path 366 dir=tmp_dir,
367 text=True,
368 )
369 return os.fdopen(fd, "w+b"), path
306 except OSError as e: 370 except OSError as e:
307 self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True) 371 self.disp(
372 u"Can't create temporary file: {reason}".format(reason=e), error=True
373 )
308 self.host.quit(1) 374 self.host.quit(1)
309 375
310 def getCurrentFile(self, profile): 376 def getCurrentFile(self, profile):
311 """Get most recently edited file 377 """Get most recently edited file
312 378
315 """ 381 """
316 # we guess the item currently edited by choosing 382 # we guess the item currently edited by choosing
317 # the most recent file corresponding to temp file pattern 383 # the most recent file corresponding to temp file pattern
318 # in tmp_dir, excluding metadata files 384 # in tmp_dir, excluding metadata files
319 cat_dir_str = self.cat_dir_str 385 cat_dir_str = self.cat_dir_str
320 tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, profile.encode('utf-8')) 386 tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, profile.encode("utf-8"))
321 available = [path for path in glob.glob(os.path.join(tmp_dir, cat_dir_str + '_*')) if not path.endswith(METADATA_SUFF)] 387 available = [
388 path
389 for path in glob.glob(os.path.join(tmp_dir, cat_dir_str + "_*"))
390 if not path.endswith(METADATA_SUFF)
391 ]
322 if not available: 392 if not available:
323 self.disp(u"Could not find any content draft in {path}".format(path=tmp_dir), error=True) 393 self.disp(
394 u"Could not find any content draft in {path}".format(path=tmp_dir),
395 error=True,
396 )
324 self.host.quit(1) 397 self.host.quit(1)
325 return max(available, key=lambda path: os.stat(path).st_mtime) 398 return max(available, key=lambda path: os.stat(path).st_mtime)
326 399
327 def getItemData(self, service, node, item): 400 def getItemData(self, service, node, item):
328 """return formatted content, metadata (or not if use_metadata is false), and item id""" 401 """return formatted content, metadata (or not if use_metadata is false), and item id"""
329 raise NotImplementedError 402 raise NotImplementedError
330 403
331 def getTmpSuff(self): 404 def getTmpSuff(self):
332 """return suffix used for content file""" 405 """return suffix used for content file"""
333 return u'xml' 406 return u"xml"
334 407
335 def getItemPath(self): 408 def getItemPath(self):
336 """retrieve item path (i.e. service and node) from item argument 409 """retrieve item path (i.e. service and node) from item argument
337 410
338 This method is obviously only useful for edition of PubSub based features 411 This method is obviously only useful for edition of PubSub based features
343 last_item = self.args.last_item 416 last_item = self.args.last_item
344 417
345 if self.args.current: 418 if self.args.current:
346 # user wants to continue current draft 419 # user wants to continue current draft
347 content_file_path = self.getCurrentFile(self.profile) 420 content_file_path = self.getCurrentFile(self.profile)
348 self.disp(u'Continuing edition of current draft', 2) 421 self.disp(u"Continuing edition of current draft", 2)
349 content_file_obj = open(content_file_path, 'r+b') 422 content_file_obj = open(content_file_path, "r+b")
350 # we seek at the end of file in case of an item already exist 423 # we seek at the end of file in case of an item already exist
351 # this will write content of the existing item at the end of the draft. 424 # this will write content of the existing item at the end of the draft.
352 # This way no data should be lost. 425 # This way no data should be lost.
353 content_file_obj.seek(0, os.SEEK_END) 426 content_file_obj.seek(0, os.SEEK_END)
354 elif self.args.draft_path: 427 elif self.args.draft_path:
355 # there is an existing draft that we use 428 # there is an existing draft that we use
356 content_file_path = os.path.expanduser(self.args.item) 429 content_file_path = os.path.expanduser(self.args.item)
357 content_file_obj = open(content_file_path, 'r+b') 430 content_file_obj = open(content_file_path, "r+b")
358 # we seek at the end for the same reason as above 431 # we seek at the end for the same reason as above
359 content_file_obj.seek(0, os.SEEK_END) 432 content_file_obj.seek(0, os.SEEK_END)
360 else: 433 else:
361 # we need a temporary file 434 # we need a temporary file
362 content_file_obj, content_file_path = self.getTmpFile() 435 content_file_obj, content_file_path = self.getTmpFile()
363 436
364 if item or last_item: 437 if item or last_item:
365 self.disp(u'Editing requested published item', 2) 438 self.disp(u"Editing requested published item", 2)
366 try: 439 try:
367 if self.use_metadata: 440 if self.use_metadata:
368 content, metadata, item = self.getItemData(service, node, item) 441 content, metadata, item = self.getItemData(service, node, item)
369 else: 442 else:
370 content, item = self.getItemData(service, node, item) 443 content, item = self.getItemData(service, node, item)
371 except Exception as e: 444 except Exception as e:
372 # FIXME: ugly but we have not good may to check errors in bridge 445 # FIXME: ugly but we have not good may to check errors in bridge
373 if u'item-not-found' in unicode(e): 446 if u"item-not-found" in unicode(e):
374 # item doesn't exist, we create a new one with requested id 447 #  item doesn't exist, we create a new one with requested id
375 metadata = None 448 metadata = None
376 if last_item: 449 if last_item:
377 self.disp(_(u'no item found at all, we create a new one'), 2) 450 self.disp(_(u"no item found at all, we create a new one"), 2)
378 else: 451 else:
379 self.disp(_(u'item "{item_id}" not found, we create a new item with this id').format(item_id=item), 2) 452 self.disp(
453 _(
454 u'item "{item_id}" not found, we create a new item with this id'
455 ).format(item_id=item),
456 2,
457 )
380 content_file_obj.seek(0) 458 content_file_obj.seek(0)
381 else: 459 else:
382 self.disp(u"Error while retrieving item: {}".format(e)) 460 self.disp(u"Error while retrieving item: {}".format(e))
383 self.host.quit(C.EXIT_ERROR) 461 self.host.quit(C.EXIT_ERROR)
384 else: 462 else:
385 # item exists, we write content 463 # item exists, we write content
386 if content_file_obj.tell() != 0: 464 if content_file_obj.tell() != 0:
387 # we already have a draft, 465 # we already have a draft,
388 # we copy item content after it and add an indicator 466 # we copy item content after it and add an indicator
389 content_file_obj.write('\n*****\n') 467 content_file_obj.write("\n*****\n")
390 content_file_obj.write(content.encode('utf-8')) 468 content_file_obj.write(content.encode("utf-8"))
391 content_file_obj.seek(0) 469 content_file_obj.seek(0)
392 self.disp(_(u'item "{item_id}" found, we edit it').format(item_id=item), 2) 470 self.disp(
393 else: 471 _(u'item "{item_id}" found, we edit it').format(item_id=item), 2
394 self.disp(u'Editing a new item', 2) 472 )
473 else:
474 self.disp(u"Editing a new item", 2)
395 if self.use_metadata: 475 if self.use_metadata:
396 metadata = None 476 metadata = None
397 477
398 if self.use_metadata: 478 if self.use_metadata:
399 return service, node, item, content_file_path, content_file_obj, metadata 479 return service, node, item, content_file_path, content_file_obj, metadata
400 else: 480 else:
401 return service, node, item, content_file_path, content_file_obj 481 return service, node, item, content_file_path, content_file_obj
402 482
403 483
404 class Table(object): 484 class Table(object):
405
406 def __init__(self, host, data, headers=None, filters=None, use_buffer=False): 485 def __init__(self, host, data, headers=None, filters=None, use_buffer=False):
407 """ 486 """
408 @param data(iterable[list]): table data 487 @param data(iterable[list]): table data
409 all lines must have the same number of columns 488 all lines must have the same number of columns
410 @param headers(iterable[unicode], None): names/titles of the columns 489 @param headers(iterable[unicode], None): names/titles of the columns
419 if not None, must have same number of columns as data 498 if not None, must have same number of columns as data
420 @param use_buffer(bool): if True, bufferise output instead of printing it directly 499 @param use_buffer(bool): if True, bufferise output instead of printing it directly
421 """ 500 """
422 self.host = host 501 self.host = host
423 self._buffer = [] if use_buffer else None 502 self._buffer = [] if use_buffer else None
424 # headers are columns names/titles, can be None 503 #  headers are columns names/titles, can be None
425 self.headers = headers 504 self.headers = headers
426 # sizes fof columns without headers, 505 #  sizes fof columns without headers,
427 # headers may be larger 506 # headers may be larger
428 self.sizes = [] 507 self.sizes = []
429 # rows countains one list per row with columns values 508 #  rows countains one list per row with columns values
430 self.rows = [] 509 self.rows = []
431 510
432 size = None 511 size = None
433 if headers: 512 if headers:
434 row_cls = namedtuple('RowData', headers) 513 row_cls = namedtuple("RowData", headers)
435 else: 514 else:
436 row_cls = tuple 515 row_cls = tuple
437 516
438 for row_data in data: 517 for row_data in data:
439 new_row = [] 518 new_row = []
460 else: 539 else:
461 self.sizes[idx] = max(self.sizes[idx], col_size) 540 self.sizes[idx] = max(self.sizes[idx], col_size)
462 if size is None: 541 if size is None:
463 size = len(new_row) 542 size = len(new_row)
464 if headers is not None and len(headers) != size: 543 if headers is not None and len(headers) != size:
465 raise exceptions.DataError(u'headers size is not coherent with rows') 544 raise exceptions.DataError(u"headers size is not coherent with rows")
466 else: 545 else:
467 if len(new_row) != size: 546 if len(new_row) != size:
468 raise exceptions.DataError(u'rows size is not coherent') 547 raise exceptions.DataError(u"rows size is not coherent")
469 self.rows.append(new_row) 548 self.rows.append(new_row)
470 549
471 if not data and headers is not None: 550 if not data and headers is not None:
472 # the table is empty, we print headers at their lenght 551 #  the table is empty, we print headers at their lenght
473 self.sizes = [len(h) for h in headers] 552 self.sizes = [len(h) for h in headers]
474 553
475 @property 554 @property
476 def string(self): 555 def string(self):
477 if self._buffer is None: 556 if self._buffer is None:
478 raise exceptions.InternalError(u'buffer must be used to get a string') 557 raise exceptions.InternalError(u"buffer must be used to get a string")
479 return u'\n'.join(self._buffer) 558 return u"\n".join(self._buffer)
480 559
481 @staticmethod 560 @staticmethod
482 def readDictValues(data, keys, defaults=None): 561 def readDictValues(data, keys, defaults=None):
483 if defaults is None: 562 if defaults is None:
484 defaults = {} 563 defaults = {}
508 @param defaults(dict[unicode, unicode]): default value to use 587 @param defaults(dict[unicode, unicode]): default value to use
509 if None, an exception will be raised if not value is found 588 if None, an exception will be raised if not value is found
510 """ 589 """
511 if keys is None and headers is not None: 590 if keys is None and headers is not None:
512 # FIXME: keys are not needed with OrderedDict, 591 # FIXME: keys are not needed with OrderedDict,
513 raise exceptions.DataError(u'You must specify keys order to used headers') 592 raise exceptions.DataError(u"You must specify keys order to used headers")
514 if keys is None: 593 if keys is None:
515 keys = data[0].keys() 594 keys = data[0].keys()
516 if headers is None: 595 if headers is None:
517 headers = keys 596 headers = keys
518 filters = [filters.get(k) for k in keys] 597 filters = [filters.get(k) for k in keys]
519 return cls(host, (cls.readDictValues(d, keys, defaults) for d in data), headers, filters) 598 return cls(
520 599 host, (cls.readDictValues(d, keys, defaults) for d in data), headers, filters
521 def _headers(self, head_sep, headers, sizes, alignment=u'left', style=None): 600 )
601
602 def _headers(self, head_sep, headers, sizes, alignment=u"left", style=None):
522 """Render headers 603 """Render headers
523 604
524 @param head_sep(unicode): sequence to use as separator 605 @param head_sep(unicode): sequence to use as separator
525 @param alignment(unicode): how to align, can be left, center or right 606 @param alignment(unicode): how to align, can be left, center or right
526 @param style(unicode, iterable[unicode], None): ANSI escape sequences to apply 607 @param style(unicode, iterable[unicode], None): ANSI escape sequences to apply
530 rendered_headers = [] 611 rendered_headers = []
531 if isinstance(style, basestring): 612 if isinstance(style, basestring):
532 style = [style] 613 style = [style]
533 for idx, header in enumerate(headers): 614 for idx, header in enumerate(headers):
534 size = sizes[idx] 615 size = sizes[idx]
535 if alignment == u'left': 616 if alignment == u"left":
536 rendered = header[:size].ljust(size) 617 rendered = header[:size].ljust(size)
537 elif alignment == u'center': 618 elif alignment == u"center":
538 rendered = header[:size].center(size) 619 rendered = header[:size].center(size)
539 elif alignment == u'right': 620 elif alignment == u"right":
540 rendered = header[:size].rjust(size) 621 rendered = header[:size].rjust(size)
541 else: 622 else:
542 raise exceptions.InternalError(u'bad alignment argument') 623 raise exceptions.InternalError(u"bad alignment argument")
543 if style: 624 if style:
544 args = style + [rendered] 625 args = style + [rendered]
545 rendered = A.color(*args) 626 rendered = A.color(*args)
546 rendered_headers.append(rendered) 627 rendered_headers.append(rendered)
547 return head_sep.join(rendered_headers) 628 return head_sep.join(rendered_headers)
551 if self._buffer is not None: 632 if self._buffer is not None:
552 self._buffer.append(data) 633 self._buffer.append(data)
553 else: 634 else:
554 self.host.disp(data) 635 self.host.disp(data)
555 636
556 def display(self, 637 def display(
557 head_alignment = u'left', 638 self,
558 columns_alignment = u'left', 639 head_alignment=u"left",
559 head_style = None, 640 columns_alignment=u"left",
560 show_header=True, 641 head_style=None,
561 show_borders=True, 642 show_header=True,
562 hide_cols=None, 643 show_borders=True,
563 col_sep=u' │ ', 644 hide_cols=None,
564 top_left=u'┌', 645 col_sep=u" │ ",
565 top=u'─', 646 top_left=u"┌",
566 top_sep=u'─┬─', 647 top=u"─",
567 top_right=u'┐', 648 top_sep=u"─┬─",
568 left=u'│', 649 top_right=u"┐",
569 right=None, 650 left=u"│",
570 head_sep=None, 651 right=None,
571 head_line=u'┄', 652 head_sep=None,
572 head_line_left=u'├', 653 head_line=u"┄",
573 head_line_sep=u'┄┼┄', 654 head_line_left=u"├",
574 head_line_right=u'┤', 655 head_line_sep=u"┄┼┄",
575 bottom_left=u'└', 656 head_line_right=u"┤",
576 bottom=None, 657 bottom_left=u"└",
577 bottom_sep=u'─┴─', 658 bottom=None,
578 bottom_right=u'┘', 659 bottom_sep=u"─┴─",
579 ): 660 bottom_right=u"┘",
661 ):
580 """Print the table 662 """Print the table
581 663
582 @param show_header(bool): True if header need no be shown 664 @param show_header(bool): True if header need no be shown
583 @param show_borders(bool): True if borders need no be shown 665 @param show_borders(bool): True if borders need no be shown
584 @param hide_cols(None, iterable(unicode)): columns which should not be displayed 666 @param hide_cols(None, iterable(unicode)): columns which should not be displayed
616 if bottom is None: 698 if bottom is None:
617 bottom = top 699 bottom = top
618 if bottom_sep is None: 700 if bottom_sep is None:
619 bottom_sep = col_sep_size * bottom 701 bottom_sep = col_sep_size * bottom
620 if not show_borders: 702 if not show_borders:
621 left = right = head_line_left = head_line_right = u'' 703 left = right = head_line_left = head_line_right = u""
622 # top border 704 # top border
623 if show_borders: 705 if show_borders:
624 self._disp( 706 self._disp(
625 top_left 707 top_left + top_sep.join([top * size for size in sizes]) + top_right
626 + top_sep.join([top*size for size in sizes]) 708 )
627 + top_right
628 )
629 709
630 # headers 710 # headers
631 if show_header: 711 if show_header:
632 self._disp( 712 self._disp(
633 left 713 left
634 + self._headers(head_sep, headers, sizes, head_alignment, head_style) 714 + self._headers(head_sep, headers, sizes, head_alignment, head_style)
635 + right 715 + right
636 ) 716 )
637 # header line 717 # header line
638 self._disp( 718 self._disp(
639 head_line_left 719 head_line_left
640 + head_line_sep.join([head_line*size for size in sizes]) 720 + head_line_sep.join([head_line * size for size in sizes])
641 + head_line_right 721 + head_line_right
642 ) 722 )
643 723
644 # content 724 # content
645 if columns_alignment == u'left': 725 if columns_alignment == u"left":
646 alignment = lambda idx, s: ansi_ljust(s, sizes[idx]) 726 alignment = lambda idx, s: ansi_ljust(s, sizes[idx])
647 elif columns_alignment == u'center': 727 elif columns_alignment == u"center":
648 alignment = lambda idx, s: ansi_center(s, sizes[idx]) 728 alignment = lambda idx, s: ansi_center(s, sizes[idx])
649 elif columns_alignment == u'right': 729 elif columns_alignment == u"right":
650 alignment = lambda idx, s: ansi_rjust(s, sizes[idx]) 730 alignment = lambda idx, s: ansi_rjust(s, sizes[idx])
651 else: 731 else:
652 raise exceptions.InternalError(u'bad columns alignment argument') 732 raise exceptions.InternalError(u"bad columns alignment argument")
653 733
654 for row in self.rows: 734 for row in self.rows:
655 if hide_cols: 735 if hide_cols:
656 row = [v for idx,v in enumerate(row) if idx not in ignore_idx] 736 row = [v for idx, v in enumerate(row) if idx not in ignore_idx]
657 self._disp(left + col_sep.join([alignment(idx,c) for idx,c in enumerate(row)]) + right) 737 self._disp(
738 left
739 + col_sep.join([alignment(idx, c) for idx, c in enumerate(row)])
740 + right
741 )
658 742
659 if show_borders: 743 if show_borders:
660 # bottom border 744 # bottom border
661 self._disp( 745 self._disp(
662 bottom_left 746 bottom_left
663 + bottom_sep.join([bottom*size for size in sizes]) 747 + bottom_sep.join([bottom * size for size in sizes])
664 + bottom_right 748 + bottom_right
665 ) 749 )
666 # we return self so string can be used after display (table.display().string) 750 #  we return self so string can be used after display (table.display().string)
667 return self 751 return self
668 752
669 def display_blank(self, **kwargs): 753 def display_blank(self, **kwargs):
670 """Display table without visible borders""" 754 """Display table without visible borders"""
671 kwargs_ = {'col_sep':u' ', 'head_line_sep':u' ', 'show_borders':False} 755 kwargs_ = {"col_sep": u" ", "head_line_sep": u" ", "show_borders": False}
672 kwargs_.update(kwargs) 756 kwargs_.update(kwargs)
673 return self.display(**kwargs_) 757 return self.display(**kwargs_)
674 758
675 759
676 class URIFinder(object): 760 class URIFinder(object):
694 self.host = command.host 778 self.host = command.host
695 self.args = command.args 779 self.args = command.args
696 self.key = key 780 self.key = key
697 self.callback = callback 781 self.callback = callback
698 self.meta_map = meta_map 782 self.meta_map = meta_map
699 self.host.bridge.URIFind(path, 783 self.host.bridge.URIFind(
700 [key], 784 path,
701 callback=self.URIFindCb, 785 [key],
702 errback=partial(command.errback, 786 callback=self.URIFindCb,
703 msg=_(u"can't find " + key + u" URI: {}"), 787 errback=partial(
704 exit_code=C.EXIT_BRIDGE_ERRBACK)) 788 command.errback,
789 msg=_(u"can't find " + key + u" URI: {}"),
790 exit_code=C.EXIT_BRIDGE_ERRBACK,
791 ),
792 )
705 else: 793 else:
706 callback() 794 callback()
707 795
708 def setMetadataList(self, uri_data, key): 796 def setMetadataList(self, uri_data, key):
709 """Helper method to set list of values from metadata 797 """Helper method to set list of values from metadata
721 return 809 return
722 810
723 try: 811 try:
724 values = getattr(self.args, key) 812 values = getattr(self.args, key)
725 except AttributeError: 813 except AttributeError:
726 raise exceptions.InternalError(u'there is no "{key}" arguments'.format( 814 raise exceptions.InternalError(
727 key=key)) 815 u'there is no "{key}" arguments'.format(key=key)
816 )
728 else: 817 else:
729 if values is None: 818 if values is None:
730 values = [] 819 values = []
731 values.extend(json.loads(new_values_json)) 820 values.extend(json.loads(new_values_json))
732 setattr(self.args, dest, values) 821 setattr(self.args, dest, values)
733 822
734
735 def URIFindCb(self, uris_data): 823 def URIFindCb(self, uris_data):
736 try: 824 try:
737 uri_data = uris_data[self.key] 825 uri_data = uris_data[self.key]
738 except KeyError: 826 except KeyError:
739 self.host.disp(_(u"No {key} URI specified for this project, please specify service and node").format(key=self.key), error=True) 827 self.host.disp(
828 _(
829 u"No {key} URI specified for this project, please specify service and node"
830 ).format(key=self.key),
831 error=True,
832 )
740 self.host.quit(C.EXIT_NOT_FOUND) 833 self.host.quit(C.EXIT_NOT_FOUND)
741 else: 834 else:
742 uri = uri_data[u'uri'] 835 uri = uri_data[u"uri"]
743 836
744 self.setMetadataList(uri_data, u'labels') 837 self.setMetadataList(uri_data, u"labels")
745 parsed_uri = xmpp_uri.parseXMPPUri(uri) 838 parsed_uri = xmpp_uri.parseXMPPUri(uri)
746 try: 839 try:
747 self.args.service = parsed_uri[u'path'] 840 self.args.service = parsed_uri[u"path"]
748 self.args.node = parsed_uri[u'node'] 841 self.args.node = parsed_uri[u"node"]
749 except KeyError: 842 except KeyError:
750 self.host.disp(_(u"Invalid URI found: {uri}").format(uri=uri), error=True) 843 self.host.disp(_(u"Invalid URI found: {uri}").format(uri=uri), error=True)
751 self.host.quit(C.EXIT_DATA_ERROR) 844 self.host.quit(C.EXIT_DATA_ERROR)
752 self.callback() 845 self.callback()