diff --git a/src/pricehist/cli.py b/src/pricehist/cli.py index 589dfb9..d05eba8 100644 --- a/src/pricehist/cli.py +++ b/src/pricehist/cli.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from textwrap import TextWrapper from pricehist import __version__, outputs, sources +from pricehist.fetch import fetch from pricehist.format import Format from pricehist.series import Series @@ -17,10 +18,10 @@ def cli(args=None, output_file=sys.stdout): parser = build_parser() args = parser.parse_args() - if args.verbose: - logging.getLogger().setLevel(logging.INFO) - elif args.debug: + if args.debug: logging.getLogger().setLevel(logging.DEBUG) + elif args.verbose: + logging.getLogger().setLevel(logging.INFO) logging.debug(f"Started pricehist run at {start_time}.") @@ -32,7 +33,18 @@ def cli(args=None, output_file=sys.stdout): elif args.command == "source": print(cmd_source(args), file=output_file) elif args.command == "fetch": - print(cmd_fetch(args), end="", file=output_file) + source = sources.by_id[args.source] + output = outputs.by_type[args.output] + series = Series( + base=args.pair.split("/")[0], + quote=args.pair.split("/")[1], + type=args.type or (source.types() + ["unknown"])[0], + start=args.start or source.start(), + end=args.end, + ) + fmt = Format.generate(args) + result = fetch(series, source, output, args.invert, args.quantize, fmt) + print(result, end="", file=output_file) else: parser.print_help(file=sys.stderr) except BrokenPipeError: @@ -92,45 +104,6 @@ def cmd_source(args): return "\n".join(filter(None, parts)) -def cmd_fetch(args): - source = sources.by_id[args.source] - start = args.start or source.start() - type = args.type or (source.types() + ["unknown"])[0] - - if start < source.start(): - logging.warn( - f"The start date {start} preceeds the {source.name()} " - f"source start date of {source.start()}." - ) - - base, quote = args.pair.split("/") - series = source.fetch(Series(base, quote, type, start, args.end)) - - if args.invert: - series = series.invert() - if args.quantize is not None: - series = series.quantize(args.quantize) - if args.renamebase: - series = series.rename_base(args.renamebase) - if args.renamequote: - series = series.rename_quote(args.renamequote) - - def if_not_none(value, default): - return default if value is None else value - - default = Format() - fmt = Format( - time=if_not_none(args.renametime, default.time), - decimal=if_not_none(args.formatdecimal, default.decimal), - thousands=if_not_none(args.formatthousands, default.thousands), - symbol=if_not_none(args.formatsymbol, default.symbol), - datesep=if_not_none(args.formatdatesep, default.datesep), - ) - - output = outputs.by_type[args.output] - return output.format(series, source, fmt=fmt) - - def build_parser(): def valid_date(s): if s == "today": @@ -214,12 +187,15 @@ def build_parser(): "fetch", help="fetch prices", usage=( + # Set usage manually to have positional arguments before options + # and show allowed values where appropriate "pricehist fetch SOURCE PAIR [-h] " - "[--type TYPE] [-s DATE | -sx DATE] [-e DATE | -ex DATE] [-o FMT] " + "[-t TYPE] [-s DATE | -sx DATE] [-e DATE | -ex DATE] " + f"[-o {'|'.join(outputs.by_type.keys())}] " "[--invert] [--quantize INT] " - "[--rename-base SYM] [--rename-quote SYM] [--rename-time TIME] " - "[--format-decimal CHAR] [--format-thousands CHAR] " - "[--format-symbol rightspace|right|leftspace|left] [--format-datesep CHAR]" + "[--fmt-base SYM] [--fmt-quote SYM] [--fmt-time TIME] " + "[--fmt-decimal CHAR] [--fmt-thousands CHAR] " + "[--fmt-symbol rightspace|right|leftspace|left] [--fmt-datesep CHAR]" ), formatter_class=formatter, ) @@ -234,7 +210,7 @@ def build_parser(): "pair", metavar="PAIR", type=str, - help="pair, usually BASE/QUOTE, e.g. BTC/USD", + help="symbols in the form BASE/QUOTE, e.g. BTC/USD", ) fetch_parser.add_argument( "-t", @@ -301,58 +277,58 @@ def build_parser(): dest="quantize", metavar="INT", type=int, - help="quantize to the given number of decimal places", - ) - fetch_parser.add_argument( - "--rename-base", - dest="renamebase", - metavar="SYM", - type=str, - help="rename base symbol", - ) - fetch_parser.add_argument( - "--rename-quote", - dest="renamequote", - metavar="SYM", - type=str, - help="rename quote symbol", + help="round to the given number of decimal places", ) default_fmt = Format() fetch_parser.add_argument( - "--rename-time", - dest="renametime", - metavar="TIME", + "--fmt-base", + dest="formatbase", + metavar="SYM", type=str, - help=f"set a particular time of day (default: {default_fmt.time})", + help="rename the base symbol in output", ) fetch_parser.add_argument( - "--format-decimal", + "--fmt-quote", + dest="formatquote", + metavar="SYM", + type=str, + help="rename the quote symbol in output", + ) + fetch_parser.add_argument( + "--fmt-time", + dest="formattime", + metavar="TIME", + type=str, + help=f"set a particular time of day in output (default: {default_fmt.time})", + ) + fetch_parser.add_argument( + "--fmt-decimal", dest="formatdecimal", metavar="CHAR", type=str, - help=f"decimal point (default: '{default_fmt.decimal}')", + help=f"decimal point in output (default: '{default_fmt.decimal}')", ) fetch_parser.add_argument( - "--format-thousands", + "--fmt-thousands", dest="formatthousands", metavar="CHAR", type=str, - help=f"thousands separator (default: '{default_fmt.thousands}')", + help=f"thousands separator in output (default: '{default_fmt.thousands}')", ) fetch_parser.add_argument( - "--format-symbol", + "--fmt-symbol", dest="formatsymbol", - metavar="LOC", + metavar="LOCATION", type=str, choices=["rightspace", "right", "leftspace", "left"], - help=f"commodity symbol placement (default: {default_fmt.symbol})", + help=f"commodity symbol placement in output (default: {default_fmt.symbol})", ) fetch_parser.add_argument( - "--format-datesep", + "--fmt-datesep", dest="formatdatesep", metavar="CHAR", type=str, - help=f"date separator (default: '{default_fmt.datesep}')", + help=f"date separator in output (default: '{default_fmt.datesep}')", ) return parser diff --git a/src/pricehist/fetch.py b/src/pricehist/fetch.py new file mode 100644 index 0000000..20a43d3 --- /dev/null +++ b/src/pricehist/fetch.py @@ -0,0 +1,18 @@ +import logging + + +def fetch(series, source, output, invert: bool, quantize: int, fmt) -> str: + if series.start < source.start(): + logging.warn( + f"The start date {series.start} preceeds the {source.name()} " + f"source start date of {source.start()}." + ) + + series = source.fetch(series) + + if invert: + series = series.invert() + if quantize is not None: + series = series.quantize(quantize) + + return output.format(series, source, fmt=fmt) diff --git a/src/pricehist/format.py b/src/pricehist/format.py index 36bb023..0ff01b6 100644 --- a/src/pricehist/format.py +++ b/src/pricehist/format.py @@ -3,12 +3,30 @@ from dataclasses import dataclass @dataclass(frozen=True) class Format: + base: str = None + quote: str = None time: str = "00:00:00" decimal: str = "." thousands: str = "" symbol: str = "rightspace" datesep: str = "-" + @classmethod + def generate(cls, args): + def if_not_none(value, default): + return default if value is None else value + + default = cls() + return cls( + base=if_not_none(args.formatbase, default.base), + quote=if_not_none(args.formatquote, default.quote), + time=if_not_none(args.formattime, default.time), + decimal=if_not_none(args.formatdecimal, default.decimal), + thousands=if_not_none(args.formatthousands, default.thousands), + symbol=if_not_none(args.formatsymbol, default.symbol), + datesep=if_not_none(args.formatdatesep, default.datesep), + ) + def format_date(self, date): return str(date).replace("-", self.datesep) diff --git a/src/pricehist/outputs/__init__.py b/src/pricehist/outputs/__init__.py index 96381de..98e9547 100644 --- a/src/pricehist/outputs/__init__.py +++ b/src/pricehist/outputs/__init__.py @@ -3,7 +3,7 @@ from .csv import CSV from .gnucashsql import GnuCashSQL from .ledger import Ledger -default = "ledger" +default = "csv" by_type = { "beancount": Beancount(), diff --git a/src/pricehist/outputs/baseoutput.py b/src/pricehist/outputs/baseoutput.py index 33cb195..f3cb58f 100644 --- a/src/pricehist/outputs/baseoutput.py +++ b/src/pricehist/outputs/baseoutput.py @@ -1,7 +1,11 @@ from abc import ABC, abstractmethod +from pricehist.format import Format +from pricehist.series import Series +from pricehist.sources.basesource import BaseSource + class BaseOutput(ABC): @abstractmethod - def format(self) -> str: + def format(self, series: Series, source: BaseSource, fmt: Format) -> str: pass diff --git a/src/pricehist/outputs/beancount.py b/src/pricehist/outputs/beancount.py index 958b62b..9f2b2b1 100644 --- a/src/pricehist/outputs/beancount.py +++ b/src/pricehist/outputs/beancount.py @@ -7,12 +7,13 @@ class Beancount(BaseOutput): def format(self, series, source=None, fmt=Format()): lines = [] for price in series.prices: - quote_amount = fmt.format_quote_amount(series.quote, price.amount) # TODO warn if fmt settings make an invalid number (not . for decimal) # TODO warn if fmt settings make an invalid quote (not right/rightspace) - date = fmt.format_date(price.date) - lines.append(f"{date} price {series.base} {quote_amount}") + base = fmt.base or series.base + quote = fmt.quote or series.quote + quote_amount = fmt.format_quote_amount(quote, price.amount) + lines.append(f"{date} price {base} {quote_amount}") return "\n".join(lines) + "\n" diff --git a/src/pricehist/outputs/csv.py b/src/pricehist/outputs/csv.py index 7921a8b..81cfc02 100644 --- a/src/pricehist/outputs/csv.py +++ b/src/pricehist/outputs/csv.py @@ -8,9 +8,9 @@ class CSV(BaseOutput): lines = ["date,base,quote,amount,source,type"] for price in series.prices: date = fmt.format_date(price.date) + base = fmt.base or series.base + quote = fmt.quote or series.quote amount = fmt.format_num(price.amount) - line = ",".join( - [date, series.base, series.quote, amount, source.id(), series.type] - ) + line = ",".join([date, base, quote, amount, source.id(), series.type]) lines.append(line) return "\n".join(lines) + "\n" diff --git a/src/pricehist/outputs/gnucashsql.py b/src/pricehist/outputs/gnucashsql.py index efca147..012901b 100644 --- a/src/pricehist/outputs/gnucashsql.py +++ b/src/pricehist/outputs/gnucashsql.py @@ -10,6 +10,8 @@ from .baseoutput import BaseOutput class GnuCashSQL(BaseOutput): def format(self, series, source=None, fmt=Format()): + base = fmt.base or series.base + quote = fmt.quote or series.quote src = f"pricehist:{source.id()}" values_parts = [] @@ -20,8 +22,8 @@ class GnuCashSQL(BaseOutput): "".join( [ date, - series.base, - series.quote, + base, + quote, src, series.type, str(price.amount), @@ -36,8 +38,8 @@ class GnuCashSQL(BaseOutput): "(" f"'{guid}', " f"'{date}', " - f"'{series.base}', " - f"'{series.quote}', " + f"'{base}', " + f"'{quote}', " f"'{src}', " f"'{series.type}', " f"{value_num}, " @@ -50,8 +52,8 @@ class GnuCashSQL(BaseOutput): sql = read_text("pricehist.resources", "gnucash.sql").format( version=__version__, timestamp=datetime.utcnow().isoformat() + "Z", - base=series.base, - quote=series.quote, + base=base, + quote=quote, values=values, ) diff --git a/src/pricehist/outputs/ledger.py b/src/pricehist/outputs/ledger.py index f69fbcc..c030f9a 100644 --- a/src/pricehist/outputs/ledger.py +++ b/src/pricehist/outputs/ledger.py @@ -8,8 +8,10 @@ class Ledger(BaseOutput): lines = [] for price in series.prices: date = fmt.format_date(price.date) - quote_amount = fmt.format_quote_amount(series.quote, price.amount) - lines.append(f"P {date} {fmt.time} {series.base} {quote_amount}") + base = fmt.base or series.base + quote = fmt.quote or series.quote + quote_amount = fmt.format_quote_amount(quote, price.amount) + lines.append(f"P {date} {fmt.time} {base} {quote_amount}") return "\n".join(lines) + "\n" # TODO support additional details of the format: