diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..18c002a3c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "C:\\Python3\\python.exe" +} \ No newline at end of file diff --git a/Work/bounce.py b/Work/bounce.py index 3660ddd82..857c9b9f7 100644 --- a/Work/bounce.py +++ b/Work/bounce.py @@ -1,3 +1,7 @@ # bounce.py -# -# Exercise 1.5 + +height = 100 +bounces = range(1, 11) +for bounce in bounces: + height *= 3/5 + print(bounce, round(height, 4)) 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..201b4698d 100644 --- a/Work/mortgage.py +++ b/Work/mortgage.py @@ -1,3 +1,25 @@ # mortgage.py -# -# Exercise 1.7 + +principal = 500000.0 +rate = 0.05 +payment = 2684.11 +total_paid = 0.0 +month = 0 +extra_payment_start_month = 60 +extra_payment_end_month = 108 +extra_payment = 1000 + +while principal > 0: + month += 1 + if principal < payment: + payment = principal * (1+rate/12) + principal = 0 + total_paid = total_paid + payment + elif extra_payment_start_month <= month <= extra_payment_end_month: + principal = principal * (1+rate/12) - payment - extra_payment + total_paid = total_paid + payment + extra_payment + else: + principal = principal * (1+rate/12) - payment + total_paid = total_paid + payment + + print(f"{month:3} {total_paid:10.2f} {principal:9.2f}") 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/pcost_csv.py b/Work/pcost_csv.py new file mode 100644 index 000000000..27bbdf829 --- /dev/null +++ b/Work/pcost_csv.py @@ -0,0 +1,29 @@ +# pcost_csv.py +import csv +import sys + + +def portfolio_cost(filename): + with open(filename, 'rt') as f: + rows = csv.reader(f) + headers = next(rows) + total_cost = 0 + for rowno, row in enumerate(rows, start=1): + record = dict(zip(headers, row)) + try: + nshares = int(record['shares']) + price = float(record['price']) + total_cost += nshares * price + except ValueError: + print(f'Row {rowno}: Bad row: {row}') + return total_cost + + +if len(sys.argv) == 2: + filename = sys.argv[1] +else: + filename = 'Data/portfolio.csv' + +cost = portfolio_cost(filename) +print(filename) +print("Total cost:", cost) diff --git a/Work/porty-app/MANIFEST.in b/Work/porty-app/MANIFEST.in new file mode 100644 index 000000000..392dc5b0a --- /dev/null +++ b/Work/porty-app/MANIFEST.in @@ -0,0 +1,2 @@ +# MANIFEST.in +include *.csv diff --git a/Work/porty-app/README.txt b/Work/porty-app/README.txt new file mode 100644 index 000000000..404ab6106 --- /dev/null +++ b/Work/porty-app/README.txt @@ -0,0 +1 @@ +Thanks Beazley! \ No newline at end of file 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/__init__.py b/Work/porty-app/porty/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Work/porty-app/porty/fileparse.py b/Work/porty-app/porty/fileparse.py new file mode 100644 index 000000000..3e60c15c6 --- /dev/null +++ b/Work/porty-app/porty/fileparse.py @@ -0,0 +1,51 @@ +# fileparse.py +import csv +import logging +log = logging.getLogger(__name__) + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + # print(f"Row {rowno}: Couldn't convert {row}") + # print(f"Row {rowno}: Reason {e}") + log.warning("Row %d: Couldn't convert %s", rowno, row) + log.debug("Row %d: Reason %s", rowno, e) + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Work/porty-app/porty/follow.py b/Work/porty-app/porty/follow.py new file mode 100644 index 000000000..7cdc58f17 --- /dev/null +++ b/Work/porty-app/porty/follow.py @@ -0,0 +1,29 @@ +# follow.py +import os +import time + + +def follow(filename): + # f = open('Data/stocklog.csv') + f = open(filename, 'r') + f.seek(0, os.SEEK_END) # Move file pointer 0 bytes from end of file + while True: + line = f.readline() + if line == '': + time.sleep(0.1) # Sleep briefly and retry + continue + yield line + + +if __name__ == '__main__': + import report + + portfolio = report.read_portfolio('../../Data/portfolio.csv') + + for line in follow('../../Data/portfolio.csv'): + fields = line.split(',') + name = fields[0].strip('"') + price = float(fields[1]) + change = float(fields[4]) + if name in portfolio: + print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') diff --git a/Work/porty-app/porty/pcost.py b/Work/porty-app/porty/pcost.py new file mode 100644 index 000000000..7e54a0f93 --- /dev/null +++ b/Work/porty-app/porty/pcost.py @@ -0,0 +1,21 @@ +# pcost.py + +from . import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + # return sum([s.cost for s in portfolio]) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +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..0ffa58b29 --- /dev/null +++ b/Work/porty-app/porty/portfolio.py @@ -0,0 +1,46 @@ +# portfoio.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() + portdicts = fileparse.parse_csv( + lines, select=['name', 'shares', 'price'], types=[str, int, float], **opts) + for d in portdicts: + 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 100644 index 000000000..db62a862b --- /dev/null +++ b/Work/porty-app/porty/report.py @@ -0,0 +1,77 @@ +# report.py + +from . import fileparse +from .stock import Stock +from . import tableformat +from .portfolio import Portfolio + + +def read_portfolio(filename, **opts): + ''' + Read a stock portfolio file into a list of Stock object instances + name, shares, and price. + ''' + with open(filename) as lines: + return Portfolio.from_csv(lines) + + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str, float], has_headers=False)) + + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for stock in portfolio: + current_price = prices[stock.name] + change = current_price - stock.price + summary = (stock.name, stock.shares, current_price, change) + rows.append(summary) + return rows + + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name', 'Shares', 'Price', 'Change']) + # print('%10s %10s %10s %10s' % headers) + # print(('-'*10 + ' ')*len(headers)) + for name, shares, price, change in reportdata: + # print('%10s %10d %10.2f %10.2f' % row) + rowdata = [name, str(shares), f'{price:0.2f}', f'{change:0.2f}'] + formatter.row(rowdata) + + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[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..ce1e30671 --- /dev/null +++ b/Work/porty-app/porty/stock.py @@ -0,0 +1,24 @@ +from .typedproperty import String, Integer, Float + + +class Stock: + # __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})' + + @property + def cost(self): + return self.shares * self.price + + def sell(self, nshares): + self.shares -= nshares diff --git a/Work/porty-app/porty/tableformat.py b/Work/porty-app/porty/tableformat.py new file mode 100644 index 000000000..b6de35f20 --- /dev/null +++ b/Work/porty-app/porty/tableformat.py @@ -0,0 +1,91 @@ +class TableFormatter: + def headings(self, headers): + ''' + Emit the table headings. + ''' + raise NotImplementedError() + + def row(self, data): + ''' + Emit a single row of data. + ''' + raise NotImplementedError() + + +class TextTableFormatter(TableFormatter): + ''' + Emit a table in plain test format. + ''' + + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + + +class CSVTableFormatter(TableFormatter): + ''' + Output portfolio data in CSV format. + ''' + + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + + +class HTMLTableFormatter(TableFormatter): + ''' + Output portfolio data in HTML format. + ''' + + def headings(self, headers): + s = '' + for h in headers: + s += f'