Extract fetch from cli, etc.
This commit is contained in:
parent
96970f736c
commit
e27e78d47a
9 changed files with 113 additions and 92 deletions
|
@ -6,6 +6,7 @@ from datetime import datetime, timedelta
|
||||||
from textwrap import TextWrapper
|
from textwrap import TextWrapper
|
||||||
|
|
||||||
from pricehist import __version__, outputs, sources
|
from pricehist import __version__, outputs, sources
|
||||||
|
from pricehist.fetch import fetch
|
||||||
from pricehist.format import Format
|
from pricehist.format import Format
|
||||||
from pricehist.series import Series
|
from pricehist.series import Series
|
||||||
|
|
||||||
|
@ -17,10 +18,10 @@ def cli(args=None, output_file=sys.stdout):
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.verbose:
|
if args.debug:
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
|
||||||
elif args.debug:
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
elif args.verbose:
|
||||||
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
logging.debug(f"Started pricehist run at {start_time}.")
|
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":
|
elif args.command == "source":
|
||||||
print(cmd_source(args), file=output_file)
|
print(cmd_source(args), file=output_file)
|
||||||
elif args.command == "fetch":
|
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:
|
else:
|
||||||
parser.print_help(file=sys.stderr)
|
parser.print_help(file=sys.stderr)
|
||||||
except BrokenPipeError:
|
except BrokenPipeError:
|
||||||
|
@ -92,45 +104,6 @@ def cmd_source(args):
|
||||||
return "\n".join(filter(None, parts))
|
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 build_parser():
|
||||||
def valid_date(s):
|
def valid_date(s):
|
||||||
if s == "today":
|
if s == "today":
|
||||||
|
@ -214,12 +187,15 @@ def build_parser():
|
||||||
"fetch",
|
"fetch",
|
||||||
help="fetch prices",
|
help="fetch prices",
|
||||||
usage=(
|
usage=(
|
||||||
|
# Set usage manually to have positional arguments before options
|
||||||
|
# and show allowed values where appropriate
|
||||||
"pricehist fetch SOURCE PAIR [-h] "
|
"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] "
|
"[--invert] [--quantize INT] "
|
||||||
"[--rename-base SYM] [--rename-quote SYM] [--rename-time TIME] "
|
"[--fmt-base SYM] [--fmt-quote SYM] [--fmt-time TIME] "
|
||||||
"[--format-decimal CHAR] [--format-thousands CHAR] "
|
"[--fmt-decimal CHAR] [--fmt-thousands CHAR] "
|
||||||
"[--format-symbol rightspace|right|leftspace|left] [--format-datesep CHAR]"
|
"[--fmt-symbol rightspace|right|leftspace|left] [--fmt-datesep CHAR]"
|
||||||
),
|
),
|
||||||
formatter_class=formatter,
|
formatter_class=formatter,
|
||||||
)
|
)
|
||||||
|
@ -234,7 +210,7 @@ def build_parser():
|
||||||
"pair",
|
"pair",
|
||||||
metavar="PAIR",
|
metavar="PAIR",
|
||||||
type=str,
|
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(
|
fetch_parser.add_argument(
|
||||||
"-t",
|
"-t",
|
||||||
|
@ -301,58 +277,58 @@ def build_parser():
|
||||||
dest="quantize",
|
dest="quantize",
|
||||||
metavar="INT",
|
metavar="INT",
|
||||||
type=int,
|
type=int,
|
||||||
help="quantize to the given number of decimal places",
|
help="round 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",
|
|
||||||
)
|
)
|
||||||
default_fmt = Format()
|
default_fmt = Format()
|
||||||
fetch_parser.add_argument(
|
fetch_parser.add_argument(
|
||||||
"--rename-time",
|
"--fmt-base",
|
||||||
dest="renametime",
|
dest="formatbase",
|
||||||
metavar="TIME",
|
metavar="SYM",
|
||||||
type=str,
|
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(
|
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",
|
dest="formatdecimal",
|
||||||
metavar="CHAR",
|
metavar="CHAR",
|
||||||
type=str,
|
type=str,
|
||||||
help=f"decimal point (default: '{default_fmt.decimal}')",
|
help=f"decimal point in output (default: '{default_fmt.decimal}')",
|
||||||
)
|
)
|
||||||
fetch_parser.add_argument(
|
fetch_parser.add_argument(
|
||||||
"--format-thousands",
|
"--fmt-thousands",
|
||||||
dest="formatthousands",
|
dest="formatthousands",
|
||||||
metavar="CHAR",
|
metavar="CHAR",
|
||||||
type=str,
|
type=str,
|
||||||
help=f"thousands separator (default: '{default_fmt.thousands}')",
|
help=f"thousands separator in output (default: '{default_fmt.thousands}')",
|
||||||
)
|
)
|
||||||
fetch_parser.add_argument(
|
fetch_parser.add_argument(
|
||||||
"--format-symbol",
|
"--fmt-symbol",
|
||||||
dest="formatsymbol",
|
dest="formatsymbol",
|
||||||
metavar="LOC",
|
metavar="LOCATION",
|
||||||
type=str,
|
type=str,
|
||||||
choices=["rightspace", "right", "leftspace", "left"],
|
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(
|
fetch_parser.add_argument(
|
||||||
"--format-datesep",
|
"--fmt-datesep",
|
||||||
dest="formatdatesep",
|
dest="formatdatesep",
|
||||||
metavar="CHAR",
|
metavar="CHAR",
|
||||||
type=str,
|
type=str,
|
||||||
help=f"date separator (default: '{default_fmt.datesep}')",
|
help=f"date separator in output (default: '{default_fmt.datesep}')",
|
||||||
)
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
18
src/pricehist/fetch.py
Normal file
18
src/pricehist/fetch.py
Normal 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)
|
|
@ -3,12 +3,30 @@ from dataclasses import dataclass
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Format:
|
class Format:
|
||||||
|
base: str = None
|
||||||
|
quote: str = None
|
||||||
time: str = "00:00:00"
|
time: str = "00:00:00"
|
||||||
decimal: str = "."
|
decimal: str = "."
|
||||||
thousands: str = ""
|
thousands: str = ""
|
||||||
symbol: str = "rightspace"
|
symbol: str = "rightspace"
|
||||||
datesep: str = "-"
|
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):
|
def format_date(self, date):
|
||||||
return str(date).replace("-", self.datesep)
|
return str(date).replace("-", self.datesep)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from .csv import CSV
|
||||||
from .gnucashsql import GnuCashSQL
|
from .gnucashsql import GnuCashSQL
|
||||||
from .ledger import Ledger
|
from .ledger import Ledger
|
||||||
|
|
||||||
default = "ledger"
|
default = "csv"
|
||||||
|
|
||||||
by_type = {
|
by_type = {
|
||||||
"beancount": Beancount(),
|
"beancount": Beancount(),
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from pricehist.format import Format
|
||||||
|
from pricehist.series import Series
|
||||||
|
from pricehist.sources.basesource import BaseSource
|
||||||
|
|
||||||
|
|
||||||
class BaseOutput(ABC):
|
class BaseOutput(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def format(self) -> str:
|
def format(self, series: Series, source: BaseSource, fmt: Format) -> str:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -7,12 +7,13 @@ class Beancount(BaseOutput):
|
||||||
def format(self, series, source=None, fmt=Format()):
|
def format(self, series, source=None, fmt=Format()):
|
||||||
lines = []
|
lines = []
|
||||||
for price in series.prices:
|
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 number (not . for decimal)
|
||||||
# TODO warn if fmt settings make an invalid quote (not right/rightspace)
|
# TODO warn if fmt settings make an invalid quote (not right/rightspace)
|
||||||
|
|
||||||
date = fmt.format_date(price.date)
|
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"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,9 @@ class CSV(BaseOutput):
|
||||||
lines = ["date,base,quote,amount,source,type"]
|
lines = ["date,base,quote,amount,source,type"]
|
||||||
for price in series.prices:
|
for price in series.prices:
|
||||||
date = fmt.format_date(price.date)
|
date = fmt.format_date(price.date)
|
||||||
|
base = fmt.base or series.base
|
||||||
|
quote = fmt.quote or series.quote
|
||||||
amount = fmt.format_num(price.amount)
|
amount = fmt.format_num(price.amount)
|
||||||
line = ",".join(
|
line = ",".join([date, base, quote, amount, source.id(), series.type])
|
||||||
[date, series.base, series.quote, amount, source.id(), series.type]
|
|
||||||
)
|
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
|
@ -10,6 +10,8 @@ from .baseoutput import BaseOutput
|
||||||
|
|
||||||
class GnuCashSQL(BaseOutput):
|
class GnuCashSQL(BaseOutput):
|
||||||
def format(self, series, source=None, fmt=Format()):
|
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()}"
|
src = f"pricehist:{source.id()}"
|
||||||
|
|
||||||
values_parts = []
|
values_parts = []
|
||||||
|
@ -20,8 +22,8 @@ class GnuCashSQL(BaseOutput):
|
||||||
"".join(
|
"".join(
|
||||||
[
|
[
|
||||||
date,
|
date,
|
||||||
series.base,
|
base,
|
||||||
series.quote,
|
quote,
|
||||||
src,
|
src,
|
||||||
series.type,
|
series.type,
|
||||||
str(price.amount),
|
str(price.amount),
|
||||||
|
@ -36,8 +38,8 @@ class GnuCashSQL(BaseOutput):
|
||||||
"("
|
"("
|
||||||
f"'{guid}', "
|
f"'{guid}', "
|
||||||
f"'{date}', "
|
f"'{date}', "
|
||||||
f"'{series.base}', "
|
f"'{base}', "
|
||||||
f"'{series.quote}', "
|
f"'{quote}', "
|
||||||
f"'{src}', "
|
f"'{src}', "
|
||||||
f"'{series.type}', "
|
f"'{series.type}', "
|
||||||
f"{value_num}, "
|
f"{value_num}, "
|
||||||
|
@ -50,8 +52,8 @@ class GnuCashSQL(BaseOutput):
|
||||||
sql = read_text("pricehist.resources", "gnucash.sql").format(
|
sql = read_text("pricehist.resources", "gnucash.sql").format(
|
||||||
version=__version__,
|
version=__version__,
|
||||||
timestamp=datetime.utcnow().isoformat() + "Z",
|
timestamp=datetime.utcnow().isoformat() + "Z",
|
||||||
base=series.base,
|
base=base,
|
||||||
quote=series.quote,
|
quote=quote,
|
||||||
values=values,
|
values=values,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,10 @@ class Ledger(BaseOutput):
|
||||||
lines = []
|
lines = []
|
||||||
for price in series.prices:
|
for price in series.prices:
|
||||||
date = fmt.format_date(price.date)
|
date = fmt.format_date(price.date)
|
||||||
quote_amount = fmt.format_quote_amount(series.quote, price.amount)
|
base = fmt.base or series.base
|
||||||
lines.append(f"P {date} {fmt.time} {series.base} {quote_amount}")
|
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"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
# TODO support additional details of the format:
|
# TODO support additional details of the format:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue