Пример #1
0
 def __init__(self, ecoghost, authfile):
     self.authfile = authfile  # saved for later
     self.access_token = EcogClient.read_auth(authfile)
     #
     self.ecog = EcogWiki(ecoghost, self.access_token)
Пример #2
0
 def __init__(self, ecoghost, authfile):
     self.authfile     = authfile # saved for later
     self.access_token = EcogClient.read_auth(authfile)
     #
     self.ecog = EcogWiki(ecoghost, self.access_token)
Пример #3
0
class EcogClient(object):
    def __init__(self, ecoghost, authfile):
        self.authfile = authfile  # saved for later
        self.access_token = EcogClient.read_auth(authfile)
        #
        self.ecog = EcogWiki(ecoghost, self.access_token)

    @staticmethod
    def parse_args():
        parser = argparse.ArgumentParser(
            prog='ecog',
            description='Ecogwiki client - Information in your fingertips',
            epilog=' ')
        parser.add_argument('--auth',
                            metavar='FILE',
                            dest='authfile',
                            default='.auth',
                            help='auth file storing access token')
        parser.add_argument('--host',
                            metavar='HOST',
                            dest='ecoghost',
                            default='www.ecogwiki.com',
                            help='ecogwiki server host')
        parser.add_argument('--version',
                            action='version',
                            version='%(prog)s ' + __version__,
                            default=None)

        subparsers = parser.add_subparsers(metavar='COMMAND',
                                           dest='command',
                                           title='ecogwiki commands')
        cat_parser = subparsers.add_parser('cat',
                                           help='print page in markdown')
        get_parser = subparsers.add_parser('get', help='print page in json')
        list_parser = subparsers.add_parser('list', help="list pages info")
        title_parser = subparsers.add_parser('title', help='list all titles')
        recent_parser = subparsers.add_parser(
            'recent', help='list recent modified pages')
        edit_parser = subparsers.add_parser(
            'edit',
            help='edit page with editor',
            description='Edit page with your favorite editor ($EDITOR)')
        append_parser = subparsers.add_parser(
            'append',
            help='only append text',
            description='Quickly append to page')
        memo_parser = subparsers.add_parser('memo',
                                            help='quick memo',
                                            description='Edit your daily memo')

        edit_parser.add_argument('title', metavar='TITLE', help='page title')
        edit_parser.add_argument('--template',
                                 metavar='TEXT',
                                 help='text on new file',
                                 default=None)
        edit_parser.add_argument('--comment',
                                 metavar='MSG',
                                 help='edit comment message',
                                 default='')
        memo_parser.add_argument('--comment',
                                 metavar='MSG',
                                 help='edit comment message',
                                 default='')
        get_parser.add_argument('title', metavar='TITLE', help='page title')
        cat_parser.add_argument('title', metavar='TITLE', help='page title')
        get_parser.add_argument('--revision',
                                metavar='REV',
                                help='specific revision number',
                                type=int)
        cat_parser.add_argument('--revision',
                                metavar='REV',
                                help='specific revision number',
                                type=int)
        get_parser.add_argument(
            '--format',
            metavar='FORMAT',
            help='one of [json|html|markdown|atom], json by default',
            choices=['json', 'txt', 'atom', 'markdown', 'html'],
            default='json')

        append_parser.add_argument('title', metavar='TITLE', help='page title')
        append_parser.add_argument('body',
                                   nargs='?',
                                   metavar='TEXT',
                                   help='body text. fires editor if not given',
                                   default='')
        append_parser.add_argument('--comment',
                                   metavar='MSG',
                                   help='comment message',
                                   default='')

        #
        args = parser.parse_args()

        if '://' not in args.ecoghost:
            args.ecoghost = 'http://' + args.ecoghost

        if not args.authfile.startswith('/'):
            args.authfile = os.path.join(CWD, args.authfile)

        if args.version:
            output(__version__)
            sys.exit(0)

        return args

    @staticmethod
    def read_auth(authfile):
        access_token = None
        if os.path.exists(authfile):
            # read from auth file
            logger.debug('found auth file at: %s', authfile)
            token, secret = open(authfile).read().strip().split('\n')
            access_token = oauth.Token(token, secret)
            logger.debug('access token: %s', access_token.key)
        return access_token

    @command
    def get(self, title, revision=None, format=None, **options):
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        @exit_on_HTTPError
        @try_auth_on_forbidden(self.ecog, self.authfile)
        def _command(title, revision, format):
            if format == 'markdown': format = 'txt'

            #
            _resp, content = self.ecog.get(title=title,
                                           revision=revision,
                                           format=format)

            if format == 'json':
                # sort by some key order
                key_order = [
                    "title", "revision", "updated_at", "modifier", "acl_read",
                    "acl_write", "data", "body"
                ]
                content = collections.OrderedDict(
                    sorted(content.items(),
                           key=lambda (k, v): key_order.index(k)))

                # trim body
                if len(
                        content['body']
                ) > 62:  # 79 - 4(indent) - 6("body") - 2(: ) - 2("") - 3(...)
                    content['body'] = content['body'][:62] + '...'

                content = json.dumps(content, indent=4)

            output(content)

        _command(title=title, revision=revision, format=format)

    @command
    def cat(self, title, revision=None, **options):
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        @exit_on_HTTPError
        @try_auth_on_forbidden(self.ecog, self.authfile)
        def _command(title, revision):
            _resp, content = self.ecog.cat(title=title, revision=revision)
            output(content)

        _command(title=title, revision=revision)

    @command
    def list(self, **options):
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        def _command():
            entries = self.ecog.list().entries
            max_author_width = max(len(e.author) for e in entries)
            for entry in entries:
                output("%s  %s  %s" %
                       (entry.author.ljust(max_author_width),
                        format_updated_datetime(entry), entry.title))

        _command()

    @command
    def title(self, **options):
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        def _command():
            for title in self.ecog.all():
                output(title)

        _command()

    @command
    def recent(self, **options):
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        def _command():
            entries = self.ecog.recent().entries

            def summary_size(entry):
                size = 0
                try:
                    size = len(entry.summary)
                except:
                    logger.warning('no summary for entry: %s', entry.title)
                return size

            max_author_width = max(len(e.author) for e in entries)
            summary_sizes = [summary_size(e) for e in entries]
            max_size_width = len(str(max(summary_sizes)))

            for i, entry in enumerate(entries):
                output("%s  %s  %s  %s" %
                       (entry.author.ljust(max_author_width),
                        str(summary_size(entry)).rjust(max_size_width),
                        format_updated_datetime(entry), entry.title))
            output()

        _command()

    @contextmanager
    def working_with_tempdir(self, prefix=None, **options):
        local_logger, logger_heading = logger, ''
        logger_option = options.get('logger', True)
        if logger_option is False: local_logger = None
        elif logger_option is True: local_logger = logger
        elif isinstance(logger_option, basestring):
            logger_heading = '[' + logger_option + '] '

        # create temp directory
        temp_dir = tempfile.mkdtemp(prefix=prefix)
        logger.debug('[edit] temp directory: %s', temp_dir)
        local_logger and local_logger.debug('%stemp directory: %s',
                                            logger_heading, temp_dir)

        try:
            yield temp_dir
        finally:
            try:
                os.removedirs(temp_dir)
                local_logger and local_logger.debug('%sremoved %s',
                                                    logger_heading, temp_dir)
            except OSError as e:
                local_logger and local_logger.debug(
                    '%sfailed removing temp directory %s, %s', logger_heading,
                    temp_dir, e)

    @contextmanager
    def working_with_tempfile(self,
                              dir=None,
                              prefix=None,
                              suffix=None,
                              **options):
        local_logger, logger_heading = logger, ''
        logger_option = options.get('logger', True)
        if logger_option is False: local_logger = None
        elif logger_option is True: local_logger = logger
        elif isinstance(logger_option, basestring):
            logger_heading = '[' + logger_option + '] '

        remove_on_final = options.get('remove_on_final', True)

        # create temp file
        fd, temp_file = tempfile.mkstemp(dir=dir, prefix=prefix, suffix=suffix)
        local_logger and local_logger.debug('%smeta file: %s', logger_heading,
                                            temp_file)

        try:
            yield temp_file
        finally:
            if remove_on_final:
                try:
                    os.remove(temp_file)
                    logger.debug('%sremoved %s', logger_heading, temp_file)
                except OSError as e:
                    logger.error('%sfailed removing temp json file %s (%s)',
                                 logger_heading, temp_file, e)

    @command
    def edit(self, title, template, comment, **options):
        ecog_get = try_auth_on_forbidden(self.ecog,
                                         self.authfile)(self.ecog.get)
        ecog_put = try_auth_on_forbidden(self.ecog,
                                         self.authfile)(self.ecog.put)

        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        @exit_on_HTTPError
        def _command(title, r0_template, comment=''):
            ''' open editor and send put after save 

            1. [GET] get page metadata and save temp file
            2. [GET] get page rawdata with revision and save temp file
            3. open tempfile with rawbody, and let user edit
            4. after edit is finished, confirm tempfile
            5. ask for comment
            6. [PUT] put page with new content
            7. remove temp files
            '''
            logger.info('[edit] %s', title)
            if r0_template and not r0_template.endswith("\n"):
                r0_template += "\n\n"

            #tempdir = tempfile.mkdtemp(prefix='ecogwiki-')
            #logger.debug('[edit] temp directory: %s', tempdir)
            #try:
            with self.working_with_tempdir(prefix='ecogwiki-',
                                           logger='edit') as tempdir:

                # 1. [GET] get page metadata
                _resp, jsondata = ecog_get(title, format='json')
                revision = jsondata['revision']
                updated_at = updated_datetime(
                    jsondata['updated_at']) if jsondata['updated_at'] else None

                safe_title = urllib.quote_plus(title)
                #fd, temp_json = tempfile.mkstemp(dir=tempdir, prefix=safe_title+'-', suffix='.json')
                #logger.debug('[edit] "%s" meta file: %s', title, temp_json)
                #try:
                with self.working_with_tempfile(dir=tempdir,
                                                prefix=safe_title + '-',
                                                suffix='.json',
                                                logger='edit') as temp_json:
                    with open(temp_json, 'w') as f:
                        json.dump(jsondata, f, indent=4)
                    # 2. [GET] get page rawdata
                    _resp, rawbody = ecog_get(title,
                                              format='txt',
                                              revision=revision)
                    fd, temp_rawbody0 = tempfile.mkstemp(
                        dir=tempdir,
                        prefix='%s.r%d-' % (safe_title, revision),
                        suffix='.markdown')
                    logger.debug('[edit] "%s" rawbody file: %s', title,
                                 temp_rawbody0)
                    with open(temp_rawbody0, 'w') as f:
                        if revision > 0:
                            f.write(rawbody)
                        elif r0_template:
                            f.write(r0_template)
                    # copy tempfile
                    temp_rawbody = os.path.splitext(temp_rawbody0)
                    temp_rawbody = temp_rawbody[0] + '.edit' + temp_rawbody[1]
                    shutil.copy(temp_rawbody0, temp_rawbody)
                    logger.debug('[edit] copy %s => %s', temp_rawbody0,
                                 temp_rawbody)
                    try:
                        # 3. open temp file with editor
                        ret = subprocess.call(editor.split() + [temp_rawbody])
                        if ret != 0:
                            output(
                                'editor %s failed with status %d, aborting.' %
                                (editor, ret))
                            return
                        # 4. confirm content
                        content = ''
                        with open(temp_rawbody) as f:
                            content = f.read()
                        if (revision > 0 and content == rawbody) or (
                                revision == 0 and content == r0_template):
                            output('nothing new, aborting.')
                            return
                        if len(content) == 0:
                            output('empty content, aborting.')
                            return
                        # 5. ask comment
                        if not comment:

                            def diff_lines(a_str,
                                           b_str,
                                           fromfile,
                                           tofile,
                                           fromfiledate=None,
                                           tofiledate=None):
                                options = {'n': 0}
                                if fromfile: options['fromfile'] = fromfile
                                if tofile: options['tofile'] = tofile
                                if fromfiledate:
                                    options['fromfiledate'] = fromfiledate
                                if tofiledate:
                                    options['tofiledate'] = tofiledate

                                diff = difflib.unified_diff(
                                    a_str.splitlines(True),
                                    b_str.splitlines(True), **options)
                                return diff

                            # display diff lines
                            fromfile, fromfiledate = '%s (rev %d)' % (
                                title, revision), updated_at.strftime(
                                    "%Y/%m/%d %H-%M")
                            for line in diff_lines(
                                    rawbody,
                                    content,
                                    fromfile=fromfile,
                                    tofile='EDIT',
                                    fromfiledate=updated_at.strftime(
                                        "%Y/%m/%d %H-%M")):
                                output(line, end='')
                            comment = raw_input(
                                'comment message (default: %s): ' %
                                self.ecog.DEFAULT_COMMENT)
                            comment = comment or self.ecog.DEFAULT_COMMENT
                        # 6. [PUT] put page with new content
                        resp, result = ecog_put(title,
                                                content,
                                                revision=revision,
                                                comment=comment)
                        logger.debug('[edit] %s', resp)
                        logger.debug('[edit] %s', result)
                        # TODO: add content-location
                        #output('updated "%s" to revision %d: %s' % (title, int(result['revision']), resp['content-location']))
                        output('updated "%s" to revision %d' %
                               (title, int(result['revision'])))

                        # 7. remove temp file on success
                        try:
                            os.remove(temp_rawbody)
                            logger.debug('[edit] removed %s', temp_rawbody)
                        except OSError as e:
                            logger.error(
                                '[edit] failed removing temp rawbody file %s, %s',
                                temp_rawbody, e)
                        try:
                            os.remove(temp_rawbody0)
                            logger.debug('[edit] removed %s', temp_rawbody0)
                        except OSError as e:
                            logger.error(
                                '[edit] failed removing temp rawbody file %s, %s',
                                temp_rawbody0, e)

                        return result
                    except Exception as e:
                        output(
                            'error during edit. temporary file is saved at: %s'
                            % temp_rawbody)
                        logger.error('[edit] %s', e)
                        raise
                    finally:
                        pass
                #finally:
                #    try:
                #        os.remove(temp_json)
                #        logger.debug('[edit] removed %s', temp_json)
                #    except OSError as e:
                #        logger.error('[edit] failed removing temp json file %s (%s)', temp_json, e)
            #finally:
            #    try:
            #        os.removedirs(tempdir)
            #        logger.debug('[edit] removed %s', tempdir)
            #    except OSError as e:
            #        logger.error('[edit] failed removing temp directory %s, %s', tempdir, e)

        _command(title=title, r0_template=template, comment=comment)

    @command
    def memo(self, template='', comment='', **options):
        now = datetime.datetime.now(dateutil.tz.tzlocal())
        title = 'memo/%s' % now.strftime("%Y-%m-%d")
        # TODO: add default .write ME to template
        self.edit(title=title, template=template, comment=comment)

    @command
    def append(self, title, body='', comment='', **options):
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        @exit_on_HTTPError
        @try_auth_on_forbidden(self.ecog, self.authfile)
        def _command(title, body='', comment=''):
            ''' if body is given, send post right away

            if not, open editor and send post after save 
            '''
            logger.info('[append] %s', title)
            if body:
                body = body.rstrip('\n') + '\n'
                self.ecog.post(title=title, body=body, comment=comment)
                return

            tempdir = tempfile.mkdtemp(prefix='ecogwiki-')
            logger.debug('[append] temp directory: %s', tempdir)
            try:
                safe_title = urllib.quote_plus(title)
                fd, temp_part = tempfile.mkstemp(dir=tempdir,
                                                 prefix=safe_title + '.part-',
                                                 suffix='.markdown')
                logger.debug('[append] "%s" partial file: %s', title,
                             temp_part)
                try:
                    # 3. open temp file with editor
                    ret = subprocess.call(editor.split() + [temp_part])
                    if ret != 0:
                        output('editor %s failed with status %d, aborting.' %
                               (editor, ret))
                        return
                    # 4. confirm content
                    content = ''
                    with open(temp_part) as f:
                        content = f.read()
                    if len(content) == 0:
                        output('empty content, aborting.')
                        return
                    # 5. ask comment
                    if not comment:
                        comment = raw_input('comment message (default: %s): ' %
                                            self.ecog.DEFAULT_COMMENT)
                        comment = comment or self.ecog.DEFAULT_COMMENT
                    # 6. [PUT] put page with new content
                    resp, result = self.ecog.post(title,
                                                  content,
                                                  comment=comment)
                    logger.debug('[append] %s', resp)
                    logger.debug('[append] %s', result)
                    # TODO: add content-location
                    #output('updated "%s" to revision %d: %s' % (title, int(result['revision']), resp['content-location']))
                    output('append to "%s"' % (title))

                    # 7. remove temp file on success
                    try:
                        os.remove(temp_part)
                        logger.debug('[append] removed %s', temp_part)
                    except OSError as e:
                        logger.error(
                            '[append] failed removing temp partial file %s, %s',
                            temp_part, e)

                    return result
                except Exception as e:
                    output(
                        'error during edit. temporary file is saved at: %s' %
                        temp_part)
                    logger.error('[append] %s', e)
                    raise
                finally:
                    pass
            finally:
                try:
                    os.removedirs(tempdir)
                    logger.debug('[append] removed %s', tempdir)
                except OSError as e:
                    logger.error(
                        '[append] failed removing temp directory %s, %s',
                        tempdir, e)

        _command(title, body=body, comment=comment)
