1
0
Fork 0
pgcli/pgcli/main.py

232 lines
9.0 KiB
Python
Raw Normal View History

2014-10-12 17:31:54 +00:00
#!/usr/bin/env python
from __future__ import unicode_literals
from __future__ import print_function
2014-10-12 22:07:34 +00:00
2014-12-20 07:12:43 +00:00
import os
import traceback
2015-01-04 08:31:17 +00:00
import logging
2014-10-12 22:07:34 +00:00
import click
2014-10-12 17:31:54 +00:00
2014-10-12 17:45:35 +00:00
from prompt_toolkit import CommandLineInterface, AbortAction, Exit
2014-10-12 17:31:54 +00:00
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.prompt import DefaultPrompt
from prompt_toolkit.layout.menus import CompletionsMenu
2014-11-23 23:31:34 +00:00
from prompt_toolkit.history import FileHistory
from prompt_toolkit.key_bindings.emacs import emacs_bindings
2014-11-24 07:30:17 +00:00
from pygments.lexers.sql import SqlLexer
from .packages.tabulate import tabulate
from .packages.expanded import expanded_table
from .packages.pgspecial import (CASE_SENSITIVE_COMMANDS,
NON_CASE_SENSITIVE_COMMANDS, is_expanded_output)
from .pgcompleter import PGCompleter
from .pgtoolbar import PGToolbar
from .pgstyle import PGStyle
from .pgexecute import PGExecute
from .pgline import PGLine
from .config import write_default_config, load_config
from .key_bindings import pgcli_bindings
2014-10-12 17:31:54 +00:00
from urlparse import urlparse
from getpass import getuser
from psycopg2 import OperationalError
2015-01-04 08:31:17 +00:00
_logger = logging.getLogger(__name__)
2014-10-12 22:07:34 +00:00
@click.command()
# Default host is '' so psycopg2 can default to either localhost or unix socket
@click.option('-h', '--host', default='', envvar='PGHOST',
help='Host address of the postgres database.')
2014-12-11 18:10:26 +00:00
@click.option('-p', '--port', default=5432, help='Port number at which the '
'postgres instance is listening.', envvar='PGPORT')
@click.option('-U', '--user', envvar='PGUSER', help='User name to '
2014-12-11 18:10:26 +00:00
'connect to the postgres database.')
@click.option('-W', '--password', 'prompt_passwd', is_flag=True, default=False,
help='Force password prompt.')
@click.option('-w', '--no-password', 'never_prompt', is_flag=True,
default=False, help='Never issue a password prompt')
@click.argument('database', default='', envvar='PGDATABASE')
def cli(database, user, host, port, prompt_passwd, never_prompt):
passwd = ''
if not database:
#default to current OS username just like psql
database = user = getuser()
elif '://' in database:
#a URI connection string
parsed = urlparse(database)
database = parsed.path[1:] # ignore the leading fwd slash
user = parsed.username
passwd = parsed.password
port = parsed.port
host = parsed.hostname
# Prompt for a password immediately if requested via the -W flag. This
# avoids wasting time trying to connect to the database and catching a
# no-password exception.
# If we successfully parsed a password from a URI, there's no need to prompt
# for it, even with the -W flag
if prompt_passwd and not passwd:
2014-12-24 23:11:47 +00:00
passwd = click.prompt('Password', hide_input=True, show_default=False,
type=str)
# Prompt for a password after 1st attempt to connect without a password
# fails. Don't prompt if the -w flag is supplied
auto_passwd_prompt = not passwd and not never_prompt
2014-12-24 23:11:47 +00:00
2014-11-27 23:02:54 +00:00
from pgcli import __file__ as package_root
package_root = os.path.dirname(package_root)
default_config = os.path.join(package_root, 'pgclirc')
write_default_config(default_config, '~/.pgclirc')
# Load config.
config = load_config('~/.pgclirc', default_config)
smart_completion = config.getboolean('main', 'smart_completion')
2014-12-29 22:15:23 +00:00
multi_line = config.getboolean('main', 'multi_line')
2015-01-04 08:31:17 +00:00
log_file = config.get('main', 'log_file')
log_level = config.get('main', 'log_level')
2014-11-27 23:02:54 +00:00
2015-01-04 08:31:17 +00:00
initialize_logging(log_file, log_level)
2014-12-20 07:12:43 +00:00
2015-01-04 22:52:04 +00:00
original_less_opts = adjust_less_opts()
_logger.debug('Launch Params: \n'
'\tdatabase: %r'
'\tuser: %r'
'\tpassword: %r'
'\thost: %r'
'\tport: %r', database, user, passwd, host, port)
2014-12-20 07:12:43 +00:00
# Attempt to connect to the database.
# Note that passwd may be empty on the first attempt. If connection fails
# because of a missing password, but we're allowed to prompt for a password,
# (no -w flag), prompt for a passwd and try again.
2014-11-23 08:09:00 +00:00
try:
try:
pgexecute = PGExecute(database, user, passwd, host, port)
except OperationalError as e:
if 'no password supplied' in e.message and auto_passwd_prompt:
passwd = click.prompt('Password', hide_input=True,
show_default=False, type=str)
pgexecute = PGExecute(database, user, passwd, host, port)
else:
raise e
except Exception as e: # Connecting to a database could fail.
_logger.debug('Database connection failed: %r.', e)
click.secho(str(e), err=True, fg='red')
2014-11-23 08:09:00 +00:00
exit(1)
layout = Layout(before_input=DefaultPrompt('%s> ' % pgexecute.dbname),
menus=[CompletionsMenu(max_height=10)],
lexer=SqlLexer,
bottom_toolbars=[
PGToolbar()])
completer = PGCompleter(smart_completion)
completer.extend_special_commands(CASE_SENSITIVE_COMMANDS.keys())
completer.extend_special_commands(NON_CASE_SENSITIVE_COMMANDS.keys())
2015-01-04 22:52:04 +00:00
refresh_completions(pgexecute, completer)
2014-12-29 22:15:23 +00:00
line = PGLine(always_multiline=multi_line, completer=completer,
2014-11-27 23:02:54 +00:00
history=FileHistory(os.path.expanduser('~/.pgcli-history')))
cli = CommandLineInterface(style=PGStyle, layout=layout, line=line,
key_binding_factories=[emacs_bindings, pgcli_bindings])
2014-10-12 17:31:54 +00:00
2014-10-12 17:45:35 +00:00
try:
while True:
cli.layout.before_input = DefaultPrompt('%s> ' % pgexecute.dbname)
2014-10-12 17:45:35 +00:00
document = cli.read_input(on_exit=AbortAction.RAISE_EXCEPTION)
2014-12-06 06:04:39 +00:00
# The reason we check here instead of inside the pgexecute is
# because we want to raise the Exit exception which will be caught
# by the try/except block that wraps the pgexecute.run() statement.
2015-01-04 22:52:04 +00:00
if quit_command(document.text):
2014-12-06 06:04:39 +00:00
raise Exit
2014-11-23 08:09:00 +00:00
try:
2015-01-04 22:52:04 +00:00
_logger.debug('sql: %r', document.text)
res = pgexecute.run(document.text)
2014-12-20 04:57:36 +00:00
output = []
for rows, headers, status in res:
2015-01-04 22:52:04 +00:00
_logger.debug("headers: %r", headers)
_logger.debug("rows: %r", rows)
if rows:
if is_expanded_output():
output.append(expanded_table(rows, headers))
else:
output.append(tabulate(rows, headers, tablefmt='psql'))
if status: # Only print the status if it's not None.
2014-12-20 04:57:36 +00:00
output.append(status)
2015-01-04 22:52:04 +00:00
_logger.debug("status: %r", status)
click.echo_via_pager('\n'.join(output))
2014-11-23 08:09:00 +00:00
except Exception as e:
2015-01-07 06:34:19 +00:00
_logger.error("sql: %r, error: %r", document.text, e)
_logger.error("traceback: %r", traceback.format_exc())
click.secho(str(e), err=True, fg='red')
# Refresh the table names and column names if necessary.
2015-01-04 22:52:04 +00:00
if need_completion_refresh(document.text):
completer.reset_completions()
2015-01-04 22:52:04 +00:00
refresh_completions(pgexecute, completer)
2014-10-12 17:45:35 +00:00
except Exit:
print ('GoodBye!')
2015-01-04 08:31:17 +00:00
finally: # Reset the less opts back to original.
2015-01-04 22:52:04 +00:00
_logger.debug('Restoring env var LESS to %r.', original_less_opts)
2015-01-04 08:31:17 +00:00
os.environ['LESS'] = original_less_opts
def need_completion_refresh(sql):
try:
first_token = sql.split()[0]
return first_token.lower() in ('alter', 'create', 'use', '\c', 'drop')
except Exception:
return False
2015-01-04 08:31:17 +00:00
def initialize_logging(log_file, log_level):
level_map = {'CRITICAL': logging.CRITICAL,
'ERROR': logging.ERROR,
'WARNING': logging.WARNING,
'INFO': logging.INFO,
'DEBUG': logging.DEBUG
}
handler = logging.FileHandler(os.path.expanduser(log_file))
formatter = logging.Formatter('%(asctime)s (%(process)d/%(threadName)s) '
'%(name)s %(levelname)s - %(message)s')
handler.setFormatter(formatter)
_logger.addHandler(handler)
_logger.setLevel(level_map[log_level.upper()])
_logger.debug('Initializing pgcli logging.')
_logger.debug('Log file "%s".' % log_file)
2015-01-04 22:52:04 +00:00
def adjust_less_opts():
2015-01-04 08:31:17 +00:00
less_opts = os.environ.get('LESS', '')
2015-01-04 22:52:04 +00:00
_logger.debug('Original value for LESS env var: %r', less_opts)
2015-01-04 08:31:17 +00:00
if not less_opts:
os.environ['LESS'] = '-RXF'
if 'X' not in less_opts:
os.environ['LESS'] += 'X'
if 'F' not in less_opts:
os.environ['LESS'] += 'F'
return less_opts
2015-01-04 22:52:04 +00:00
def quit_command(sql):
return (sql.strip().lower() == 'exit'
or sql.strip().lower() == 'quit'
or sql.strip() == '\q'
or sql.strip() == ':q')
def refresh_completions(pgexecute, completer):
tables = pgexecute.tables()
completer.extend_table_names(tables)
for table in tables:
table = table[1:-1] if table[0] == '"' and table[-1] == '"' else table
2015-01-04 22:52:04 +00:00
completer.extend_column_names(table, pgexecute.columns(table))
completer.extend_database_names(pgexecute.databases())
if __name__ == "__main__":
cli()