object patch

This commit is contained in:
David Beazley
2020-05-28 17:52:19 -05:00
parent a83a9cf064
commit a31a9ee0f4
57 changed files with 558 additions and 40 deletions

View File

@@ -1,6 +1,6 @@
# stock.py
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -1,6 +1,6 @@
# stock.py
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# stock.py
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -1,6 +1,6 @@
# portfolio.py
class Portfolio(object):
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings

View File

@@ -1,6 +1,6 @@
# stock.py
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -1,6 +1,6 @@
# portfolio.py
class Portfolio(object):
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings

View File

@@ -1,6 +1,6 @@
# stock.py
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -1,6 +1,6 @@
# portfolio.py
class Portfolio(object):
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings

View File

@@ -1,6 +1,6 @@
# stock.py
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -1,6 +1,6 @@
# portfolio.py
class Portfolio(object):
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings

View File

@@ -1,6 +1,6 @@
# stock.py
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -3,7 +3,7 @@
import fileparse
import stock
class Portfolio(object):
class Portfolio:
def __init__(self):
self._holdings = []

View File

@@ -2,7 +2,7 @@
from typedproperty import String, Integer, Float
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -20,7 +20,7 @@ Float = lambda name: typedproperty(name, float)
# Example
if __name__ == '__main__':
class Stock(object):
class Stock:
name = typedproperty('name', str)
shares = typedproperty('shares', int)
price = typedproperty('price', float)
@@ -30,5 +30,16 @@ if __name__ == '__main__':
self.shares = shares
self.price = price
class Stock2:
name = String('name')
shares = Integer('shares')
price = Float('price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price

View File

@@ -1,6 +1,6 @@
# portfolio.py
class Portfolio(object):
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings

View File

@@ -1,6 +1,6 @@
# stock.py
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -1,6 +1,6 @@
# portfolio.py
class Portfolio(object):
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings

View File

@@ -2,7 +2,7 @@
from typedproperty import String, Integer, Float
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -20,7 +20,7 @@ Float = lambda name: typedproperty(name, float)
# Example
if __name__ == '__main__':
class Stock(object):
class Stock:
name = typedproperty('name', str)
shares = typedproperty('shares', int)
price = typedproperty('price', float)
@@ -30,5 +30,15 @@ if __name__ == '__main__':
self.shares = shares
self.price = price
class Stock2:
name = String('name')
shares = Integer('shares')
price = Float('price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price

View File

@@ -3,7 +3,7 @@
import fileparse
import stock
class Portfolio(object):
class Portfolio:
def __init__(self):
self._holdings = []

View File

@@ -2,7 +2,7 @@
from typedproperty import String, Integer, Float
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -20,7 +20,7 @@ Float = lambda name: typedproperty(name, float)
# Example
if __name__ == '__main__':
class Stock(object):
class Stock:
name = typedproperty('name', str)
shares = typedproperty('shares', int)
price = typedproperty('price', float)

View File

@@ -3,7 +3,7 @@
import fileparse
import stock
class Portfolio(object):
class Portfolio:
def __init__(self):
self._holdings = []

View File

@@ -2,7 +2,7 @@
from typedproperty import String, Integer, Float
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -20,7 +20,7 @@ Float = lambda name: typedproperty(name, float)
# Example
if __name__ == '__main__':
class Stock(object):
class Stock:
name = typedproperty('name', str)
shares = typedproperty('shares', int)
price = typedproperty('price', float)

View File

@@ -3,7 +3,7 @@
from . import fileparse
from . import stock
class Portfolio(object):
class Portfolio:
def __init__(self):
self._holdings = []

View File

@@ -2,7 +2,7 @@
from .typedproperty import String, Integer, Float
class Stock(object):
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''

View File

@@ -1,6 +1,6 @@
# tableformat.py
class TableFormatter(object):
class TableFormatter:
def headings(self, headers):
'''
Emit the table headers

View File

@@ -20,7 +20,7 @@ Float = lambda name: typedproperty(name, float)
# Example
if __name__ == '__main__':
class Stock(object):
class Stock:
name = typedproperty('name', str)
shares = typedproperty('shares', int)
price = typedproperty('price', float)

View File

@@ -0,0 +1 @@
include *.csv

View File

@@ -0,0 +1,17 @@
Code from Practical Python.
The "porty" directory is a Python package of code that's loaded via
import. The "print-report.py" program is a top-level script that
produces a report. Try it:
shell % python3 print-report.py portfolio.csv prices.csv txt
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
shell %

View File

@@ -0,0 +1,8 @@
name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
"MSFT",200,51.23
"GE",95,40.37
"MSFT",50,65.10
"IBM",100,70.44
1 name shares price
2 AA 100 32.20
3 IBM 50 91.10
4 CAT 150 83.44
5 MSFT 200 51.23
6 GE 95 40.37
7 MSFT 50 65.10
8 IBM 100 70.44

View File

@@ -0,0 +1,47 @@
# fileparse.py
import csv
import logging
log = logging.getLogger(__name__)
def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False):
'''
Parse a CSV file into a list of records with type conversion.
'''
assert not (select and not has_headers), 'select requires column headers'
rows = csv.reader(lines, delimiter=delimiter)
# Read the file headers (if any)
headers = next(rows) if has_headers else []
# If specific columns have been selected, make indices for filtering and set output columns
if select:
indices = [ headers.index(colname) for colname in select ]
headers = select
records = []
for rowno, row in enumerate(rows, 1):
if not row: # Skip rows with no data
continue
# If specific column indices are selected, pick them out
if select:
row = [ row[index] for index in indices]
# Apply type conversion to the row
if types:
try:
row = [func(val) for func, val in zip(types, row)]
except ValueError as e:
if not silence_errors:
log.warning("Row %d: Couldn't convert %s", rowno, row)
log.debug("Row %d: Reason %s", rowno, e)
continue
# Make a dictionary or a tuple
if headers:
record = dict(zip(headers, row))
else:
record = tuple(row)
records.append(record)
return records

View File

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

View File

@@ -0,0 +1,20 @@
# pcost.py
from . import report
def portfolio_cost(filename):
'''
Computes the total cost (shares*price) of a portfolio file
'''
portfolio = report.read_portfolio(filename)
return portfolio.total_cost
def main(args):
if len(args) != 2:
raise SystemExit('Usage: %s portfoliofile' % args[0])
filename = args[1]
print('Total cost:', portfolio_cost(filename))
if __name__ == '__main__':
import sys
main(sys.argv)

View File

@@ -0,0 +1,51 @@
# portfolio.py
from . import fileparse
from . import stock
class Portfolio:
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

View File

@@ -0,0 +1,67 @@
# report.py
from . import fileparse
from .stock import Stock
from .portfolio import Portfolio
from . 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)

View File

@@ -0,0 +1,33 @@
# stock.py
from .typedproperty import String, Integer, Float
class Stock:
'''
An instance of a stock holding consisting of name, shares, and price.
'''
if __debug__:
name = String('name')
shares = Integer('shares')
price = Float('price')
def __init__(self,name, shares, price):
self.name = name
self.shares = shares
self.price = price
def __repr__(self):
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
@property
def cost(self):
'''
Return the cost as shares*price
'''
return self.shares * self.price
def sell(self, nshares):
'''
Sell a number of shares and return the remaining number.
'''
self.shares -= nshares

View File

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

View File

@@ -0,0 +1,28 @@
# test_stock.py
import unittest
from . 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()

View File

@@ -0,0 +1,43 @@
# ticker.py
import csv
from . import report
from . 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)

View File

@@ -0,0 +1,34 @@
# typedproperty.py
def typedproperty(name, expected_type):
private_name = '_' + name
@property
def prop(self):
return getattr(self, private_name)
@prop.setter
def prop(self, value):
if not isinstance(value, expected_type):
raise TypeError(f'Expected {expected_type}')
setattr(self, private_name, value)
return prop
String = lambda name: typedproperty(name, str)
Integer = lambda name: typedproperty(name, int)
Float = lambda name: typedproperty(name, float)
# Example
if __name__ == '__main__':
class Stock:
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

View File

@@ -0,0 +1,31 @@
"AA",9.22
"AXP",24.85
"BA",44.85
"BAC",11.27
"C",3.72
"CAT",35.46
"CVX",66.67
"DD",28.47
"DIS",24.22
"GE",13.48
"GM",0.75
"HD",23.16
"HPQ",34.35
"IBM",106.28
"INTC",15.72
"JNJ",55.16
"JPM",36.90
"KFT",26.11
"KO",49.16
"MCD",58.99
"MMM",57.10
"MRK",27.58
"MSFT",20.89
"PFE",15.19
"PG",51.94
"T",24.79
"UTX",52.61
"VZ",29.26
"WMT",49.74
"XOM",69.35
1 AA 9.22
2 AXP 24.85
3 BA 44.85
4 BAC 11.27
5 C 3.72
6 CAT 35.46
7 CVX 66.67
8 DD 28.47
9 DIS 24.22
10 GE 13.48
11 GM 0.75
12 HD 23.16
13 HPQ 34.35
14 IBM 106.28
15 INTC 15.72
16 JNJ 55.16
17 JPM 36.90
18 KFT 26.11
19 KO 49.16
20 MCD 58.99
21 MMM 57.10
22 MRK 27.58
23 MSFT 20.89
24 PFE 15.19
25 PG 51.94
26 T 24.79
27 UTX 52.61
28 VZ 29.26
29 WMT 49.74
30 XOM 69.35

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
# print-report.py
import sys
from porty.report import main
main(sys.argv)

View File

@@ -0,0 +1,13 @@
# setup.py
import setuptools
setuptools.setup(
name="porty",
version="0.0.1",
author="Your Name",
author_email="you@example.com",
description="Practical Python Code",
packages=setuptools.find_packages(),
scripts=['print-report.py'],
)