Added solution code

This commit is contained in:
David Beazley
2020-05-27 17:03:35 -05:00
parent 960d4fa2fa
commit 5b6f15db17
136 changed files with 5828 additions and 350 deletions

View File

@@ -0,0 +1,49 @@
# 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:
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

17
Solutions/8_2/follow.py Normal file
View File

@@ -0,0 +1,17 @@
# follow.py
import time
import os
def follow(filename):
'''
Generator that produces a sequence of lines being written at the end of a file.
'''
with open(filename,'r') as f:
f.seek(0,os.SEEK_END)
while True:
line = f.readline()
if line != '':
yield line
else:
time.sleep(0.1) # Sleep briefly to avoid busy wait

20
Solutions/8_2/pcost.py Normal file
View File

@@ -0,0 +1,20 @@
# pcost.py
import report
def portfolio_cost(filename):
'''
Computes the total cost (shares*price) of a portfolio file
'''
portfolio = report.read_portfolio(filename)
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)

View File

@@ -0,0 +1,51 @@
# portfolio.py
import fileparse
import stock
class Portfolio(object):
def __init__(self):
self._holdings = []
@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 append(self, holding):
self._holdings.append(holding)
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.shares * s.price 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

67
Solutions/8_2/report.py Normal file
View File

@@ -0,0 +1,67 @@
# report.py
import fileparse
from stock import Stock
from portfolio import Portfolio
import tableformat
def read_portfolio(filename, **opts):
'''
Read a stock portfolio file into a list of dictionaries with keys
name, shares, and price.
'''
with open(filename) as lines:
return Portfolio.from_csv(lines, **opts)
def read_prices(filename, **opts):
'''
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, **opts))
def make_report(portfolio, prices):
'''
Make a list of (name, shares, price, change) tuples given a portfolio list
and prices dictionary.
'''
rows = []
for s in portfolio:
current_price = prices[s.name]
change = current_price - s.price
summary = (s.name, s.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'])
for name, shares, price, change in reportdata:
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(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)

32
Solutions/8_2/stock.py Normal file
View File

@@ -0,0 +1,32 @@
# stock.py
from typedproperty import String, Integer, Float
class Stock(object):
'''
An instance of a stock holding consisting of name, shares, and 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!r}, {self.shares!r}, {self.price!r})'
@property
def cost(self):
'''
Return the cost as shares*price
'''
return self.shares * self.price
def sell(self, nshares):
'''
Sell a number of shares and return the remaining number.
'''
self.shares -= nshares

View File

@@ -0,0 +1,81 @@
# tableformat.py
class TableFormatter(object):
def headings(self, headers):
'''
Emit the table headers
'''
raise NotImplementedError()
def row(self, rowdata):
'''
Emit a single row of table data
'''
raise NotImplementedError()
class TextTableFormatter(TableFormatter):
'''
Output data in plain-text 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 data in CSV format.
'''
def headings(self, headers):
print(','.join(headers))
def row(self, rowdata):
print(','.join(rowdata))
class HTMLTableFormatter(TableFormatter):
'''
Output data in HTML format.
'''
def headings(self, headers):
print('<tr>', end='')
for h in headers:
print(f'<th>{h}</th>', end='')
print('</tr>')
def row(self, rowdata):
print('<tr>', end='')
for d in rowdata:
print(f'<td>{d}</td>', end='')
print('</tr>')
class FormatError(Exception):
pass
def create_formatter(name):
'''
Create an appropriate formatter given an output format name
'''
if name == 'txt':
return TextTableFormatter()
elif name == 'csv':
return CSVTableFormatter()
elif name == 'html':
return HTMLTableFormatter()
else:
raise FormatError(f'Unknown table format {name}')
def print_table(objects, columns, formatter):
'''
Make a nicely formatted table from a list of objects and attribute names.
'''
formatter.headings(columns)
for obj in objects:
rowdata = [ str(getattr(obj, name)) for name in columns ]
formatter.row(rowdata)

View File

@@ -0,0 +1,28 @@
# 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)
def test_cost(self):
s = stock.Stock('GOOG', 100, 490.1)
self.assertEqual(s.cost, 49010.0)
def test_sell(self):
s = stock.Stock('GOOG', 100, 490.1)
s.sell(25)
self.assertEqual(s.shares, 75)
def test_shares_check(self):
s = stock.Stock('GOOG', 100, 490.1)
with self.assertRaises(TypeError):
s.shares = '100'
if __name__ == '__main__':
unittest.main()

43
Solutions/8_2/ticker.py Normal file
View File

@@ -0,0 +1,43 @@
# ticker.py
import csv
import report
import tableformat
from follow import follow
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):
return (dict(zip(headers,row)) for row in rows)
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)
lines = follow(logfile)
rows = parse_stock_data(lines)
rows = (row for row in rows if row['name'] in portfolio)
formatter = tableformat.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}"] )
def main(args):
if len(args) != 4:
raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0])
ticker(args[1], args[2], args[3])
if __name__ == '__main__':
import sys
main(sys.argv)

21
Solutions/8_2/timethis.py Normal file
View File

@@ -0,0 +1,21 @@
# timethis.py
import time
def timethis(func):
def wrapper(*args, **kwargs):
start = time.time()
try:
return func(*args,**kwargs)
finally:
end = time.time()
print("%s.%s : %f" % (func.__module__,func.__name__,end-start))
return wrapper
if __name__ == '__main__':
@timethis
def countdown(n):
while n > 0:
n-= 1
countdown(1000000)

View File

@@ -0,0 +1,34 @@
# 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(object):
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