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'{h}' + print(f'{s}') + + def row(self, rowdata): + s = '' + for r in rowdata: + s += f'{r}' + print(f'{s}') + + + +class FormatError(Exception): + pass + + +def create_formatter(fmt): + ''' + Create a formatter based on the fmt, e.g. 'txt', 'csv', 'html' + ''' + if fmt not in ['txt', 'csv', 'html']: + raise FormatError('Unknown table format %s' % fmt) + if fmt == 'txt': + formatter = TextTableFormatter() + elif fmt == 'csv': + formatter = CSVTableFormatter() + elif fmt == 'html': + formatter = HTMLTableFormatter() + else: + raise RuntimeError(f'Unknown format {fmt}') + return formatter + + +def print_table(objects, columns, formatter): + ''' + Columns designate what to print with formatter object. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [str(getattr(obj, name)) for name in columns] + formatter.row(rowdata) diff --git a/Work/porty-app/porty/test_stock.py b/Work/porty-app/porty/test_stock.py new file mode 100644 index 000000000..c485bf474 --- /dev/null +++ b/Work/porty-app/porty/test_stock.py @@ -0,0 +1,24 @@ +# test_stock.py + +import unittest +import stock + + +class TestStock(unittest.TestCase): + def test_create(self): + s = stock.Stock('GOOG', 100, 490.1) + self.assertEqual(s.name, 'GOOG') + self.assertEqual(s.shares, 100) + self.assertEqual(s.price, 490.1) + self.assertEqual(s.cost, 49010.0) + s.sell(50) + self.assertEqual(s.shares, 50) + + def test_bad_shares(self): + s = stock.Stock('GOOG', 100, 490.1) + with self.assertRaises(TypeError): + s.shares = '100' + + +if __name__ == '__main__': + unittest.main() diff --git a/Work/porty-app/porty/ticker.py b/Work/porty-app/porty/ticker.py new file mode 100644 index 000000000..fd24ce232 --- /dev/null +++ b/Work/porty-app/porty/ticker.py @@ -0,0 +1,54 @@ +# ticker.py + +from .follow import follow +import csv +from . import report +from .tableformat import create_formatter, print_table + + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + + +def make_dicts(rows, headers): + for row in rows: + yield dict(zip(headers, row)) + + +def filter_symbols(rows, names): + # for row in rows: + # if row['name'] in names: + # yield row + return (row for row in rows if row['name'] in names) + + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str, float, float]) + rows = make_dicts(rows, ['name', 'price', 'change']) + return rows + + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + rows = parse_stock_data(follow(logfile)) + rows = filter_symbols(rows, portfolio) + formatter = create_formatter(fmt) + formatter.headings(['Name', 'Price', 'Change']) + for row in rows: + formatter.row( + [row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"]) + + +if __name__ == '__main__': + lines = follow('Data/stocklog.csv') + rows = parse_stock_data(lines) + for row in rows: + print(row) diff --git a/Work/porty-app/porty/typedproperty.py b/Work/porty-app/porty/typedproperty.py new file mode 100644 index 000000000..ab59fad9d --- /dev/null +++ b/Work/porty-app/porty/typedproperty.py @@ -0,0 +1,44 @@ +# typedproperty.py + + +def typedproperty(name, expected_type): + private_name = '_' + name + @property + def prop(self): + return getattr(self, private_name) + + @prop.setter + def prop(self, value): + if not isinstance(value, expected_type): + raise TypeError(f'Expected {expected_type}') + setattr(self, private_name, value) + + return prop + + +String = lambda name: typedproperty(name, str) +Integer = lambda name: typedproperty(name, int) +Float = lambda name: typedproperty(name, float) + + +# Example +if __name__ == '__main__': + class Stock: + name = typedproperty('name', str) + shares = typedproperty('shares', int) + price = typedproperty('price', float) + + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + class Stock2: + name = String('name') + shares = Integer('shares') + price = Float('price') + + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price diff --git a/Work/porty-app/prices.csv b/Work/porty-app/prices.csv new file mode 100644 index 000000000..d317cdc15 --- /dev/null +++ b/Work/porty-app/prices.csv @@ -0,0 +1,31 @@ +"AA",9.22 +"AXP",24.85 +"BA",44.85 +"BAC",11.27 +"C",3.72 +"CAT",35.46 +"CVX",66.67 +"DD",28.47 +"DIS",24.22 +"GE",13.48 +"GM",0.75 +"HD",23.16 +"HPQ",34.35 +"IBM",106.28 +"INTC",15.72 +"JNJ",55.16 +"JPM",36.90 +"KFT",26.11 +"KO",49.16 +"MCD",58.99 +"MMM",57.10 +"MRK",27.58 +"MSFT",20.89 +"PFE",15.19 +"PG",51.94 +"T",24.79 +"UTX",52.61 +"VZ",29.26 +"WMT",49.74 +"XOM",69.35 + diff --git a/Work/porty-app/print-report.py b/Work/porty-app/print-report.py new file mode 100644 index 000000000..547e6129b --- /dev/null +++ b/Work/porty-app/print-report.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +# print-report.py + +import sys +from porty.report import main +main(sys.argv) \ No newline at end of file diff --git a/Work/porty-app/setup.py b/Work/porty-app/setup.py new file mode 100644 index 000000000..ba226fc31 --- /dev/null +++ b/Work/porty-app/setup.py @@ -0,0 +1,11 @@ +# setup.py +import setuptools + +setuptools.setup( + name="porty", + version="0.0.1", + author="Mark Henderson", + author_email="you@example.com", + description="Practical Python Code", + packages=setuptools.find_packages(), +) \ No newline at end of file diff --git a/Work/report.py b/Work/report.py deleted file mode 100644 index 47d5da7b1..000000000 --- a/Work/report.py +++ /dev/null @@ -1,3 +0,0 @@ -# report.py -# -# Exercise 2.4 diff --git a/Work/sears.py b/Work/sears.py new file mode 100644 index 000000000..05a8d8027 --- /dev/null +++ b/Work/sears.py @@ -0,0 +1,15 @@ +# sears.py + +bill_thickness = 0.11 * 0.001 # meters (0.11 mm) +sears_height = 442 # meters +num_bills = 1 +day = 1 + +while num_bills * bill_thickness < sears_height: + print(day, num_bills, num_bills * bill_thickness) + day += 1 + num_bills *= 2 + +print("Number of days", day) +print("Number of bills", num_bills) +print("Final height", num_bills * bill_thickness) diff --git a/Work/timethis.py b/Work/timethis.py new file mode 100644 index 000000000..2cbe1fbb4 --- /dev/null +++ b/Work/timethis.py @@ -0,0 +1,21 @@ +# timethis.py +import time + +def timethis(func): + def wrapper(*args, **kwargs): + start = time.time() + try: + func(*args, **kwargs) + finally: + end = time.time() + print(f'{func.__module__}.{func.__name__} : {end-start}') + return wrapper + +@timethis +def countdown(n): + while n > 0: + n -= 1 + +if __name__ == '__main__': + countdown(1000000) +