# This file exists within 'dob':
#
# https://github.com/hotoffthehamster/dob
#
# Copyright © 2018-2020 Landon Bouma, 2015-2016 Eric Goller. All rights reserved.
#
# 'dob' is free software: you can redistribute it and/or modify it under the terms
# of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
#
# 'dob' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You can find the GNU General Public License reprinted in the file titled 'LICENSE',
# or visit <http://www.gnu.org/licenses/>.
from gettext import gettext as _
import sys
from inflector import English, Inflector
from nark.managers.query_terms import QueryTerms
from dob_bright.reports.render_results import render_results
from dob_bright.termio import (
click_echo,
dob_in_user_exit,
dob_in_user_warning,
highlight_value
)
from ..clickux.cmd_options_search import cmd_options_output_format_facts_only
from ..clickux.query_assist import error_exit_no_results
__all__ = (
'list_facts',
)
[docs]def list_facts(
controller,
# - Save for controller, all parameters are CLI --options.
# - The named keyword arguments listed first are used for output
# formatting and not for the query.
# - Args: Select columns to output.
show_usage=False,
show_duration=False,
hide_description=False,
column=None,
# - Args: Output formatter processor writer.
output_format='csv',
# - Args: Format- and Row-specific arguments.
table_type='texttable', # Applies when output_format == 'table'.
max_width=-1,
row_limit=None,
factoid_rule='',
# - Args: Output constraints.
output_path=None,
# - Args: Cell-specific arguments.
spark_total=None,
spark_width=None,
spark_secs=None,
# - Args: Total reporting arguments.
show_totals=False,
hide_totals=False,
# - Developer controls.
re_sort=False,
# - Any unnamed arguments are used as search terms in the query.
*args,
# - All remaining keyword arguments correspond to nark.QueryTerms.
**kwargs
):
"""
Finds and lists facts according to specified filtering, sorting and display options.
Writes to stdout, or to the file specified by ``output_path``.
Arguments:
Most of the arguments are documented elsewhere.
Returns:
None: If success.
"""
format_restricted = output_format in cmd_options_output_format_facts_only()
def _list_facts():
# MAYBE: (lb): Verify QueryTerms output format options now, before searching.
# - When I refactored query_terms, I moved validation later, so, e.g., you
# could specify an incorrect output_format, but it won't get caught until
# after find_facts returns. Which means the code does unnecessary processing,
# and the user has to wait a little longer until they're told they're wrong.
qt = prepare_query_terms(*args, **kwargs)
results = find_facts(controller, query_terms=qt)
if not results:
error_exit_no_results(_('facts'))
n_total = len(results)
n_written = display_results(results, qt, output_path)
report_report_written(controller, output_path, n_total, n_written)
# ***
def prepare_query_terms(*args, **kwargs):
qt = QueryTerms(*args, **kwargs)
must_grouping_allowed(qt)
qt.include_stats = should_include_stats(qt)
qt.sort_cols = decide_sort_cols(qt)
return qt
def must_grouping_allowed(qt):
if not qt.is_grouped or not format_restricted:
return
dob_in_user_exit(_(
'ERROR: Specified format type does not support grouping results.'
))
# Note that the two complementary commands, dob-list and dob-usage,
# have complementary options, --show-usage and --hide-usage options,
# and --show-duration and --hide-duration, that dictate if we need
# the query to include the aggregate columns or not.
def should_include_stats(qt):
# If preparing a Factoid export, the FactoidWriter
# expects a list of items, and not prepared tuples.
if format_restricted:
return False
# If a group-by is specified, the report will show the aggregated names,
# such as showing 'Activities' instead of 'Activity'. Include the extra
# aggregate columns.
if qt.is_grouped:
return True
# Not grouping results, so each result is a Fact, but check if the user
# wants the 'Duration'. (It's an either-or with the SQL code, either you
# retrieve just item columns, or you retrieve item columns and all extra
# columns, whether or not you want them all.)
# - The 'duration' is a simple julianday() - julianday() value, which
# the tabulate_results.prepare_duration may or may not use (it might
# call fact.delta instead) so really adding stats just to get the
# 'Duration' is about as unnecessary as this comment.
# - Ignore `show_usage` -- 'uses' obviously 1 because not qt.is_grouped.
# - Check for 'sparkline' output, which is calculated from 'duration'.
# - Again, we could just do this during post-processing in tabulate_results.
if (
show_duration
and (
not column
or 'duration' in column
or 'sparkline' in column
)
):
return True
return False
def decide_sort_cols(qt):
if qt.sort_cols is not None:
return qt.sort_cols
# If request includes the 'duration' column, might as well use it.
if not qt.include_stats or not show_usage:
# No 'duration' column, or user not asking to see it.
return ('start',) # The get_all default.
return ('time',) # Aka, sort-by 'duration'.
# ***
def find_facts(controller, **kwargs):
"""
Search for one or more facts, given a set of search criteria and sort options.
Args:
query_terms: A QueryTerms object that defines the query.
**kwargs: Alternatively, pass any or all of the QueryTerms attributes.
Returns:
A list of matching Facts, or matching (Fact, *statistics) tuples,
depending on the QueryTerms.
"""
try:
return controller.facts.get_all(**kwargs)
except Exception as err:
# - NotImplementedError happens if db.engine != 'sqlite', because
# get_all uses SQLite-specific aggregate functions.
# - ParserInvalidDatetimeException happens on bad since or until.
dob_in_user_exit(str(err))
# ***
def display_results(results, qt, output_path):
row_limit = suss_row_limit(qt)
n_written = render_results(
controller,
results,
query_terms=qt,
show_usage=show_usage,
show_duration=show_duration,
hide_description=hide_description,
custom_columns=column,
output_format=output_format,
table_type=table_type,
max_width=max_width,
row_limit=row_limit,
factoid_rule=factoid_rule,
output_path=output_path,
spark_total=spark_total,
spark_width=spark_width,
spark_secs=spark_secs,
show_totals=show_totals,
hide_totals=hide_totals,
re_sort=re_sort,
)
return n_written
def suss_row_limit(qt):
# Limit the number of rows dumped, unless user specified --limit,
# or if not dumping to the terminal.
_row_limit = row_limit
# Note: Not caring about qt.limit here, as the query limit is
# a separate concern from the row limit.
if _row_limit is None and not output_path and sys.stdout.isatty():
_row_limit = controller.config['term.row_limit']
return _row_limit
# ***
def report_report_written(controller, output_path, n_total, n_written):
if (
not output_path
or not sys.stdout.isatty()
# (lb): I don't quite like this coupling, but it works:
# - Don't click_echo if carousel_active.
or controller.ctx.command.name == 'edit'
):
# If writ to stdout or pager, skip count and path report.
return
# Otherwise, path was formed from, e.g., "export.{format}", so display actual.
if n_written < n_total:
echo_warn_if_truncated(controller, n_written, n_total)
click_echo(_(
"Wrote {n_written} {facts} to {output_path}"
).format(
n_written=highlight_value(n_written),
facts=Inflector(English).conditional_plural(n_written, _('Fact')),
output_path=highlight_value(output_path),
))
def echo_warn_if_truncated(controller, n_results, n_rows):
if n_results <= n_rows:
return
dob_in_user_warning(_(
'Showed only {} of {} results. Use `-C term.row_limit=0` to see all results.'
).format(format(n_results, ','), format(n_rows, ',')))
# ***
_list_facts()