# 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 click_hotoffthehamster as click
from dob_bright.reports.tabulate_results import report_table_columns
from dob_bright.termio import dob_in_user_exit, dob_in_user_warning
__all__ = (
# One decorator is all you need for each list and usage command.
'cmd_options_any_search_query',
# Some other commands share use of the ASCII output table feature.
'cmd_options_output_format_any_input',
'cmd_options_output_format_facts_only',
# Argument parsing helpers, to facilitate **kwargs passing.
'postprocess_options_normalize_search_args',
'postprocess_options_output_format_any_input',
# Private module variables:
# '_cmd_options_*',
# Private module functions:
# '_cmd_options_output_format_choices',
# '_cmd_options_output_format_multiple_choices_option',
# '_cmd_options_output_format_singular_options_any',
# '_cmd_options_output_format_singular_options_fact',
# '_cmd_options_output_format_tabling',
# '_cmd_options_output_formats_basic',
# '_cmd_options_output_formats_table',
# '_cmd_options_results_group',
# '_cmd_options_results_output_path',
# '_cmd_options_results_sort_order',
# '_cmd_options_search_time_window',
# '_postprocess_options_grouping',
# '_postprocess_options_match_activities',
# '_postprocess_options_match_categories',
# '_postprocess_options_match_tags',
# '_postprocess_options_matching',
# '_postprocess_options_sort_dirs',
# '_postprocess_options_sort_cols',
# '_postprocess_options_results_show_hide',
# '_postprocess_options_results_show_hide_option',
# '_postprocess_options_search_term',
# '_postprocess_options_sparkline',
# '_postprocess_options_sparkline_float',
# '_postprocess_options_sparkline_secs',
# '_postprocess_options_sparkline_total',
)
# ***
# *** [SEARCH QUERY] Item ID.
# ***
_cmd_options_search_item_key = [
click.option(
'-k', '--key',
metavar='ID',
help=_('The database key of the item.'),
),
]
# ***
# *** [SEARCH QUERY] Search Terms.
# ***
_cmd_options_search_search_term = [
click.argument(
'search_term',
nargs=-1,
default=None,
),
]
def _postprocess_options_search_term(kwargs):
if 'search_term' not in kwargs:
return
# (lb): Me, the pedant.
kwargs['search_terms'] = kwargs['search_term']
del kwargs['search_term']
# ***
# *** [SEARCH QUERY] Time Window.
# ***
def _cmd_options_search_time_window(command=''):
since_default_value = ''
since_show_default = False
if command == 'journal':
since_default_value = 'last week'
since_show_default = True
cmd_options = [
click.option(
'-s', '--since', '--after',
metavar='TIME',
default=since_default_value,
show_default=since_show_default,
help=_('Show items newer than a specific date.'),
),
click.option(
'-u', '--until', '--before',
metavar='TIME',
help=_('Show items older than a specific date.'),
),
]
return cmd_options
# ***
# *** [SEARCH QUERY] Deleted and Hidden.
# ***
# FIXME/2020-05-16: (lb): Cleanup these options upon broader cleanup.
_cmd_options_search_deleted_hidden = [
click.option(
'--deleted', is_flag=True, help=_('Show deleted items.'),
),
# FIXME/2020-05-20: Scrub hidden from everywhere.
# click.option(
# '--hidden', is_flag=True, help=_('Show hidden items.'),
# ),
]
# ***
# *** [SEARCH MATCH] Activity.
# ***
_cmd_options_search_match_activities = [
click.option(
'-a', '--activity', multiple=True,
help=_('Restrict results by exact activity name(s).'),
),
]
def _postprocess_options_match_activities(kwargs):
if 'activity' not in kwargs:
return
match_activities = _postprocess_assemble_match_names(kwargs['activity'])
if match_activities:
kwargs['match_activities'] = match_activities
del kwargs['activity']
def _postprocess_assemble_match_names(match_names):
match_list = []
for name in match_names:
match_list.append(name)
if not name:
# Match empty string or NULL (this is a special case,
# otherwise user has no way to match NULL from CLI).
match_list.append(None)
return match_list
# ***
# *** [SEARCH MATCH] Category.
# ***
_cmd_options_search_match_categories = [
click.option(
'-c', '--category', multiple=True,
help=_('Restrict results by exact category name(s).'),
),
]
def _postprocess_options_match_categories(kwargs):
if 'category' not in kwargs:
return
match_categories = _postprocess_assemble_match_names(kwargs['category'])
if match_categories:
kwargs['match_categories'] = match_categories
del kwargs['category']
# ***
# *** [SEARCH MATCH] Tag names.
# ***
_cmd_options_search_match_tags = [
click.option(
'-t', '--tag', multiple=True,
help=_('Restrict results by exact tag name(s).'),
),
]
def _postprocess_options_match_tags(kwargs):
if 'tag' not in kwargs:
return
match_tags = _postprocess_assemble_match_names(kwargs['tag'])
if match_tags:
kwargs['match_tags'] = match_tags
del kwargs['tag']
# ***
# *** [SEARCH MATCH] Broad matching.
# ***
_cmd_options_search_broad_match = [
click.option(
'--broad-match', '--broad',
is_flag=True,
help=_(
'Try SEARCH_TERM matching on activity, category, and tag names'
' (otherwise SEARCH_TERM only matches description).'
),
),
]
# ***
# *** [RESULTS GROUP] Option.
# ***
def _cmd_options_results_group(item):
choices = []
if item != 'activity':
choices.append('activity')
if item != 'category':
choices.append('category')
if item != 'tags':
choices.append('tags')
if item == 'fact':
choices.append('days')
return [
click.option(
'-g', '--group', multiple=True,
type=click.Choice(choices),
help=_('Alias of `--group-<attribute>` options (multiple allowed).'),
),
]
def _postprocess_options_grouping(kwargs, cmd_journal=False):
def __postprocess_options_grouping():
if 'group' not in kwargs:
return
process_grouping_option('activity')
process_grouping_option('category')
process_grouping_option('tags')
process_grouping_option('days')
del kwargs['group']
ensure_default_grouping()
def process_grouping_option(type_name):
# Form the group name, e.g., group_activity, group_category, etc.
group_name = 'group_{}'.format(type_name)
if group_name not in kwargs:
return
kwargs[group_name] = kwargs[group_name] or type_name in kwargs['group']
def ensure_default_grouping():
if not cmd_journal:
return
if not (
kwargs['group_activity']
or kwargs['group_category']
or kwargs['group_tags']
or kwargs['group_days']
):
kwargs['group_activity'] = True
kwargs['group_category'] = True
kwargs['group_days'] = True
__postprocess_options_grouping()
# ***
# *** [RESULTS GROUP] Activity.
# ***
_cmd_options_results_group_activity = [
click.option(
'-A', '--group-activity', is_flag=True,
help=_('Group results by activity name.'),
),
]
# ***
# *** [RESULTS GROUP] Category.
# ***
_cmd_options_results_group_category = [
# There's a global -C/--config option, but globals need to be
# specified before the command name (just how Click works), so
# this re-usage of the same single -C option is perfectly fine.
click.option(
'-C', '--group-category', is_flag=True,
help=_('Group results by category name.'),
),
]
# ***
# *** [RESULTS GROUP] Tags.
# ***
_cmd_options_results_group_tags = [
click.option(
'-T', '--group-tags', is_flag=True,
help=_('Group results by tag names.'),
),
]
# ***
# *** [RESULTS GROUP] Days.
# ***
_cmd_options_results_group_days = [
click.option(
'-Y', '--group-days', is_flag=True,
help=_('Group results by day.'),
),
]
# ***
# *** [SEARCH RESULTS] Order.
# ***
def _cmd_options_results_sort_order(item, command, group):
choices = ['name', 'activity', 'category']
default_sort_cols = ['name']
default_sort_orders = ['asc']
if item == 'fact' or command == 'usage':
choices += ['start', 'time']
if item == 'fact':
default_sort_cols = ['start']
if group or command == 'usage':
choices += ['usage']
default_sort_cols = ['usage']
default_sort_orders = ['desc']
if command == 'journal':
default_sort_cols = ['day', 'time']
default_sort_orders = ['asc', 'desc']
if group and item == 'fact':
choices.append('day')
if item in ('tag', 'fact'):
# For item == 'tag', this option same as --sort 'name'.
choices.append('tag')
if item == 'fact':
# Sorts by Fact PK. (lb): Not sure how useful.
# - Because Momentaneous Facts, potentially useful.
choices.append('fact')
return [
click.option(
'-S', '--sort', '--order',
default=default_sort_cols,
show_default=True,
type=click.Choice(choices),
multiple=True,
help=_('Order by column(s) (multiple allowed).'),
),
click.option(
'-D', '--direction', '--dir',
default=default_sort_orders,
type=click.Choice(['asc', 'desc']),
multiple=True,
help=_('Order by direction(s) (one for each --sort).'),
),
]
# ***
# *** [POST PROCESS] Sort/Order Options.
# ***
def _postprocess_options_sort_cols(kwargs):
if 'sort' not in kwargs:
return
if kwargs['sort']:
kwargs['sort_cols'] = kwargs['sort']
del kwargs['sort']
def _postprocess_options_sort_dirs(kwargs):
if 'direction' not in kwargs:
return
last_direction = 'asc'
sort_orders = []
if 'sort_cols' in kwargs:
# Ensure sort_orders same length as sort_cols.
for idx in range(len(kwargs['sort_cols'])):
try:
last_direction = kwargs['direction'][idx]
except IndexError:
pass
sort_orders.append(last_direction)
del kwargs['direction']
if sort_orders:
kwargs['sort_orders'] = sort_orders
# ***
# *** [DOUBLE-SORT] Developer Option.
# ***
_postprocess_options_results_options_sort_double_sort = [
click.option(
'--re-sort',
is_flag=True,
hidden=True,
help=_('Resort query after SELECT post-processing.'),
),
]
# ***
# *** [SEARCH RESULTS] Limit and Offset.
# ***
_cmd_options_search_limit_offset = [
click.option(
'-L', '--limit', default=0, show_default=False,
help=_('Limit the number of records to fetch.'),
),
click.option(
'-O', '--offset', default=0, show_default=False,
help=_('Skip this number of records before returning results.'),
),
]
# ***
# *** [RESULTS HIDE] Description.
# ***
_cmd_options_results_hide_description = [
click.option(
'-P', '--hide-description', is_flag=True,
help=_('Omit Fact description from results.'),
),
]
# ***
# *** [RESULTS SHOW/HIDE] Duration.
# ***
_cmd_options_results_hide_duration = [
click.option(
'-N', '--hide-duration', is_flag=True,
help=_('Omit duration from results.'),
),
]
_cmd_options_results_show_duration = [
click.option(
'-N', '--show-duration', is_flag=True,
help=_('Display duration time in results.'),
),
]
# ***
# *** [RESULTS HIDE/SHOW] Usage.
# ***
_cmd_options_results_hide_usage = [
click.option(
'-U', '--hide-usage', is_flag=True,
help=_('Omit usage count from results.'),
),
]
_cmd_options_results_show_usage = [
click.option(
'-U', '--show-usage', is_flag=True,
help=_('Display usage count in results.'),
),
]
# ***
# *** [POST PROCESS] Show/Hide.
# ***
def _postprocess_options_results_show_hide(kwargs):
# The list command have --show-* options; the usage commands
# have --hide-* options; the export command has neither.
_postprocess_options_results_show_hide_option(kwargs, 'duration')
_postprocess_options_results_show_hide_option(kwargs, 'usage')
def _postprocess_options_results_show_hide_option(kwargs, argname):
show_item = None
attr_show = 'show_{}'.format(argname)
attr_hide = 'hide_{}'.format(argname)
if attr_show in kwargs:
show_item = kwargs[attr_show]
elif attr_hide in kwargs:
show_item = not kwargs[attr_hide]
if show_item is not None:
kwargs[attr_show] = show_item
if attr_hide in kwargs:
del kwargs[attr_hide]
# ***
# *** [RESULTS CUSTOM] Columns.
# ***
_cmd_options_results_show_columns = [
click.option(
'-l', '--column', multiple=True,
type=click.Choice(report_table_columns()),
help=_('Specify custom report columns.'),
),
]
# ***
# *** [SEARCH RESULTS] Table/Line width constraint.
# ***
_cmd_options_results_max_width = [
click.option(
'-W', '--max-width', '--width', default=-1,
help=_('Set the table width or Factoid line length.'),
),
]
# ***
# *** [SEARCH RESULTS] Output file.
# ***
def _cmd_options_results_output_path(for_export=False):
return [
click.option(
'-o', '--output',
# Not setting `type=click.File('r')` because
# refuses empty string, which we want user to
# be able to use to choose stdout (i.e., when
# for_export=True).
help=_('Write to file instead of stdout.'),
metavar='PATH',
required=for_export,
),
]
def _postprocess_options_output_filename(kwargs):
if 'output' not in kwargs:
return
kwargs['output_path'] = kwargs['output']
del kwargs['output']
# ***
# *** [REPORT FORMAT] Basic Formats.
# ***
_standard_formats = [
'csv',
'json',
'tsv',
'xml',
'table',
]
_single_fact_formats = [
'factoid',
'ical',
]
_fact_usage_formats = [
'journal',
]
def _cmd_options_output_formats_basic(item=''):
format_choices = []
format_choices += _fact_usage_formats if item == 'fact' else []
format_choices += _single_fact_formats if item == 'fact' else []
format_choices += _standard_formats
return format_choices
def _cmd_options_output_format_singular_options_any():
# --format <format> aliases.
cmd_options = [
click.option(
'--csv', is_flag=True,
help=_('Output results as comma-separated values (CSV).'),
),
click.option(
'--json', '-J', is_flag=True,
help=_('Output results as JavaScript Object Notation (JSON).'),
),
click.option(
'--tsv', is_flag=True,
help=_('Output results as tab-separated values (TSV).'),
),
click.option(
'--xml', is_flag=True,
help=_('Output results as Extensible Markup Language (XML).'),
),
click.option(
'--table', is_flag=True,
help=_('Output results as format indicated by --table-type.'),
),
]
return cmd_options
def _cmd_options_output_format_singular_options_fact():
cmd_options = [
click.option(
'--journal', is_flag=True,
help=_("Output Facts using dob's Journal format."),
),
click.option(
'--factoid', is_flag=True,
help=_("Output Facts using dob's Factoid format."),
),
click.option(
'--ical', is_flag=True,
help=_('Output Facts using iCalendar format.'),
),
]
return cmd_options
def _cmd_options_output_format_multiple_choices_option(command='', item=''):
format_choices = _cmd_options_output_formats_basic(item)
if command == 'journal':
default_format = 'journal'
else:
default_format = 'table'
cmd_options = [
click.option(
'-f', '--format',
type=click.Choice(format_choices),
default=default_format,
show_default=True,
help=_('Alias of format options (one allowed).'),
),
]
return cmd_options
def _cmd_options_output_format_choices(command='', item=''):
cmd_options = []
if item == 'fact':
cmd_options += _cmd_options_output_format_singular_options_fact()
cmd_options += _cmd_options_output_format_singular_options_any()
cmd_options += _cmd_options_output_format_multiple_choices_option(command, item)
return cmd_options
def _postprocess_options_output_format_choices(kwargs):
if 'format' not in kwargs:
return
format_choices = _cmd_options_output_formats_basic(item='fact')
fmts_specified = []
for choice in format_choices:
try:
if kwargs[choice]:
kwargs['format'] = choice
fmts_specified.append(choice)
del kwargs[choice]
except KeyError:
# 'choice' is 'factoid' or 'journal', because item=='fact' unknown, so
# always trying, even for commands that do not output Fact reports.
pass
if len(fmts_specified) > 1:
dob_in_user_warning(_(
'More than one format specified: {}'
).format(fmts_specified))
kwargs['output_format'] = kwargs['format']
del kwargs['format']
# ***
# *** [REPORT FORMAT] Table Formats.
# ***
_tabulate_tablefmts_markup = [
'github',
# | Header 1 | Header 2 | Header 3 |
# |------------|------------|------------|
# | value 1 | value 2 | value 3 |
'html',
# <table>
# <thead>
# <tr><th>Header 1 </th><th>Header 2 </th><th>Header 3 </th></tr>
# </thead>
# <tbody>
# <tr><td>value 1 </td><td>value 2 </td><td>value 3 </td></tr>
# </tbody>
# </table>
# 'jira', # MEH: Could add if requested.
# || Header 1 || Header 2 || Header 3 ||
# | value 1 | value 2 | value 3 |
# 'latex', # MEH: Could add if requested.
# \begin{tabular}{lll}
# \hline
# Header 1 & Header 2 & Header 3 \\
# \hline
# value 1 & value 2 & value 3 \\
# \hline
# \end{tabular}
# 'latex_raw', # SKIP: Same as 'latex'
# \begin{tabular}{lll}
# \hline
# Header 1 & Header 2 & Header 3 \\
# \hline
# value 1 & value 2 & value 3 \\
# \hline
# \end{tabular}
# 'latex_booktabs', # MEH: Could add if requested.
# \begin{tabular}{lll}
# \toprule
# Header 1 & Header 2 & Header 3 \\
# \midrule
# value 1 & value 2 & value 3 \\
# \bottomrule
# \end{tabular}
'mediawiki',
# {| class="wikitable" style="text-align: left;"
# |+ <!-- caption -->
# |-
# ! Header 1 !! Header 2 !! Header 3
# |-
# | value 1 || value 2 || value 3
# |}
# 'moinmoin', # For MoinMoin Wiki Engine. MEH: Could add if requested.
# || ''' Header 1 ''' || ''' Header 2 ''' || ''' Header 3 ''' ||
# || value 1 || value 2 || value 3 ||
'orgtbl',
# | Header 1 | Header 2 | Header 3 |
# |------------+------------+------------|
# | value 1 | value 2 | value 3 |
'rst',
# ========== ========== ==========
# Header 1 Header 2 Header 3
# ========== ========== ==========
# value 1 value 2 value 3
# ========== ========== ==========
# 'textile', # MEH: Could add if requested.
# |_. Header 1 |_. Header 2 |_. Header 3 |
# |<. value 1 |<. value 2 |<. value 3 |
# 'unsafehtml', # MEH.
# Header 1 Header 2 Header 3
# ---------- ---------- ----------
# value 1 value 2 value 3
# 'youtrack', # Similar to 'jira'
# || Header 1 || Header 2 || Header 3 ||
# | value 1 | value 2 | value 3 |
]
# (lb): I tested 3 ASCII table packages: texttable, tabulate, and humanfriendly.
# - The texttable package (as far as I am aware) is the only library that wraps
# cell values automatically. So this is the package we use to generate an ASCII
# table that is meant to be displayed in the user's terminal.
# - We omit the humanfriendly table generator because it does not wrap column
# values automatically, and does not offer anything that texttable does not,
# other than perhaps slightly differently table borders.
# - The tabulate package has a number of seemingly interesting table formats that
# the user could choose (like 'fancy_grid', 'grid', 'pretty', 'simple', etc.),
# but it also does not wrap column values automatically, so really none of these
# formats is useable (and none are essential, either; they're just stylistic).
# - As such, if we wanted to use tabulate or friendly to generate a readable (in the
# user's terminal) ASCII table, we'd have to do the column value wrapping ourself.
# For instance, tabulate will wrap column values, but only on existing newlines, so
# we'd have to, say, determine the appropriate column widths first, and then call
# ansiwrap.wrap on all the values before calling tabulate.
# - Without the upfront wrapping, the tabulate and humanfriendly packages print
# the table as wide as the column values dictate, which can cause the table
# rows to bleed across lines, which makes the table impractical to read.
# - So, at least for generating a useable ASCII table, we use texttable, and we
# make texttable the default table choice, although we label it the 'normal'
# table type, so that the user is not distracted by package naming minutia.
# - However, we can expose a few tabulate format options that are not meant
# to be displayed or read in the terminal, such as HTML and reST formats.
# - tl;dr We let the user choose `texttable` to generate an ASCII table; and we
# let them choose a few of the `tabulate` formats to generate alternative
# (non-table) outputs.
def _cmd_options_output_formats_table(item=''):
table_choices = []
# MAGIC_VALUE: Use 'normal' to refer to the nice, wrapped ASCII table
# that 'texttable' generates by default.
table_choices += ['normal']
# Include also those tabulate package output formats that are not
# ASCII table formats (and not destined for terminal viewage).
table_choices += _tabulate_tablefmts_markup
return table_choices
def _cmd_options_output_format_tabling():
formats_table = _cmd_options_output_formats_table()
cmd_options = [
click.option(
'--table-type', '--type',
type=click.Choice(formats_table),
nargs=1,
default='normal',
show_default=True,
help=_('Table format style.'),
),
]
return cmd_options
def _postprocess_options_output_format_tabling(kwargs):
if 'table_type' not in kwargs:
return
if kwargs['table_type'] == 'normal':
kwargs['table_type'] = 'texttable'
# ***
# *** [REPORT FORMAT] Basic Formats and Table Styles.
# ***
[docs]def postprocess_options_output_format_any_input(kwargs, cmd_journal=False):
_postprocess_options_output_format_choices(kwargs)
_postprocess_options_output_format_tabling(kwargs)
# ***
# *** [EXPORT FORMAT] Factoid Option.
# ***
_cmd_options_output_factoids_hrule = [
click.option(
# MEH: We could alias '-R', or reserve -R for later.
'--factoid-rule', '--rule',
nargs=1,
default='',
metavar='SEP',
help=_('Separate Factoids with a horizontal rule.'),
),
]
# ***
# *** [REPORT/EXPORT/OUTPUT FORMAT] Sparkline Options.
# ***
_cmd_options_output_sparkline_format = [
click.option(
# MEH: We could alias '-W', or reserve -W for later.
'--spark-width', '--swid',
type=int,
default='12',
metavar='INT',
# (lb): I like the idea of showing "block (█) character"
# but it's very distracting.
help=_("Number of block characters in full spark [default: 12]."),
),
click.option(
# MEH: We could alias '-V', or reserve -V for later.
'--spark-total', '--stot',
nargs=1,
default='max',
metavar='MAX',
# show_default=True,
help=_(
"Number of seconds in full spark: "
"'max' or 'net' duration, or number [default: 'max']."
),
),
click.option(
# MEH: We could alias '-S', or reserve -S for later.
'--spark-secs', '--ssec',
metavar='WIDTH',
help=_("Seconds per block character [default: total / width]."),
),
]
# ***
# *** [POST PROCESS] Show/Hide.
# ***
def _postprocess_options_sparkline(kwargs):
if 'spark_total' not in kwargs:
return
_postprocess_options_sparkline_total(kwargs)
_postprocess_options_sparkline_secs(kwargs)
def _postprocess_options_sparkline_total(kwargs):
if kwargs['spark_total'] in ('max', 'net'):
return
_postprocess_options_sparkline_float(kwargs, 'spark_total')
def _postprocess_options_sparkline_secs(kwargs):
_postprocess_options_sparkline_float(kwargs, 'spark_secs')
def _postprocess_options_sparkline_float(kwargs, spark_attr):
try:
kwargs[spark_attr] = float(kwargs[spark_attr] or 0)
except ValueError:
try:
# Is this a security issue? Ha!
# - Let user specify math on the command line, e.g., to
# specify 8 hours as the full spark width, user'd use:
# --spark-total '8 * 60 * 60'
kwargs[spark_attr] = float(eval(kwargs[spark_attr]))
except Exception:
msg = _(
"Unable to parse --{} value as (eval'able) seconds: {}"
).format(spark_attr.replace('_', '-'), kwargs[spark_attr])
dob_in_user_exit(msg)
# ***
# *** [RESULTS OUTPUT] Aggregate Totals.
# ***
_cmd_options_output_show_totals = [
click.option(
'--show-totals', '--totals',
is_flag=True,
help=_('Calculate and show totals in final row of output.'),
),
]
_cmd_options_output_hide_totals = [
click.option(
'--hide-totals', '--totals',
is_flag=True,
help=_('Do not calculate and show totals in final row of output.'),
),
]
# ***
# *** [POST PROCESS] Adjust **kwargs.
# ***
def _postprocess_options_matching(kwargs):
_postprocess_options_match_activities(kwargs)
_postprocess_options_match_categories(kwargs)
_postprocess_options_match_tags(kwargs)
[docs]def postprocess_options_normalize_search_args(kwargs, cmd_journal=False):
_postprocess_options_matching(kwargs)
_postprocess_options_grouping(kwargs, cmd_journal=cmd_journal)
_postprocess_options_sort_cols(kwargs)
_postprocess_options_sort_dirs(kwargs)
_postprocess_options_results_show_hide(kwargs)
postprocess_options_output_format_any_input(kwargs, cmd_journal=cmd_journal)
_postprocess_options_output_filename(kwargs)
_postprocess_options_sparkline(kwargs)
_postprocess_options_search_term(kwargs)
# ***
# *** [ALL TOGETHER NOW] One @decorator is all you need.
# ***
# *** One @decorator for all your search command option needs.
[docs]def cmd_options_any_search_query(command='', item='', match=False, group=False):
def _cmd_options_any_search_query():
options = []
append_cmd_options_filter_by_pk(options)
append_cmd_options_filter_by_time(options)
append_cmd_options_matching(options)
append_cmd_options_group_by(options)
append_cmd_options_results_sort_limit(options)
append_cmd_options_results_column_choices(options)
append_cmd_options_output_file(options)
append_cmd_options_results_report_formats(options)
append_cmd_options_results_report_totals(options)
append_cmd_options_filter_by_search_terms(options)
def _cmd_options_search_query(func):
for option in reversed(options):
func = option(func)
return func
return _cmd_options_search_query
# +++
def append_cmd_options_filter_by_pk(options):
if command == 'export':
return
options.extend(_cmd_options_search_item_key)
def append_cmd_options_filter_by_time(options):
options.extend(_cmd_options_search_time_window(command))
def append_cmd_options_filter_by_search_terms(options):
options.extend(_cmd_options_search_search_term)
# +++
def append_cmd_options_matching(options):
if not match:
return
# We could exclude the --activity item from the `dob list activity`
# and `dob usage activity` commands, but by including it, we allow
# the user access to a strict text match search, as opposed to using
# search_term, which is a loose match. E.g., the difference between
# `dob list activity -a "Must Match Fully"` as compared to
# `dob list activity fully`, given Activity named "Must Match Fully".
options.extend(_cmd_options_search_match_activities)
options.extend(_cmd_options_search_match_categories)
if item == 'fact':
options.extend(_cmd_options_search_match_tags)
options.extend(_cmd_options_search_broad_match)
# +++
def append_cmd_options_group_by(options):
if not group:
return
if item != 'activity':
options.extend(_cmd_options_results_group_activity)
if item != 'category':
options.extend(_cmd_options_results_group_category)
if item != 'tags':
options.extend(_cmd_options_results_group_tags)
if item == 'fact':
options.extend(_cmd_options_results_group_days)
options.extend(_cmd_options_results_group(item))
# +++
def append_cmd_options_results_sort_limit(options):
options.extend(_cmd_options_results_sort_order(item, command, group))
options.extend(_cmd_options_search_limit_offset)
# +++
def append_cmd_options_results_column_choices(options):
if command == 'export':
return
append_cmd_options_results_basic_usage_hide_show(options)
append_cmd_options_results_fact_attrs_hide_show(options)
def append_cmd_options_results_report_formats(options):
if command == 'export':
return
append_cmd_options_output_format(options)
append_cmd_options_table_type(options)
append_cmd_options_results_max_width(options)
append_cmd_options_format_factoid_output(options)
append_cmd_options_format_sparkline_output(options)
append_cmd_options_results_re_sort(options)
def append_cmd_options_results_report_totals(options):
if command == 'export':
return
if command == 'journal':
options.extend(_cmd_options_output_show_totals)
else:
options.extend(_cmd_options_output_hide_totals)
# +++
def append_cmd_options_results_basic_usage_hide_show(options):
# Search results report output column values hide/show options.
if command == 'usage':
options.extend(_cmd_options_results_hide_usage)
options.extend(_cmd_options_results_hide_duration)
elif command in ['list', 'journal']:
options.extend(_cmd_options_results_show_usage)
options.extend(_cmd_options_results_show_duration)
elif command == 'export':
pass # Show neither.
else: # Impossible.
raise False # pragma: no cover
def append_cmd_options_results_fact_attrs_hide_show(options):
# Search results report output column values hide/show options.
if item != 'fact':
return
options.extend(_cmd_options_results_hide_description)
# (lb): I sorta wanna hide final_start and final_end by default.
# - But then I'm afraid no one would find them.
# - So instead, supply --column option to fine-tune the output.
options.extend(_cmd_options_results_show_columns)
def append_cmd_options_results_max_width(options):
if command == 'export':
return
options.extend(_cmd_options_results_max_width)
def append_cmd_options_results_re_sort(options):
if item != 'fact':
return
# (lb): It'd be nice to squelch this option unless config['dev.catch_errors']
# but I guess that's what Click 'hidden' option feature is for.
options.extend(_postprocess_options_results_options_sort_double_sort)
# +++
def append_cmd_options_output_file(options):
for_export = (command == 'export')
options.extend(_cmd_options_results_output_path(for_export=for_export))
# +++
def append_cmd_options_output_format(options):
options.extend(_cmd_options_output_format_choices(command, item))
def append_cmd_options_format_factoid_output(options):
if item != 'fact':
return
options.extend(_cmd_options_output_factoids_hrule)
def append_cmd_options_format_sparkline_output(options):
if item != 'fact':
return
options.extend(_cmd_options_output_sparkline_format)
def append_cmd_options_table_type(options):
options.extend(_cmd_options_output_format_tabling())
# +++
return _cmd_options_any_search_query()