Пример #4
0
class EcogClient(object):
    def __init__(self, ecoghost, authfile):
        self.authfile     = authfile # saved for later
        self.access_token = EcogClient.read_auth(authfile)
        #
        self.ecog = EcogWiki(ecoghost, self.access_token)

    @staticmethod
    def parse_args():
        parser = argparse.ArgumentParser(prog='ecog', description='Ecogwiki client - Information in your fingertips', epilog=' ')
        parser.add_argument('--auth', metavar='FILE', dest='authfile', default='.auth',
                           help='auth file storing access token')
        parser.add_argument('--host', metavar='HOST', dest='ecoghost', default='www.ecogwiki.com',
                           help='ecogwiki server host')
        parser.add_argument('--version',  action='version', version='%(prog)s ' + __version__, default=None)

        subparsers = parser.add_subparsers(metavar='COMMAND', dest='command', title='ecogwiki commands')
        cat_parser    = subparsers.add_parser('cat',    help='print page in markdown')
        get_parser    = subparsers.add_parser('get',    help='print page in json')
        list_parser   = subparsers.add_parser('list',   help="list pages info")
        title_parser  = subparsers.add_parser('title',  help='list all titles')
        recent_parser = subparsers.add_parser('recent', help='list recent modified pages')
        edit_parser   = subparsers.add_parser('edit',   help='edit page with editor', description='Edit page with your favorite editor ($EDITOR)')
        append_parser = subparsers.add_parser('append', help='only append text',      description='Quickly append to page')
        memo_parser   = subparsers.add_parser('memo',   help='quick memo',            description='Edit your daily memo')
        
        edit_parser.add_argument('title', metavar='TITLE', help='page title')
        edit_parser.add_argument('--template', metavar='TEXT', help='text on new file', default=None)
        edit_parser.add_argument('--comment',  metavar='MSG',  help='edit comment message', default='')
        memo_parser.add_argument('--comment',  metavar='MSG',  help='edit comment message', default='')
        get_parser.add_argument('title', metavar='TITLE', help='page title')
        cat_parser.add_argument('title', metavar='TITLE', help='page title')
        get_parser.add_argument('--revision', metavar='REV', help='specific revision number', type=int)
        cat_parser.add_argument('--revision', metavar='REV', help='specific revision number', type=int)
        get_parser.add_argument('--format', metavar='FORMAT', help='one of [json|html|markdown|atom], json by default',
            choices=['json', 'txt', 'atom', 'markdown', 'html'], default='json')

        append_parser.add_argument('title',           metavar='TITLE', help='page title')
        append_parser.add_argument('body', nargs='?', metavar='TEXT',  help='body text. fires editor if not given', default='')
        append_parser.add_argument('--comment',       metavar='MSG',   help='comment message', default='')

        #
        args = parser.parse_args()

        if '://' not in args.ecoghost:
            args.ecoghost = 'http://' + args.ecoghost

        if not args.authfile.startswith('/'):
            args.authfile = os.path.join(CWD, args.authfile)

        if args.version:
            output(__version__)
            sys.exit(0)

        return args

    @staticmethod
    def read_auth(authfile):
        access_token = None
        if os.path.exists(authfile):
            # read from auth file
            logger.debug('found auth file at: %s', authfile)
            token, secret = open(authfile).read().strip().split('\n')
            access_token  = oauth.Token(token, secret)
            logger.debug('access token: %s', access_token.key)
        return access_token

    @command
    def get(self, title, revision=None, format=None, **options): 
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        @exit_on_HTTPError
        @try_auth_on_forbidden(self.ecog, self.authfile)
        def _command(title, revision, format):
            if format == 'markdown': format = 'txt'

            #
            _resp,content = self.ecog.get(title=title, revision=revision, format=format)

            if format == 'json':
                # sort by some key order
                key_order = ["title", "revision", "updated_at", "modifier", "acl_read", "acl_write", "data", "body"]
                content = collections.OrderedDict(sorted(content.items(), key=lambda (k,v): key_order.index(k)))

                # trim body
                if len(content['body']) > 62: # 79 - 4(indent) - 6("body") - 2(: ) - 2("") - 3(...)
                    content['body'] = content['body'][:62] + '...'

                content = json.dumps(content, indent=4)

            output(content)

        _command(title=title, revision=revision, format=format)

    @command
    def cat(self, title, revision=None, **options): 
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        @exit_on_HTTPError
        @try_auth_on_forbidden(self.ecog, self.authfile)
        def _command(title, revision):
            _resp,content = self.ecog.cat(title=title, revision=revision)
            output(content)

        _command(title=title, revision=revision)

    @command
    def list(self, **options): 
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        def _command():
            entries = self.ecog.list().entries
            max_author_width = max(len(e.author) for e in entries)
            for entry in entries:
                output("%s  %s  %s" % (
                    entry.author.ljust(max_author_width), 
                    format_updated_datetime(entry),
                    entry.title
                ))
        _command()

    @command
    def title(self, **options): 
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        def _command():
            for title in self.ecog.all():
                output(title)
        _command()

    @command
    def recent(self, **options): 
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        def _command():
            entries = self.ecog.recent().entries

            def summary_size(entry):
                size = 0
                try:
                    size = len(entry.summary)
                except:
                    logger.warning('no summary for entry: %s', entry.title)
                return size

            max_author_width = max(len(e.author) for e in entries)
            summary_sizes    = [summary_size(e)  for e in entries]
            max_size_width   = len(str(max(summary_sizes)))

            for i,entry in enumerate(entries):
                output("%s  %s  %s  %s" % (
                    entry.author.ljust(max_author_width), 
                    str(summary_size(entry)).rjust(max_size_width), 
                    format_updated_datetime(entry),
                    entry.title
                ))
            output()
        
        _command()


    @contextmanager
    def working_with_tempdir(self, prefix=None, **options):
        local_logger, logger_heading = logger, ''
        logger_option = options.get('logger', True)
        if   logger_option is False:    local_logger = None
        elif logger_option is True:     local_logger = logger
        elif isinstance(logger_option, basestring):
            logger_heading = '[' + logger_option + '] '

        # create temp directory
        temp_dir = tempfile.mkdtemp(prefix=prefix)
        logger.debug('[edit] temp directory: %s', temp_dir)
        local_logger and local_logger.debug('%stemp directory: %s', logger_heading, temp_dir)

        try:
            yield temp_dir
        finally:
            try:
                os.removedirs(temp_dir)
                local_logger and local_logger.debug('%sremoved %s', logger_heading, temp_dir)
            except OSError as e:
                local_logger and local_logger.debug('%sfailed removing temp directory %s, %s', logger_heading, temp_dir, e)
        

    @contextmanager
    def working_with_tempfile(self, dir=None, prefix=None, suffix=None, **options):
        local_logger, logger_heading = logger, ''
        logger_option = options.get('logger', True)
        if   logger_option is False:    local_logger = None
        elif logger_option is True:     local_logger = logger
        elif isinstance(logger_option, basestring):
            logger_heading = '[' + logger_option + '] '

        remove_on_final = options.get('remove_on_final', True)

        # create temp file
        fd, temp_file = tempfile.mkstemp(dir=dir, prefix=prefix, suffix=suffix)
        local_logger and local_logger.debug('%smeta file: %s', logger_heading, temp_file)

        try:
            yield temp_file
        finally:
            if remove_on_final:
                try:
                    os.remove(temp_file)
                    logger.debug('%sremoved %s', logger_heading, temp_file)
                except OSError as e:
                    logger.error('%sfailed removing temp json file %s (%s)', logger_heading, temp_file, e)

    @command
    def edit(self, title, template, comment, **options): 
        ecog_get = try_auth_on_forbidden(self.ecog, self.authfile)(self.ecog.get)
        ecog_put = try_auth_on_forbidden(self.ecog, self.authfile)(self.ecog.put)

        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        @exit_on_HTTPError
        def _command(title, r0_template, comment=''):
            ''' open editor and send put after save 

            1. [GET] get page metadata and save temp file
            2. [GET] get page rawdata with revision and save temp file
            3. open tempfile with rawbody, and let user edit
            4. after edit is finished, confirm tempfile
            5. ask for comment
            6. [PUT] put page with new content
            7. remove temp files
            '''
            logger.info('[edit] %s', title)
            if r0_template and not r0_template.endswith("\n"):
                r0_template += "\n\n"

            #tempdir = tempfile.mkdtemp(prefix='ecogwiki-')
            #logger.debug('[edit] temp directory: %s', tempdir)
            #try:
            with self.working_with_tempdir(prefix='ecogwiki-', logger='edit') as tempdir:

                # 1. [GET] get page metadata
                _resp,jsondata = ecog_get(title, format='json')
                revision       = jsondata['revision']
                updated_at     = updated_datetime(jsondata['updated_at']) if jsondata['updated_at'] else None

                safe_title = urllib.quote_plus(title)
                #fd, temp_json = tempfile.mkstemp(dir=tempdir, prefix=safe_title+'-', suffix='.json')
                #logger.debug('[edit] "%s" meta file: %s', title, temp_json)
                #try:
                with self.working_with_tempfile(dir=tempdir, prefix=safe_title+'-', suffix='.json', logger='edit') as temp_json:
                    with open(temp_json, 'w') as f:
                        json.dump(jsondata, f, indent=4)
                    # 2. [GET] get page rawdata
                    _resp,rawbody = ecog_get(title, format='txt', revision=revision)
                    fd, temp_rawbody0 = tempfile.mkstemp(dir=tempdir, prefix='%s.r%d-' % (safe_title, revision), suffix='.markdown')
                    logger.debug('[edit] "%s" rawbody file: %s', title, temp_rawbody0)
                    with open(temp_rawbody0, 'w') as f:
                        if revision > 0:
                            f.write(rawbody)
                        elif r0_template:
                            f.write(r0_template)
                    # copy tempfile
                    temp_rawbody = os.path.splitext(temp_rawbody0)
                    temp_rawbody = temp_rawbody[0] + '.edit' + temp_rawbody[1]
                    shutil.copy(temp_rawbody0, temp_rawbody)
                    logger.debug('[edit] copy %s => %s', temp_rawbody0, temp_rawbody)
                    try:
                        # 3. open temp file with editor
                        ret = subprocess.call(editor.split() + [temp_rawbody])
                        if ret != 0:
                            output('editor %s failed with status %d, aborting.' % (editor, ret))
                            return
                        # 4. confirm content
                        content = ''
                        with open(temp_rawbody) as f:
                            content = f.read()
                        if (revision > 0 and content == rawbody) or (revision == 0 and content == r0_template):
                            output('nothing new, aborting.')
                            return
                        if len(content) == 0:
                            output('empty content, aborting.')
                            return
                        # 5. ask comment
                        if not comment:
                            def diff_lines(a_str,b_str,fromfile,tofile,fromfiledate=None,tofiledate=None):
                                options = { 'n': 0 }
                                if fromfile:     options['fromfile']     = fromfile
                                if tofile:       options['tofile']       = tofile
                                if fromfiledate: options['fromfiledate'] = fromfiledate
                                if tofiledate:   options['tofiledate']   = tofiledate

                                diff = difflib.unified_diff(a_str.splitlines(True),b_str.splitlines(True), **options)
                                return diff

                            # display diff lines
                            fromfile,fromfiledate = '%s (rev %d)' % (title, revision), updated_at.strftime("%Y/%m/%d %H-%M")
                            for line in diff_lines(rawbody,content, fromfile=fromfile, tofile='EDIT', 
                                    fromfiledate=updated_at.strftime("%Y/%m/%d %H-%M")):
                                output(line, end='')
                            comment = raw_input('comment message (default: %s): ' % self.ecog.DEFAULT_COMMENT)
                            comment = comment or self.ecog.DEFAULT_COMMENT
                        # 6. [PUT] put page with new content
                        resp,result = ecog_put(title, content, revision=revision, comment=comment)
                        logger.debug('[edit] %s', resp)
                        logger.debug('[edit] %s', result)
                        # TODO: add content-location
                        #output('updated "%s" to revision %d: %s' % (title, int(result['revision']), resp['content-location']))
                        output('updated "%s" to revision %d' % (title, int(result['revision'])))

                        # 7. remove temp file on success
                        try:
                            os.remove(temp_rawbody)
                            logger.debug('[edit] removed %s', temp_rawbody)
                        except OSError as e:
                            logger.error('[edit] failed removing temp rawbody file %s, %s', temp_rawbody, e)
                        try:
                            os.remove(temp_rawbody0)
                            logger.debug('[edit] removed %s', temp_rawbody0)
                        except OSError as e:
                            logger.error('[edit] failed removing temp rawbody file %s, %s', temp_rawbody0, e)

                        return result
                    except Exception as e:
                        output('error during edit. temporary file is saved at: %s' % temp_rawbody)
                        logger.error('[edit] %s', e)
                        raise
                    finally:
                        pass
                #finally:
                #    try:
                #        os.remove(temp_json)
                #        logger.debug('[edit] removed %s', temp_json)
                #    except OSError as e:
                #        logger.error('[edit] failed removing temp json file %s (%s)', temp_json, e)
            #finally:
            #    try:
            #        os.removedirs(tempdir)
            #        logger.debug('[edit] removed %s', tempdir)
            #    except OSError as e:
            #        logger.error('[edit] failed removing temp directory %s, %s', tempdir, e)

        _command(title=title, r0_template=template, comment=comment)

    @command
    def memo(self, template='', comment='', **options): 
        now = datetime.datetime.now(dateutil.tz.tzlocal())
        title = 'memo/%s' % now.strftime("%Y-%m-%d")
        # TODO: add default .write ME to template
        self.edit(title=title, template=template, comment=comment)

    @command
    def append(self, title, body='', comment='', **options):
        @exit_on_exception
        @terminate_on_KeyboardInterrupt
        @exit_on_HTTPError
        @try_auth_on_forbidden(self.ecog, self.authfile)
        def _command(title, body='', comment=''):
            ''' if body is given, send post right away

            if not, open editor and send post after save 
            '''
            logger.info('[append] %s', title)
            if body:
                body = body.rstrip('\n') + '\n'
                self.ecog.post(title=title, body=body, comment=comment)
                return

            tempdir = tempfile.mkdtemp(prefix='ecogwiki-')
            logger.debug('[append] temp directory: %s', tempdir)
            try:
                safe_title = urllib.quote_plus(title)
                fd, temp_part = tempfile.mkstemp(dir=tempdir, prefix=safe_title+'.part-', suffix='.markdown')
                logger.debug('[append] "%s" partial file: %s', title, temp_part)
                try:
                    # 3. open temp file with editor
                    ret = subprocess.call(editor.split() + [temp_part])
                    if ret != 0:
                        output('editor %s failed with status %d, aborting.' % (editor, ret))
                        return
                    # 4. confirm content
                    content = ''
                    with open(temp_part) as f:
                        content = f.read()
                    if len(content) == 0:
                        output('empty content, aborting.')
                        return
                    # 5. ask comment
                    if not comment:
                        comment = raw_input('comment message (default: %s): ' % self.ecog.DEFAULT_COMMENT)
                        comment = comment or self.ecog.DEFAULT_COMMENT
                    # 6. [PUT] put page with new content
                    resp,result = self.ecog.post(title, content, comment=comment)
                    logger.debug('[append] %s', resp)
                    logger.debug('[append] %s', result)
                    # TODO: add content-location
                    #output('updated "%s" to revision %d: %s' % (title, int(result['revision']), resp['content-location']))
                    output('append to "%s"' % (title))

                    # 7. remove temp file on success
                    try:
                        os.remove(temp_part)
                        logger.debug('[append] removed %s', temp_part)
                    except OSError as e:
                        logger.error('[append] failed removing temp partial file %s, %s', temp_part, e)

                    return result
                except Exception as e:
                    output('error during edit. temporary file is saved at: %s' % temp_part)
                    logger.error('[append] %s', e)
                    raise
                finally:
                    pass
            finally:
                try:
                    os.removedirs(tempdir)
                    logger.debug('[append] removed %s', tempdir)
                except OSError as e:
                    logger.error('[append] failed removing temp directory %s, %s', tempdir, e)

        _command(title, body=body, comment=comment)