1
0
mirror of https://github.com/dbcli/pgcli synced 2024-05-31 01:17:54 +00:00

Merge branch 'master' into master

This commit is contained in:
Amjith Ramanujam 2018-05-17 05:57:46 -07:00 committed by GitHub
commit 0643fd6534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 94 additions and 48 deletions

View File

@ -5,5 +5,5 @@
## Checklist ## Checklist
<!--- We appreciate your help and want to give you credit. Please take a moment to put an `x` in the boxes below as you complete them. --> <!--- We appreciate your help and want to give you credit. Please take a moment to put an `x` in the boxes below as you complete them. -->
- [ ] I've added this contribution to the `changelog.md`. - [ ] I've added this contribution to the `changelog.rst`.
- [ ] I've added my name to the `AUTHORS` file (or it's already there). - [ ] I've added my name to the `AUTHORS` file (or it's already there).

View File

@ -8,6 +8,7 @@ python:
install: install:
- pip install . - pip install .
- pip install -r requirements-dev.txt - pip install -r requirements-dev.txt
- pip install keyrings.alt>=3.1
script: script:
- set -e - set -e

View File

@ -76,6 +76,8 @@ Contributors:
* Pierre Giraud * Pierre Giraud
* Andrew Kuchling * Andrew Kuchling
* Dan Clark * Dan Clark
* Catherine Devlin
* Jason Ribeiro
Creator: Creator:

View File

@ -148,8 +148,8 @@ To see stdout/stderr, use the following command:
$ behave --no-capture $ behave --no-capture
PEP8 checks PEP8 checks (lint)
----------- -----------------_
When you submit a PR, the changeset is checked for pep8 compliance using When you submit a PR, the changeset is checked for pep8 compliance using
`pep8radius <https://github.com/hayd/pep8radius>`_. If you see a build failing because `pep8radius <https://github.com/hayd/pep8radius>`_. If you see a build failing because
@ -158,7 +158,7 @@ of these checks, install pep8radius and apply style fixes:
:: ::
$ pip install pep8radius $ pip install pep8radius
$ pep8radius --docformatter --diff # view a diff of proposed fixes $ pep8radius master --docformatter --diff # view a diff of proposed fixes
$ pep8radius --docformatter --in-place # apply the fixes $ pep8radius master --docformatter --in-place # apply the fixes
Then commit and push the fixes. Then commit and push the fixes.

View File

@ -48,7 +48,7 @@ Usage
or or
$ pgcli postgresql://[user[:password]@][netloc][:port][/dbname] $ pgcli postgresql://[user[:password]@][netloc][:port][/dbname][?extra=value[&other=other-value]]
Examples: Examples:
@ -56,7 +56,7 @@ Examples:
$ pgcli local_database $ pgcli local_database
$ pgcli postgres://amjith:pa$$w0rd@example.com:5432/app_db $ pgcli postgres://amjith:pa$$w0rd@example.com:5432/app_db?sslmode=verify-ca&sslrootcert=/myrootcert
Features Features
-------- --------

View File

