Source

hydra / apps / upload.py

Full commit
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 Alexander Shorin
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.
#

import copy
import getopt
import os
import logging
import platform
import socket
import sys
import couchdb
from getpass import getpass
from couchapp.localdoc import document
from couchdb.http import extract_credentials, HTTPError
from hydra import __version__

SYSTEM = platform.system().lower()

_HELP = """Usage: %(name)s [OPTIONS] URL

Arguments:

  URL              CouchDB database URL in next form:
                   http[s]://[user[:password]@]host[:port]/dbname
                   Note that setting password in URL will make it visible in
                   shell history.

Options:

  -h, --help       Display a this help message and exit.
  -u, --user=      User name to access CouchDB. Could be also defined in URL.
                   Password will be requested.
  -t, --template=  Path to CouchApp directory that will be used as base template
                   for others. May be omitted.
  -a, --app=       Path to CouchApp directory to upload on server. May be used
                   multiple times.
""" % dict(name=os.path.basename(sys.argv[0]))

_NO_URL = """URL argument must be specified.
""" + _HELP

_USER_DUPLICATE = """Multiple users defined, couldn't decide which one to use:
%s or %s
"""


class ColoredFormatter(logging.Formatter):

    def __init__(self, fmt=None, datefmt=None):
        self.colors = {
            'CRITICAL': '\x1b[1;31m',
            'ERROR': '\x1b[1;31m',
            'INFO': '\x1b[1;32m',
            'WARN': '\x1b[1;33m',
            'WARNING': '\x1b[1;33m',
            'DEBUG': '\x1b[1;37m',
            }
        self.reset = '\033[0m'
        logging.Formatter.__init__(self, fmt, datefmt)

    def format(self, record):
        if SYSTEM == 'linux':
            record.reset = self.reset
        else:
            record.reset = ''
        record.funcName = '[%s]' % record.funcName
        levelname = record.levelname
        if SYSTEM == 'linux' and levelname in self.colors:
            color = self.colors[levelname]
            levelname = ''.join((color, levelname[0] * 2, self.reset))
            record.levelname = '[%s]' % levelname
        else:
            record.levelname = '[%s]' % (levelname[0] * 2)
        return logging.Formatter.format(self, record)


def get_logger(name, level=logging.DEBUG):
    fmt = '%(reset)s%(levelname)s  %(message)s'
    instance = logging.Logger('couchdb.audit.%s' % name, level)
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(ColoredFormatter(fmt))
    instance.addHandler(handler)
    instance.propagate = False
    return instance


root = logging.Logger('hydra')
handler = logging.StreamHandler(sys.stdout)
root.addHandler(handler)
log = get_logger('hydra.uploader')
log.setLevel(logging.INFO)


def merge_dict(dst, src):
    is_dict = lambda d: isinstance(d, dict)
    stack = [(dst, src)]
    while stack:
        current_dst, current_src = stack.pop()
        for key in current_src:
            if key not in current_dst:
                current_dst[key] = current_src[key]
            elif is_dict(current_src[key]) and is_dict(current_dst[key]):
                stack.append((current_dst[key], current_src[key]))
            elif current_src[key] != current_dst[key]:
                current_dst[key] = current_src[key]
    return dst


def upload(db, template, apps):
    if template:
        base_ddoc = document(template).doc()

    if not apps:
        apps = (
            item
            for item in os.listdir('.')
            if (os.path.isdir(item)
                and not item.startswith(('.', '_'))
                and item != template)
        )
    for app in apps:
        if template:
            ddoc = copy.deepcopy(base_ddoc)
            merge_dict(ddoc, document(app).doc())
            ddoc['couchapp']['manifest'] = sorted(
                set(ddoc['couchapp']['manifest'])
                | set(base_ddoc['couchapp']['manifest']))
        else:
            ddoc = document(app).doc()
        _id = ddoc['_id']
        if _id in db:
            ddoc['_rev'] = db.resource.head(_id)[1]['etag'].strip('"')
        ddoc['version'] = __version__
        log.info('Store couchapp %r', _id)
        db.save(ddoc)


def run(url, credentials, template, apps):
    db = couchdb.Database(url)
    if credentials:
        db.resource.credentials = credentials

    root.info('* Database: %s', db.resource.url)

    try:
        db.info()['db_name']
    except socket.error as err:
        log.error('%s: %s', err.__class__.__name__, err)
        return
    except HTTPError as err:
        log.error('%s: %s', err.__class__.__name__, err.args[0][1])
        return
    except KeyError as err:
        log.critical('%s: %s; Possible not database?',
                     err.__class__.__name__, err)
        return

    return upload(db, template, apps)


def main():
    try:
        options, arguments = getopt.gnu_getopt(
            sys.argv[1:], 'hu:t:a:', ['help', 'user=', 'template=', 'app=']
        )
    except getopt.GetoptError, err:
        sys.stdout.write(('%s\n\n' % err).capitalize())
        sys.stdout.write(_HELP)
        sys.stdout.flush()
        sys.exit(1)

    apps = []
    message = None
    template = None
    user = None
    for option, value in options:
        if option in ['-h', '--help']:
            message = _HELP
        elif option in ['-u', '--user'] and value:
            user = value
        elif option in ['-t', '--template']:
            template = value
        elif option in ['-a', '--app']:
            apps.append(value)

    if message:
        sys.stdout.write(message)
        sys.stdout.flush()
        sys.exit(0)

    if not arguments:
        sys.stdout.write(_NO_URL)
        sys.stdout.flush()
        sys.exit(1)

    url = arguments[0]
    if not url.startswith(('http://', 'https://')):
        url = 'http://' + url
    _, credentials = extract_credentials(url)

    if credentials:
        if user is not None and credentials[0] != user:
            sys.stdout.write(_USER_DUPLICATE % (credentials[0], user))
            sys.stdout.flush()
            sys.exit(1)
        credentials = list(credentials)
    elif user:
        credentials = [user]

    if credentials and len(credentials) == 1:
        credentials.append(getpass('Enter password for %s: ' % credentials[0]))

    if credentials:
        credentials = tuple(credentials)

    sys.exit(run(url, credentials, template, apps))

if __name__ == '__main__':
    main()