Extract fetch from cli, etc.

This commit is contained in:
Chris Berkhout 2021-05-28 17:39:23 +02:00
parent 96970f736c
commit e27e78d47a
9 changed files with 113 additions and 92 deletions

View file

@ -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

18
src/pricehist/fetch.py Normal file
View file

@ -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)

View file

@ -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)

View file

@ -3,7 +3,7 @@ from .csv import CSV
from .gnucashsql import GnuCashSQL
from .ledger import Ledger
default = "ledger"
default = "csv"
by_type = {
"beancount": Beancount(),

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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,
)

View file

@ -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: