Skip to content

Commit 37c9329

Browse files
committed
Long overdue checkin of the state of code at the end of chapter 5
1 parent ac6ebe5 commit 37c9329

5 files changed

Lines changed: 262 additions & 8 deletions

File tree

Work/fileparse.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,52 @@
11
# fileparse.py
22
#
33
# Exercise 3.3
4+
import csv
5+
6+
7+
def parse_csv(lines, select=[], types=[], has_headers = True, delimiter=',', silence_errors=False) -> list:
8+
"""
9+
Parse a CSV file into a list of records.
10+
"""
11+
if select and not has_headers:
12+
raise RuntimeError("Select argument requires column headers")
13+
if isinstance(lines, str):
14+
raise SystemExit("Expecting an iterable argument that can be parsed as csv.")
15+
16+
rows = csv.reader(lines, delimiter=delimiter)
17+
18+
# Read the file headers
19+
headers = next(rows) if has_headers else []
20+
21+
# If a column selector was given, find indices of the specified columns.
22+
# Also narrow the set of headers user for resulting dictionaries.
23+
if select:
24+
indices = [headers.index(colname) for colname in select]
25+
headers = select
26+
27+
records = []
28+
for row_no, row in enumerate(rows, start=1):
29+
if not row:
30+
continue
31+
# Filter the row if specified columns were selected
32+
if select:
33+
row = [row[index] for index in indices]
34+
35+
if types:
36+
try:
37+
row = [func(val) for func, val in zip(types, row)]
38+
except ValueError as e:
39+
if silence_errors:
40+
continue
41+
print(f'Row {row_no} Invalid: `{row}` cannot be converted.')
42+
print(f' Reason: {e}')
43+
44+
if headers:
45+
# Make a dictionary
46+
record = dict(zip(headers, row))
47+
else:
48+
# Make a tuple
49+
record = tuple(row)
50+
records.append(record)
51+
52+
return records

Work/pcost.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1+
#!/usr/bin/env python3
12
# pcost.py
23
#
34
# Exercise 1.27
4-
cost = 0
5-
with open('Data/portfolio.csv') as f:
6-
next(f)
7-
for line in f:
8-
parts = line.split(',')
9-
cost += int(parts[1]) * float(parts[2])
10-
11-
print(f'Total cost {cost:.2f}')
5+
import os, csv, sys
6+
from report import read_portfolio
7+
8+
9+
def portfolio_cost(filename):
10+
portfolio = read_portfolio(filename)
11+
cost = 0
12+
for holding in portfolio:
13+
cost += holding.cost
14+
return cost
15+
16+
17+
def main(argv):
18+
if len(argv) != 2:
19+
raise SystemExit(f'Usage: {argv[0]} portfoliofile')
20+
filename = argv[1]
21+
cost = portfolio_cost(filename)
22+
print(f'Total cost {cost:.2f}')
23+
24+
25+
if __name__ == '__main__':
26+
import sys
27+
main(sys.argv)

Work/report.py

100644100755
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,72 @@
1+
#!/usr/bin/env python3
12
# report.py
23
#
34
# Exercise 2.4
5+
import csv
6+
from fileparse import parse_csv
7+
import stock
8+
import tableformat
9+
10+
11+
def read_portfolio(filename: str) -> list:
12+
"""
13+
Computes the total cost (shares*price) of a portfolio file
14+
"""
15+
with open(filename, 'rt') as f:
16+
holdings = parse_csv(f, types=[str, int, float])
17+
portfolio = [stock.Stock(holding['name'], holding['shares'], holding['price']) for holding in holdings]
18+
return portfolio
19+
20+
21+
def read_prices(filename: str) -> dict:
22+
"""
23+
Read prices from a CSV file of name, price data
24+
:param filename:
25+
:return:
26+
"""
27+
with open(filename, 'rt') as f:
28+
prices = parse_csv(f, types=[str, float], has_headers=False)
29+
return dict(prices)
30+
31+
32+
def make_report(portfolio, prices):
33+
data = []
34+
for holding in portfolio:
35+
price = prices[holding.name]
36+
data.append((holding.name, holding.shares, price, price - holding.price))
37+
return data
38+
39+
40+
def print_report(report_data, formatter):
41+
formatter.headings(['Name', 'Shares', 'Price', 'Change'])
42+
for name, shares, price, change in report_data:
43+
row_data = [name, str(shares), f'{price:0.2f}', f'{change:0.2f}']
44+
formatter.row(row_data)
45+
46+
47+
def portfolio_report(portfolio_file, prices_file, fmt='txt'):
48+
"""
49+
Make a stock report given portfolio and price data files.
50+
"""
51+
# Read data files
52+
portfolio = read_portfolio(portfolio_file)
53+
prices = read_prices(prices_file)
54+
55+
# Create the report data
56+
report = make_report(portfolio, prices)
57+
58+
# Print it out
59+
formatter = tableformat.create_formatter(fmt)
60+
print_report(report, formatter)
61+
62+
63+
def main(argv):
64+
if len(argv) not in [3, 4]:
65+
raise SystemExit(f'Usage: {argv[0]} portfoliofile pricefile')
66+
print(argv)
67+
portfolio_report(*argv[1:])
68+
69+
70+
if __name__ == '__main__':
71+
import sys
72+
main(sys.argv)

