From dc6e2141df8c3d1068c821512cb11468bfac5b7a Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 17 Jan 2024 14:46:18 -0700 Subject: [PATCH 01/62] Complete to spec. --- Work/bounce.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Work/bounce.py b/Work/bounce.py index 3660ddd82..60ff9a6dc 100644 --- a/Work/bounce.py +++ b/Work/bounce.py @@ -1,3 +1,13 @@ # bounce.py # # Exercise 1.5 + +def main(): + cur_height = 100 + + for bounce in range(10): + cur_height *= 3/5 + print(bounce+1, round(cur_height, 4)) + +if __name__ == "__main__": + main() From 115ec00befeea314507a0a809b710f1b61ac67c4 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 17 Jan 2024 14:56:05 -0700 Subject: [PATCH 02/62] Debugging --- Work/sears.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Work/sears.py diff --git a/Work/sears.py b/Work/sears.py new file mode 100644 index 000000000..df968fff7 --- /dev/null +++ b/Work/sears.py @@ -0,0 +1,15 @@ +# sears.py + +bill_thickness = 0.11 * 0.001 # Meters (0.11 mm) +sears_height = 442 # Height (meters) +num_bills = 1 +day = 1 + +while num_bills * bill_thickness < sears_height: + print(day, num_bills, num_bills * bill_thickness) + day = day + 1 + num_bills = num_bills * 2 + +print('Number of days', day) +print('Number of bills', num_bills) +print('Final height', num_bills * bill_thickness) From 7b011066beaef2d8db87ea4cd60fa0f979941dc4 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 17 Jan 2024 15:48:27 -0700 Subject: [PATCH 03/62] Extra payments --- Work/mortgage.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Work/mortgage.py b/Work/mortgage.py index d527314e3..f613f2f57 100644 --- a/Work/mortgage.py +++ b/Work/mortgage.py @@ -1,3 +1,19 @@ # mortgage.py # # Exercise 1.7 + +principal = 500000.0 +rate = 0.05 +payment = 2684.11 +extra_payment = 1000 +npayments = 0 +total_paid = 0.0 + +while principal > 0: + npayments = npayments + 1 + cur_payment = payment + extra_payment if npayments <= 12 else payment + principal = principal * (1+rate/12) - cur_payment + total_paid = total_paid + cur_payment + print(npayments, round(cur_payment, 2), round(total_paid), round(principal, 2)) + +print('Total paid', round(total_paid, 2), "in", npayments, "months") From 3cbd1fee89325066caa221c7f05ea9f7614738d4 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 17 Jan 2024 16:18:31 -0700 Subject: [PATCH 04/62] User input to calculator, table, fix overpayment. --- Work/mortgage.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Work/mortgage.py b/Work/mortgage.py index f613f2f57..60da929f3 100644 --- a/Work/mortgage.py +++ b/Work/mortgage.py @@ -2,18 +2,28 @@ # # Exercise 1.7 -principal = 500000.0 -rate = 0.05 -payment = 2684.11 -extra_payment = 1000 -npayments = 0 -total_paid = 0.0 +def calc(start, end, extra_payment): + principal = 500000.0 + rate = 0.05 + payment = 2684.11 + npayments = 0 + total_paid = 0.0 -while principal > 0: - npayments = npayments + 1 - cur_payment = payment + extra_payment if npayments <= 12 else payment - principal = principal * (1+rate/12) - cur_payment - total_paid = total_paid + cur_payment - print(npayments, round(cur_payment, 2), round(total_paid), round(principal, 2)) + while principal > 0: + npayments = npayments + 1 + cur_payment = payment + extra_payment if start <= npayments <= end else payment + principal = principal * (1+rate/12) - cur_payment + total_paid = total_paid + cur_payment + print(npayments, round(cur_payment, 2), round(total_paid, 2), round(principal, 2)) -print('Total paid', round(total_paid, 2), "in", npayments, "months") + if principal < 0: # overpaid? + total_paid -= -principal + + print('Total paid', round(total_paid, 2), "in", npayments, "months") + +if __name__ == "__main__": + start = input('start month: ') + end = input('end month: ') + extra_payment = input('extra payment: ') + args = [int(s) for s in (start, end, extra_payment)] + calc(*args) From b68d74100681cb601c0650741f83211d867125c3 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 17 Jan 2024 20:51:04 -0700 Subject: [PATCH 05/62] Better handling of final payment. --- Work/mortgage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Work/mortgage.py b/Work/mortgage.py index 60da929f3..d46406473 100644 --- a/Work/mortgage.py +++ b/Work/mortgage.py @@ -12,13 +12,13 @@ def calc(start, end, extra_payment): while principal > 0: npayments = npayments + 1 cur_payment = payment + extra_payment if start <= npayments <= end else payment - principal = principal * (1+rate/12) - cur_payment + remaining = principal * (1+rate/12) + if cur_payment > remaining: # overpayment? + cur_payment = remaining + principal = remaining - cur_payment total_paid = total_paid + cur_payment print(npayments, round(cur_payment, 2), round(total_paid, 2), round(principal, 2)) - if principal < 0: # overpaid? - total_paid -= -principal - print('Total paid', round(total_paid, 2), "in", npayments, "months") if __name__ == "__main__": From 57369c456077ec17a013e7242f373a4896e09149 Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 18 Jan 2024 09:23:22 -0700 Subject: [PATCH 06/62] Format output using f-strings. --- Work/mortgage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Work/mortgage.py b/Work/mortgage.py index d46406473..53b71d23b 100644 --- a/Work/mortgage.py +++ b/Work/mortgage.py @@ -17,9 +17,9 @@ def calc(start, end, extra_payment): cur_payment = remaining principal = remaining - cur_payment total_paid = total_paid + cur_payment - print(npayments, round(cur_payment, 2), round(total_paid, 2), round(principal, 2)) + print(f'{npayments} {cur_payment:>8.2f} {total_paid:>10.2f} {principal:>10.2f}') - print('Total paid', round(total_paid, 2), "in", npayments, "months") + print(f'{34*"-"}\nTotal paid: {total_paid:0.2f} in {npayments} months') if __name__ == "__main__": start = input('start month: ') From 88d05d092af0c8b01f3d4397639e105194069878 Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 18 Jan 2024 10:01:29 -0700 Subject: [PATCH 07/62] Setup tests. --- Work/pcost.py | 3 +++ Work/pcost_tests.py | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 Work/pcost_tests.py diff --git a/Work/pcost.py b/Work/pcost.py index e68aa20b4..b6e2270f7 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -1,3 +1,6 @@ # pcost.py # # Exercise 1.27 + +def main(): + pass diff --git a/Work/pcost_tests.py b/Work/pcost_tests.py new file mode 100644 index 000000000..060a561bd --- /dev/null +++ b/Work/pcost_tests.py @@ -0,0 +1,9 @@ +import unittest +import pcost as pc + +class KnownOutput(unittest.TestCase): + def test_known_output(self): + self.assertEqual(pc.main(), 'Total cost 44671.15') + +if __name__ == "__main__": + unittest.main() From 26130b1eeee609dc586730efc0d7a5eb530b4950 Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 18 Jan 2024 10:59:56 -0700 Subject: [PATCH 08/62] Sum portfolio total. --- Work/pcost.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Work/pcost.py b/Work/pcost.py index b6e2270f7..28ba789ce 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -3,4 +3,12 @@ # Exercise 1.27 def main(): - pass + total = 0 + f = open('./Data/portfolio.csv', 'rt') + _ = next(f) # skip headers + for line in f: + stock, shares, price = line.split(',') + shares, price = int(shares), float(price) + total += shares * price + f.close() + return f'Total cost {total:0.2f}' From 63cb2b5c100ad629eef4016e6bcee8e951de6e6b Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 18 Jan 2024 11:09:35 -0700 Subject: [PATCH 09/62] Refactor --- Work/pcost.py | 7 ++++++- Work/pcost_tests.py | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Work/pcost.py b/Work/pcost.py index 28ba789ce..1e5602ffe 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -2,7 +2,7 @@ # # Exercise 1.27 -def main(): +def portfolio_cost(): total = 0 f = open('./Data/portfolio.csv', 'rt') _ = next(f) # skip headers @@ -11,4 +11,9 @@ def main(): shares, price = int(shares), float(price) total += shares * price f.close() + return total + +def main(): + total = portfolio_cost() return f'Total cost {total:0.2f}' + diff --git a/Work/pcost_tests.py b/Work/pcost_tests.py index 060a561bd..7d8967dce 100644 --- a/Work/pcost_tests.py +++ b/Work/pcost_tests.py @@ -2,8 +2,10 @@ import pcost as pc class KnownOutput(unittest.TestCase): - def test_known_output(self): + def test_main_output(self): self.assertEqual(pc.main(), 'Total cost 44671.15') + def test_portfolio_cost(self): + self.assertEqual(pc.portfolio_cost(), 44671.15) if __name__ == "__main__": unittest.main() From e623ba845f92ab14320f84ec1d5073dd5b48a7ec Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 18 Jan 2024 11:25:12 -0700 Subject: [PATCH 10/62] Handle missing fields --- Work/pcost.py | 14 +++++++++----- Work/pcost_tests.py | 7 ++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Work/pcost.py b/Work/pcost.py index 1e5602ffe..ff1129a83 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -2,18 +2,22 @@ # # Exercise 1.27 -def portfolio_cost(): +def portfolio_cost(path): total = 0 - f = open('./Data/portfolio.csv', 'rt') + f = open(path, 'rt') _ = next(f) # skip headers for line in f: stock, shares, price = line.split(',') - shares, price = int(shares), float(price) - total += shares * price + try: + shares, price = int(shares), float(price) + total += shares * price + except ValueError: + print("Couldn't parse", line.strip()) + f.close() return total def main(): - total = portfolio_cost() + total = portfolio_cost('./Data/portfolio.csv') return f'Total cost {total:0.2f}' diff --git a/Work/pcost_tests.py b/Work/pcost_tests.py index 7d8967dce..d71ed54a1 100644 --- a/Work/pcost_tests.py +++ b/Work/pcost_tests.py @@ -2,10 +2,15 @@ import pcost as pc class KnownOutput(unittest.TestCase): + def test_main_output(self): self.assertEqual(pc.main(), 'Total cost 44671.15') + def test_portfolio_cost(self): - self.assertEqual(pc.portfolio_cost(), 44671.15) + self.assertEqual(pc.portfolio_cost('./Data/portfolio.csv'), 44671.15) + + def test_missing_fields(self): + self.assertEqual(pc.portfolio_cost('Data/missing.csv'), 27381.15) if __name__ == "__main__": unittest.main() From da279f1078b0853b39df7aeaa1611996fb288c04 Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 18 Jan 2024 11:39:07 -0700 Subject: [PATCH 11/62] CLI input --- Work/pcost.py | 21 ++++++++++++++------- Work/pcost_tests.py | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Work/pcost.py b/Work/pcost.py index ff1129a83..eb0db4d36 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -2,22 +2,29 @@ # # Exercise 1.27 +import csv +import sys + def portfolio_cost(path): total = 0 f = open(path, 'rt') + rows = csv.reader(f) _ = next(f) # skip headers - for line in f: - stock, shares, price = line.split(',') + for row in rows: + stock, shares, price = row try: - shares, price = int(shares), float(price) - total += shares * price + total += int(shares) * float(price) except ValueError: - print("Couldn't parse", line.strip()) + print("Couldn't parse", row) f.close() return total -def main(): - total = portfolio_cost('./Data/portfolio.csv') +def main(path): + total = portfolio_cost(path) return f'Total cost {total:0.2f}' +if __name__ == "__main__": + path = sys.argv[1] if len(sys.argv)==2 else 'Data/portfolio.csv' + print(main(path)) + diff --git a/Work/pcost_tests.py b/Work/pcost_tests.py index d71ed54a1..f21d89bec 100644 --- a/Work/pcost_tests.py +++ b/Work/pcost_tests.py @@ -4,7 +4,7 @@ class KnownOutput(unittest.TestCase): def test_main_output(self): - self.assertEqual(pc.main(), 'Total cost 44671.15') + self.assertEqual(pc.main('Data/portfolio.csv'), 'Total cost 44671.15') def test_portfolio_cost(self): self.assertEqual(pc.portfolio_cost('./Data/portfolio.csv'), 44671.15) From 4cb99d8b09cd7637be747cdfa661fa28ac6ff964 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 19 Jan 2024 09:45:42 -0700 Subject: [PATCH 12/62] Use argv unpacking to get path. --- Work/pcost.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Work/pcost.py b/Work/pcost.py index eb0db4d36..4bee4d2fc 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -25,6 +25,7 @@ def main(path): return f'Total cost {total:0.2f}' if __name__ == "__main__": - path = sys.argv[1] if len(sys.argv)==2 else 'Data/portfolio.csv' + _, *rest = sys.argv + path = rest[0] if rest else 'Data/portfolio.csv' print(main(path)) From a03d6fef46feb15baf3b60f90a510afdde0fb337 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 19 Jan 2024 10:14:20 -0700 Subject: [PATCH 13/62] Minor cleanup. --- Work/pcost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Work/pcost.py b/Work/pcost.py index 4bee4d2fc..7a63b8223 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -9,7 +9,7 @@ def portfolio_cost(path): total = 0 f = open(path, 'rt') rows = csv.reader(f) - _ = next(f) # skip headers + _ = next(rows) # skip headers for row in rows: stock, shares, price = row try: From 3a1bce9f32888e63c21a2304d307fc3ca534be17 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 19 Jan 2024 10:50:02 -0700 Subject: [PATCH 14/62] Refactor to containers. --- Work/report.py | 30 ++++++++++++++++++++++++++++++ Work/report_tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 Work/report_tests.py diff --git a/Work/report.py b/Work/report.py index 47d5da7b1..f94959854 100644 --- a/Work/report.py +++ b/Work/report.py @@ -1,3 +1,33 @@ # report.py # # Exercise 2.4 + +import csv +import sys + +def portfolio_cost(path): + '''Computes the total cost of a portfolio file''' + portfolio = [] + + with open(path, 'rt') as f: + rows = csv.reader(f) + _ = next(rows) # skip headers + for row in rows: + try: + stock, shares, price = row + portfolio.append((stock, int(shares), float(price))) + except ValueError: + print("Couldn't parse", row) + + return portfolio + +def main(path): + portfolio = portfolio_cost(path) + total = sum(n*p for (s,n,p) in portfolio) + return f'Total cost {total:0.2f}' + +if __name__ == "__main__": + _, *rest = sys.argv + path = rest[0] if rest else 'Data/portfolio.csv' + print(main(path)) + diff --git a/Work/report_tests.py b/Work/report_tests.py new file mode 100644 index 000000000..c92873ce3 --- /dev/null +++ b/Work/report_tests.py @@ -0,0 +1,32 @@ +import unittest +import report as rep + +class KnownOutput(unittest.TestCase): + + def test_main_output(self): + self.assertEqual(rep.main('Data/portfolio.csv'), 'Total cost 44671.15') + + def test_portfolio_cost(self): + expected = [ + ('AA', 100, 32.2), + ('IBM', 50, 91.1), + ('CAT', 150, 83.44), + ('MSFT', 200, 51.23), + ('GE', 95, 40.37), + ('MSFT', 50, 65.1), + ('IBM', 100, 70.44) + ] + self.assertEqual(rep.portfolio_cost('./Data/portfolio.csv'), expected) + + def test_missing_fields(self): + expected = [ + ('AA', 100, 32.2), + ('IBM', 50, 91.1), + ('CAT', 150, 83.44), + ('GE', 95, 40.37), + ('MSFT', 50, 65.1), + ] + self.assertEqual(rep.portfolio_cost('Data/missing.csv'), expected) + +if __name__ == "__main__": + unittest.main() From c59630e52fa5aaa92783162574081189f5a2300a Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 19 Jan 2024 15:42:16 -0700 Subject: [PATCH 15/62] Dictionaries --- Work/report.py | 9 +++++---- Work/report_tests.py | 34 +++++++++++++++++----------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Work/report.py b/Work/report.py index f94959854..075f4b121 100644 --- a/Work/report.py +++ b/Work/report.py @@ -5,7 +5,7 @@ import csv import sys -def portfolio_cost(path): +def read_portfolio(path): '''Computes the total cost of a portfolio file''' portfolio = [] @@ -19,11 +19,12 @@ def portfolio_cost(path): except ValueError: print("Couldn't parse", row) - return portfolio + return [{'name': stock, 'shares': shares, 'price': price} + for (stock,shares,price) in portfolio] def main(path): - portfolio = portfolio_cost(path) - total = sum(n*p for (s,n,p) in portfolio) + portfolio = read_portfolio(path) + total = sum(s['shares']*s['price'] for s in portfolio) return f'Total cost {total:0.2f}' if __name__ == "__main__": diff --git a/Work/report_tests.py b/Work/report_tests.py index c92873ce3..431ecac97 100644 --- a/Work/report_tests.py +++ b/Work/report_tests.py @@ -6,27 +6,27 @@ class KnownOutput(unittest.TestCase): def test_main_output(self): self.assertEqual(rep.main('Data/portfolio.csv'), 'Total cost 44671.15') - def test_portfolio_cost(self): + def test_read_portfolio(self): expected = [ - ('AA', 100, 32.2), - ('IBM', 50, 91.1), - ('CAT', 150, 83.44), - ('MSFT', 200, 51.23), - ('GE', 95, 40.37), - ('MSFT', 50, 65.1), - ('IBM', 100, 70.44) - ] - self.assertEqual(rep.portfolio_cost('./Data/portfolio.csv'), expected) + {'name': 'AA', 'shares': 100, 'price': 32.2}, + {'name': 'IBM', 'shares': 50, 'price': 91.1}, + {'name': 'CAT', 'shares': 150, 'price': 83.44}, + {'name': 'MSFT', 'shares': 200, 'price': 51.23}, + {'name': 'GE', 'shares': 95, 'price': 40.37}, + {'name': 'MSFT', 'shares': 50, 'price': 65.1}, + {'name': 'IBM', 'shares': 100, 'price': 70.44} + ] + self.assertEqual(rep.read_portfolio('./Data/portfolio.csv'), expected) def test_missing_fields(self): expected = [ - ('AA', 100, 32.2), - ('IBM', 50, 91.1), - ('CAT', 150, 83.44), - ('GE', 95, 40.37), - ('MSFT', 50, 65.1), - ] - self.assertEqual(rep.portfolio_cost('Data/missing.csv'), expected) + {'name': 'AA', 'shares': 100, 'price': 32.2}, + {'name': 'IBM', 'shares': 50, 'price': 91.1}, + {'name': 'CAT', 'shares': 150, 'price': 83.44}, + {'name': 'GE', 'shares': 95, 'price': 40.37}, + {'name': 'MSFT', 'shares': 50, 'price': 65.1}, + ] + self.assertEqual(rep.read_portfolio('Data/missing.csv'), expected) if __name__ == "__main__": unittest.main() From a7da6675988756fdd971b55a9f126b6ba382c840 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 19 Jan 2024 15:47:50 -0700 Subject: [PATCH 16/62] Formatting --- Work/report.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Work/report.py b/Work/report.py index 075f4b121..c778285fd 100644 --- a/Work/report.py +++ b/Work/report.py @@ -15,12 +15,14 @@ def read_portfolio(path): for row in rows: try: stock, shares, price = row - portfolio.append((stock, int(shares), float(price))) + portfolio.append({ + 'name': stock, + 'shares': int(shares), + 'price': float(price)}) except ValueError: print("Couldn't parse", row) - return [{'name': stock, 'shares': shares, 'price': price} - for (stock,shares,price) in portfolio] + return portfolio def main(path): portfolio = read_portfolio(path) From 99b5e173da35d26e9d9e04740e6cd691fbc4e578 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 19 Jan 2024 15:53:05 -0700 Subject: [PATCH 17/62] pretty print --- Work/report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Work/report.py b/Work/report.py index c778285fd..31b6261ca 100644 --- a/Work/report.py +++ b/Work/report.py @@ -4,6 +4,7 @@ import csv import sys +from pprint import pprint def read_portfolio(path): '''Computes the total cost of a portfolio file''' @@ -26,6 +27,7 @@ def read_portfolio(path): def main(path): portfolio = read_portfolio(path) + pprint(portfolio) total = sum(s['shares']*s['price'] for s in portfolio) return f'Total cost {total:0.2f}' @@ -33,4 +35,3 @@ def main(path): _, *rest = sys.argv path = rest[0] if rest else 'Data/portfolio.csv' print(main(path)) - From b291bd2b3e41a70ca1ce9db5f8ba0b75d0bfa3f9 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 19 Jan 2024 16:08:10 -0700 Subject: [PATCH 18/62] Add price lookup --- Work/report.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Work/report.py b/Work/report.py index 31b6261ca..fde4a04d8 100644 --- a/Work/report.py +++ b/Work/report.py @@ -25,6 +25,14 @@ def read_portfolio(path): return portfolio +def read_prices(path): + prices = {} + with open(path, 'rt') as f: + rows = csv.reader(f) + rows = [r for r in rows if r] # sanitize + prices = {s:float(p) for s,p in rows} + return prices + def main(path): portfolio = read_portfolio(path) pprint(portfolio) From 22753a032bc8278e6cc297745da368613e5a999b Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 19 Jan 2024 16:41:05 -0700 Subject: [PATCH 19/62] Finish containers section. --- Work/report.py | 21 +++++++++++++-------- Work/report_tests.py | 3 --- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Work/report.py b/Work/report.py index fde4a04d8..0471da60b 100644 --- a/Work/report.py +++ b/Work/report.py @@ -29,17 +29,22 @@ def read_prices(path): prices = {} with open(path, 'rt') as f: rows = csv.reader(f) - rows = [r for r in rows if r] # sanitize - prices = {s:float(p) for s,p in rows} + prices = {s:float(p) for s,p in [r for r in rows if r]} return prices -def main(path): - portfolio = read_portfolio(path) +def main(path_portfolio, path_prices): + portfolio = read_portfolio(path_portfolio) + prices = read_prices(path_prices) pprint(portfolio) - total = sum(s['shares']*s['price'] for s in portfolio) - return f'Total cost {total:0.2f}' + pprint(prices) + total_cost = sum(s['shares']*s['price'] for s in portfolio) + cur_value = sum(s['shares']*prices[s['name']] for s in portfolio) + print(f'Total cost: {total_cost:0.2f}') + print(f'Current value: {cur_value:0.2f}') + print(f'Total gain: {cur_value-total_cost:0.2f}') if __name__ == "__main__": _, *rest = sys.argv - path = rest[0] if rest else 'Data/portfolio.csv' - print(main(path)) + path_portfolio = rest[0] if len(rest)>0 else 'Data/portfolio.csv' + path_prices = rest[1] if len(rest)>1 else 'Data/prices.csv' + main(path_portfolio, path_prices) diff --git a/Work/report_tests.py b/Work/report_tests.py index 431ecac97..5931a071f 100644 --- a/Work/report_tests.py +++ b/Work/report_tests.py @@ -3,9 +3,6 @@ class KnownOutput(unittest.TestCase): - def test_main_output(self): - self.assertEqual(rep.main('Data/portfolio.csv'), 'Total cost 44671.15') - def test_read_portfolio(self): expected = [ {'name': 'AA', 'shares': 100, 'price': 32.2}, From b0dcd51af1f7aed46e956ead46f528e53200a86e Mon Sep 17 00:00:00 2001 From: bitmole Date: Sun, 21 Jan 2024 14:54:36 -0700 Subject: [PATCH 20/62] Initial make report function. --- Work/report.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Work/report.py b/Work/report.py index 0471da60b..873eab861 100644 --- a/Work/report.py +++ b/Work/report.py @@ -32,6 +32,16 @@ def read_prices(path): prices = {s:float(p) for s,p in [r for r in rows if r]} return prices +def make_report(portfolio, prices): + rows = [] + for s in portfolio: + name = s['name'] + nshares = s['shares'] + cur_price = prices[s['name']] + change = cur_price - s['price'] + rows.append((name, nshares, cur_price, change)) + return rows + def main(path_portfolio, path_prices): portfolio = read_portfolio(path_portfolio) prices = read_prices(path_prices) From b33a4e9a1e98632d5d168a9853a6e79f9b62a2ef Mon Sep 17 00:00:00 2001 From: bitmole Date: Sun, 21 Jan 2024 15:34:11 -0700 Subject: [PATCH 21/62] Printing --- Work/report.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Work/report.py b/Work/report.py index 873eab861..4af8687f5 100644 --- a/Work/report.py +++ b/Work/report.py @@ -45,13 +45,18 @@ def make_report(portfolio, prices): def main(path_portfolio, path_prices): portfolio = read_portfolio(path_portfolio) prices = read_prices(path_prices) - pprint(portfolio) - pprint(prices) - total_cost = sum(s['shares']*s['price'] for s in portfolio) + headers = ('Name', 'Shares', 'Price', 'Change') + print('%10s %10s %10s %10s' % headers) + print(len(headers) * ((10*'-') + ' ')) + row_format = '%10s %10d %10.2f %10.2f' + for r in make_report(portfolio, prices): + print(row_format % r) + print(43 * '=') + purchase_price = sum(s['shares']*s['price'] for s in portfolio) cur_value = sum(s['shares']*prices[s['name']] for s in portfolio) - print(f'Total cost: {total_cost:0.2f}') - print(f'Current value: {cur_value:0.2f}') - print(f'Total gain: {cur_value-total_cost:0.2f}') + print(f'Purchase price: {purchase_price:>10.2f}') + print(f' Current value: {cur_value:>10.2f}') + print(f' Total gain: {cur_value-purchase_price:>10.2f}') if __name__ == "__main__": _, *rest = sys.argv From 0a62dbb5ea5ec482fef759599581bd648e0556ba Mon Sep 17 00:00:00 2001 From: bitmole Date: Sun, 21 Jan 2024 20:02:17 -0700 Subject: [PATCH 22/62] More messy printing --- Work/report.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Work/report.py b/Work/report.py index 4af8687f5..5eb0242bc 100644 --- a/Work/report.py +++ b/Work/report.py @@ -43,17 +43,24 @@ def make_report(portfolio, prices): return rows def main(path_portfolio, path_prices): + # collect report data portfolio = read_portfolio(path_portfolio) prices = read_prices(path_prices) - headers = ('Name', 'Shares', 'Price', 'Change') - print('%10s %10s %10s %10s' % headers) - print(len(headers) * ((10*'-') + ' ')) - row_format = '%10s %10d %10.2f %10.2f' - for r in make_report(portfolio, prices): - print(row_format % r) - print(43 * '=') purchase_price = sum(s['shares']*s['price'] for s in portfolio) cur_value = sum(s['shares']*prices[s['name']] for s in portfolio) + + # print headers + headers = ('Name', 'Shares', 'Price', 'Change') + print('%10s '*len(headers) % headers) + print(len(headers) * ((10*'-') + ' ')) + + # print rows + for s, n, p, d in make_report(portfolio, prices): + print('{:>10s} {:>10d} {:>10s} {:>10.2f}' + .format(s, n, '${:0.2f}'.format(p), d)) + + # print summary + print('='*43) print(f'Purchase price: {purchase_price:>10.2f}') print(f' Current value: {cur_value:>10.2f}') print(f' Total gain: {cur_value-purchase_price:>10.2f}') From e9d8d36487fed15764407367af50ec67059bdfce Mon Sep 17 00:00:00 2001 From: bitmole Date: Mon, 22 Jan 2024 09:11:47 -0700 Subject: [PATCH 23/62] Simplify pre-formatting. --- Work/report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Work/report.py b/Work/report.py index 5eb0242bc..a7a5f2e5c 100644 --- a/Work/report.py +++ b/Work/report.py @@ -56,8 +56,8 @@ def main(path_portfolio, path_prices): # print rows for s, n, p, d in make_report(portfolio, prices): - print('{:>10s} {:>10d} {:>10s} {:>10.2f}' - .format(s, n, '${:0.2f}'.format(p), d)) + p = f'${p:0.2f}' + print(f'{s:>10s} {n:>10d} {p:>10s} {d:>10.2f}') # print summary print('='*43) From fbdb55345683b00d8fbd7e2026ff8ff421b7c840 Mon Sep 17 00:00:00 2001 From: bitmole Date: Mon, 22 Jan 2024 10:30:23 -0700 Subject: [PATCH 24/62] Sequences, zip, dict. --- Work/pcost.py | 14 +++++++------- Work/report.py | 11 +++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Work/pcost.py b/Work/pcost.py index 7a63b8223..fbc2a234b 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -6,19 +6,19 @@ import sys def portfolio_cost(path): - total = 0 + total_cost = 0 f = open(path, 'rt') rows = csv.reader(f) - _ = next(rows) # skip headers - for row in rows: - stock, shares, price = row + headers = next(rows) + for i, row in enumerate(rows, start=1): + rec = dict(zip(headers, row)) try: - total += int(shares) * float(price) + total_cost += int(rec['shares']) * float(rec['price']) except ValueError: - print("Couldn't parse", row) + print(f"Row {i}: Couldn't convert: {row}") f.close() - return total + return total_cost def main(path): total = portfolio_cost(path) diff --git a/Work/report.py b/Work/report.py index a7a5f2e5c..02a4035f4 100644 --- a/Work/report.py +++ b/Work/report.py @@ -12,14 +12,13 @@ def read_portfolio(path): with open(path, 'rt') as f: rows = csv.reader(f) - _ = next(rows) # skip headers + headers = next(rows) for row in rows: + rec = dict(zip(headers, row)) try: - stock, shares, price = row - portfolio.append({ - 'name': stock, - 'shares': int(shares), - 'price': float(price)}) + rec['shares'] = int(rec['shares']) + rec['price'] = float(rec['price']) + portfolio.append(rec) except ValueError: print("Couldn't parse", row) From b04a3a17d8c30310a4a94c392e35a4f419be5c11 Mon Sep 17 00:00:00 2001 From: bitmole Date: Tue, 23 Jan 2024 11:41:05 -0700 Subject: [PATCH 25/62] Simplify conversion. --- Work/report.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Work/report.py b/Work/report.py index 02a4035f4..9c2ae40fd 100644 --- a/Work/report.py +++ b/Work/report.py @@ -13,11 +13,10 @@ def read_portfolio(path): with open(path, 'rt') as f: rows = csv.reader(f) headers = next(rows) + types = [str, int, float] for row in rows: - rec = dict(zip(headers, row)) try: - rec['shares'] = int(rec['shares']) - rec['price'] = float(rec['price']) + rec = {h:f(val) for h,f,val in zip(headers,types,row)} portfolio.append(rec) except ValueError: print("Couldn't parse", row) From eeede8ccc86591cd7aa8174e1c99023a6a189048 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 24 Jan 2024 13:18:31 -0700 Subject: [PATCH 26/62] Refactor file parsing. --- Work/fileparse.py | 27 +++++++++++++++++++++++++++ Work/report.py | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index 1d499e733..82ba92d14 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -1,3 +1,30 @@ # fileparse.py # # Exercise 3.3 +import csv + +def parse_csv(filename, select=None): + """Parse a CSV file into a list of records + structured as dicts. + + :filename: path to file + :returns: list of records (dicts) + + """ + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + + if select: + indices = [headers.index(c) for c in select] + headers = select + else: + indices = [] + + records = [] + for row in rows: + if indices: + row = [row[i] for i in indices] + records.append(dict(zip(headers, row))) + + return records diff --git a/Work/report.py b/Work/report.py index 9c2ae40fd..45a98c126 100644 --- a/Work/report.py +++ b/Work/report.py @@ -40,7 +40,7 @@ def make_report(portfolio, prices): rows.append((name, nshares, cur_price, change)) return rows -def main(path_portfolio, path_prices): +def print_report(path_portfolio, path_prices): # collect report data portfolio = read_portfolio(path_portfolio) prices = read_prices(path_prices) @@ -67,4 +67,4 @@ def main(path_portfolio, path_prices): _, *rest = sys.argv path_portfolio = rest[0] if len(rest)>0 else 'Data/portfolio.csv' path_prices = rest[1] if len(rest)>1 else 'Data/prices.csv' - main(path_portfolio, path_prices) + print_report(path_portfolio, path_prices) From d4d76479a2ed7e176afa1b2feab213f424b40d57 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 24 Jan 2024 17:27:16 -0700 Subject: [PATCH 27/62] Improve column selection. --- Work/fileparse.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index 82ba92d14..309322005 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -18,13 +18,9 @@ def parse_csv(filename, select=None): if select: indices = [headers.index(c) for c in select] headers = select - else: - indices = [] + # filter & sanitize + rows = [[row[i] for i in indices] for row in rows if row] - records = [] - for row in rows: - if indices: - row = [row[i] for i in indices] - records.append(dict(zip(headers, row))) + records = [dict(zip(headers, row)) for row in rows] return records From b16dcd742f08c377a660d20bbc75f0cd5a00a227 Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 25 Jan 2024 10:42:39 -0700 Subject: [PATCH 28/62] Parameterize type conversion. --- Work/fileparse.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index 309322005..e0207fc07 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -3,7 +3,7 @@ # Exercise 3.3 import csv -def parse_csv(filename, select=None): +def parse_csv(filename, select=None, types=None): """Parse a CSV file into a list of records structured as dicts. @@ -11,16 +11,27 @@ def parse_csv(filename, select=None): :returns: list of records (dicts) """ + records = [] with open(filename) as f: rows = csv.reader(f) headers = next(rows) + if select and types: + assert len(select)==len(types) + + rows = (r for r in rows if r) # ignore empty rows + + # filter columns if select: indices = [headers.index(c) for c in select] headers = select - # filter & sanitize - rows = [[row[i] for i in indices] for row in rows if row] + rows = [[row[i] for i in indices] for row in rows] + + # convert + if types: + rows = [[t(val) for t, val in zip(types, row)] for row in rows] + # package to records records = [dict(zip(headers, row)) for row in rows] return records From 6f18277d0c417df67082587043ed79deb99fed16 Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 25 Jan 2024 10:44:23 -0700 Subject: [PATCH 29/62] Update docstrings. --- Work/fileparse.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Work/fileparse.py b/Work/fileparse.py index e0207fc07..1b8ee74ec 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -8,6 +8,8 @@ def parse_csv(filename, select=None, types=None): structured as dicts. :filename: path to file + :select: columns + :types: conversion functions :returns: list of records (dicts) """ From 7e4b825970218eb1e8a119603c48279cdf0c6eeb Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 25 Jan 2024 11:13:08 -0700 Subject: [PATCH 30/62] Support files without headers. --- Work/fileparse.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index 1b8ee74ec..105a81510 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -3,7 +3,7 @@ # Exercise 3.3 import csv -def parse_csv(filename, select=None, types=None): +def parse_csv(filename, select=None, types=None, has_headers=True): """Parse a CSV file into a list of records structured as dicts. @@ -13,13 +13,17 @@ def parse_csv(filename, select=None, types=None): :returns: list of records (dicts) """ + if select and types: + assert len(select)==len(types) + + if select: + assert has_headers + records = [] with open(filename) as f: rows = csv.reader(f) - headers = next(rows) - - if select and types: - assert len(select)==len(types) + if has_headers: + headers = next(rows) rows = (r for r in rows if r) # ignore empty rows @@ -28,12 +32,17 @@ def parse_csv(filename, select=None, types=None): indices = [headers.index(c) for c in select] headers = select rows = [[row[i] for i in indices] for row in rows] + elif types: + assert len(headers)==len(types) # convert if types: rows = [[t(val) for t, val in zip(types, row)] for row in rows] # package to records - records = [dict(zip(headers, row)) for row in rows] + if has_headers: + records = [dict(zip(headers, row)) for row in rows] + else: + records = [tuple(r) for r in rows] return records From 2f6772cbb9071d84abe398fd15b559bf9bee359c Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 25 Jan 2024 11:17:19 -0700 Subject: [PATCH 31/62] Support different delimiters. --- Work/fileparse.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index 105a81510..bee1e3c5d 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -3,13 +3,15 @@ # Exercise 3.3 import csv -def parse_csv(filename, select=None, types=None, has_headers=True): +def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=','): """Parse a CSV file into a list of records structured as dicts. - :filename: path to file - :select: columns - :types: conversion functions + :filename: path to file + :select: columns + :types: conversion functions + :has_headers + :delimiter :returns: list of records (dicts) """ @@ -21,7 +23,7 @@ def parse_csv(filename, select=None, types=None, has_headers=True): records = [] with open(filename) as f: - rows = csv.reader(f) + rows = csv.reader(f, delimiter=delimiter) if has_headers: headers = next(rows) From aed56cd523549efc42e68e03fafde923d9010ae4 Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 25 Jan 2024 17:09:53 -0700 Subject: [PATCH 32/62] Support optional error reporting. --- Work/fileparse.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index bee1e3c5d..77f06f059 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -3,7 +3,12 @@ # Exercise 3.3 import csv -def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=','): +def parse_csv(filename, + select=None, + types=None, + has_headers=True, + delimiter=',', + silence_errors=False): """Parse a CSV file into a list of records structured as dicts. @@ -12,14 +17,15 @@ def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',' :types: conversion functions :has_headers :delimiter + :silence_errors :returns: list of records (dicts) """ - if select and types: - assert len(select)==len(types) + if select and not has_headers: + raise RuntimeError('select argument requires column headers') - if select: - assert has_headers + if select and types and len(select) != len(types): + raise RuntimeError('select and types lists must have the same shape') records = [] with open(filename) as f: @@ -38,8 +44,19 @@ def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',' assert len(headers)==len(types) # convert + def convert(row, i): + converted = [] + for val, t in zip(row, types): + try: + converted.append(t(val)) + except ValueError as e: + if not silence_errors: + print(f"Row {i}: Couldn't convert", row) + print(f"Row {i}: Reason", e) + return converted + if types: - rows = [[t(val) for t, val in zip(types, row)] for row in rows] + rows = [[convert(row, i)] for i, row in enumerate(rows, start=1)] # package to records if has_headers: From 9a5085500ef261aff612109d5e99059adaae00bf Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 25 Jan 2024 19:57:13 -0700 Subject: [PATCH 33/62] Reorg --- Work/fileparse.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index 77f06f059..8ff082f07 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -27,6 +27,18 @@ def parse_csv(filename, if select and types and len(select) != len(types): raise RuntimeError('select and types lists must have the same shape') + # type conversion helper + def convert(row, i): + converted = [] + for val, t in zip(row, types): + try: + converted.append(t(val)) + except ValueError as e: + if not silence_errors: + print(f"Row {i}: Couldn't convert", row) + print(f"Row {i}: Reason", e) + return converted + records = [] with open(filename) as f: rows = csv.reader(f, delimiter=delimiter) @@ -43,18 +55,7 @@ def parse_csv(filename, elif types: assert len(headers)==len(types) - # convert - def convert(row, i): - converted = [] - for val, t in zip(row, types): - try: - converted.append(t(val)) - except ValueError as e: - if not silence_errors: - print(f"Row {i}: Couldn't convert", row) - print(f"Row {i}: Reason", e) - return converted - + # convert to types if types: rows = [[convert(row, i)] for i, row in enumerate(rows, start=1)] From 462b440b944fe766293ca9596038b2c4ad91702f Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 26 Jan 2024 09:56:16 -0700 Subject: [PATCH 34/62] Refactor parsing to its module. --- Work/fileparse.py | 26 +++++++++++--------------- Work/report.py | 32 +++++++++++--------------------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index 8ff082f07..4665ac56f 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -29,35 +29,31 @@ def parse_csv(filename, # type conversion helper def convert(row, i): - converted = [] - for val, t in zip(row, types): - try: - converted.append(t(val)) - except ValueError as e: - if not silence_errors: - print(f"Row {i}: Couldn't convert", row) - print(f"Row {i}: Reason", e) - return converted + assert types + try: + return [t(val) for t, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {i}: Couldn't convert", row) + print(f"Row {i}: Reason", e) records = [] - with open(filename) as f: + with open(filename, 'rt') as f: rows = csv.reader(f, delimiter=delimiter) if has_headers: headers = next(rows) - rows = (r for r in rows if r) # ignore empty rows - # filter columns if select: indices = [headers.index(c) for c in select] headers = select rows = [[row[i] for i in indices] for row in rows] - elif types: - assert len(headers)==len(types) # convert to types if types: - rows = [[convert(row, i)] for i, row in enumerate(rows, start=1)] + rows = [convert(row, i) for i, row in enumerate(rows, start=1)] + + rows = (r for r in rows if r) # ignore empty rows # package to records if has_headers: diff --git a/Work/report.py b/Work/report.py index 45a98c126..7ab922794 100644 --- a/Work/report.py +++ b/Work/report.py @@ -5,30 +5,20 @@ import csv import sys from pprint import pprint +import fileparse def read_portfolio(path): - '''Computes the total cost of a portfolio file''' - portfolio = [] - - with open(path, 'rt') as f: - rows = csv.reader(f) - headers = next(rows) - types = [str, int, float] - for row in rows: - try: - rec = {h:f(val) for h,f,val in zip(headers,types,row)} - portfolio.append(rec) - except ValueError: - print("Couldn't parse", row) - - return portfolio + """ + Parses data file into stock portfolio + """ + return fileparse.parse_csv(path, types=[str, int, float]) def read_prices(path): - prices = {} - with open(path, 'rt') as f: - rows = csv.reader(f) - prices = {s:float(p) for s,p in [r for r in rows if r]} - return prices + """ + Parses price data file into stock price dictionary + """ + prices = fileparse.parse_csv(path, types=[str, float], has_headers=False) + return {n:p for n, p in prices} def make_report(portfolio, prices): rows = [] @@ -54,7 +44,7 @@ def print_report(path_portfolio, path_prices): # print rows for s, n, p, d in make_report(portfolio, prices): - p = f'${p:0.2f}' + p = f'${p:0.2f}' # pre-format price print(f'{s:>10s} {n:>10d} {p:>10s} {d:>10.2f}') # print summary From 9fc89046b59f98d9864751090391397b3955ecb1 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 26 Jan 2024 10:06:46 -0700 Subject: [PATCH 35/62] Reuse through module imports. --- Work/pcost.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Work/pcost.py b/Work/pcost.py index fbc2a234b..a37d9a565 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -4,21 +4,11 @@ import csv import sys +import report def portfolio_cost(path): - total_cost = 0 - f = open(path, 'rt') - rows = csv.reader(f) - headers = next(rows) - for i, row in enumerate(rows, start=1): - rec = dict(zip(headers, row)) - try: - total_cost += int(rec['shares']) * float(rec['price']) - except ValueError: - print(f"Row {i}: Couldn't convert: {row}") - - f.close() - return total_cost + portfolio = report.read_portfolio(path) + return sum(s['shares'] * s['price'] for s in portfolio) def main(path): total = portfolio_cost(path) From 1cb293f206a02eb248d85ec930b20fcf30018509 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 26 Jan 2024 21:22:51 -0700 Subject: [PATCH 36/62] Improve conversion with generator. --- Work/fileparse.py | 23 ++++++++++++----------- Work/report.py | 10 ++++------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index 4665ac56f..b9215e2f4 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -28,14 +28,15 @@ def parse_csv(filename, raise RuntimeError('select and types lists must have the same shape') # type conversion helper - def convert(row, i): + def convert(rows): assert types - try: - return [t(val) for t, val in zip(types, row)] - except ValueError as e: - if not silence_errors: - print(f"Row {i}: Couldn't convert", row) - print(f"Row {i}: Reason", e) + for i, row in enumerate(rows): + try: + yield [t(val) for t, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {i}: Couldn't convert", row) + print(f"Row {i}: Reason", e) records = [] with open(filename, 'rt') as f: @@ -43,17 +44,17 @@ def convert(row, i): if has_headers: headers = next(rows) + rows = (r for r in rows if r) # generate only non-empty rows + # filter columns if select: indices = [headers.index(c) for c in select] headers = select - rows = [[row[i] for i in indices] for row in rows] + rows = ([row[i] for i in indices] for row in rows) # convert to types if types: - rows = [convert(row, i) for i, row in enumerate(rows, start=1)] - - rows = (r for r in rows if r) # ignore empty rows + rows = convert(rows) # package to records if has_headers: diff --git a/Work/report.py b/Work/report.py index 7ab922794..fd61a14fb 100644 --- a/Work/report.py +++ b/Work/report.py @@ -3,8 +3,6 @@ # Exercise 2.4 import csv -import sys -from pprint import pprint import fileparse def read_portfolio(path): @@ -54,7 +52,7 @@ def print_report(path_portfolio, path_prices): print(f' Total gain: {cur_value-purchase_price:>10.2f}') if __name__ == "__main__": - _, *rest = sys.argv - path_portfolio = rest[0] if len(rest)>0 else 'Data/portfolio.csv' - path_prices = rest[1] if len(rest)>1 else 'Data/prices.csv' - print_report(path_portfolio, path_prices) + import sys + if len(sys.argv) != 3: + raise SystemExit(f'Usage: {sys.argv[0]} portfile pricefile') + print_report(sys.argv[1], sys.argv[2]) From 4a02b26e3ac46caa79f0fec4d5234dec0ebc48e6 Mon Sep 17 00:00:00 2001 From: bitmole Date: Sat, 27 Jan 2024 10:37:34 -0700 Subject: [PATCH 37/62] Run as scripts. --- Work/pcost.py | 16 +++++++++------- Work/pcost_tests.py | 3 --- Work/report.py | 10 +++++++--- 3 files changed, 16 insertions(+), 13 deletions(-) mode change 100644 => 100755 Work/pcost.py mode change 100644 => 100755 Work/report.py diff --git a/Work/pcost.py b/Work/pcost.py old mode 100644 new mode 100755 index a37d9a565..209bcf989 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + # pcost.py # # Exercise 1.27 @@ -10,12 +12,12 @@ def portfolio_cost(path): portfolio = report.read_portfolio(path) return sum(s['shares'] * s['price'] for s in portfolio) -def main(path): - total = portfolio_cost(path) - return f'Total cost {total:0.2f}' +def main(argv): + if len(argv) != 2: + raise SystemExit(f'Usage: {argv[0]} portfile') + total = portfolio_cost(argv[1]) + print(f'Total cost ${total}') if __name__ == "__main__": - _, *rest = sys.argv - path = rest[0] if rest else 'Data/portfolio.csv' - print(main(path)) - + import sys + main(sys.argv) diff --git a/Work/pcost_tests.py b/Work/pcost_tests.py index f21d89bec..1e4cd5475 100644 --- a/Work/pcost_tests.py +++ b/Work/pcost_tests.py @@ -3,9 +3,6 @@ class KnownOutput(unittest.TestCase): - def test_main_output(self): - self.assertEqual(pc.main('Data/portfolio.csv'), 'Total cost 44671.15') - def test_portfolio_cost(self): self.assertEqual(pc.portfolio_cost('./Data/portfolio.csv'), 44671.15) diff --git a/Work/report.py b/Work/report.py old mode 100644 new mode 100755 index fd61a14fb..89f583dbd --- a/Work/report.py +++ b/Work/report.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # report.py # # Exercise 2.4 @@ -51,8 +52,11 @@ def print_report(path_portfolio, path_prices): print(f' Current value: {cur_value:>10.2f}') print(f' Total gain: {cur_value-purchase_price:>10.2f}') +def main(argv): + if len(argv) != 3: + raise SystemExit(f'Usage: {argv[0]} portfile pricefile') + print_report(argv[1], argv[2]) + if __name__ == "__main__": import sys - if len(sys.argv) != 3: - raise SystemExit(f'Usage: {sys.argv[0]} portfile pricefile') - print_report(sys.argv[1], sys.argv[2]) + main(sys.argv) From b75394ce7f15e7670c1e33be6ca93f46f6136e0a Mon Sep 17 00:00:00 2001 From: bitmole Date: Sat, 27 Jan 2024 13:14:08 -0700 Subject: [PATCH 38/62] Make parser more flexible by accepting any file-like object. --- Work/fileparse.py | 46 +++++++++++++++++++++------------------------- Work/report.py | 7 +++++-- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index b9215e2f4..da7828e86 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -3,17 +3,17 @@ # Exercise 3.3 import csv -def parse_csv(filename, +def parse_csv(data, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): - """Parse a CSV file into a list of records + """Parse a CSV stream into a list of records structured as dicts. - :filename: path to file - :select: columns + :data: file-like object + :select: selected columns :types: conversion functions :has_headers :delimiter @@ -38,28 +38,24 @@ def convert(rows): print(f"Row {i}: Couldn't convert", row) print(f"Row {i}: Reason", e) - records = [] - with open(filename, 'rt') as f: - rows = csv.reader(f, delimiter=delimiter) - if has_headers: - headers = next(rows) + rows = csv.reader(data, delimiter=delimiter) + if has_headers: + headers = next(rows) - rows = (r for r in rows if r) # generate only non-empty rows + rows = (r for r in rows if r) # generate only non-empty rows - # filter columns - if select: - indices = [headers.index(c) for c in select] - headers = select - rows = ([row[i] for i in indices] for row in rows) + # filter columns + if select: + indices = [headers.index(c) for c in select] + headers = select + rows = ([row[i] for i in indices] for row in rows) - # convert to types - if types: - rows = convert(rows) + # convert to types + if types: + rows = convert(rows) - # package to records - if has_headers: - records = [dict(zip(headers, row)) for row in rows] - else: - records = [tuple(r) for r in rows] - - return records + # package to records + if has_headers: + return [dict(zip(headers, row)) for row in rows] + else: + return [tuple(r) for r in rows] diff --git a/Work/report.py b/Work/report.py index 89f583dbd..04ec2cd40 100755 --- a/Work/report.py +++ b/Work/report.py @@ -10,13 +10,16 @@ def read_portfolio(path): """ Parses data file into stock portfolio """ - return fileparse.parse_csv(path, types=[str, int, float]) + with open(path, 'rt') as file: + portfolio = fileparse.parse_csv(file, types=[str, int, float]) + return portfolio def read_prices(path): """ Parses price data file into stock price dictionary """ - prices = fileparse.parse_csv(path, types=[str, float], has_headers=False) + with open(path, 'rt') as file: + prices = fileparse.parse_csv(file, types=[str, float], has_headers=False) return {n:p for n, p in prices} def make_report(portfolio, prices): From 72bf3143e45a3ef687d7f6d73c7c3f349e45c5cb Mon Sep 17 00:00:00 2001 From: bitmole Date: Mon, 29 Jan 2024 16:32:34 -0700 Subject: [PATCH 39/62] Initial OOP. --- Work/pcost.py | 2 +- Work/report.py | 17 ++++++----------- Work/report_tests.py | 20 ++------------------ Work/stock.py | 11 +++++++++++ 4 files changed, 20 insertions(+), 30 deletions(-) create mode 100644 Work/stock.py diff --git a/Work/pcost.py b/Work/pcost.py index 209bcf989..c461ba0e7 100755 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -10,7 +10,7 @@ def portfolio_cost(path): portfolio = report.read_portfolio(path) - return sum(s['shares'] * s['price'] for s in portfolio) + return sum(s.cost() for s in portfolio) def main(argv): if len(argv) != 2: diff --git a/Work/report.py b/Work/report.py index 04ec2cd40..88571726b 100755 --- a/Work/report.py +++ b/Work/report.py @@ -5,6 +5,7 @@ import csv import fileparse +from stock import Stock def read_portfolio(path): """ @@ -12,7 +13,7 @@ def read_portfolio(path): """ with open(path, 'rt') as file: portfolio = fileparse.parse_csv(file, types=[str, int, float]) - return portfolio + return [Stock(d['name'], d['shares'], d['price']) for d in portfolio] def read_prices(path): """ @@ -23,21 +24,15 @@ def read_prices(path): return {n:p for n, p in prices} def make_report(portfolio, prices): - rows = [] - for s in portfolio: - name = s['name'] - nshares = s['shares'] - cur_price = prices[s['name']] - change = cur_price - s['price'] - rows.append((name, nshares, cur_price, change)) - return rows + return [(s.name, s.shares, prices[s.name], prices[s.name] - s.price) + for s in portfolio] def print_report(path_portfolio, path_prices): # collect report data portfolio = read_portfolio(path_portfolio) prices = read_prices(path_prices) - purchase_price = sum(s['shares']*s['price'] for s in portfolio) - cur_value = sum(s['shares']*prices[s['name']] for s in portfolio) + purchase_price = sum(s.cost() for s in portfolio) + cur_value = sum(s.shares * prices[s.name] for s in portfolio) # print headers headers = ('Name', 'Shares', 'Price', 'Change') diff --git a/Work/report_tests.py b/Work/report_tests.py index 5931a071f..70eb18795 100644 --- a/Work/report_tests.py +++ b/Work/report_tests.py @@ -4,26 +4,10 @@ class KnownOutput(unittest.TestCase): def test_read_portfolio(self): - expected = [ - {'name': 'AA', 'shares': 100, 'price': 32.2}, - {'name': 'IBM', 'shares': 50, 'price': 91.1}, - {'name': 'CAT', 'shares': 150, 'price': 83.44}, - {'name': 'MSFT', 'shares': 200, 'price': 51.23}, - {'name': 'GE', 'shares': 95, 'price': 40.37}, - {'name': 'MSFT', 'shares': 50, 'price': 65.1}, - {'name': 'IBM', 'shares': 100, 'price': 70.44} - ] - self.assertEqual(rep.read_portfolio('./Data/portfolio.csv'), expected) + self.assertEqual(len(rep.read_portfolio('./Data/portfolio.csv')), 7) def test_missing_fields(self): - expected = [ - {'name': 'AA', 'shares': 100, 'price': 32.2}, - {'name': 'IBM', 'shares': 50, 'price': 91.1}, - {'name': 'CAT', 'shares': 150, 'price': 83.44}, - {'name': 'GE', 'shares': 95, 'price': 40.37}, - {'name': 'MSFT', 'shares': 50, 'price': 65.1}, - ] - self.assertEqual(rep.read_portfolio('Data/missing.csv'), expected) + self.assertEqual(len(rep.read_portfolio('./Data/missing.csv')), 5) if __name__ == "__main__": unittest.main() diff --git a/Work/stock.py b/Work/stock.py new file mode 100644 index 000000000..a85bda2ec --- /dev/null +++ b/Work/stock.py @@ -0,0 +1,11 @@ +class Stock(object): + """Represents stock holding.""" + def __init__(self, name, shares, price): + self.name, self.shares, self.price = name, shares, price + + def cost(self): + return self.shares * self.price + + def sell(self, n): + assert n <= self.shares + self.shares -= n From 2549b6823f653f0de9618ca4177f777d9d75fdd2 Mon Sep 17 00:00:00 2001 From: bitmole Date: Tue, 30 Jan 2024 16:03:49 -0700 Subject: [PATCH 40/62] Inheritance - refactor printing to formatters. --- Work/report.py | 31 ++++++++++++------------------- Work/tableformat.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 Work/tableformat.py diff --git a/Work/report.py b/Work/report.py index 88571726b..d9ab7eb95 100755 --- a/Work/report.py +++ b/Work/report.py @@ -5,6 +5,7 @@ import csv import fileparse +import tableformat from stock import Stock def read_portfolio(path): @@ -27,33 +28,25 @@ def make_report(portfolio, prices): return [(s.name, s.shares, prices[s.name], prices[s.name] - s.price) for s in portfolio] -def print_report(path_portfolio, path_prices): +def print_report(report, formatter): + formatter.headings(['Name', 'Shares', 'Price', 'Change']) + for name, shares, price, change in report: + formatter.row((name, str(shares), f'{price:0.2f}', f'{change:0.2f}')) + +def portfolio_report(path_portfolio, path_prices): # collect report data portfolio = read_portfolio(path_portfolio) prices = read_prices(path_prices) - purchase_price = sum(s.cost() for s in portfolio) - cur_value = sum(s.shares * prices[s.name] for s in portfolio) - - # print headers - headers = ('Name', 'Shares', 'Price', 'Change') - print('%10s '*len(headers) % headers) - print(len(headers) * ((10*'-') + ' ')) - - # print rows - for s, n, p, d in make_report(portfolio, prices): - p = f'${p:0.2f}' # pre-format price - print(f'{s:>10s} {n:>10d} {p:>10s} {d:>10.2f}') + report = make_report(portfolio, prices) - # print summary - print('='*43) - print(f'Purchase price: {purchase_price:>10.2f}') - print(f' Current value: {cur_value:>10.2f}') - print(f' Total gain: {cur_value-purchase_price:>10.2f}') + # print it! + formatter = tableformat.TextTableFormatter() + print_report(report, formatter) def main(argv): if len(argv) != 3: raise SystemExit(f'Usage: {argv[0]} portfile pricefile') - print_report(argv[1], argv[2]) + portfolio_report(argv[1], argv[2]) if __name__ == "__main__": import sys diff --git a/Work/tableformat.py b/Work/tableformat.py new file mode 100644 index 000000000..9f1dd5910 --- /dev/null +++ b/Work/tableformat.py @@ -0,0 +1,29 @@ +# tableformat.py + +class TableFormatter: + """ + Formatter ABC + """ + def headings(self, headers): + """ + Emit the table headings. + """ + raise NotImplementedError() + + def row(self, data): + """ + Emit a single row of table data. + """ + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, data): + for d in data: + print(f'{d:>10s}', end=' ') + print() From 099353716b822a7fb581f0ca2f9c16092e58cd25 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 31 Jan 2024 09:24:20 -0700 Subject: [PATCH 41/62] HTML formatter --- .gitignore | 2 ++ Work/report.py | 2 +- Work/tableformat.py | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b6e47617d..7f6b81740 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,5 @@ dmypy.json # Pyre type checker .pyre/ + +.DS_Store diff --git a/Work/report.py b/Work/report.py index d9ab7eb95..a1bec65e6 100755 --- a/Work/report.py +++ b/Work/report.py @@ -40,7 +40,7 @@ def portfolio_report(path_portfolio, path_prices): report = make_report(portfolio, prices) # print it! - formatter = tableformat.TextTableFormatter() + formatter = tableformat.HTMLTableFormatter() print_report(report, formatter) def main(argv): diff --git a/Work/tableformat.py b/Work/tableformat.py index 9f1dd5910..a219c1ca5 100644 --- a/Work/tableformat.py +++ b/Work/tableformat.py @@ -27,3 +27,24 @@ def row(self, data): for d in data: print(f'{d:>10s}', end=' ') print() + +class CSVTableFormatter(TableFormatter): + def headings(self, headers): + print(','.join(headers)) + + def row(self, data): + print(','.join(data)) + +class HTMLTableFormatter(TableFormatter): + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, data): + print('', end='') + for d in data: + print(f'{d}', end='') + print('') + From 64d4255d9a55e12e940414d1c0cff2041ad81dd0 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 31 Jan 2024 09:41:42 -0700 Subject: [PATCH 42/62] Polymorphism. --- Work/report.py | 15 +++++++++------ Work/tableformat.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Work/report.py b/Work/report.py index a1bec65e6..fb0998bd8 100755 --- a/Work/report.py +++ b/Work/report.py @@ -31,22 +31,25 @@ def make_report(portfolio, prices): def print_report(report, formatter): formatter.headings(['Name', 'Shares', 'Price', 'Change']) for name, shares, price, change in report: - formatter.row((name, str(shares), f'{price:0.2f}', f'{change:0.2f}')) + formatter.row((name, str(shares), f'${price:0.2f}', f'{change:0.2f}')) -def portfolio_report(path_portfolio, path_prices): +def portfolio_report(path_portfolio, path_prices, fmt='txt'): # collect report data portfolio = read_portfolio(path_portfolio) prices = read_prices(path_prices) report = make_report(portfolio, prices) # print it! - formatter = tableformat.HTMLTableFormatter() + formatter = tableformat.create_formatter(fmt) print_report(report, formatter) def main(argv): - if len(argv) != 3: - raise SystemExit(f'Usage: {argv[0]} portfile pricefile') - portfolio_report(argv[1], argv[2]) + if len(argv) < 3: + raise SystemExit(f'Usage: {argv[0]} portfile pricefile [format(txt|csv|html)]') + elif len(argv) == 3: + portfolio_report(argv[1], argv[2]) + else: # optional format arg? + portfolio_report(argv[1], argv[2], fmt=argv[3]) if __name__ == "__main__": import sys diff --git a/Work/tableformat.py b/Work/tableformat.py index a219c1ca5..66406ebf6 100644 --- a/Work/tableformat.py +++ b/Work/tableformat.py @@ -1,5 +1,15 @@ # tableformat.py +def create_formatter(fmt): + if fmt=='txt': + return TextTableFormatter() + elif fmt=='csv': + return CSVTableFormatter() + elif fmt=='html': + return HTMLTableFormatter() + else: + raise RuntimeError(f'Unknown format {fmt}') + class TableFormatter: """ Formatter ABC From 8dbb20fef97f3724ab269a266a55b215325d010a Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 31 Jan 2024 10:54:58 -0700 Subject: [PATCH 43/62] Special methods: generalize printing with getattr. --- Work/stock.py | 9 +++++++++ Work/tableformat.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/Work/stock.py b/Work/stock.py index a85bda2ec..704f82382 100644 --- a/Work/stock.py +++ b/Work/stock.py @@ -3,6 +3,15 @@ class Stock(object): def __init__(self, name, shares, price): self.name, self.shares, self.price = name, shares, price + def __repr__(self): + return f"Stock('{self.name}', {self.shares}, {self.price})" + + def __str__(self): + pass + + def __unicode__(self): + pass + def cost(self): return self.shares * self.price diff --git a/Work/tableformat.py b/Work/tableformat.py index 66406ebf6..fab4ba640 100644 --- a/Work/tableformat.py +++ b/Work/tableformat.py @@ -9,6 +9,12 @@ def create_formatter(fmt): return HTMLTableFormatter() else: raise RuntimeError(f'Unknown format {fmt}') + +def print_table(portfolio, columns, formatter): + formatter.headings(columns) + for s in portfolio: + data = [str(getattr(s, a)) for a in columns] + formatter.row(data) class TableFormatter: """ From 691246fc7c3f2eb0a5413f3282fdc9885c50bd2f Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 1 Feb 2024 10:30:48 -0700 Subject: [PATCH 44/62] Improve formatter lookup; more uniform interface for Stock. --- Work/pcost.py | 2 +- Work/stock.py | 1 + Work/tableformat.py | 20 ++++++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Work/pcost.py b/Work/pcost.py index c461ba0e7..3ac8d4ef0 100755 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -10,7 +10,7 @@ def portfolio_cost(path): portfolio = report.read_portfolio(path) - return sum(s.cost() for s in portfolio) + return sum(s.cost for s in portfolio) def main(argv): if len(argv) != 2: diff --git a/Work/stock.py b/Work/stock.py index 704f82382..eb050cd4a 100644 --- a/Work/stock.py +++ b/Work/stock.py @@ -12,6 +12,7 @@ def __str__(self): def __unicode__(self): pass + @property def cost(self): return self.shares * self.price diff --git a/Work/tableformat.py b/Work/tableformat.py index fab4ba640..30e384b65 100644 --- a/Work/tableformat.py +++ b/Work/tableformat.py @@ -1,14 +1,16 @@ # tableformat.py def create_formatter(fmt): - if fmt=='txt': - return TextTableFormatter() - elif fmt=='csv': - return CSVTableFormatter() - elif fmt=='html': - return HTMLTableFormatter() - else: - raise RuntimeError(f'Unknown format {fmt}') + formatters = { + 'txt': TextTableFormatter, + 'csv': CSVTableFormatter, + 'html': HTMLTableFormatter, + } + + if fmt not in formatters.keys(): + raise FormatError(f'Unknown table format {fmt}') + + return formatters[fmt]() def print_table(portfolio, columns, formatter): formatter.headings(columns) @@ -64,3 +66,5 @@ def row(self, data): print(f'{d}', end='') print('') +class FormatError(Exception): + pass From 8801972685a3c0b6dfafb5817349a4d26a5f86bf Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 1 Feb 2024 10:46:23 -0700 Subject: [PATCH 45/62] Properties, setters, slots. --- Work/stock.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Work/stock.py b/Work/stock.py index eb050cd4a..84814ad44 100644 --- a/Work/stock.py +++ b/Work/stock.py @@ -1,5 +1,7 @@ class Stock(object): """Represents stock holding.""" + __slots__ = ('name', '_shares', 'price') + def __init__(self, name, shares, price): self.name, self.shares, self.price = name, shares, price @@ -16,6 +18,16 @@ def __unicode__(self): def cost(self): return self.shares * self.price + @property + def shares(self): + return self._shares + + @shares.setter + def shares(self, value): + if not isinstance(value, int): + raise TypeError('expected an integer') + self._shares = value + def sell(self, n): assert n <= self.shares self.shares -= n From edfe142e0fb91a21aa19d52fecf5e4ded7d1851e Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 2 Feb 2024 10:14:23 -0700 Subject: [PATCH 46/62] Portfolio container with iteration. --- Work/pcost.py | 2 +- Work/portfolio.py | 23 +++++++++++++++++++++++ Work/report.py | 6 ++++-- Work/report_tests.py | 7 +++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 Work/portfolio.py diff --git a/Work/pcost.py b/Work/pcost.py index 3ac8d4ef0..5da49af5a 100755 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -10,7 +10,7 @@ def portfolio_cost(path): portfolio = report.read_portfolio(path) - return sum(s.cost for s in portfolio) + return portfolio.total_cost def main(argv): if len(argv) != 2: diff --git a/Work/portfolio.py b/Work/portfolio.py new file mode 100644 index 000000000..8f824cf3a --- /dev/null +++ b/Work/portfolio.py @@ -0,0 +1,23 @@ +# portfolio.py + +class Portfolio: + + def __init__(self, holdings): + self._holdings = holdings + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return self._holdings.__len__() + + @property + def total_cost(self): + return sum(s.cost for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares diff --git a/Work/report.py b/Work/report.py index fb0998bd8..1addc6221 100755 --- a/Work/report.py +++ b/Work/report.py @@ -7,14 +7,16 @@ import fileparse import tableformat from stock import Stock +from portfolio import Portfolio def read_portfolio(path): """ Parses data file into stock portfolio """ with open(path, 'rt') as file: - portfolio = fileparse.parse_csv(file, types=[str, int, float]) - return [Stock(d['name'], d['shares'], d['price']) for d in portfolio] + dicts = fileparse.parse_csv(file, types=[str, int, float]) + portfolio = [Stock(d['name'], d['shares'], d['price']) for d in dicts] + return Portfolio(portfolio) def read_prices(path): """ diff --git a/Work/report_tests.py b/Work/report_tests.py index 70eb18795..9f96fcdcc 100644 --- a/Work/report_tests.py +++ b/Work/report_tests.py @@ -1,5 +1,6 @@ import unittest import report as rep +from portfolio import Portfolio class KnownOutput(unittest.TestCase): @@ -9,5 +10,11 @@ def test_read_portfolio(self): def test_missing_fields(self): self.assertEqual(len(rep.read_portfolio('./Data/missing.csv')), 5) +class PortfolioTests(unittest.TestCase): + + def test_total_cost(self): + port = Portfolio(rep.read_portfolio('Data/portfolio.csv')) + self.assertEqual(port.total_cost, 44671.15) + if __name__ == "__main__": unittest.main() From da8c97b8b52ed2e85396b24b6e7ff5b121bbdbfb Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 2 Feb 2024 10:26:16 -0700 Subject: [PATCH 47/62] Complete portfolio container. --- Work/portfolio.py | 8 +++++++- Work/stock.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Work/portfolio.py b/Work/portfolio.py index 8f824cf3a..7b3e9d9f1 100644 --- a/Work/portfolio.py +++ b/Work/portfolio.py @@ -9,7 +9,13 @@ def __iter__(self): return self._holdings.__iter__() def __len__(self): - return self._holdings.__len__() + 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): diff --git a/Work/stock.py b/Work/stock.py index 84814ad44..c91ad0464 100644 --- a/Work/stock.py +++ b/Work/stock.py @@ -9,10 +9,10 @@ def __repr__(self): return f"Stock('{self.name}', {self.shares}, {self.price})" def __str__(self): - pass + return repr(self) def __unicode__(self): - pass + return str(self) @property def cost(self): From 8a11c26db370468bf902327ec02f7224b6bf5c21 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 2 Feb 2024 17:13:52 -0700 Subject: [PATCH 48/62] Stock ticker with generators. --- .gitignore | 3 +++ Work/follow.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 Work/follow.py diff --git a/.gitignore b/.gitignore index 7f6b81740..badc4b460 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,7 @@ dmypy.json # Pyre type checker .pyre/ +# generated files +*stocklog.csv + .DS_Store diff --git a/Work/follow.py b/Work/follow.py new file mode 100644 index 000000000..aa85318eb --- /dev/null +++ b/Work/follow.py @@ -0,0 +1,34 @@ +# follow.py +import os, sys +import time + +def follow(logfile): + with open(logfile) as f: + f.seek(0, os.SEEK_END) # move file pointer 0 bytes from EOF + + while True: + line = f.readline() + if not line: + time.sleep(0.1) # sleep briefly & retry + continue + yield line + +def ticker(portfile, tickerfile): + import report + portfolio = report.read_portfolio(portfile) + for line in follow(tickerfile): + fields = line.split(',') + name = fields[0].strip('"') + price = float(fields[1]) + change = float(fields[4]) + if name in portfolio: + print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') + +def main(argv): + if len(argv) != 3: + raise SystemExit(f'usage: {argv[0]} portfile tickerfile') + + ticker(argv[1], argv[2]) + +if __name__ == "__main__": + main(sys.argv) From 763210ff81b39439f1267ee0de17468b42c34926 Mon Sep 17 00:00:00 2001 From: bitmole Date: Sun, 4 Feb 2024 16:01:27 -0700 Subject: [PATCH 49/62] Refactor type conversion processor. --- Work/follow.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Work/follow.py b/Work/follow.py index aa85318eb..91326af09 100644 --- a/Work/follow.py +++ b/Work/follow.py @@ -13,14 +13,23 @@ def follow(logfile): continue yield line -def ticker(portfile, tickerfile): - import report - portfolio = report.read_portfolio(portfile) - for line in follow(tickerfile): +def filematch(lines, substr): + for line in lines: + if substr in line: + yield line + +def convert(lines): + for line in lines: fields = line.split(',') name = fields[0].strip('"') price = float(fields[1]) change = float(fields[4]) + yield (name, price, change) + +def ticker(portfile, tickerfile): + import report + portfolio = report.read_portfolio(portfile) + for name, price, change in convert(follow(tickerfile)): if name in portfolio: print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') From 88ab8ccd7f9f0b32d90950ebef2a6dd878249a7d Mon Sep 17 00:00:00 2001 From: bitmole Date: Sun, 4 Feb 2024 17:00:59 -0700 Subject: [PATCH 50/62] Real-time ticker pipeline! --- Work/ticker.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 Work/ticker.py diff --git a/Work/ticker.py b/Work/ticker.py new file mode 100644 index 000000000..ca7131fee --- /dev/null +++ b/Work/ticker.py @@ -0,0 +1,54 @@ +# ticker.py + +from follow import follow +import csv + +def filter_symbols(rows, portfolio): + for r in rows: + if r['name'] in portfolio: + yield r + +def convert_types(rows, types): + for r in rows: + yield [func(val) for func, val in zip(types, r)] + +def make_dicts(rows, headers): + for r in rows: + yield dict(zip(headers, r)) + +def select_columns(rows, indices): + for r in rows: + yield [r[i] for i in indices] + +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='txt'): + import report, tableformat + portfolio = report.read_portfolio(portfile) + rows = parse_stock_data(follow(logfile)) + rows = filter_symbols(rows, portfolio) + + # print it! + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name', 'Price', 'Change']) + for r in rows: + formatter.row((r['name'], str(r['price']), str(r['change']))) + + +def main(args): + if len(args) < 3: + raise SystemExit('usage: ticker portfile logfile [fmt(txt|csv|html)]') + + if len(args) == 3: + ticker(args[1], args[2]) + else: + ticker(args[1], args[2], fmt=args[3]) + +if __name__ == "__main__": + import sys + main(sys.argv) From 78e8ef0b59c293fb2c013ae64b879b1bde265346 Mon Sep 17 00:00:00 2001 From: bitmole Date: Sun, 4 Feb 2024 20:32:28 -0700 Subject: [PATCH 51/62] Generator expressions. --- Work/ticker.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/Work/ticker.py b/Work/ticker.py index ca7131fee..c68fb93b5 100644 --- a/Work/ticker.py +++ b/Work/ticker.py @@ -3,35 +3,30 @@ from follow import follow import csv -def filter_symbols(rows, portfolio): - for r in rows: - if r['name'] in portfolio: - yield r - def convert_types(rows, types): for r in rows: yield [func(val) for func, val in zip(types, r)] -def make_dicts(rows, headers): - for r in rows: - yield dict(zip(headers, r)) - def select_columns(rows, indices): for r in rows: yield [r[i] for i in indices] -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']) +def stock_pipeline(stream, portfolio): + rows = csv.reader(stream) + # select columns + rows = ([r[i] for i in [0, 1, 4]] for r in rows) + # convert types + rows = ([func(val) for func, val in zip((str,float,float), r)] for r in rows) + # transform to dicts + rows = (dict(zip(('name', 'price', 'change'), r)) for r in rows) + # filter portfolio stocks + rows = (r for r in rows if r['name'] in portfolio) return rows def ticker(portfile, logfile, fmt='txt'): import report, tableformat - portfolio = report.read_portfolio(portfile) - rows = parse_stock_data(follow(logfile)) - rows = filter_symbols(rows, portfolio) + + rows = stock_pipeline(follow(logfile), report.read_portfolio(portfile)) # print it! formatter = tableformat.create_formatter(fmt) From 722fa56fc52eeda0caecae603bb45f7d5b0d8efb Mon Sep 17 00:00:00 2001 From: bitmole Date: Sun, 4 Feb 2024 20:34:00 -0700 Subject: [PATCH 52/62] Remove unused funcs. --- Work/ticker.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Work/ticker.py b/Work/ticker.py index c68fb93b5..5ca7d9018 100644 --- a/Work/ticker.py +++ b/Work/ticker.py @@ -3,14 +3,6 @@ from follow import follow import csv -def convert_types(rows, types): - for r in rows: - yield [func(val) for func, val in zip(types, r)] - -def select_columns(rows, indices): - for r in rows: - yield [r[i] for i in indices] - def stock_pipeline(stream, portfolio): rows = csv.reader(stream) # select columns From af09552b162f4052c213f8542e8da8b63ab0a9b1 Mon Sep 17 00:00:00 2001 From: bitmole Date: Mon, 5 Feb 2024 16:01:52 -0700 Subject: [PATCH 53/62] Refactor formatting. --- Work/tableformat.py | 9 ++++++--- Work/ticker.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Work/tableformat.py b/Work/tableformat.py index 30e384b65..b420f20db 100644 --- a/Work/tableformat.py +++ b/Work/tableformat.py @@ -17,6 +17,9 @@ def print_table(portfolio, columns, formatter): for s in portfolio: data = [str(getattr(s, a)) for a in columns] formatter.row(data) + +def stringify(row): + return (str(val) for val in row) class TableFormatter: """ @@ -42,7 +45,7 @@ def headings(self, headers): print(('-'*10 + ' ')*len(headers)) def row(self, data): - for d in data: + for d in stringify(data): print(f'{d:>10s}', end=' ') print() @@ -51,7 +54,7 @@ def headings(self, headers): print(','.join(headers)) def row(self, data): - print(','.join(data)) + print(','.join(stringify(data))) class HTMLTableFormatter(TableFormatter): def headings(self, headers): @@ -62,7 +65,7 @@ def headings(self, headers): def row(self, data): print('', end='') - for d in data: + for d in stringify(data): print(f'{d}', end='') print('') diff --git a/Work/ticker.py b/Work/ticker.py index 5ca7d9018..7a56cf803 100644 --- a/Work/ticker.py +++ b/Work/ticker.py @@ -24,7 +24,7 @@ def ticker(portfile, logfile, fmt='txt'): formatter = tableformat.create_formatter(fmt) formatter.headings(['Name', 'Price', 'Change']) for r in rows: - formatter.row((r['name'], str(r['price']), str(r['change']))) + formatter.row((r['name'], f"${r['price']}", r['change'])) def main(args): From c505c5034e4ebf7f1dfe7a7e4f1200fffc241f6e Mon Sep 17 00:00:00 2001 From: bitmole Date: Tue, 6 Feb 2024 11:44:18 -0700 Subject: [PATCH 54/62] args, kwargs --- Work/report.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Work/report.py b/Work/report.py index 1addc6221..aef64e12f 100755 --- a/Work/report.py +++ b/Work/report.py @@ -9,13 +9,16 @@ from stock import Stock from portfolio import Portfolio -def read_portfolio(path): +def read_portfolio(path, **opts): """ - Parses data file into stock portfolio + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. """ with open(path, 'rt') as file: - dicts = fileparse.parse_csv(file, types=[str, int, float]) - portfolio = [Stock(d['name'], d['shares'], d['price']) for d in dicts] + dicts = fileparse.parse_csv(file, + types=[str, int, float], + **opts) + portfolio = [Stock(**d) for d in dicts] return Portfolio(portfolio) def read_prices(path): @@ -33,7 +36,7 @@ def make_report(portfolio, prices): def print_report(report, formatter): formatter.headings(['Name', 'Shares', 'Price', 'Change']) for name, shares, price, change in report: - formatter.row((name, str(shares), f'${price:0.2f}', f'{change:0.2f}')) + formatter.row((name, shares, f'${price:0.2f}', f'{change:0.2f}')) def portfolio_report(path_portfolio, path_prices, fmt='txt'): # collect report data From 74b051f489e9e954ee2ee16553fcca28115ca01d Mon Sep 17 00:00:00 2001 From: bitmole Date: Tue, 6 Feb 2024 16:22:49 -0700 Subject: [PATCH 55/62] Closures, typed properties --- Work/stock.py | 22 ++++++++++------------ Work/typedproperty.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 Work/typedproperty.py diff --git a/Work/stock.py b/Work/stock.py index c91ad0464..cb94ad314 100644 --- a/Work/stock.py +++ b/Work/stock.py @@ -1,9 +1,17 @@ +from typedproperty import String, Integer, Float + class Stock(object): """Represents stock holding.""" - __slots__ = ('name', '_shares', 'price') + __slots__ = ('_name', '_shares', '_price') + name = String('name') + shares = Integer('shares') + price = Float('price') + def __init__(self, name, shares, price): - self.name, self.shares, self.price = name, shares, price + self.name = name + self.shares = shares + self.price = price def __repr__(self): return f"Stock('{self.name}', {self.shares}, {self.price})" @@ -18,16 +26,6 @@ def __unicode__(self): def cost(self): return self.shares * self.price - @property - def shares(self): - return self._shares - - @shares.setter - def shares(self, value): - if not isinstance(value, int): - raise TypeError('expected an integer') - self._shares = value - def sell(self, n): assert n <= self.shares self.shares -= n diff --git a/Work/typedproperty.py b/Work/typedproperty.py new file mode 100644 index 000000000..6a4bdffe0 --- /dev/null +++ b/Work/typedproperty.py @@ -0,0 +1,19 @@ +# 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) From 77e8c9c86b278d4473665165b316a87f3a674aa9 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 7 Feb 2024 09:56:20 -0700 Subject: [PATCH 56/62] Refactor portfolio instantiation. --- Work/portfolio.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Work/portfolio.py b/Work/portfolio.py index 7b3e9d9f1..bd29dbe75 100644 --- a/Work/portfolio.py +++ b/Work/portfolio.py @@ -1,9 +1,28 @@ # portfolio.py -class Portfolio: +import fileparse +import stock - def __init__(self, holdings): - self._holdings = holdings +class Portfolio: + def __init__(self): + self._holdings = [] + + def append(self, holding): + if not isinstance(holding, stock.Stock): + raise TypeError('Expected a stock instance') + self._holdings.append(holding) + + @classmethod + def from_csv(cls, lines, **opts): + self = cls() + portfolio_dicts = fileparse.parse_csv(lines, + select=['name', 'shares', 'price'], + types=[str, int, float], + **opts) + for d in portfolio_dicts: + self.append(stock.Stock(**d)) + + return self def __iter__(self): return self._holdings.__iter__() From b31cbe781bd2b4dcc826dd2946e4c1b903f99e6b Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 7 Feb 2024 10:02:35 -0700 Subject: [PATCH 57/62] Use refactored portfolio instantiation. --- Work/report.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Work/report.py b/Work/report.py index aef64e12f..177180807 100755 --- a/Work/report.py +++ b/Work/report.py @@ -15,11 +15,8 @@ def read_portfolio(path, **opts): name, shares, and price. """ with open(path, 'rt') as file: - dicts = fileparse.parse_csv(file, - types=[str, int, float], - **opts) - portfolio = [Stock(**d) for d in dicts] - return Portfolio(portfolio) + portfolio = Portfolio.from_csv(file) + return portfolio def read_prices(path): """ From 0b7eac23d0d4a588ed3d69d408a1535d912e6c94 Mon Sep 17 00:00:00 2001 From: bitmole Date: Wed, 7 Feb 2024 13:09:08 -0700 Subject: [PATCH 58/62] Stock tests. --- Work/stock.py | 1 + Work/test_stock.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 Work/test_stock.py diff --git a/Work/stock.py b/Work/stock.py index cb94ad314..bc269c4bf 100644 --- a/Work/stock.py +++ b/Work/stock.py @@ -29,3 +29,4 @@ def cost(self): def sell(self, n): assert n <= self.shares self.shares -= n + return self diff --git a/Work/test_stock.py b/Work/test_stock.py new file mode 100644 index 000000000..93fb54b7f --- /dev/null +++ b/Work/test_stock.py @@ -0,0 +1,39 @@ +# test_stock.py + +import unittest +import stock + +class TestStock(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + 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) + self.assertEqual(90, s.sell(10).shares) + + def test_sell_too_much(self): + s = stock.Stock('GOOG', 100, 490.1) + with self.assertRaises(AssertionError): + s.sell(200) + + def test_bad_shares(self): + s = stock.Stock('GOOG', 100, 490.1) + with self.assertRaises(TypeError): + s.shares = '200' + +if __name__ == "__main__": + unittest.main() From 8ca39abb0793d9ff9821f14cb63d54ccde854c33 Mon Sep 17 00:00:00 2001 From: bitmole Date: Thu, 8 Feb 2024 10:33:38 -0700 Subject: [PATCH 59/62] Logging --- Work/fileparse.py | 8 +++++--- Work/report.py | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Work/fileparse.py b/Work/fileparse.py index da7828e86..eae23874c 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -2,6 +2,8 @@ # # Exercise 3.3 import csv +import logging +log = logging.getLogger(__name__) def parse_csv(data, select=None, @@ -30,13 +32,13 @@ def parse_csv(data, # type conversion helper def convert(rows): assert types - for i, row in enumerate(rows): + for i, row in enumerate(rows, 1): try: yield [t(val) for t, val in zip(types, row)] except ValueError as e: if not silence_errors: - print(f"Row {i}: Couldn't convert", row) - print(f"Row {i}: Reason", e) + log.warning("Row %d: Couldn't convert %s", i, row) + log.debug("Row %d: Reason %s", i, e) rows = csv.reader(data, delimiter=delimiter) if has_headers: diff --git a/Work/report.py b/Work/report.py index 177180807..2fea4caec 100755 --- a/Work/report.py +++ b/Work/report.py @@ -8,6 +8,12 @@ import tableformat from stock import Stock from portfolio import Portfolio +import logging +logging.basicConfig( + # filename = 'app.log', + # filemode = 'w', + level = logging.WARNING, + ) def read_portfolio(path, **opts): """ From 1ec83242d927d6096375defab8d28e92e405e36b Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 9 Feb 2024 10:32:27 -0700 Subject: [PATCH 60/62] Packages --- Work/{ => porty}/fileparse.py | 0 Work/{ => porty}/follow.py | 0 Work/{ => porty}/pcost.py | 2 +- Work/{ => porty}/pcost_tests.py | 0 Work/{ => porty}/portfolio.py | 4 ++-- Work/{ => porty}/report.py | 8 ++++---- Work/{ => porty}/report_tests.py | 0 Work/{ => porty}/stock.py | 2 +- Work/{ => porty}/tableformat.py | 0 Work/{ => porty}/test_stock.py | 0 Work/{ => porty}/ticker.py | 2 +- Work/{ => porty}/typedproperty.py | 0 12 files changed, 9 insertions(+), 9 deletions(-) rename Work/{ => porty}/fileparse.py (100%) rename Work/{ => porty}/follow.py (100%) rename Work/{ => porty}/pcost.py (95%) rename Work/{ => porty}/pcost_tests.py (100%) rename Work/{ => porty}/portfolio.py (96%) rename Work/{ => porty}/report.py (94%) rename Work/{ => porty}/report_tests.py (100%) rename Work/{ => porty}/stock.py (93%) rename Work/{ => porty}/tableformat.py (100%) rename Work/{ => porty}/test_stock.py (100%) rename Work/{ => porty}/ticker.py (97%) rename Work/{ => porty}/typedproperty.py (100%) diff --git a/Work/fileparse.py b/Work/porty/fileparse.py similarity index 100% rename from Work/fileparse.py rename to Work/porty/fileparse.py diff --git a/Work/follow.py b/Work/porty/follow.py similarity index 100% rename from Work/follow.py rename to Work/porty/follow.py diff --git a/Work/pcost.py b/Work/porty/pcost.py similarity index 95% rename from Work/pcost.py rename to Work/porty/pcost.py index 5da49af5a..cbe2d0600 100755 --- a/Work/pcost.py +++ b/Work/porty/pcost.py @@ -6,7 +6,7 @@ import csv import sys -import report +from . import report def portfolio_cost(path): portfolio = report.read_portfolio(path) diff --git a/Work/pcost_tests.py b/Work/porty/pcost_tests.py similarity index 100% rename from Work/pcost_tests.py rename to Work/porty/pcost_tests.py diff --git a/Work/portfolio.py b/Work/porty/portfolio.py similarity index 96% rename from Work/portfolio.py rename to Work/porty/portfolio.py index bd29dbe75..73c08a4f4 100644 --- a/Work/portfolio.py +++ b/Work/porty/portfolio.py @@ -1,7 +1,7 @@ # portfolio.py -import fileparse -import stock +from . import fileparse +from . import stock class Portfolio: def __init__(self): diff --git a/Work/report.py b/Work/porty/report.py similarity index 94% rename from Work/report.py rename to Work/porty/report.py index 2fea4caec..4bc91b8fe 100755 --- a/Work/report.py +++ b/Work/porty/report.py @@ -4,10 +4,10 @@ # Exercise 2.4 import csv -import fileparse -import tableformat -from stock import Stock -from portfolio import Portfolio +from . import fileparse +from . import tableformat +from .stock import Stock +from .portfolio import Portfolio import logging logging.basicConfig( # filename = 'app.log', diff --git a/Work/report_tests.py b/Work/porty/report_tests.py similarity index 100% rename from Work/report_tests.py rename to Work/porty/report_tests.py diff --git a/Work/stock.py b/Work/porty/stock.py similarity index 93% rename from Work/stock.py rename to Work/porty/stock.py index bc269c4bf..a9cb08441 100644 --- a/Work/stock.py +++ b/Work/porty/stock.py @@ -1,4 +1,4 @@ -from typedproperty import String, Integer, Float +from .typedproperty import String, Integer, Float class Stock(object): """Represents stock holding.""" diff --git a/Work/tableformat.py b/Work/porty/tableformat.py similarity index 100% rename from Work/tableformat.py rename to Work/porty/tableformat.py diff --git a/Work/test_stock.py b/Work/porty/test_stock.py similarity index 100% rename from Work/test_stock.py rename to Work/porty/test_stock.py diff --git a/Work/ticker.py b/Work/porty/ticker.py similarity index 97% rename from Work/ticker.py rename to Work/porty/ticker.py index 7a56cf803..0110a4d81 100644 --- a/Work/ticker.py +++ b/Work/porty/ticker.py @@ -1,6 +1,6 @@ # ticker.py -from follow import follow +from .follow import follow import csv def stock_pipeline(stream, portfolio): diff --git a/Work/typedproperty.py b/Work/porty/typedproperty.py similarity index 100% rename from Work/typedproperty.py rename to Work/porty/typedproperty.py From 1ffd6cc5346c4159860c1a995252375f6d18acae Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 9 Feb 2024 10:48:52 -0700 Subject: [PATCH 61/62] App packaging, outside scripts. --- Work/porty-app/portfolio.csv | 8 ++++++ Work/{ => porty-app}/porty/fileparse.py | 0 Work/{ => porty-app}/porty/follow.py | 0 Work/{ => porty-app}/porty/pcost.py | 0 Work/{ => porty-app}/porty/pcost_tests.py | 0 Work/{ => porty-app}/porty/portfolio.py | 0 Work/{ => porty-app}/porty/report.py | 0 Work/{ => porty-app}/porty/report_tests.py | 0 Work/{ => porty-app}/porty/stock.py | 0 Work/{ => porty-app}/porty/tableformat.py | 0 Work/{ => porty-app}/porty/test_stock.py | 0 Work/{ => porty-app}/porty/ticker.py | 0 Work/{ => porty-app}/porty/typedproperty.py | 0 Work/porty-app/prices.csv | 31 +++++++++++++++++++++ Work/porty-app/print-report.py | 3 ++ 15 files changed, 42 insertions(+) create mode 100644 Work/porty-app/portfolio.csv rename Work/{ => porty-app}/porty/fileparse.py (100%) rename Work/{ => porty-app}/porty/follow.py (100%) rename Work/{ => porty-app}/porty/pcost.py (100%) rename Work/{ => porty-app}/porty/pcost_tests.py (100%) rename Work/{ => porty-app}/porty/portfolio.py (100%) rename Work/{ => porty-app}/porty/report.py (100%) rename Work/{ => porty-app}/porty/report_tests.py (100%) rename Work/{ => porty-app}/porty/stock.py (100%) rename Work/{ => porty-app}/porty/tableformat.py (100%) rename Work/{ => porty-app}/porty/test_stock.py (100%) rename Work/{ => porty-app}/porty/ticker.py (100%) rename Work/{ => porty-app}/porty/typedproperty.py (100%) create mode 100644 Work/porty-app/prices.csv create mode 100644 Work/porty-app/print-report.py diff --git a/Work/porty-app/portfolio.csv b/Work/porty-app/portfolio.csv new file mode 100644 index 000000000..6c16f65b5 --- /dev/null +++ b/Work/porty-app/portfolio.csv @@ -0,0 +1,8 @@ +name,shares,price +"AA",100,32.20 +"IBM",50,91.10 +"CAT",150,83.44 +"MSFT",200,51.23 +"GE",95,40.37 +"MSFT",50,65.10 +"IBM",100,70.44 diff --git a/Work/porty/fileparse.py b/Work/porty-app/porty/fileparse.py similarity index 100% rename from Work/porty/fileparse.py rename to Work/porty-app/porty/fileparse.py diff --git a/Work/porty/follow.py b/Work/porty-app/porty/follow.py similarity index 100% rename from Work/porty/follow.py rename to Work/porty-app/porty/follow.py diff --git a/Work/porty/pcost.py b/Work/porty-app/porty/pcost.py similarity index 100% rename from Work/porty/pcost.py rename to Work/porty-app/porty/pcost.py diff --git a/Work/porty/pcost_tests.py b/Work/porty-app/porty/pcost_tests.py similarity index 100% rename from Work/porty/pcost_tests.py rename to Work/porty-app/porty/pcost_tests.py diff --git a/Work/porty/portfolio.py b/Work/porty-app/porty/portfolio.py similarity index 100% rename from Work/porty/portfolio.py rename to Work/porty-app/porty/portfolio.py diff --git a/Work/porty/report.py b/Work/porty-app/porty/report.py similarity index 100% rename from Work/porty/report.py rename to Work/porty-app/porty/report.py diff --git a/Work/porty/report_tests.py b/Work/porty-app/porty/report_tests.py similarity index 100% rename from Work/porty/report_tests.py rename to Work/porty-app/porty/report_tests.py diff --git a/Work/porty/stock.py b/Work/porty-app/porty/stock.py similarity index 100% rename from Work/porty/stock.py rename to Work/porty-app/porty/stock.py diff --git a/Work/porty/tableformat.py b/Work/porty-app/porty/tableformat.py similarity index 100% rename from Work/porty/tableformat.py rename to Work/porty-app/porty/tableformat.py diff --git a/Work/porty/test_stock.py b/Work/porty-app/porty/test_stock.py similarity index 100% rename from Work/porty/test_stock.py rename to Work/porty-app/porty/test_stock.py diff --git a/Work/porty/ticker.py b/Work/porty-app/porty/ticker.py similarity index 100% rename from Work/porty/ticker.py rename to Work/porty-app/porty/ticker.py diff --git a/Work/porty/typedproperty.py b/Work/porty-app/porty/typedproperty.py similarity index 100% rename from Work/porty/typedproperty.py rename to Work/porty-app/porty/typedproperty.py diff --git a/Work/porty-app/prices.csv b/Work/porty-app/prices.csv new file mode 100644 index 000000000..6bbcb2099 --- /dev/null +++ b/Work/porty-app/prices.csv @@ -0,0 +1,31 @@ +"AA",9.22 +"AXP",24.85 +"BA",44.85 +"BAC",11.27 +"C",3.72 +"CAT",35.46 +"CVX",66.67 +"DD",28.47 +"DIS",24.22 +"GE",13.48 +"GM",0.75 +"HD",23.16 +"HPQ",34.35 +"IBM",106.28 +"INTC",15.72 +"JNJ",55.16 +"JPM",36.90 +"KFT",26.11 +"KO",49.16 +"MCD",58.99 +"MMM",57.10 +"MRK",27.58 +"MSFT",20.89 +"PFE",15.19 +"PG",51.94 +"T",24.79 +"UTX",52.61 +"VZ",29.26 +"WMT",49.74 +"XOM",69.35 + diff --git a/Work/porty-app/print-report.py b/Work/porty-app/print-report.py new file mode 100644 index 000000000..89781bc2d --- /dev/null +++ b/Work/porty-app/print-report.py @@ -0,0 +1,3 @@ +import sys +from porty.report import main +main(sys.argv) From 9a8cedc600be40c5087a66917a5055ba8fa87c41 Mon Sep 17 00:00:00 2001 From: bitmole Date: Fri, 9 Feb 2024 10:51:43 -0700 Subject: [PATCH 62/62] Rename test files. --- Work/porty-app/porty/{pcost_tests.py => test_pcost.py} | 0 Work/porty-app/porty/{report_tests.py => test_report.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Work/porty-app/porty/{pcost_tests.py => test_pcost.py} (100%) rename Work/porty-app/porty/{report_tests.py => test_report.py} (100%) diff --git a/Work/porty-app/porty/pcost_tests.py b/Work/porty-app/porty/test_pcost.py similarity index 100% rename from Work/porty-app/porty/pcost_tests.py rename to Work/porty-app/porty/test_pcost.py diff --git a/Work/porty-app/porty/report_tests.py b/Work/porty-app/porty/test_report.py similarity index 100% rename from Work/porty-app/porty/report_tests.py rename to Work/porty-app/porty/test_report.py