Skip to content

Commit 187d474

Browse files
committed
Add 'help function <type>' subcommand, join amount and commodity functions, as well as position and inventory functions
1 parent 31b8909 commit 187d474

File tree

2 files changed

+129
-69
lines changed

2 files changed

+129
-69
lines changed

beanquery/query_env.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def str_(x):
185185
return str(x)
186186

187187

188-
@function([datetime.date], datetime.date, groups=['atomic', 'date'])
188+
@function([datetime.date], datetime.date, name = 'date', groups=['date'])
189189
@function([str], datetime.date, name='date', groups=['atomic', 'date'])
190190
@function([object], datetime.date, name='date', groups=['atomic', 'date'])
191191
def date_(x):
@@ -320,7 +320,7 @@ def weekday_(x):
320320
return x.strftime('%a')
321321

322322

323-
@function([], datetime.date)
323+
@function([], datetime.date, groups=['date'])
324324
def today():
325325
"""Today's date"""
326326
return datetime.date.today()
@@ -387,7 +387,7 @@ def lower(string):
387387
NONENONE = None, None
388388

389389

390-
@function([str], datetime.date, pass_context=True, groups=['account'])
390+
@function([str], datetime.date, pass_context=True, groups=['account', 'metadata'])
391391
def open_date(context, acc):
392392
"""Get the date of the open directive of the account."""
393393
open_entry, _ = context.tables['accounts'].accounts.get(acc, NONENONE)
@@ -396,7 +396,7 @@ def open_date(context, acc):
396396
return open_entry.date
397397

398398

399-
@function([str], datetime.date, pass_context=True, groups=['account'])
399+
@function([str], datetime.date, pass_context=True, groups=['account', 'metadata'])
400400
def close_date(context, acc):
401401
"""Get the date of the close directive of the account."""
402402
_, close_entry = context.tables['accounts'].accounts.get(acc, NONENONE)
@@ -405,8 +405,8 @@ def close_date(context, acc):
405405
return close_entry.date
406406

407407

