mirror of https://github.com/dbcli/pgcli
Merge branch 'master' into master
This commit is contained in:
commit
0643fd6534
|
@ -5,5 +5,5 @@
|
|||
|
||||
## 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. -->
|
||||
- [ ] 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).
|
||||
|
|
|
@ -8,6 +8,7 @@ python:
|
|||
install:
|
||||
- pip install .
|
||||
- pip install -r requirements-dev.txt
|
||||
- pip install keyrings.alt>=3.1
|
||||
|
||||
script:
|
||||
- set -e
|
||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -76,6 +76,8 @@ Contributors:
|
|||
* Pierre Giraud
|
||||
* Andrew Kuchling
|
||||
* Dan Clark
|
||||
* Catherine Devlin
|
||||
* Jason Ribeiro
|
||||
|
||||
|
||||
Creator:
|
||||
|
|
|
@ -148,8 +148,8 @@ To see stdout/stderr, use the following command:
|
|||
$ behave --no-capture
|
||||
|
||||
|
||||
PEP8 checks
|
||||
-----------
|
||||
PEP8 checks (lint)
|
||||
-----------------_
|
||||
|
||||
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
|
||||
|
@ -158,7 +158,7 @@ of these checks, install pep8radius and apply style fixes:
|
|||
::
|
||||
|
||||
$ pip install pep8radius
|
||||
$ pep8radius --docformatter --diff # view a diff of proposed fixes
|
||||
$ pep8radius --docformatter --in-place # apply the fixes
|
||||
$ pep8radius master --docformatter --diff # view a diff of proposed fixes
|
||||
$ pep8radius master --docformatter --in-place # apply the fixes
|
||||
|
||||
Then commit and push the fixes.
|
||||
|
|
|
@ -48,7 +48,7 @@ Usage
|
|||
|
||||
or
|
||||
|
||||
$ pgcli postgresql://[user[:password]@][netloc][:port][/dbname]
|
||||
$ pgcli postgresql://[user[:password]@][netloc][:port][/dbname][?extra=value[&other=other-value]]
|
||||
|
||||
Examples:
|
||||
|
||||
|
@ -56,7 +56,7 @@ Examples:
|
|||
|
||||
$ 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
|
||||
--------
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
Upcoming:
|
||||
=========
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Add quit commands to the completion menu. (Thanks: `Jason Ribeiro`_)
|
||||
* Add table formats to ``\T`` completion. (Thanks: `Jason Ribeiro`_)
|
||||
|
||||
Internal changes:
|
||||
-----------------
|
||||
|
||||
|
@ -8,6 +13,11 @@ Internal changes:
|
|||
* Add ``application_name`` to help identify pgcli connection to database (issue #868) (Thanks: `François Pietka`_)
|
||||
* 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:
|
||||
======
|
||||
|
||||
|
@ -746,6 +756,7 @@ Bug Fixes:
|
|||
----------
|
||||
* Fix the broken behavior of \d+. (Thanks: https://github.com/macobo)
|
||||
* 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:
|
||||
-------------
|
||||
|
@ -814,3 +825,4 @@ Improvements:
|
|||
.. _`Isank`: https://github.com/isank
|
||||
.. _`Bojan Delić`: https://github.com/delicb
|
||||
.. _`Frederic Aoustin`: https://github.com/fraoustin
|
||||
.. _`Jason Ribeiro`: https://github.com/jrib
|
||||
|
|
|
@ -40,6 +40,7 @@ from pygments.token import Token
|
|||
|
||||
from pgspecial.main import (PGSpecial, NO_QUERY, PAGER_OFF)
|
||||
import pgspecial as special
|
||||
import keyring
|
||||
from .pgcompleter import PGCompleter
|
||||
from .pgtoolbar import create_toolbar_tokens_func
|
||||
from .pgstyle import style_factory, style_factory_output
|
||||
|
@ -91,6 +92,10 @@ OutputSettings.__new__.__defaults__ = (
|
|||
)
|
||||
|
||||
|
||||
class PgCliQuitError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PGCli(object):
|
||||
|
||||
default_prompt = '\\u@\\h:\\d> '
|
||||
|
@ -202,6 +207,9 @@ class PGCli(object):
|
|||
self.eventloop = create_eventloop()
|
||||
self.cli = None
|
||||
|
||||
def quit(self):
|
||||
raise PgCliQuitError
|
||||
|
||||
def register_special_commands(self):
|
||||
|
||||
self.pgspecial.register(
|
||||
|
@ -211,6 +219,12 @@ class PGCli(object):
|
|||
refresh_callback = lambda: self.refresh_completions(
|
||||
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, '\\#', '\\#',
|
||||
'Refresh auto-completions.', arg_type=NO_QUERY)
|
||||
self.pgspecial.register(refresh_callback, '\\refresh', '\\refresh',
|
||||
|
@ -367,7 +381,7 @@ class PGCli(object):
|
|||
user=fixup_possible_percent_encoding(uri.username),
|
||||
port=fixup_possible_percent_encoding(uri.port),
|
||||
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:
|
||||
arguments = dict(
|
||||
{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:
|
||||
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
|
||||
# avoids wasting time trying to connect to the database and catching a
|
||||
# no-password exception.
|
||||
|
@ -412,6 +431,8 @@ class PGCli(object):
|
|||
try:
|
||||
pgexecute = PGExecute(database, user, passwd, host, port, dsn,
|
||||
application_name='pgcli', **kwargs)
|
||||
if passwd:
|
||||
keyring.set_password('pgcli', key, passwd)
|
||||
except (OperationalError, InterfaceError) as e:
|
||||
if ('no password supplied' in utf8tounicode(e.args[0]) and
|
||||
auto_passwd_prompt):
|
||||
|
@ -421,6 +442,8 @@ class PGCli(object):
|
|||
pgexecute = PGExecute(database, user, passwd, host, port,
|
||||
dsn, application_name='pgcli',
|
||||
**kwargs)
|
||||
if passwd:
|
||||
keyring.set_password('pgcli', key, passwd)
|
||||
else:
|
||||
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.
|
||||
# We may find a better way to do it in the future.
|
||||
saved_callables = cli.application.pre_run_callables
|
||||
while special.editor_command(document.text):
|
||||
filename = special.get_filename(document.text)
|
||||
query = (special.get_editor_query(document.text) or
|
||||
self.get_last_query())
|
||||
sql, message = special.open_external_editor(filename, sql=query)
|
||||
if message:
|
||||
# Something went wrong. Raise an exception and bail.
|
||||
raise RuntimeError(message)
|
||||
cli.current_buffer.document = Document(sql, cursor_position=len(sql))
|
||||
cli.application.pre_run_callables = []
|
||||
document = cli.run()
|
||||
continue
|
||||
cli.application.pre_run_callables = saved_callables
|
||||
try:
|
||||
while special.editor_command(document.text):
|
||||
filename = special.get_filename(document.text)
|
||||
query = (special.get_editor_query(document.text) or
|
||||
self.get_last_query())
|
||||
sql, message = special.open_external_editor(
|
||||
filename, sql=query)
|
||||
if message:
|
||||
# Something went wrong. Raise an exception and bail.
|
||||
raise RuntimeError(message)
|
||||
cli.current_buffer.document = Document(sql,
|
||||
cursor_position=len(sql))
|
||||
cli.application.pre_run_callables = []
|
||||
document = cli.run()
|
||||
finally:
|
||||
cli.application.pre_run_callables = saved_callables
|
||||
return document
|
||||
|
||||
def execute_command(self, text, query):
|
||||
|
@ -489,6 +515,8 @@ class PGCli(object):
|
|||
logger.error("sql: %r, error: %r", text, e)
|
||||
logger.error("traceback: %r", traceback.format_exc())
|
||||
self._handle_server_closed_connection()
|
||||
except PgCliQuitError as e:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("sql: %r, error: %r", text, e)
|
||||
logger.error("traceback: %r", traceback.format_exc())
|
||||
|
@ -555,13 +583,6 @@ class PGCli(object):
|
|||
while True:
|
||||
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:
|
||||
document = self.handle_editor_command(self.cli, document)
|
||||
except RuntimeError as e:
|
||||
|
@ -573,15 +594,19 @@ class PGCli(object):
|
|||
# Initialize default metaquery in case execution fails
|
||||
query = MetaQuery(query=document.text, successful=False)
|
||||
|
||||
watch_command, timing = special.get_watch_command(document.text)
|
||||
if watch_command:
|
||||
while watch_command:
|
||||
self.watch_command, timing = special.get_watch_command(
|
||||
document.text)
|
||||
if self.watch_command:
|
||||
while self.watch_command:
|
||||
try:
|
||||
query = self.execute_command(watch_command, query)
|
||||
click.echo('Waiting for {0} seconds before repeating'.format(timing))
|
||||
query = self.execute_command(
|
||||
self.watch_command, query)
|
||||
click.echo(
|
||||
'Waiting for {0} seconds before repeating'
|
||||
.format(timing))
|
||||
sleep(timing)
|
||||
except KeyboardInterrupt:
|
||||
watch_command = None
|
||||
self.watch_command = None
|
||||
else:
|
||||
query = self.execute_command(document.text, query)
|
||||
|
||||
|
@ -593,7 +618,7 @@ class PGCli(object):
|
|||
|
||||
self.query_history.append(query)
|
||||
|
||||
except EOFError:
|
||||
except PgCliQuitError:
|
||||
if not self.less_chatty:
|
||||
print ('Goodbye!')
|
||||
|
||||
|
@ -849,7 +874,7 @@ class PGCli(object):
|
|||
return self.query_history[-1][0] if self.query_history else 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)
|
||||
else:
|
||||
click.echo_via_pager(text, color)
|
||||
|
@ -1044,13 +1069,6 @@ def is_select(status):
|
|||
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):
|
||||
return click.style(utf8tounicode(str(e)), fg='red')
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ Schema.__new__.__defaults__ = (False,)
|
|||
# used to ensure that the alias we suggest is unique
|
||||
FromClauseItem = namedtuple('FromClauseItem', 'schema table_refs local_tables')
|
||||
Table = namedtuple('Table', ['schema', 'table_refs', 'local_tables'])
|
||||
TableFormat = namedtuple('TableFormat', [])
|
||||
View = namedtuple('View', ['schema', 'table_refs'])
|
||||
# JoinConditions are suggested after ON, e.g. 'foo.barid = bar.barid'
|
||||
JoinCondition = namedtuple('JoinCondition', ['table_refs', 'parent'])
|
||||
|
@ -252,6 +253,9 @@ def suggest_special(text):
|
|||
if cmd in ('\\c', '\\connect'):
|
||||
return (Database(),)
|
||||
|
||||
if cmd == '\\T':
|
||||
return (TableFormat(),)
|
||||
|
||||
if cmd == '\\dn':
|
||||
return (Schema(),)
|
||||
|
||||
|
|
|
@ -4,13 +4,15 @@ import re
|
|||
from itertools import count, repeat, chain
|
||||
import operator
|
||||
from collections import namedtuple, defaultdict, OrderedDict
|
||||
from cli_helpers.tabular_output import TabularOutputFormatter
|
||||
from pgspecial.namedqueries import NamedQueries
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.contrib.completers import PathCompleter
|
||||
from prompt_toolkit.document import Document
|
||||
from .packages.sqlcompletion import (FromClauseItem,
|
||||
suggest_type, Special, Database, Schema, Table, Function, Column, View,
|
||||
Keyword, NamedQuery, Datatype, Alias, Path, JoinCondition, Join)
|
||||
from .packages.sqlcompletion import (
|
||||
FromClauseItem, suggest_type, Special, Database, Schema, Table,
|
||||
TableFormat, Function, Column, View, Keyword, NamedQuery,
|
||||
Datatype, Alias, Path, JoinCondition, Join)
|
||||
from .packages.parseutils.meta import ColumnMetadata, ForeignKey
|
||||
from .packages.parseutils.utils import last_word
|
||||
from .packages.parseutils.tables import TableReference
|
||||
|
@ -321,7 +323,8 @@ class PGCompleter(Completer):
|
|||
return []
|
||||
prio_order = [
|
||||
'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
|
||||
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]
|
||||
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):
|
||||
views = self.populate_schema_objects(suggestion.schema, 'views')
|
||||
|
@ -861,6 +867,7 @@ class PGCompleter(Completer):
|
|||
Function: get_function_matches,
|
||||
Schema: get_schema_matches,
|
||||
Table: get_table_matches,
|
||||
TableFormat: get_table_formats,
|
||||
View: get_view_matches,
|
||||
Alias: get_alias_matches,
|
||||
Database: get_database_matches,
|
||||
|
|
|
@ -346,7 +346,8 @@ class PGExecute(object):
|
|||
"""
|
||||
return (isinstance(e, psycopg2.OperationalError) and
|
||||
(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):
|
||||
"""Returns tuple (title, rows, headers, status)"""
|
||||
|
|
Loading…
Reference in New Issue