diff --git a/.gitignore b/.gitignore index b6e47617d..badc4b460 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,8 @@ dmypy.json # Pyre type checker .pyre/ + +# generated files +*stocklog.csv + +.DS_Store diff --git a/Work/bounce.py b/Work/bounce.py index 3660ddd82..60ff9a6dc 100644 --- a/Work/bounce.py +++ b/Work/bounce.py @@ -1,3 +1,13 @@ # bounce.py # # Exercise 1.5 + +def main(): + cur_height = 100 + + for bounce in range(10): + cur_height *= 3/5 + print(bounce+1, round(cur_height, 4)) + +if __name__ == "__main__": + main() diff --git a/Work/fileparse.py b/Work/fileparse.py deleted file mode 100644 index 1d499e733..000000000 --- a/Work/fileparse.py +++ /dev/null @@ -1,3 +0,0 @@ -# fileparse.py -# -# Exercise 3.3 diff --git a/Work/mortgage.py b/Work/mortgage.py index d527314e3..53b71d23b 100644 --- a/Work/mortgage.py +++ b/Work/mortgage.py @@ -1,3 +1,29 @@ # mortgage.py # # Exercise 1.7 + +def calc(start, end, extra_payment): + principal = 500000.0 + rate = 0.05 + payment = 2684.11 + npayments = 0 + total_paid = 0.0 + + while principal > 0: + npayments = npayments + 1 + cur_payment = payment + extra_payment if start <= npayments <= end else payment + remaining = principal * (1+rate/12) + if cur_payment > remaining: # overpayment? + cur_payment = remaining + principal = remaining - cur_payment + total_paid = total_paid + cur_payment + print(f'{npayments} {cur_payment:>8.2f} {total_paid:>10.2f} {principal:>10.2f}') + + print(f'{34*"-"}\nTotal paid: {total_paid:0.2f} in {npayments} months') + +if __name__ == "__main__": + start = input('start month: ') + end = input('end month: ') + extra_payment = input('extra payment: ') + args = [int(s) for s in (start, end, extra_payment)] + calc(*args) diff --git a/Work/pcost.py b/Work/pcost.py deleted file mode 100644 index e68aa20b4..000000000 --- a/Work/pcost.py +++ /dev/null @@ -1,3 +0,0 @@ -# pcost.py -# -# Exercise 1.27 diff --git a/Work/porty-app/portfolio.csv b/Work/porty-app/portfolio.csv new file mode 100644 index 000000000..6c16f65b5 --- /dev/null +++ b/Work/porty-app/portfolio.csv @@ -0,0 +1,8 @@ +name,shares,price +"AA",100,32.20 +"IBM",50,91.10 +"CAT",150,83.44 +"MSFT",200,51.23 +"GE",95,40.37 +"MSFT",50,65.10 +"IBM",100,70.44 diff --git a/Work/porty-app/porty/fileparse.py b/Work/porty-app/porty/fileparse.py new file mode 100644 index 000000000..eae23874c --- /dev/null +++ b/Work/porty-app/porty/fileparse.py @@ -0,0 +1,63 @@ +# fileparse.py +# +# Exercise 3.3 +import csv +import logging +log = logging.getLogger(__name__) + +def parse_csv(data, + select=None, + types=None, + has_headers=True, + delimiter=',', + silence_errors=False): + """Parse a CSV stream into a list of records + structured as dicts. + + :data: file-like object + :select: selected columns + :types: conversion functions + :has_headers + :delimiter + :silence_errors + :returns: list of records (dicts) + + """ + if select and not has_headers: + raise RuntimeError('select argument requires column headers') + + if select and types and len(select) != len(types): + raise RuntimeError('select and types lists must have the same shape') + + # type conversion helper + def convert(rows): + assert types + for i, row in enumerate(rows, 1): + try: + yield [t(val) for t, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + log.warning("Row %d: Couldn't convert %s", i, row) + log.debug("Row %d: Reason %s", i, e) + + rows = csv.reader(data, delimiter=delimiter) + if has_headers: + headers = next(rows) + + rows = (r for r in rows if r) # generate only non-empty rows + + # filter columns + if select: + indices = [headers.index(c) for c in select] + headers = select + rows = ([row[i] for i in indices] for row in rows) + + # convert to types + if types: + rows = convert(rows) + + # package to records + if has_headers: + return [dict(zip(headers, row)) for row in rows] + else: + return [tuple(r) for r in rows] diff --git a/Work/porty-app/porty/follow.py b/Work/porty-app/porty/follow.py new file mode 100644 index 000000000..91326af09 --- /dev/null +++ b/Work/porty-app/porty/follow.py @@ -0,0 +1,43 @@ +# follow.py +import os, sys +import time + +def follow(logfile): + with open(logfile) as f: + f.seek(0, os.SEEK_END) # move file pointer 0 bytes from EOF + + while True: + line = f.readline() + if not line: + time.sleep(0.1) # sleep briefly & retry + continue + yield line + +def filematch(lines, substr): + for line in lines: + if substr in line: + yield line + +def convert(lines): + for line in lines: + fields = line.split(',') + name = fields[0].strip('"') + price = float(fields[1]) + change = float(fields[4]) + yield (name, price, change) + +def ticker(portfile, tickerfile): + import report + portfolio = report.read_portfolio(portfile) + for name, price, change in convert(follow(tickerfile)): + if name in portfolio: + print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') + +def main(argv): + if len(argv) != 3: + raise SystemExit(f'usage: {argv[0]} portfile tickerfile') + + ticker(argv[1], argv[2]) + +if __name__ == "__main__": + main(sys.argv) diff --git a/Work/porty-app/porty/pcost.py b/Work/porty-app/porty/pcost.py new file mode 100755 index 000000000..cbe2d0600 --- /dev/null +++ b/Work/porty-app/porty/pcost.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# pcost.py +# +# Exercise 1.27 + +import csv +import sys +from . import report + +def portfolio_cost(path): + portfolio = report.read_portfolio(path) + return portfolio.total_cost + +def main(argv): + if len(argv) != 2: + raise SystemExit(f'Usage: {argv[0]} portfile') + total = portfolio_cost(argv[1]) + print(f'Total cost ${total}') + +if __name__ == "__main__": + import sys + main(sys.argv) diff --git a/Work/porty-app/porty/portfolio.py b/Work/porty-app/porty/portfolio.py new file mode 100644 index 000000000..73c08a4f4 --- /dev/null +++ b/Work/porty-app/porty/portfolio.py @@ -0,0 +1,48 @@ +# portfolio.py + +from . import fileparse +from . import stock + +class Portfolio: + def __init__(self): + self._holdings = [] + + def append(self, holding): + if not isinstance(holding, stock.Stock): + raise TypeError('Expected a stock instance') + self._holdings.append(holding) + + @classmethod + def from_csv(cls, lines, **opts): + self = cls() + portfolio_dicts = fileparse.parse_csv(lines, + select=['name', 'shares', 'price'], + types=[str, int, float], + **opts) + for d in portfolio_dicts: + self.append(stock.Stock(**d)) + + return self + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any(s.name==name for s in self._holdings) + + @property + def total_cost(self): + return sum(s.cost for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares diff --git a/Work/porty-app/porty/report.py b/Work/porty-app/porty/report.py new file mode 100755 index 000000000..4bc91b8fe --- /dev/null +++ b/Work/porty-app/porty/report.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# report.py +# +# Exercise 2.4 + +import csv +from . import fileparse +from . import tableformat +from .stock import Stock +from .portfolio import Portfolio +import logging +logging.basicConfig( + # filename = 'app.log', + # filemode = 'w', + level = logging.WARNING, + ) + +def read_portfolio(path, **opts): + """ + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + """ + with open(path, 'rt') as file: + portfolio = Portfolio.from_csv(file) + return portfolio + +def read_prices(path): + """ + Parses price data file into stock price dictionary + """ + with open(path, 'rt') as file: + prices = fileparse.parse_csv(file, types=[str, float], has_headers=False) + return {n:p for n, p in prices} + +def make_report(portfolio, prices): + return [(s.name, s.shares, prices[s.name], prices[s.name] - s.price) + for s in portfolio] + +def print_report(report, formatter): + formatter.headings(['Name', 'Shares', 'Price', 'Change']) + for name, shares, price, change in report: + formatter.row((name, shares, f'${price:0.2f}', f'{change:0.2f}')) + +def portfolio_report(path_portfolio, path_prices, fmt='txt'): + # collect report data + portfolio = read_portfolio(path_portfolio) + prices = read_prices(path_prices) + report = make_report(portfolio, prices) + + # print it! + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(argv): + if len(argv) < 3: + raise SystemExit(f'Usage: {argv[0]} portfile pricefile [format(txt|csv|html)]') + elif len(argv) == 3: + portfolio_report(argv[1], argv[2]) + else: # optional format arg? + portfolio_report(argv[1], argv[2], fmt=argv[3]) + +if __name__ == "__main__": + import sys + main(sys.argv) diff --git a/Work/porty-app/porty/stock.py b/Work/porty-app/porty/stock.py new file mode 100644 index 000000000..a9cb08441 --- /dev/null +++ b/Work/porty-app/porty/stock.py @@ -0,0 +1,32 @@ +from .typedproperty import String, Integer, Float + +class Stock(object): + """Represents stock holding.""" + __slots__ = ('_name', '_shares', '_price') + name = String('name') + shares = Integer('shares') + price = Float('price') + + + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f"Stock('{self.name}', {self.shares}, {self.price})" + + def __str__(self): + return repr(self) + + def __unicode__(self): + return str(self) + + @property + def cost(self): + return self.shares * self.price + + def sell(self, n): + assert n <= self.shares + self.shares -= n + return self diff --git a/Work/porty-app/porty/tableformat.py b/Work/porty-app/porty/tableformat.py new file mode 100644 index 000000000..b420f20db --- /dev/null +++ b/Work/porty-app/porty/tableformat.py @@ -0,0 +1,73 @@ +# tableformat.py + +def create_formatter(fmt): + formatters = { + 'txt': TextTableFormatter, + 'csv': CSVTableFormatter, + 'html': HTMLTableFormatter, + } + + if fmt not in formatters.keys(): + raise FormatError(f'Unknown table format {fmt}') + + return formatters[fmt]() + +def print_table(portfolio, columns, formatter): + formatter.headings(columns) + for s in portfolio: + data = [str(getattr(s, a)) for a in columns] + formatter.row(data) + +def stringify(row): + return (str(val) for val in row) + +class TableFormatter: + """ + Formatter ABC + """ + def headings(self, headers): + """ + Emit the table headings. + """ + raise NotImplementedError() + + def row(self, data): + """ + Emit a single row of table data. + """ + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, data): + for d in stringify(data): + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + def headings(self, headers): + print(','.join(headers)) + + def row(self, data): + print(','.join(stringify(data))) + +class HTMLTableFormatter(TableFormatter): + def headings(self, headers): + print('