408-
@function([str], dict, pass_context=True, groups=['account'])
409-
@function([str, str], object, pass_context=True, groups=['account'])
408+
@function([str], dict, pass_context=True, groups=['account', 'metadata'])
409+
@function([str, str], object, pass_context=True, groups=['account', 'metadata'])
410410
def open_meta(context, account, key=None):
411411
"""Get the metadata dict of the open directive of the account.
412412
With one argument, returns all metadata as a dict object. With two
@@ -420,30 +420,30 @@ def open_meta(context, account, key=None):
420420

421421

422422
# Stub kept only for function type checking and for generating documentation.
423-
@function([str], object, groups=['transaction'])
423+
@function([str], object, groups = ['metadata'])
424424
def meta(context, key):
425425
"""Get some metadata key of the posting."""
426426
raise NotImplementedError
427427

428428

429429
# Stub kept only for function type checking and for generating documentation.
430-
@function([str], object, groups=['transaction'])
430+
@function([str], object, groups = ['metadata'])
431431
def entry_meta(context, key):
432432
"""Get some metadata key of the transaction."""
433433
raise NotImplementedError
434434

435435

436436
# Stub kept only for function type checking and for generating documentation.
437-
@function([str], object, groups=['posting'])
437+
@function([str], object, groups = ['metadata'])
438438
def any_meta(context, key):
439439
"""Get metadata from the posting or its parent transaction if not present."""
440440
raise NotImplementedError
441441

442442

443-
@function([str], dict, pass_context=True)
444-
@function([str, str], object, pass_context=True)
445-
@function([str], dict, pass_context=True, name='commodity_meta')
446-
@function([str, str], object, pass_context=True, name='commodity_meta')
443+
@function([str], dict, pass_context=True, groups = ['amount', 'metadata'])
444+
@function([str, str], object, pass_context=True, groups = ['amount', 'metadata'])
445+
@function([str], dict, pass_context=True, name='commodity_meta', groups = ['amount', 'metadata'])
446+
@function([str, str], object, pass_context=True, name='commodity_meta', groups = ['amount', 'metadata'])
447447
def currency_meta(context, commodity, key=None):
448448
"""Get the metadata dict of the commodity directive of the currency."""
449449
entry = context.tables['commodities'].commodities.get(commodity)
@@ -550,10 +550,10 @@ def inventory_value(context, inv, date=None):
550550
return inv.reduce(convert.get_value, price_map, date)
551551

552552

553-
@function([str, str], Decimal, pass_context=True)
554-
@function([str, str, datetime.date], Decimal, pass_context=True, name='getprice')
553+
@function([str, str], Decimal, pass_context=True, groups = ['position'])
554+
@function([str, str, datetime.date], Decimal, pass_context=True, name='getprice', groups = ['position'])
555555
def getprice(context, base, quote, date=None):
556-
"""Fetch a price."""
556+
"""Fetch a price. Arguments: Base currency, e.g. 'EUR'; Commodity name (string); Date: Price as of this date. Default: Latest price."""
557557
price_map = context.tables['prices'].price_map
558558
pair = (base.upper(), quote.upper())
559559
_, price = prices.get_price(price_map, pair, date)
@@ -872,7 +872,7 @@ def update(self, store, context):
872872
store[self.handle].add_amount(value)
873873

874874

875-
@aggregator([position.Position], name='sum', groups = ['position', 'inventory'])
875+
@aggregator([position.Position], name='sum', groups = ['position'])
876876
class SumPosition(query_compile.EvalAggregator):
877877
"""Calculate the sum of the position. The result is an Inventory."""
878878
def __init__(self, context, operands):

beanquery/shell.py

Lines changed: 112 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,22 @@
5555
TYPE_CATEGORIES = {
5656
'amount': [amount.Amount],
5757
'account': [], # Must be manually assigned as account names are strings
58-
'position': [position.Position],
59-
'inventory': [inventory.Inventory],
58+
'position': [position.Position, inventory.Inventory],
59+
'metadata': [], # Must be manually assigned, signatures in query_env.py: 'object'
6060
'date': [datetime.date],
6161
'atomic': [] # fallback, default category
6262
}
6363

64+
# Category information for help display. Tuples: (title, description)
65+
CATEGORY_INFO = {
66+
'amount': ("Amount and Commodity Functions", "An amount is a value with a currency/commodity."),
67+
'account': ("Account Functions", ""),
68+
'position': ("Position & Inventory Functions", "A position is a single amount held at cost.\n\nExample: 10 HOOL {100.30 USD}\n\nA collection of multiple positions is an inventory"),
69+
'metadata': ("Access metadata", "Access metadata from postings, transactions, and accounts."),
70+
'date': ("Date Functions", ""),
71+
'atomic': ("Atomic Functions", "Work on basic types: strings, numbers, etc.")
72+
}
73+
6474

6575
class style:
6676
ERROR = '\033[31;1m'
@@ -296,7 +306,27 @@ def completenames(self, text, *ignored):
296306

297307
def do_help(self, arg):
298308
"""List available commands with "help" or detailed help with "help cmd"."""
299-
super().do_help(arg.lower())
309+
if not arg:
310+
super().do_help(arg)
311+
return
312+
313+
# Split arg by space to get command and additional arguments
314+
# e.g. "help functions amount" -> command="functions", args=["amount"]
315+
parts = arg.split()
316+
command = parts[0].lower()
317+
args = parts[1:] if len(parts) > 1 else []
318+
319+
# Check if there's a help method for this command
320+
help_method = getattr(self, f'help_{command}', None)
321+
if help_method and args:
322+
# Call the help method with the additional arguments
323+
try:
324+
help_method(' '.join(args))
325+
except TypeError:
326+
# Fallback if the help method doesn't accept arguments
327+
super().do_help(command)
328+
else:
329+
super().do_help(command)
300330

301331
def do_history(self, arg):
302332
"""Print the command-line history."""
@@ -575,9 +605,9 @@ def on_Select(self, statement):
575605
directive are made available in this context as well. Simple functions
576606
(that return a single value per row) and aggregation functions (that
577607
return a single value per group) are available. For the complete
578-
list of supported columns and functions, see help on "targets".
579-
You can also provide a wildcard here, which will select a reasonable
580-
default set of columns for rendering a journal.
608+
list of supported columns and functions, see help on "targets" and
609+
"functions". You can also provide a wildcard here, which will
610+
select a reasonable default set of columns for rendering a journal.
581611
582612
from_expr: A logical expression that matches on the attributes of
583613
the directives (not postings). This allows you to select a subset of
@@ -654,10 +684,13 @@ def help_targets(self):
654684
655685
{columns}
656686
687+
----------------------------------------------------------------------
688+
657689
For available functions and aggregates, see "help functions".
658690
659691
""")
660-
print(template.format(**_describe_columns(self.context.tables['postings'].columns)),
692+
693+
print(template.format(columns = _describe_columns(self.context.tables['postings'].columns)),
661694
file=self.outfile)
662695

663696
def help_from(self):
@@ -694,51 +727,78 @@ def help_where(self):
694727
print(template.format(columns=_describe_columns(self.context.tables['postings'].columns)),
695728
file=self.outfile)
696729

697-
def help_functions(self):
698-
"""Show all available functions and aggregates grouped by type."""
699-
template = textwrap.dedent("""
700-
701-
Functions are organized by the type of their first argument:
702-
703-
Amount Functions (work on amounts)
704-
--------------------------------
705-
706-
{amount_functions}
707-
708-
Position Functions (work on positions)
709-
------------------------------------
710-
711-
{position_functions}
712-
713-
Inventory Functions (work on inventories)
714-
---------------------------------------
715-
716-
{inventory_functions}
717-
718-
Date Functions (work on dates)
719-
----------------------------
720-
721-
{date_functions}
722-
723-
Atomic Functions (work on basic types: strings, numbers, etc.)
724-
-----------------------------------------------------------
725-
726-
{atomic_functions}
727-
728-
Aggregates
729-
----------
730-
731-
{aggregates}
732-
733-
""")
734-
print(template.format(
735-
amount_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='amount'),
736-
position_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='position'),
737-
inventory_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='inventory'),
738-
date_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='date'),
739-
atomic_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='atomic'),
740-
aggregates=_describe_functions(query_compile.FUNCTIONS, aggregates=True)
741-
), file=self.outfile)
730+
def help_functions(self, arg=None):
731+
"""Show all available functions and aggregates grouped by type.
732+
733+
Usage: help functions [type]
734+
735+
Without type argument, shows only category descriptions.
736+
With type argument, shows functions for that specific type.
737+
Available types: amount, position, date, atomic, aggregates
738+
"""
739+
if arg is None:
740+
arg = ''
741+
else:
742+
arg = arg.strip().lower()
743+
744+
745+
if not arg:
746+
# Show only category descriptions when no type specified
747+
sections = []
748+
sections.append("\nUsage: help functions [type]\n")
749+
sections.append("Functions are organized by the type of their main argument:\n")
750+
751+
for category in TYPE_CATEGORIES.keys():
752+
title, description = CATEGORY_INFO.get(category, (f"{category.title()} Functions", ""))
753+
section = f" {category}: {title}"
754+
if description:
755+
section += f" - {description}"
756+
sections.append(section)
757+
758+
# Add aggregates info
759+
sections.append(" aggregates: Aggregation Functions - Functions that compute summary values across groups, as defined by the SELECT ... GROUP BY clause.")
760+
761+
# Format output with proper wrapping and indentation
762+
wrapper = textwrap.TextWrapper(width=80, subsequent_indent=' ')
763+
formatted_sections = []
764+
for section in sections:
765+
if section.startswith(' '):
766+
# For category lines, wrap with proper indentation
767+
wrapped = wrapper.fill(section)
768+
formatted_sections.append(wrapped)
769+
else:
770+
# For headers and usage, keep as is
771+
formatted_sections.append(section)
772+
print('\n'.join(formatted_sections), file=self.outfile)
773+
return
774+
775+
# Show functions for specific type
776+
if arg == 'aggregates':
777+
aggregates_content = _describe_functions(query_compile.FUNCTIONS, aggregates=True)
778+
if aggregates_content.strip():
779+
print("Aggregates\n----------\n", file=self.outfile)
780+
print(aggregates_content, file=self.outfile)
781+
else:
782+
print("No aggregate functions found.", file=self.outfile)
783+
return
784+
785+
if arg not in TYPE_CATEGORIES:
786+
available_types = list(TYPE_CATEGORIES.keys()) + ['aggregates']
787+
available_types = [t for t in available_types if t != 'account'] # Skip account
788+
print(f"Unknown type '{arg}'. Available types: {', '.join(available_types)}", file=self.outfile)
789+
return
790+
791+
title, description = CATEGORY_INFO.get(arg, (f"{arg.title()} Functions", ""))
792+
underline = '-' * len(title)
793+
functions_content = _describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter=arg)
794+
795+
if functions_content.strip():
796+
print(f"{title}\n{underline}", file=self.outfile)
797+
if description:
798+
print(f"\n{description}", file=self.outfile)
799+
print(f"\n{functions_content}", file=self.outfile)
800+
else:
801+
print(f"No functions found for type '{arg}'.", file=self.outfile)
742802

743803

744804
def _describe_columns(columns):

0 commit comments

Comments
 (0)