Mercurial > libervia-backend
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() |