Mercurial > libervia-backend
comparison frontends/src/jp/common.py @ 2532:772447ec070f
jp: pubsub options refactoring:
There is now only "use_pubsub", and specification are set using "pubsub_flags" argument when instantiating CommandBase.
Options are more Python Zen compliant by using explicit arguments for item, draft, url instead of trying to guess with magic keyword and type detection.
Pubsub node and item are now always using respecively "-n" and "-i" even when required, this way shell history can be used to change command more easily, and it's globally less confusing for user.
if --pubsub-url is used, elements can be overwritten with individual option (e.g. change item id with --item).
New "use_draft" argument in CommandBase, to re-use current draft or open a file path as draft.
Item can now be specified when using a draft. If it already exists, its content will be added to current draft (with a separator), to avoid loosing data.
common.BaseEdit.getItemPath could be simplified thanks to those changes.
Pubsub URI handling has been moved to base.py.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 21 Mar 2018 19:13:22 +0100 |
parents | 21d43eab3fb9 |
children | b27165bf160c |
comparison
equal
deleted
inserted
replaced
2531:1dfc5516dead | 2532:772447ec070f |
---|---|
19 | 19 |
20 from sat_frontends.jp.constants import Const as C | 20 from sat_frontends.jp.constants import Const as C |
21 from sat.core.i18n import _ | 21 from sat.core.i18n import _ |
22 from sat.core import exceptions | 22 from sat.core import exceptions |
23 from sat.tools.common import regex | 23 from sat.tools.common import regex |
24 from sat.tools.common import uri | |
25 from sat.tools.common.ansi import ANSI as A | 24 from sat.tools.common.ansi import ANSI as A |
26 from sat.tools import config | 25 from sat.tools import config |
27 from ConfigParser import NoSectionError, NoOptionError | 26 from ConfigParser import NoSectionError, NoOptionError |
28 from collections import namedtuple | 27 from collections import namedtuple |
29 import json | 28 import json |
103 except ValueError as e: | 102 except ValueError as e: |
104 host.disp(u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=cmd_line, reason=e)) | 103 host.disp(u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=cmd_line, reason=e)) |
105 return [] | 104 return [] |
106 | 105 |
107 | 106 |
108 def checkURI(args): | |
109 """check if args.node is an URI | |
110 | |
111 if a valid xmpp: URI is found, args.service, args.node and args.item will be set | |
112 """ | |
113 # FIXME: Q&D way to handle xmpp: uris, a generic way is needed | |
114 # and it should be merged with code in BaseEdit | |
115 if not args.service and args.node.startswith('xmpp:'): | |
116 try: | |
117 uri_data = uri.parseXMPPUri(args.node) | |
118 except ValueError: | |
119 pass | |
120 else: | |
121 if uri_data[u'type'] == 'pubsub': | |
122 args.service = uri_data[u'path'] | |
123 args.node = uri_data[u'node'] | |
124 if u'item' in uri_data: | |
125 try: | |
126 item = getattr(uri_data, 'item') | |
127 except AttributeError: | |
128 pass | |
129 else: | |
130 if item is None: | |
131 args.item = uri_data | |
132 | |
133 | |
134 class BaseEdit(object): | 107 class BaseEdit(object): |
135 u"""base class for editing commands | 108 u"""base class for editing commands |
136 | 109 |
137 This class allows to edit file for PubSub or something else. | 110 This class allows to edit file for PubSub or something else. |
138 It works with temporary files in SàT local_dir, in a "cat_dir" subdir | 111 It works with temporary files in SàT local_dir, in a "cat_dir" subdir |
139 """ | 112 """ |
140 # use_items(bool): True if items are used, will then add item related options | |
141 use_items=True | |
142 | 113 |
143 def __init__(self, host, cat_dir, use_metadata=False): | 114 def __init__(self, host, cat_dir, use_metadata=False): |
144 """ | 115 """ |
145 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration | 116 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration |
146 @param cat_dir(unicode): directory to use for drafts | 117 @param cat_dir(unicode): directory to use for drafts |
151 """ | 122 """ |
152 self.host = host | 123 self.host = host |
153 self.sat_conf = config.parseMainConf() | 124 self.sat_conf = config.parseMainConf() |
154 self.cat_dir_str = cat_dir.encode('utf-8') | 125 self.cat_dir_str = cat_dir.encode('utf-8') |
155 self.use_metadata = use_metadata | 126 self.use_metadata = use_metadata |
156 | |
157 def add_parser_options(self): | |
158 if self.use_items: | |
159 group = self.parser.add_mutually_exclusive_group() | |
160 group.add_argument("--force-item", action='store_true', help=_(u"don't use magic and take item argument as an actual item")) | |
161 group.add_argument("--last-item", action='store_true', help=_(u"take last item instead of creating a new one if no item id is found")) | |
162 | 127 |
163 def secureUnlink(self, path): | 128 def secureUnlink(self, path): |
164 """Unlink given path after keeping it for a while | 129 """Unlink given path after keeping it for a while |
165 | 130 |
166 This method is used to prevent accidental deletion of a draft | 131 This method is used to prevent accidental deletion of a draft |
363 | 328 |
364 def getTmpSuff(self): | 329 def getTmpSuff(self): |
365 """return suffix used for content file""" | 330 """return suffix used for content file""" |
366 return u'xml' | 331 return u'xml' |
367 | 332 |
368 def getItemPath(self, item): | 333 def getItemPath(self): |
369 """retrieve item path (i.e. service and node) from item argument | 334 """retrieve item path (i.e. service and node) from item argument |
370 | 335 |
371 This method is obviously only useful for edition of PubSub based features | 336 This method is obviously only useful for edition of PubSub based features |
372 service, node and item must be named like this in args | 337 """ |
373 @param item(unicode): item to get or url or magic keyword | 338 service = self.args.service |
374 item argument can be used to specify : | 339 node = self.args.node |
375 - HTTP(S) URL | 340 item = self.args.item |
376 - XMPP URL | 341 last_item = self.args.last_item |
377 - keyword, which can be: | 342 |
378 - new: create new item | 343 if self.args.current: |
379 - last: retrieve last published item | 344 # user wants to continue current draft |
380 - current: continue current local draft | 345 content_file_path = self.getCurrentFile(self.profile) |
381 - file path | 346 self.disp(u'Continuing edition of current draft', 2) |
382 - item id | 347 content_file_obj = open(content_file_path, 'r+b') |
383 """ | 348 # we seek at the end of file in case of an item already exist |
384 force_item = self.args.force_item | 349 # this will write content of the existing item at the end of the draft. |
385 if force_item and not item: | 350 # This way no data should be lost. |
386 self.parser.error(_(u"an item id must be specified if you use --force-item")) | 351 content_file_obj.seek(0, os.SEEK_END) |
387 command = item.lower() | 352 elif self.args.draft_path: |
388 pubsub_service = self.args.service | 353 # there is an existing draft that we use |
389 pubsub_node = self.args.node | 354 content_file_path = os.path.expanduser(self.args.item) |
390 pubsub_item = None | 355 content_file_obj = open(content_file_path, 'r+b') |
391 | 356 # we seek at the end for the same reason as above |
392 if not force_item and command not in ('new', 'last', 'current'): | 357 content_file_obj.seek(0, os.SEEK_END) |
393 # we have probably an URL, we try to parse it | 358 else: |
394 import urlparse | |
395 url = self.args.item | |
396 parsed_url = urlparse.urlsplit(url.encode('utf-8')) | |
397 if parsed_url.scheme.startswith('http'): | |
398 self.disp(u"{} URL found, trying to find associated xmpp: URI".format(parsed_url.scheme.upper()),1) | |
399 # HTTP URL, we try to find xmpp: links | |
400 try: | |
401 from lxml import etree | |
402 except ImportError: | |
403 self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True) | |
404 self.host.quit(1) | |
405 import urllib2 | |
406 parser = etree.HTMLParser() | |
407 try: | |
408 root = etree.parse(urllib2.urlopen(url), parser) | |
409 except etree.XMLSyntaxError as e: | |
410 self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e)) | |
411 links = [] | |
412 else: | |
413 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") | |
414 if not links: | |
415 self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True) | |
416 self.host.quit(1) | |
417 url = links[0].get('href') | |
418 parsed_url = urlparse.urlsplit(url) | |
419 | |
420 if parsed_url.scheme == 'xmpp': | |
421 if self.args.service or self.args.node: | |
422 self.parser.error(_(u"You can't use URI and --service or --node at the same time")) | |
423 | |
424 self.disp(u"XMPP URI used: {}".format(url),2) | |
425 # XXX: if we have not xmpp: URI here, we'll take the data as a file path | |
426 pubsub_service = parsed_url.path.decode('utf-8') | |
427 pubsub_data = urlparse.parse_qs(parsed_url.query) | |
428 try: | |
429 pubsub_node = pubsub_data['node'][0].decode('utf-8') | |
430 except KeyError: | |
431 self.disp(u'No node found in xmpp: URI, can\'t retrieve item', error=True) | |
432 self.host.quit(1) | |
433 pubsub_item = pubsub_data.get('item',[None])[0] | |
434 if pubsub_item is not None: | |
435 pubsub_item = pubsub_item.decode('utf-8') | |
436 if pubsub_item is None and self.args.last_item: | |
437 command = 'last' | |
438 elif pubsub_item is not None: | |
439 command = 'edit' # XXX: edit command is only used internaly, it similar to last, but with the item given in the URL | |
440 else: | |
441 command = 'new' | |
442 | |
443 if self.args.last_item: | |
444 if pubsub_item is None: | |
445 command = 'last' | |
446 elif command != 'last': | |
447 self.parser.error(_(u"--last-item can't be used with a specified item")) | |
448 | |
449 if not force_item and command in ('new', 'last', 'edit'): | |
450 # we need a temporary file | 359 # we need a temporary file |
451 content_file_obj, content_file_path = self.getTmpFile() | 360 content_file_obj, content_file_path = self.getTmpFile() |
452 if command == 'new': | 361 |
453 self.disp(u'Editing a new item', 2) | 362 if item or last_item: |
363 self.disp(u'Editing requested published item', 2) | |
364 try: | |
454 if self.use_metadata: | 365 if self.use_metadata: |
366 content, metadata, item = self.getItemData(service, node, item) | |
367 else: | |
368 content, item = self.getItemData(service, node, item) | |
369 except Exception as e: | |
370 # FIXME: ugly but we have not good may to check errors in bridge | |
371 if u'item-not-found' in unicode(e): | |
372 # item doesn't exist, we create a new one with requested id | |
455 metadata = None | 373 metadata = None |
456 elif command in ('last', 'edit'): | 374 if last_item: |
457 self.disp(u'Editing requested published item', 2) | 375 self.disp(_(u'no item found at all, we create a new one'), 2) |
458 try: | |
459 if self.use_metadata: | |
460 content, metadata, pubsub_item = self.getItemData(pubsub_service, pubsub_node, pubsub_item) | |
461 else: | 376 else: |
462 content, pubsub_item = self.getItemData(pubsub_service, pubsub_node, pubsub_item) | 377 self.disp(_(u'item "{item_id}" not found, we create a new item with this id').format(item_id=item), 2) |
463 except Exception as e: | 378 content_file_obj.seek(0) |
464 self.disp(u"Error while retrieving last item: {}".format(e)) | 379 else: |
465 self.host.quit(1) | 380 self.disp(u"Error while retrieving item: {}".format(e)) |
381 self.host.quit(C.EXIT_ERROR) | |
382 else: | |
383 # item exists, we write content | |
384 if content_file_obj.tell() != 0: | |
385 # we already have a draft, | |
386 # we copy item content after it and add an indicator | |
387 content_file_obj.write('\n*****\n') | |
466 content_file_obj.write(content.encode('utf-8')) | 388 content_file_obj.write(content.encode('utf-8')) |
467 content_file_obj.seek(0) | 389 content_file_obj.seek(0) |
468 else: | 390 self.disp(_(u'item "{item_id}" found, we edit it').format(item_id=item), 2) |
391 else: | |
392 self.disp(u'Editing a new item', 2) | |
469 if self.use_metadata: | 393 if self.use_metadata: |
470 metadata = None | 394 metadata = None |
471 if not force_item and command == 'current': | |
472 # user wants to continue current draft | |
473 content_file_path = self.getCurrentFile(self.profile) | |
474 self.disp(u'Continuing edition of current draft', 2) | |
475 content_file_obj = open(content_file_path, 'r+b') | |
476 elif not force_item and os.path.isfile(self.args.item): | |
477 # there is an existing draft that we use | |
478 content_file_path = os.path.expanduser(self.args.item) | |
479 content_file_obj = open(content_file_path, 'r+b') | |
480 else: | |
481 # last chance, it should be an item | |
482 content_file_obj, content_file_path = self.getTmpFile() | |
483 pubsub_item = self.args.item | |
484 | |
485 try: | |
486 # we try to get existing item | |
487 if self.use_metadata: | |
488 content, metadata, pubsub_item = self.getItemData(pubsub_service, pubsub_node, self.args.item) | |
489 else: | |
490 content, pubsub_item = self.getItemData(pubsub_service, pubsub_node, self.args.item) | |
491 except Exception as e: | |
492 # FIXME: ugly but we have not good may to check errors in bridge | |
493 if u'item-not-found' in unicode(e): | |
494 # item doesn't exist, we create a new one with requested id | |
495 metadata = None | |
496 self.disp(_(u'item "{item_id}" not found, we create a new item with this id').format(item_id=pubsub_item), 2) | |
497 else: | |
498 # item exists, we write content if content file | |
499 content_file_obj.write(content.encode('utf-8')) | |
500 content_file_obj.seek(0) | |
501 self.disp(_(u'item "{item_id}" found, we edit it').format(item_id=pubsub_item), 2) | |
502 | 395 |
503 if self.use_metadata: | 396 if self.use_metadata: |
504 return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj, metadata | 397 return service, node, item, content_file_path, content_file_obj, metadata |
505 else: | 398 else: |
506 return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj | 399 return service, node, item, content_file_path, content_file_obj |
507 | 400 |
508 | 401 |
509 class Table(object): | 402 class Table(object): |
510 | 403 |
511 def __init__(self, host, data, headers=None, filters=None, use_buffer=False): | 404 def __init__(self, host, data, headers=None, filters=None, use_buffer=False): |