@ -1,6 +1,11 @@
Upcoming: Upcoming:
========= =========
Features:
---------
* Add quit commands to the completion menu. (Thanks: `Jason Ribeiro`_)
* Add table formats to ``\T`` completion. (Thanks: `Jason Ribeiro`_)
Internal changes: Internal changes:
----------------- -----------------
@ -8,6 +13,11 @@ Internal changes:
* Add ``application_name`` to help identify pgcli connection to database (issue #868) (Thanks: `François Pietka`_) * Add ``application_name`` to help identify pgcli connection to database (issue #868) (Thanks: `François Pietka`_)
* Ported Destructive Warning from mycli. * Ported Destructive Warning from mycli.
Bug Fixes:
----------
* Disable pager when using \watch (#837). (Thanks: `Jason Ribeiro`_)
* Don't offer to reconnect when we can't change a param in realtime (#807). (Thanks: `Amjith Ramanujam`_)
1.9.1: 1.9.1:
====== ======
@ -746,6 +756,7 @@ Bug Fixes:
---------- ----------
* Fix the broken behavior of \d+. (Thanks: https://github.com/macobo) * Fix the broken behavior of \d+. (Thanks: https://github.com/macobo)
* Fix a crash during auto-completion. (Thanks: https://github.com/Erethon) * Fix a crash during auto-completion. (Thanks: https://github.com/Erethon)
* Avoid losing pre_run_callables on error in editing. (Thanks: https://github.com/catherinedevlin)
Improvements: Improvements:
------------- -------------
@ -814,3 +825,4 @@ Improvements:
.. _`Isank`: https://github.com/isank .. _`Isank`: https://github.com/isank
.. _`Bojan Delić`: https://github.com/delicb .. _`Bojan Delić`: https://github.com/delicb
.. _`Frederic Aoustin`: https://github.com/fraoustin .. _`Frederic Aoustin`: https://github.com/fraoustin
.. _`Jason Ribeiro`: https://github.com/jrib

View File

@ -40,6 +40,7 @@ from pygments.token import Token
from pgspecial.main import (PGSpecial, NO_QUERY, PAGER_OFF) from pgspecial.main import (PGSpecial, NO_QUERY, PAGER_OFF)
import pgspecial as special import pgspecial as special
import keyring
from .pgcompleter import PGCompleter from .pgcompleter import PGCompleter
from .pgtoolbar import create_toolbar_tokens_func from .pgtoolbar import create_toolbar_tokens_func
from .pgstyle import style_factory, style_factory_output from .pgstyle import style_factory, style_factory_output
@ -91,6 +92,10 @@ OutputSettings.__new__.__defaults__ = (
) )
class PgCliQuitError(Exception):
pass
class PGCli(object): class PGCli(object):
default_prompt = '\\u@\\h:\\d> ' default_prompt = '\\u@\\h:\\d> '
@ -202,6 +207,9 @@ class PGCli(object):
self.eventloop = create_eventloop() self.eventloop = create_eventloop()
self.cli = None self.cli = None
def quit(self):
raise PgCliQuitError
def register_special_commands(self): def register_special_commands(self):
self.pgspecial.register( self.pgspecial.register(
@ -211,6 +219,12 @@ class PGCli(object):
refresh_callback = lambda: self.refresh_completions( refresh_callback = lambda: self.refresh_completions(
persist_priorities='all') persist_priorities='all')
self.pgspecial.register(self.quit, '\\q', '\\q',
'Quit pgcli.', arg_type=NO_QUERY, case_sensitive=True,
aliases=(':q',))
self.pgspecial.register(self.quit, 'quit', 'quit',
'Quit pgcli.', arg_type=NO_QUERY, case_sensitive=False,
aliases=('exit',))
self.pgspecial.register(refresh_callback, '\\#', '\\#', self.pgspecial.register(refresh_callback, '\\#', '\\#',
'Refresh auto-completions.', arg_type=NO_QUERY) 'Refresh auto-completions.', arg_type=NO_QUERY)
self.pgspecial.register(refresh_callback, '\\refresh', '\\refresh', self.pgspecial.register(refresh_callback, '\\refresh', '\\refresh',
@ -367,7 +381,7 @@ class PGCli(object):
user=fixup_possible_percent_encoding(uri.username), user=fixup_possible_percent_encoding(uri.username),
port=fixup_possible_percent_encoding(uri.port), port=fixup_possible_percent_encoding(uri.port),
passwd=fixup_possible_percent_encoding(uri.password)) passwd=fixup_possible_percent_encoding(uri.password))
# Deal with extra params e.g. ?sslmode=verify-ca&ssl-cert=/mycert # Deal with extra params e.g. ?sslmode=verify-ca&sslrootcert=/myrootcert
if uri.query: if uri.query:
arguments = dict( arguments = dict(
{k: v for k, (v,) in parse_qs(uri.query).items()}, {k: v for k, (v,) in parse_qs(uri.query).items()},
@ -391,6 +405,11 @@ class PGCli(object):
if not self.force_passwd_prompt and not passwd: if not self.force_passwd_prompt and not passwd:
passwd = os.environ.get('PGPASSWORD', '') passwd = os.environ.get('PGPASSWORD', '')
# Find password from store
key = '%s@%s' % (user, host)
if not passwd:
passwd = keyring.get_password('pgcli', key)
# Prompt for a password immediately if requested via the -W flag. This # Prompt for a password immediately if requested via the -W flag. This
# avoids wasting time trying to connect to the database and catching a # avoids wasting time trying to connect to the database and catching a
# no-password exception. # no-password exception.
@ -412,6 +431,8 @@ class PGCli(object):
try: try:
pgexecute = PGExecute(database, user, passwd, host, port, dsn, pgexecute = PGExecute(database, user, passwd, host, port, dsn,
application_name='pgcli', **kwargs) application_name='pgcli', **kwargs)
if passwd:
keyring.set_password('pgcli', key, passwd)
except (OperationalError, InterfaceError) as e: except (OperationalError, InterfaceError) as e:
if ('no password supplied' in utf8tounicode(e.args[0]) and if ('no password supplied' in utf8tounicode(e.args[0]) and
auto_passwd_prompt): auto_passwd_prompt):
@ -421,6 +442,8 @@ class PGCli(object):
pgexecute = PGExecute(database, user, passwd, host, port, pgexecute = PGExecute(database, user, passwd, host, port,
dsn, application_name='pgcli', dsn, application_name='pgcli',
**kwargs) **kwargs)
if passwd:
keyring.set_password('pgcli', key, passwd)
else: else:
raise e raise e
@ -449,19 +472,22 @@ class PGCli(object):
# It's internal api of prompt_toolkit that may change. This was added to fix #668. # It's internal api of prompt_toolkit that may change. This was added to fix #668.
# We may find a better way to do it in the future. # We may find a better way to do it in the future.
saved_callables = cli.application.pre_run_callables saved_callables = cli.application.pre_run_callables
while special.editor_command(document.text): try:
filename = special.get_filename(document.text) while special.editor_command(document.text):
query = (special.get_editor_query(document.text) or filename = special.get_filename(document.text)
self.get_last_query()) query = (special.get_editor_query(document.text) or
sql, message = special.open_external_editor(filename, sql=query) self.get_last_query())
if message: sql, message = special.open_external_editor(
# Something went wrong. Raise an exception and bail. filename, sql=query)
raise RuntimeError(message) if message:
cli.current_buffer.document = Document(sql, cursor_position=len(sql)) # Something went wrong. Raise an exception and bail.
cli.application.pre_run_callables = [] raise RuntimeError(message)
document = cli.run() cli.current_buffer.document = Document(sql,
continue cursor_position=len(sql))
cli.application.pre_run_callables = saved_callables cli.application.pre_run_callables = []
document = cli.run()
finally:
cli.application.pre_run_callables = saved_callables
return document return document
def execute_command(self, text, query): def execute_command(self, text, query):
@ -489,6 +515,8 @@ class PGCli(object):
logger.error("sql: %r, error: %r", text, e) logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc()) logger.error("traceback: %r", traceback.format_exc())
self._handle_server_closed_connection() self._handle_server_closed_connection()
except PgCliQuitError as e:
raise
except Exception as e: except Exception as e:
logger.error("sql: %r, error: %r", text, e) logger.error("sql: %r, error: %r", text, e)
logger.error("traceback: %r", traceback.format_exc()) logger.error("traceback: %r", traceback.format_exc())
@ -555,13 +583,6 @@ class PGCli(object):
while True: while True:
document = self.cli.run() document = self.cli.run()
# 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.
if quit_command(document.text):
raise EOFError
try: try:
document = self.handle_editor_command(self.cli, document) document = self.handle_editor_command(self.cli, document)
except RuntimeError as e: except RuntimeError as e:
@ -573,15 +594,19 @@ class PGCli(object):
# Initialize default metaquery in case execution fails # Initialize default metaquery in case execution fails
query = MetaQuery(query=document.text, successful=False) query = MetaQuery(query=document.text, successful=False)
watch_command, timing = special.get_watch_command(document.text) self.watch_command, timing = special.get_watch_command(
if watch_command: document.text)
while watch_command: if self.watch_command:
while self.watch_command:
try: try:
query = self.execute_command(watch_command, query) query = self.execute_command(
click.echo('Waiting for {0} seconds before repeating'.format(timing)) self.watch_command, query)
click.echo(
'Waiting for {0} seconds before repeating'
.format(timing))
sleep(timing) sleep(timing)
except KeyboardInterrupt: except KeyboardInterrupt:
watch_command = None self.watch_command = None
else: else:
query = self.execute_command(document.text, query) query = self.execute_command(document.text, query)
@ -593,7 +618,7 @@ class PGCli(object):
self.query_history.append(query) self.query_history.append(query)
except EOFError: except PgCliQuitError:
if not self.less_chatty: if not self.less_chatty:
print ('Goodbye!') print ('Goodbye!')
@ -849,7 +874,7 @@ class PGCli(object):
return self.query_history[-1][0] if self.query_history else None return self.query_history[-1][0] if self.query_history else None
def echo_via_pager(self, text, color=None): def echo_via_pager(self, text, color=None):
if self.pgspecial.pager_config == PAGER_OFF: if self.pgspecial.pager_config == PAGER_OFF or self.watch_command:
click.echo(text, color=color) click.echo(text, color=color)
else: else:
click.echo_via_pager(text, color) click.echo_via_pager(text, color)
@ -1044,13 +1069,6 @@ def is_select(status):
return status.split(None, 1)[0].lower() == 'select' return status.split(None, 1)[0].lower() == 'select'
def quit_command(sql):
return (sql.strip().lower() == 'exit'
or sql.strip().lower() == 'quit'
or sql.strip() == r'\q'
or sql.strip() == ':q')
def exception_formatter(e): def exception_formatter(e):
return click.style(utf8tounicode(str(e)), fg='red') return click.style(utf8tounicode(str(e)), fg='red')

View File

@ -28,6 +28,7 @@ Schema.__new__.__defaults__ = (False,)
# used to ensure that the alias we suggest is unique # used to ensure that the alias we suggest is unique
FromClauseItem = namedtuple('FromClauseItem', 'schema table_refs local_tables') FromClauseItem = namedtuple('FromClauseItem', 'schema table_refs local_tables')
Table = namedtuple('Table', ['schema', 'table_refs', 'local_tables']) Table = namedtuple('Table', ['schema', 'table_refs', 'local_tables'])
TableFormat = namedtuple('TableFormat', [])
View = namedtuple('View', ['schema', 'table_refs']) View = namedtuple('View', ['schema', 'table_refs'])
# JoinConditions are suggested after ON, e.g. 'foo.barid = bar.barid' # JoinConditions are suggested after ON, e.g. 'foo.barid = bar.barid'
JoinCondition = namedtuple('JoinCondition', ['table_refs', 'parent']) JoinCondition = namedtuple('JoinCondition', ['table_refs', 'parent'])
@ -252,6 +253,9 @@ def suggest_special(text):
if cmd in ('\\c', '\\connect'): if cmd in ('\\c', '\\connect'):
return (Database(),) return (Database(),)
if cmd == '\\T':
return (TableFormat(),)
if cmd == '\\dn': if cmd == '\\dn':
return (Schema(),) return (Schema(),)

View File

@ -4,13 +4,15 @@ import re
from itertools import count, repeat, chain from itertools import count, repeat, chain
import operator import operator
from collections import namedtuple, defaultdict, OrderedDict from collections import namedtuple, defaultdict, OrderedDict
from cli_helpers.tabular_output import TabularOutputFormatter
from pgspecial.namedqueries import NamedQueries from pgspecial.namedqueries import NamedQueries
from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.contrib.completers import PathCompleter from prompt_toolkit.contrib.completers import PathCompleter
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
from .packages.sqlcompletion import (FromClauseItem, from .packages.sqlcompletion import (
suggest_type, Special, Database, Schema, Table, Function, Column, View, FromClauseItem, suggest_type, Special, Database, Schema, Table,
Keyword, NamedQuery, Datatype, Alias, Path, JoinCondition, Join) TableFormat, Function, Column, View, Keyword, NamedQuery,
Datatype, Alias, Path, JoinCondition, Join)
from .packages.parseutils.meta import ColumnMetadata, ForeignKey from .packages.parseutils.meta import ColumnMetadata, ForeignKey
from .packages.parseutils.utils import last_word from .packages.parseutils.utils import last_word
from .packages.parseutils.tables import TableReference from .packages.parseutils.tables import TableReference
@ -321,7 +323,8 @@ class PGCompleter(Completer):
return [] return []
prio_order = [ prio_order = [
'keyword', 'function', 'view', 'table', 'datatype', 'database', 'keyword', 'function', 'view', 'table', 'datatype', 'database',
'schema', 'column', 'table alias', 'join', 'name join', 'fk join' 'schema', 'column', 'table alias', 'join', 'name join', 'fk join',
'table format'
] ]
type_priority = prio_order.index(meta) if meta in prio_order else -1 type_priority = prio_order.index(meta) if meta in prio_order else -1
text = last_word(text, include='most_punctuations').lower() text = last_word(text, include='most_punctuations').lower()
@ -778,6 +781,9 @@ class PGCompleter(Completer):
tables = [self._make_cand(t, alias, suggestion) for t in tables] tables = [self._make_cand(t, alias, suggestion) for t in tables]
return self.find_matches(word_before_cursor, tables, meta='table') return self.find_matches(word_before_cursor, tables, meta='table')
def get_table_formats(self, _, word_before_cursor):
formats = TabularOutputFormatter().supported_formats
return self.find_matches(word_before_cursor, formats, meta='table format')
def get_view_matches(self, suggestion, word_before_cursor, alias=False): def get_view_matches(self, suggestion, word_before_cursor, alias=False):
views = self.populate_schema_objects(suggestion.schema, 'views') views = self.populate_schema_objects(suggestion.schema, 'views')
@ -861,6 +867,7 @@ class PGCompleter(Completer):
Function: get_function_matches, Function: get_function_matches,
Schema: get_schema_matches, Schema: get_schema_matches,
Table: get_table_matches, Table: get_table_matches,
TableFormat: get_table_formats,
View: get_view_matches, View: get_view_matches,
Alias: get_alias_matches, Alias: get_alias_matches,
Database: get_database_matches, Database: get_database_matches,

View File

@ -346,7 +346,8 @@ class PGExecute(object):
""" """
return (isinstance(e, psycopg2.OperationalError) and return (isinstance(e, psycopg2.OperationalError) and
(not e.pgcode or (not e.pgcode or
psycopg2.errorcodes.lookup(e.pgcode) != 'LOCK_NOT_AVAILABLE')) psycopg2.errorcodes.lookup(e.pgcode) not in
('LOCK_NOT_AVAILABLE', 'CANT_CHANGE_RUNTIME_PARAM')))
def execute_normal_sql(self, split_sql): def execute_normal_sql(self, split_sql):
"""Returns tuple (title, rows, headers, status)""" """Returns tuple (title, rows, headers, status)"""

View File

@ -21,6 +21,7 @@ install_requirements = [
'configobj >= 5.0.6', 'configobj >= 5.0.6',
'humanize >= 0.5.1', 'humanize >= 0.5.1',
'cli_helpers[styles] >= 1.0.1', 'cli_helpers[styles] >= 1.0.1',
'keyring >= 12.2.0'
] ]