Added solution code
This commit is contained in:
28
Solutions/1_10/mortgage.py
Normal file
28
Solutions/1_10/mortgage.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# mortgage.py
|
||||
|
||||
principal = 500000.0
|
||||
rate = 0.05
|
||||
payment = 2684.11
|
||||
total_paid = 0.0
|
||||
month = 0
|
||||
|
||||
extra_payment = 1000.0
|
||||
extra_payment_start_month = 60
|
||||
extra_payment_end_month = 108
|
||||
|
||||
while principal > 0:
|
||||
month = month + 1
|
||||
principal = principal * (1+rate/12) - payment
|
||||
total_paid = total_paid + payment
|
||||
|
||||
if month >= extra_payment_start_month and month <= extra_payment_end_month:
|
||||
principal = principal - extra_payment
|
||||
total_paid = total_paid + extra_payment
|
||||
|
||||
print(month, round(total_paid,2), round(principal, 2))
|
||||
|
||||
print('Total paid', round(total_paid, 2))
|
||||
print('Months', month)
|
||||
|
||||
|
||||
|
||||
13
Solutions/1_27/pcost.py
Normal file
13
Solutions/1_27/pcost.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# pcost.py
|
||||
|
||||
total_cost = 0.0
|
||||
|
||||
with open('../../Work/Data/portfolio.csv', 'rt') as f:
|
||||
headers = next(f)
|
||||
for line in f:
|
||||
row = line.split(',')
|
||||
nshares = int(row[1])
|
||||
price = float(row[2])
|
||||
total_cost += nshares * price
|
||||
|
||||
print('Total cost', total_cost)
|
||||
31
Solutions/1_32/pcost.py
Normal file
31
Solutions/1_32/pcost.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# pcost.py
|
||||
|
||||
import csv
|
||||
def portfolio_cost(filename):
|
||||
'''
|
||||
Computes the total cost (shares*price) of a portfolio file
|
||||
'''
|
||||
total_cost = 0.0
|
||||
|
||||
with open(filename, 'rt') as f:
|
||||
rows = csv.reader(f)
|
||||
headers = next(rows)
|
||||
for row in rows:
|
||||
try:
|
||||
nshares = int(row[1])
|
||||
price = float(row[2])
|
||||
total_cost += nshares * price
|
||||
# This catches errors in int() and float() conversions above
|
||||
except ValueError:
|
||||
print('Bad row:', row)
|
||||
|
||||
return total_cost
|
||||
|
||||
import sys
|
||||
if len(sys.argv) == 2:
|
||||
filename = sys.argv[1]
|
||||
else:
|
||||
filename = input('Enter a filename:')
|
||||
|
||||
cost = portfolio_cost(filename)
|
||||
print('Total cost:', cost)
|
||||
8
Solutions/1_5/bounce.py
Normal file
8
Solutions/1_5/bounce.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# bounce.py
|
||||
|
||||
height = 100
|
||||
bounce = 1
|
||||
while bounce <= 10:
|
||||
height = height * (3/5)
|
||||
print(bounce, round(height, 4))
|
||||
bounce += 1
|
||||
66
Solutions/2_11/report.py
Normal file
66
Solutions/2_11/report.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# report.py
|
||||
import csv
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
portfolio = []
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
headers = next(rows)
|
||||
|
||||
for row in rows:
|
||||
stock = {
|
||||
'name' : row[0],
|
||||
'shares' : int(row[1]),
|
||||
'price' : float(row[2])
|
||||
}
|
||||
portfolio.append(stock)
|
||||
|
||||
return portfolio
|
||||
|
||||
def read_prices(filename):
|
||||
'''
|
||||
Read a CSV file of price data into a dict mapping names to prices.
|
||||
'''
|
||||
prices = {}
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
for row in rows:
|
||||
try:
|
||||
prices[row[0]] = float(row[1])
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return prices
|
||||
|
||||
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
|
||||
|
||||
# Read data files and create the report data
|
||||
|
||||
portfolio = read_portfolio('../../Work/Data/portfolio.csv')
|
||||
prices = read_prices('../../Work/Data/prices.csv')
|
||||
|
||||
# Generate the report data
|
||||
|
||||
report = make_report_data(portfolio, prices)
|
||||
|
||||
# Output the report
|
||||
headers = ('Name', 'Shares', 'Price', 'Change')
|
||||
print('%10s %10s %10s %10s' % headers)
|
||||
print(('-' * 10 + ' ') * len(headers))
|
||||
for row in report:
|
||||
print('%10s %10d %10.2f %10.2f' % row)
|
||||
32
Solutions/2_16/pcost.py
Normal file
32
Solutions/2_16/pcost.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# pcost.py
|
||||
|
||||
import csv
|
||||
def portfolio_cost(filename):
|
||||
'''
|
||||
Computes the total cost (shares*price) of a portfolio file
|
||||
'''
|
||||
total_cost = 0.0
|
||||
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
headers = next(rows)
|
||||
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
|
||||
# This catches errors in int() and float() conversions above
|
||||
except ValueError:
|
||||
print(f'Row {rowno}: Bad row: {row}')
|
||||
|
||||
return total_cost
|
||||
|
||||
import sys
|
||||
if len(sys.argv) == 2:
|
||||
filename = sys.argv[1]
|
||||
else:
|
||||
filename = input('Enter a filename:')
|
||||
|
||||
cost = portfolio_cost(filename)
|
||||
print('Total cost:', cost)
|
||||
67
Solutions/2_16/report.py
Normal file
67
Solutions/2_16/report.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# report.py
|
||||
import csv
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
portfolio = []
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
headers = next(rows)
|
||||
|
||||
for row in rows:
|
||||
record = dict(zip(headers, row))
|
||||
stock = {
|
||||
'name' : record['name'],
|
||||
'shares' : int(record['shares']),
|
||||
'price' : float(record['price'])
|
||||
}
|
||||
portfolio.append(stock)
|
||||
|
||||
return portfolio
|
||||
|
||||
def read_prices(filename):
|
||||
'''
|
||||
Read a CSV file of price data into a dict mapping names to prices.
|
||||
'''
|
||||
prices = {}
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
for row in rows:
|
||||
try:
|
||||
prices[row[0]] = float(row[1])
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return prices
|
||||
|
||||
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
|
||||
|
||||
# Read data files and create the report data
|
||||
|
||||
portfolio = read_portfolio('../../Work/Data/portfolio.csv')
|
||||
prices = read_prices('../../Work/Data/prices.csv')
|
||||
|
||||
# Generate the report data
|
||||
|
||||
report = make_report_data(portfolio, prices)
|
||||
|
||||
# Output the report
|
||||
headers = ('Name', 'Shares', 'Price', 'Change')
|
||||
print('%10s %10s %10s %10s' % headers)
|
||||
print(('-' * 10 + ' ') * len(headers))
|
||||
for row in report:
|
||||
print('%10s %10d %10.2f %10.2f' % row)
|
||||
55
Solutions/2_7/report.py
Normal file
55
Solutions/2_7/report.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# report.py
|
||||
import csv
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
portfolio = []
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
headers = next(rows)
|
||||
|
||||
for row in rows:
|
||||
stock = {
|
||||
'name' : row[0],
|
||||
'shares' : int(row[1]),
|
||||
'price' : float(row[2])
|
||||
}
|
||||
portfolio.append(stock)
|
||||
|
||||
return portfolio
|
||||
|
||||
def read_prices(filename):
|
||||
'''
|
||||
Read a CSV file of price data into a dict mapping names to prices.
|
||||
'''
|
||||
prices = {}
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
for row in rows:
|
||||
try:
|
||||
prices[row[0]] = float(row[1])
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return prices
|
||||
|
||||
portfolio = read_portfolio('../../Work/Data/portfolio.csv')
|
||||
prices = read_prices('../../Work/Data/prices.csv')
|
||||
|
||||
# Calculate the total cost of the portfolio
|
||||
total_cost = 0.0
|
||||
for s in portfolio:
|
||||
total_cost += s['shares']*s['price']
|
||||
|
||||
print('Total cost', total_cost)
|
||||
|
||||
# Compute the current value of the portfolio
|
||||
total_value = 0.0
|
||||
for s in portfolio:
|
||||
total_value += s['shares']*prices[s['name']]
|
||||
|
||||
print('Current value', total_value)
|
||||
print('Gain', total_value - total_cost)
|
||||
48
Solutions/3_10/fileparse.py
Normal file
48
Solutions/3_10/fileparse.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
def parse_csv(filename, 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')
|
||||
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f, 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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
48
Solutions/3_14/fileparse.py
Normal file
48
Solutions/3_14/fileparse.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
def parse_csv(filename, 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')
|
||||
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f, 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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
19
Solutions/3_14/pcost.py
Normal file
19
Solutions/3_14/pcost.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# pcost.py
|
||||
|
||||
import report
|
||||
|
||||
def portfolio_cost(filename):
|
||||
'''
|
||||
Computes the total cost (shares*price) of a portfolio file
|
||||
'''
|
||||
portfolio = report.read_portfolio(filename)
|
||||
return sum([s['shares']*s['price'] for s in portfolio])
|
||||
|
||||
import sys
|
||||
if len(sys.argv) == 2:
|
||||
filename = sys.argv[1]
|
||||
else:
|
||||
filename = input('Enter a filename:')
|
||||
|
||||
cost = portfolio_cost(filename)
|
||||
print('Total cost:', cost)
|
||||
56
Solutions/3_14/report.py
Normal file
56
Solutions/3_14/report.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
return fileparse.parse_csv(filename, select=['name','shares','price'], types=[str,int,float])
|
||||
|
||||
def read_prices(filename):
|
||||
'''
|
||||
Read a CSV file of price data into a dict mapping names to prices.
|
||||
'''
|
||||
return dict(fileparse.parse_csv(filename,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):
|
||||
'''
|
||||
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||
'''
|
||||
headers = ('Name','Shares','Price','Change')
|
||||
print('%10s %10s %10s %10s' % headers)
|
||||
print(('-'*10 + ' ')*len(headers))
|
||||
for row in reportdata:
|
||||
print('%10s %10d %10.2f %10.2f' % row)
|
||||
|
||||
def portfolio_report(portfoliofile,pricefile):
|
||||
'''
|
||||
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
|
||||
print_report(report)
|
||||
|
||||
portfolio_report('../../Work/Data/portfolio.csv',
|
||||
'../../Work/Data/prices.csv')
|
||||
48
Solutions/3_16/fileparse.py
Normal file
48
Solutions/3_16/fileparse.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
def parse_csv(filename, 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')
|
||||
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f, 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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
20
Solutions/3_16/pcost.py
Normal file
20
Solutions/3_16/pcost.py
Normal 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 sum([s['shares'] * s['price'] for s in portfolio])
|
||||
|
||||
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)
|
||||
62
Solutions/3_16/report.py
Normal file
62
Solutions/3_16/report.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
return fileparse.parse_csv(filename, select=['name','shares','price'], types=[str,int,float])
|
||||
|
||||
def read_prices(filename):
|
||||
'''
|
||||
Read a CSV file of price data into a dict mapping names to prices.
|
||||
'''
|
||||
return dict(fileparse.parse_csv(filename,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):
|
||||
'''
|
||||
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||
'''
|
||||
headers = ('Name','Shares','Price','Change')
|
||||
print('%10s %10s %10s %10s' % headers)
|
||||
print(('-'*10 + ' ')*len(headers))
|
||||
for row in reportdata:
|
||||
print('%10s %10d %10.2f %10.2f' % row)
|
||||
|
||||
def portfolio_report(portfoliofile, pricefile):
|
||||
'''
|
||||
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
|
||||
print_report(report)
|
||||
|
||||
def main(args):
|
||||
if len(args) != 3:
|
||||
raise SystemExit('Usage: %s portfile pricefile' % args[0])
|
||||
portfolio_report(args[1], args[2])
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
main(sys.argv)
|
||||
47
Solutions/3_18/fileparse.py
Normal file
47
Solutions/3_18/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
20
Solutions/3_18/pcost.py
Normal file
20
Solutions/3_18/pcost.py
Normal 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 sum([s['shares'] * s['price'] for s in portfolio])
|
||||
|
||||
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)
|
||||
64
Solutions/3_18/report.py
Normal file
64
Solutions/3_18/report.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
return fileparse.parse_csv(lines, select=['name','shares','price'], types=[str,int,float])
|
||||
|
||||
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):
|
||||
'''
|
||||
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||
'''
|
||||
headers = ('Name','Shares','Price','Change')
|
||||
print('%10s %10s %10s %10s' % headers)
|
||||
print(('-'*10 + ' ')*len(headers))
|
||||
for row in reportdata:
|
||||
print('%10s %10d %10.2f %10.2f' % row)
|
||||
|
||||
def portfolio_report(portfoliofile, pricefile):
|
||||
'''
|
||||
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
|
||||
print_report(report)
|
||||
|
||||
def main(args):
|
||||
if len(args) != 3:
|
||||
raise SystemExit('Usage: %s portfile pricefile' % args[0])
|
||||
portfolio_report(args[1], args[2])
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
main(sys.argv)
|
||||
78
Solutions/3_2/report.py
Normal file
78
Solutions/3_2/report.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# report.py
|
||||
import csv
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
portfolio = []
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
headers = next(rows)
|
||||
|
||||
for row in rows:
|
||||
record = dict(zip(headers, row))
|
||||
stock = {
|
||||
'name' : record['name'],
|
||||
'shares' : int(record['shares']),
|
||||
'price' : float(record['price'])
|
||||
}
|
||||
portfolio.append(stock)
|
||||
|
||||
return portfolio
|
||||
|
||||
def read_prices(filename):
|
||||
'''
|
||||
Read a CSV file of price data into a dict mapping names to prices.
|
||||
'''
|
||||
prices = {}
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f)
|
||||
for row in rows:
|
||||
try:
|
||||
prices[row[0]] = float(row[1])
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return prices
|
||||
|
||||
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):
|
||||
'''
|
||||
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||
'''
|
||||
headers = ('Name','Shares','Price','Change')
|
||||
print('%10s %10s %10s %10s' % headers)
|
||||
print(('-'*10 + ' ')*len(headers))
|
||||
for row in reportdata:
|
||||
print('%10s %10d %10.2f %10.2f' % row)
|
||||
|
||||
def portfolio_report(portfoliofile,pricefile):
|
||||
'''
|
||||
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
|
||||
print_report(report)
|
||||
|
||||
portfolio_report('../../Work/Data/portfolio.csv',
|
||||
'../../Work/Data/prices.csv')
|
||||
39
Solutions/3_7/fileparse.py
Normal file
39
Solutions/3_7/fileparse.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=','):
|
||||
'''
|
||||
Parse a CSV file into a list of records with type conversion.
|
||||
'''
|
||||
with open(filename) as f:
|
||||
rows = csv.reader(f, 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
|
||||
if select:
|
||||
indices = [ headers.index(colname) for colname in select ]
|
||||
headers = select
|
||||
|
||||
records = []
|
||||
for row in rows:
|
||||
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:
|
||||
row = [func(val) for func, val in zip(types, row)]
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
47
Solutions/4_10/fileparse.py
Normal file
47
Solutions/4_10/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
20
Solutions/4_10/pcost.py
Normal file
20
Solutions/4_10/pcost.py
Normal 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 sum([s.cost() for s in portfolio])
|
||||
|
||||
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)
|
||||
71
Solutions/4_10/report.py
Normal file
71
Solutions/4_10/report.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
import tableformat
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float])
|
||||
|
||||
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||
return portfolio
|
||||
|
||||
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 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_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)
|
||||
26
Solutions/4_10/stock.py
Normal file
26
Solutions/4_10/stock.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# stock.py
|
||||
|
||||
class Stock(object):
|
||||
'''
|
||||
An instance of a stock holding consisting of name, shares, and 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})'
|
||||
|
||||
def cost(self):
|
||||
'''
|
||||
Return the cost as shares*price
|
||||
'''
|
||||
return self.shares * self.price
|
||||
|
||||
def sell(self, nshares):
|
||||
'''
|
||||
Sell a number of shares
|
||||
'''
|
||||
self.shares -= nshares
|
||||
|
||||
81
Solutions/4_10/tableformat.py
Normal file
81
Solutions/4_10/tableformat.py
Normal 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)
|
||||
|
||||
47
Solutions/4_4/fileparse.py
Normal file
47
Solutions/4_4/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
20
Solutions/4_4/pcost.py
Normal file
20
Solutions/4_4/pcost.py
Normal 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 sum([s.cost() for s in portfolio])
|
||||
|
||||
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)
|
||||
70
Solutions/4_4/report.py
Normal file
70
Solutions/4_4/report.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float])
|
||||
|
||||
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||
return portfolio
|
||||
|
||||
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 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):
|
||||
'''
|
||||
Print a nicely formated table from a list of (name, shares, price, change) tuples.
|
||||
'''
|
||||
headers = ('Name','Shares','Price','Change')
|
||||
print('%10s %10s %10s %10s' % headers)
|
||||
print(('-'*10 + ' ')*len(headers))
|
||||
for row in reportdata:
|
||||
print('%10s %10d %10.2f %10.2f' % row)
|
||||
|
||||
def portfolio_report(portfoliofile, pricefile):
|
||||
'''
|
||||
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
|
||||
print_report(report)
|
||||
|
||||
def main(args):
|
||||
if len(args) != 3:
|
||||
raise SystemExit('Usage: %s portfile pricefile' % args[0])
|
||||
portfolio_report(args[1], args[2])
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
main(sys.argv)
|
||||
23
Solutions/4_4/stock.py
Normal file
23
Solutions/4_4/stock.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# stock.py
|
||||
|
||||
class Stock(object):
|
||||
'''
|
||||
An instance of a stock holding consisting of name, shares, and price.
|
||||
'''
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
self.shares = shares
|
||||
self.price = price
|
||||
|
||||
def cost(self):
|
||||
'''
|
||||
Return the cost as shares*price
|
||||
'''
|
||||
return self.shares * self.price
|
||||
|
||||
def sell(self, nshares):
|
||||
'''
|
||||
Sell a number of shares
|
||||
'''
|
||||
self.shares -= nshares
|
||||
|
||||
47
Solutions/5_8/fileparse.py
Normal file
47
Solutions/5_8/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
20
Solutions/5_8/pcost.py
Normal file
20
Solutions/5_8/pcost.py
Normal 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 sum([s.cost() for s in portfolio])
|
||||
|
||||
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)
|
||||
71
Solutions/5_8/report.py
Normal file
71
Solutions/5_8/report.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
import tableformat
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float])
|
||||
|
||||
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||
return portfolio
|
||||
|
||||
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 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_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)
|
||||
37
Solutions/5_8/stock.py
Normal file
37
Solutions/5_8/stock.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# stock.py
|
||||
|
||||
class Stock(object):
|
||||
'''
|
||||
An instance of a stock holding consisting of name, shares, and price.
|
||||
'''
|
||||
__slots__ = ('name','_shares','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 shares(self):
|
||||
return self._shares
|
||||
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value,int):
|
||||
raise TypeError("Must be integer")
|
||||
self._shares = value
|
||||
|
||||
@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
|
||||
81
Solutions/5_8/tableformat.py
Normal file
81
Solutions/5_8/tableformat.py
Normal 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)
|
||||
|
||||
47
Solutions/6_12/fileparse.py
Normal file
47
Solutions/6_12/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
30
Solutions/6_12/follow.py
Normal file
30
Solutions/6_12/follow.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# follow.py
|
||||
import os
|
||||
import time
|
||||
|
||||
def follow(filename):
|
||||
'''
|
||||
Generator that produces a sequence of lines being written at the end of a file.
|
||||
'''
|
||||
f = open(filename, 'r')
|
||||
f.seek(0,os.SEEK_END)
|
||||
while True:
|
||||
line = f.readline()
|
||||
if line == '':
|
||||
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||
continue
|
||||
yield line
|
||||
|
||||
# Example use
|
||||
if __name__ == '__main__':
|
||||
import report
|
||||
|
||||
portfolio = report.read_portfolio('../../Data/portfolio.csv')
|
||||
|
||||
for line in follow('../../Data/stocklog.csv'):
|
||||
row = line.split(',')
|
||||
name = row[0].strip('"')
|
||||
price = float(row[1])
|
||||
change = float(row[4])
|
||||
if name in portfolio:
|
||||
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
|
||||
20
Solutions/6_12/pcost.py
Normal file
20
Solutions/6_12/pcost.py
Normal 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)
|
||||
31
Solutions/6_12/portfolio.py
Normal file
31
Solutions/6_12/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# portfolio.py
|
||||
|
||||
class Portfolio(object):
|
||||
def __init__(self, holdings):
|
||||
self._holdings = holdings
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
72
Solutions/6_12/report.py
Normal file
72
Solutions/6_12/report.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
import tableformat
|
||||
from portfolio import Portfolio
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float])
|
||||
|
||||
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
|
||||
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 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_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)
|
||||
37
Solutions/6_12/stock.py
Normal file
37
Solutions/6_12/stock.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# stock.py
|
||||
|
||||
class Stock(object):
|
||||
'''
|
||||
An instance of a stock holding consisting of name, shares, and price.
|
||||
'''
|
||||
__slots__ = ('name','_shares','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 shares(self):
|
||||
return self._shares
|
||||
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value,int):
|
||||
raise TypeError("Must be integer")
|
||||
self._shares = value
|
||||
|
||||
@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
|
||||
81
Solutions/6_12/tableformat.py
Normal file
81
Solutions/6_12/tableformat.py
Normal 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)
|
||||
|
||||
50
Solutions/6_12/ticker.py
Normal file
50
Solutions/6_12/ticker.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# ticker.py
|
||||
|
||||
import csv
|
||||
import report
|
||||
import tableformat
|
||||
from follow import follow
|
||||
import time
|
||||
|
||||
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 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 filter_symbols(rows, names):
|
||||
for row in rows:
|
||||
if row['name'] in names:
|
||||
yield row
|
||||
|
||||
def ticker(portfile, logfile, fmt):
|
||||
portfolio = report.read_portfolio(portfile)
|
||||
lines = follow(logfile)
|
||||
rows = parse_stock_data(lines)
|
||||
rows = filter_symbols(rows, 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)
|
||||
47
Solutions/6_15/fileparse.py
Normal file
47
Solutions/6_15/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
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/6_15/follow.py
Normal file
17
Solutions/6_15/follow.py
Normal 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/6_15/pcost.py
Normal file
20
Solutions/6_15/pcost.py
Normal 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)
|
||||
31
Solutions/6_15/portfolio.py
Normal file
31
Solutions/6_15/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# portfolio.py
|
||||
|
||||
class Portfolio(object):
|
||||
def __init__(self, holdings):
|
||||
self._holdings = holdings
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
72
Solutions/6_15/report.py
Normal file
72
Solutions/6_15/report.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
import tableformat
|
||||
from portfolio import Portfolio
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float])
|
||||
|
||||
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
|
||||
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 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_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)
|
||||
37
Solutions/6_15/stock.py
Normal file
37
Solutions/6_15/stock.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# stock.py
|
||||
|
||||
class Stock(object):
|
||||
'''
|
||||
An instance of a stock holding consisting of name, shares, and price.
|
||||
'''
|
||||
__slots__ = ('name','_shares','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 shares(self):
|
||||
return self._shares
|
||||
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value,int):
|
||||
raise TypeError("Must be integer")
|
||||
self._shares = value
|
||||
|
||||
@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
|
||||
81
Solutions/6_15/tableformat.py
Normal file
81
Solutions/6_15/tableformat.py
Normal 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)
|
||||
|
||||
44
Solutions/6_15/ticker.py
Normal file
44
Solutions/6_15/ticker.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# ticker.py
|
||||
|
||||
import csv
|
||||
import report
|
||||
import tableformat
|
||||
from follow import follow
|
||||
import time
|
||||
|
||||
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)
|
||||
47
Solutions/6_3/fileparse.py
Normal file
47
Solutions/6_3/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
20
Solutions/6_3/pcost.py
Normal file
20
Solutions/6_3/pcost.py
Normal 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)
|
||||
31
Solutions/6_3/portfolio.py
Normal file
31
Solutions/6_3/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# portfolio.py
|
||||
|
||||
class Portfolio(object):
|
||||
def __init__(self, holdings):
|
||||
self._holdings = holdings
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
72
Solutions/6_3/report.py
Normal file
72
Solutions/6_3/report.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
import tableformat
|
||||
from portfolio import Portfolio
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float])
|
||||
|
||||
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
|
||||
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 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_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)
|
||||
37
Solutions/6_3/stock.py
Normal file
37
Solutions/6_3/stock.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# stock.py
|
||||
|
||||
class Stock(object):
|
||||
'''
|
||||
An instance of a stock holding consisting of name, shares, and price.
|
||||
'''
|
||||
__slots__ = ('name','_shares','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 shares(self):
|
||||
return self._shares
|
||||
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value,int):
|
||||
raise TypeError("Must be integer")
|
||||
self._shares = value
|
||||
|
||||
@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
|
||||
81
Solutions/6_3/tableformat.py
Normal file
81
Solutions/6_3/tableformat.py
Normal 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)
|
||||
|
||||
47
Solutions/6_7/fileparse.py
Normal file
47
Solutions/6_7/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
# Make a dictionary or a tuple
|
||||
if headers:
|
||||
record = dict(zip(headers, row))
|
||||
else:
|
||||
record = tuple(row)
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
30
Solutions/6_7/follow.py
Normal file
30
Solutions/6_7/follow.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# follow.py
|
||||
import os
|
||||
import time
|
||||
|
||||
def follow(filename):
|
||||
'''
|
||||
Generator that produces a sequence of lines being written at the end of a file.
|
||||
'''
|
||||
f = open(filename, 'r')
|
||||
f.seek(0,os.SEEK_END)
|
||||
while True:
|
||||
line = f.readline()
|
||||
if line == '':
|
||||
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||
continue
|
||||
yield line
|
||||
|
||||
# Example use
|
||||
if __name__ == '__main__':
|
||||
import report
|
||||
|
||||
portfolio = report.read_portfolio('../../Data/portfolio.csv')
|
||||
|
||||
for line in follow('../../Data/stocklog.csv'):
|
||||
row = line.split(',')
|
||||
name = row[0].strip('"')
|
||||
price = float(row[1])
|
||||
change = float(row[4])
|
||||
if name in portfolio:
|
||||
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
|
||||
20
Solutions/6_7/pcost.py
Normal file
20
Solutions/6_7/pcost.py
Normal 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)
|
||||
31
Solutions/6_7/portfolio.py
Normal file
31
Solutions/6_7/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# portfolio.py
|
||||
|
||||
class Portfolio(object):
|
||||
def __init__(self, holdings):
|
||||
self._holdings = holdings
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
72
Solutions/6_7/report.py
Normal file
72
Solutions/6_7/report.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
import tableformat
|
||||
from portfolio import Portfolio
|
||||
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float])
|
||||
|
||||
portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
|
||||
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 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_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)
|
||||
37
Solutions/6_7/stock.py
Normal file
37
Solutions/6_7/stock.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# stock.py
|
||||
|
||||
class Stock(object):
|
||||
'''
|
||||
An instance of a stock holding consisting of name, shares, and price.
|
||||
'''
|
||||
__slots__ = ('name','_shares','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 shares(self):
|
||||
return self._shares
|
||||
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value,int):
|
||||
raise TypeError("Must be integer")
|
||||
self._shares = value
|
||||
|
||||
@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
|
||||
81
Solutions/6_7/tableformat.py
Normal file
81
Solutions/6_7/tableformat.py
Normal 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)
|
||||
|
||||
21
Solutions/7_10/timethis.py
Normal file
21
Solutions/7_10/timethis.py
Normal 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)
|
||||
47
Solutions/7_12/fileparse.py
Normal file
47
Solutions/7_12/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
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/7_12/follow.py
Normal file
17
Solutions/7_12/follow.py
Normal 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/7_12/pcost.py
Normal file
20
Solutions/7_12/pcost.py
Normal 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)
|
||||
51
Solutions/7_12/portfolio.py
Normal file
51
Solutions/7_12/portfolio.py
Normal 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/7_12/report.py
Normal file
67
Solutions/7_12/report.py
Normal 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/7_12/stock.py
Normal file
32
Solutions/7_12/stock.py
Normal 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
|
||||
81
Solutions/7_12/tableformat.py
Normal file
81
Solutions/7_12/tableformat.py
Normal 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)
|
||||
|
||||
43
Solutions/7_12/ticker.py
Normal file
43
Solutions/7_12/ticker.py
Normal 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/7_12/timethis.py
Normal file
21
Solutions/7_12/timethis.py
Normal 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)
|
||||
34
Solutions/7_12/typedproperty.py
Normal file
34
Solutions/7_12/typedproperty.py
Normal 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
|
||||
|
||||
|
||||
|
||||
47
Solutions/7_4/fileparse.py
Normal file
47
Solutions/7_4/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
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/7_4/follow.py
Normal file
17
Solutions/7_4/follow.py
Normal 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/7_4/pcost.py
Normal file
20
Solutions/7_4/pcost.py
Normal 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)
|
||||
31
Solutions/7_4/portfolio.py
Normal file
31
Solutions/7_4/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# portfolio.py
|
||||
|
||||
class Portfolio(object):
|
||||
def __init__(self, holdings):
|
||||
self._holdings = holdings
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
73
Solutions/7_4/report.py
Normal file
73
Solutions/7_4/report.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
import tableformat
|
||||
from portfolio import Portfolio
|
||||
|
||||
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:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float],
|
||||
**opts)
|
||||
|
||||
portfolio = [ Stock(**d) for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
|
||||
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_data(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_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)
|
||||
37
Solutions/7_4/stock.py
Normal file
37
Solutions/7_4/stock.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# stock.py
|
||||
|
||||
class Stock(object):
|
||||
'''
|
||||
An instance of a stock holding consisting of name, shares, and price.
|
||||
'''
|
||||
__slots__ = ('name','_shares','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 shares(self):
|
||||
return self._shares
|
||||
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value,int):
|
||||
raise TypeError("Must be integer")
|
||||
self._shares = value
|
||||
|
||||
@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
|
||||
81
Solutions/7_4/tableformat.py
Normal file
81
Solutions/7_4/tableformat.py
Normal 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)
|
||||
|
||||
44
Solutions/7_4/ticker.py
Normal file
44
Solutions/7_4/ticker.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# ticker.py
|
||||
|
||||
import csv
|
||||
import report
|
||||
import tableformat
|
||||
from follow import follow
|
||||
import time
|
||||
|
||||
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)
|
||||
47
Solutions/7_9/fileparse.py
Normal file
47
Solutions/7_9/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
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/7_9/follow.py
Normal file
17
Solutions/7_9/follow.py
Normal 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/7_9/pcost.py
Normal file
20
Solutions/7_9/pcost.py
Normal 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)
|
||||
31
Solutions/7_9/portfolio.py
Normal file
31
Solutions/7_9/portfolio.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# portfolio.py
|
||||
|
||||
class Portfolio(object):
|
||||
def __init__(self, holdings):
|
||||
self._holdings = holdings
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
73
Solutions/7_9/report.py
Normal file
73
Solutions/7_9/report.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# report.py
|
||||
|
||||
import fileparse
|
||||
from stock import Stock
|
||||
import tableformat
|
||||
from portfolio import Portfolio
|
||||
|
||||
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:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float],
|
||||
**opts)
|
||||
|
||||
portfolio = [ Stock(**d) for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
|
||||
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_data(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_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)
|
||||
32
Solutions/7_9/stock.py
Normal file
32
Solutions/7_9/stock.py
Normal 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
|
||||
81
Solutions/7_9/tableformat.py
Normal file
81
Solutions/7_9/tableformat.py
Normal 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)
|
||||
|
||||
44
Solutions/7_9/ticker.py
Normal file
44
Solutions/7_9/ticker.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# ticker.py
|
||||
|
||||
import csv
|
||||
import report
|
||||
import tableformat
|
||||
from follow import follow
|
||||
import time
|
||||
|
||||
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)
|
||||
34
Solutions/7_9/typedproperty.py
Normal file
34
Solutions/7_9/typedproperty.py
Normal 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
|
||||
|
||||
|
||||
|
||||
47
Solutions/8_1/fileparse.py
Normal file
47
Solutions/8_1/fileparse.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# fileparse.py
|
||||
import csv
|
||||
|
||||
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}")
|
||||
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_1/follow.py
Normal file
17
Solutions/8_1/follow.py
Normal 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_1/pcost.py
Normal file
20
Solutions/8_1/pcost.py
Normal 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)
|
||||
51
Solutions/8_1/portfolio.py
Normal file
51
Solutions/8_1/portfolio.py
Normal 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_1/report.py
Normal file
67
Solutions/8_1/report.py
Normal 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_1/stock.py
Normal file
32
Solutions/8_1/stock.py
Normal 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
|
||||
81
Solutions/8_1/tableformat.py
Normal file
81
Solutions/8_1/tableformat.py
Normal 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)
|
||||
|
||||
28
Solutions/8_1/test_stock.py
Normal file
28
Solutions/8_1/test_stock.py
Normal 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_1/ticker.py
Normal file
43
Solutions/8_1/ticker.py
Normal 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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user