Work/stock.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
class Stock:
2+
__slots__ = ('name', '_shares', 'price')
3+
def __init__(self, name, shares, price):
4+
self.name = name
5+
self.shares = shares
6+
self.price = price
7+
8+
@property
9+
def cost(self):
10+
return self.shares * self.price
11+
12+
@property
13+
def shares(self):
14+
return self._shares
15+
16+
@shares.setter
17+
def shares(self, value):
18+
if not isinstance(value, int):
19+
raise TypeError('Expected an integer')
20+
self._shares = value
21+
22+
def sell(self, number):
23+
self.shares -= number
24+
25+
def __repr__(self):
26+
return f'Stock({self.name}, {self.shares}, {self.price})'
27+
28+
29+
class MyStock(Stock):
30+
def __init__(self, name, shares, price, factor):
31+
# Check the call to `super` and `__init__`
32+
super().__init__(name, shares, price)
33+
self.factor = factor
34+
35+
def panic(self):
36+
self.sell(self.shares)
37+
38+
@property
39+
def cost(self):
40+
return self.factor * super().cost

Work/tableformat.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# tableformat.py
2+
3+
class TableFormatter:
4+
def headings(self, headers):
5+
"""
6+
Emit the table headings.
7+
"""
8+
raise NotImplementedError()
9+
10+
def row(self, rowdata):
11+
"""
12+
Emit a single row of table data.
13+
"""
14+
raise NotImplementedError()
15+
16+
17+
class TextTableFormatter(TableFormatter):
18+
"""
19+
Emit a table in plain-text format
20+
"""
21+
def headings(self, headers):
22+
for h in headers:
23+
print(f'{h:>10s}', end=' ')
24+
print()
25+
print(('-' * 10 + ' ') * len(headers))
26+
27+
def row(self, row_data):
28+
for column in row_data:
29+
print(f'{column:>10}', end=' ')
30+
print()
31+
32+
33+
class CSVTableFormatter(TableFormatter):
34+
"""
35+
Output portfolio data in CSV format.
36+
"""
37+
def headings(self, headers):
38+
print(','.join(headers))
39+
40+
def row(self, row_data):
41+
print(','.join(row_data))
42+
43+
class HTMLTableFormatter(TableFormatter):
44+
"""
45+
Output portfolio data in HTML format.
46+
"""
47+
def print_row(self, data, wrapper='td'):
48+
print('<tr>', end='')
49+
for column in data:
50+
print(f'<{wrapper}>{column}</{wrapper}>', end='')
51+
print('</tr>')
52+
53+
def headings(self, headers):
54+
self.print_row(headers, 'th')
55+
56+
def row(self, row_data):
57+
self.print_row(row_data)
58+
59+
60+
def create_formatter(name):
61+
if name == 'txt':
62+
formatter = TextTableFormatter()
63+
elif name == 'csv':
64+
formatter = CSVTableFormatter()
65+
elif name == 'html':
66+
formatter = HTMLTableFormatter()
67+
else:
68+
raise FormatError(f'Unknown table format {name}')
69+
return formatter
70+
71+
72+
def print_table(portfolio, columns, formatter):
73+
formatter.headings(columns)
74+
for holding in portfolio:
75+
line = [getattr(holding, column) for column in columns]
76+
formatter.row(line)
77+
78+
79+
class FormatError(Exception):
80+
pass

0 commit comments

Comments
 (0)