From 5b6f15db179d993bd0e8a79872422f9efb0100e5 Mon Sep 17 00:00:00 2001 From: David Beazley Date: Wed, 27 May 2020 17:03:35 -0500 Subject: [PATCH] Added solution code --- Notes/04_Classes_objects/01_Class.md | 168 ++++---- Notes/04_Classes_objects/02_Inheritance.md | 405 +++++++++++------- .../04_Classes_objects/03_Special_methods.md | 106 ++--- Notes/05_Object_model/01_Dicts_revisited.md | 85 ++-- .../02_Classes_encapsulation.md | 40 +- .../06_Generators/02_Customizing_iteration.md | 2 +- .../02_Anonymous_function.md | 2 +- Solutions/1_10/mortgage.py | 28 ++ Solutions/1_27/pcost.py | 13 + Solutions/1_32/pcost.py | 31 ++ Solutions/1_5/bounce.py | 8 + Solutions/2_11/report.py | 66 +++ Solutions/2_16/pcost.py | 32 ++ Solutions/2_16/report.py | 67 +++ Solutions/2_7/report.py | 55 +++ Solutions/3_10/fileparse.py | 48 +++ Solutions/3_14/fileparse.py | 48 +++ Solutions/3_14/pcost.py | 19 + Solutions/3_14/report.py | 56 +++ Solutions/3_16/fileparse.py | 48 +++ Solutions/3_16/pcost.py | 20 + Solutions/3_16/report.py | 62 +++ Solutions/3_18/fileparse.py | 47 ++ Solutions/3_18/pcost.py | 20 + Solutions/3_18/report.py | 64 +++ Solutions/3_2/report.py | 78 ++++ Solutions/3_7/fileparse.py | 39 ++ Solutions/4_10/fileparse.py | 47 ++ Solutions/4_10/pcost.py | 20 + Solutions/4_10/report.py | 71 +++ Solutions/4_10/stock.py | 26 ++ Solutions/4_10/tableformat.py | 81 ++++ Solutions/4_4/fileparse.py | 47 ++ Solutions/4_4/pcost.py | 20 + Solutions/4_4/report.py | 70 +++ Solutions/4_4/stock.py | 23 + Solutions/5_8/fileparse.py | 47 ++ Solutions/5_8/pcost.py | 20 + Solutions/5_8/report.py | 71 +++ Solutions/5_8/stock.py | 37 ++ Solutions/5_8/tableformat.py | 81 ++++ Solutions/6_12/fileparse.py | 47 ++ Solutions/6_12/follow.py | 30 ++ Solutions/6_12/pcost.py | 20 + Solutions/6_12/portfolio.py | 31 ++ Solutions/6_12/report.py | 72 ++++ Solutions/6_12/stock.py | 37 ++ Solutions/6_12/tableformat.py | 81 ++++ Solutions/6_12/ticker.py | 50 +++ Solutions/6_15/fileparse.py | 47 ++ Solutions/6_15/follow.py | 17 + Solutions/6_15/pcost.py | 20 + Solutions/6_15/portfolio.py | 31 ++ Solutions/6_15/report.py | 72 ++++ Solutions/6_15/stock.py | 37 ++ Solutions/6_15/tableformat.py | 81 ++++ Solutions/6_15/ticker.py | 44 ++ Solutions/6_3/fileparse.py | 47 ++ Solutions/6_3/pcost.py | 20 + Solutions/6_3/portfolio.py | 31 ++ Solutions/6_3/report.py | 72 ++++ Solutions/6_3/stock.py | 37 ++ Solutions/6_3/tableformat.py | 81 ++++ Solutions/6_7/fileparse.py | 47 ++ Solutions/6_7/follow.py | 30 ++ Solutions/6_7/pcost.py | 20 + Solutions/6_7/portfolio.py | 31 ++ Solutions/6_7/report.py | 72 ++++ Solutions/6_7/stock.py | 37 ++ Solutions/6_7/tableformat.py | 81 ++++ Solutions/7_10/timethis.py | 21 + Solutions/7_12/fileparse.py | 47 ++ Solutions/7_12/follow.py | 17 + Solutions/7_12/pcost.py | 20 + Solutions/7_12/portfolio.py | 51 +++ Solutions/7_12/report.py | 67 +++ Solutions/7_12/stock.py | 32 ++ Solutions/7_12/tableformat.py | 81 ++++ Solutions/7_12/ticker.py | 43 ++ Solutions/7_12/timethis.py | 21 + Solutions/7_12/typedproperty.py | 34 ++ Solutions/7_4/fileparse.py | 47 ++ Solutions/7_4/follow.py | 17 + Solutions/7_4/pcost.py | 20 + Solutions/7_4/portfolio.py | 31 ++ Solutions/7_4/report.py | 73 ++++ Solutions/7_4/stock.py | 37 ++ Solutions/7_4/tableformat.py | 81 ++++ Solutions/7_4/ticker.py | 44 ++ Solutions/7_9/fileparse.py | 47 ++ Solutions/7_9/follow.py | 17 + Solutions/7_9/pcost.py | 20 + Solutions/7_9/portfolio.py | 31 ++ Solutions/7_9/report.py | 73 ++++ Solutions/7_9/stock.py | 32 ++ Solutions/7_9/tableformat.py | 81 ++++ Solutions/7_9/ticker.py | 44 ++ Solutions/7_9/typedproperty.py | 34 ++ Solutions/8_1/fileparse.py | 47 ++ Solutions/8_1/follow.py | 17 + Solutions/8_1/pcost.py | 20 + Solutions/8_1/portfolio.py | 51 +++ Solutions/8_1/report.py | 67 +++ Solutions/8_1/stock.py | 32 ++ Solutions/8_1/tableformat.py | 81 ++++ Solutions/8_1/test_stock.py | 28 ++ Solutions/8_1/ticker.py | 43 ++ Solutions/8_1/timethis.py | 21 + Solutions/8_1/typedproperty.py | 34 ++ Solutions/8_2/fileparse.py | 49 +++ Solutions/8_2/follow.py | 17 + Solutions/8_2/pcost.py | 20 + Solutions/8_2/portfolio.py | 51 +++ Solutions/8_2/report.py | 67 +++ Solutions/8_2/stock.py | 32 ++ Solutions/8_2/tableformat.py | 81 ++++ Solutions/8_2/test_stock.py | 28 ++ Solutions/8_2/ticker.py | 43 ++ Solutions/8_2/timethis.py | 21 + Solutions/8_2/typedproperty.py | 34 ++ Solutions/9_3/porty-app/README.txt | 17 + Solutions/9_3/porty-app/portfolio.csv | 8 + Solutions/9_3/porty-app/porty/__init__.py | 0 Solutions/9_3/porty-app/porty/fileparse.py | 47 ++ Solutions/9_3/porty-app/porty/follow.py | 17 + Solutions/9_3/porty-app/porty/pcost.py | 20 + Solutions/9_3/porty-app/porty/portfolio.py | 51 +++ Solutions/9_3/porty-app/porty/report.py | 67 +++ Solutions/9_3/porty-app/porty/stock.py | 33 ++ Solutions/9_3/porty-app/porty/tableformat.py | 81 ++++ Solutions/9_3/porty-app/porty/test_stock.py | 28 ++ Solutions/9_3/porty-app/porty/ticker.py | 43 ++ .../9_3/porty-app/porty/typedproperty.py | 34 ++ Solutions/9_3/porty-app/prices.csv | 31 ++ Solutions/9_3/porty-app/print-report.py | 6 + Solutions/README.md | 7 + 136 files changed, 5828 insertions(+), 350 deletions(-) create mode 100644 Solutions/1_10/mortgage.py create mode 100644 Solutions/1_27/pcost.py create mode 100644 Solutions/1_32/pcost.py create mode 100644 Solutions/1_5/bounce.py create mode 100644 Solutions/2_11/report.py create mode 100644 Solutions/2_16/pcost.py create mode 100644 Solutions/2_16/report.py create mode 100644 Solutions/2_7/report.py create mode 100644 Solutions/3_10/fileparse.py create mode 100644 Solutions/3_14/fileparse.py create mode 100644 Solutions/3_14/pcost.py create mode 100644 Solutions/3_14/report.py create mode 100644 Solutions/3_16/fileparse.py create mode 100644 Solutions/3_16/pcost.py create mode 100644 Solutions/3_16/report.py create mode 100644 Solutions/3_18/fileparse.py create mode 100644 Solutions/3_18/pcost.py create mode 100644 Solutions/3_18/report.py create mode 100644 Solutions/3_2/report.py create mode 100644 Solutions/3_7/fileparse.py create mode 100644 Solutions/4_10/fileparse.py create mode 100644 Solutions/4_10/pcost.py create mode 100644 Solutions/4_10/report.py create mode 100644 Solutions/4_10/stock.py create mode 100644 Solutions/4_10/tableformat.py create mode 100644 Solutions/4_4/fileparse.py create mode 100644 Solutions/4_4/pcost.py create mode 100644 Solutions/4_4/report.py create mode 100644 Solutions/4_4/stock.py create mode 100644 Solutions/5_8/fileparse.py create mode 100644 Solutions/5_8/pcost.py create mode 100644 Solutions/5_8/report.py create mode 100644 Solutions/5_8/stock.py create mode 100644 Solutions/5_8/tableformat.py create mode 100644 Solutions/6_12/fileparse.py create mode 100644 Solutions/6_12/follow.py create mode 100644 Solutions/6_12/pcost.py create mode 100644 Solutions/6_12/portfolio.py create mode 100644 Solutions/6_12/report.py create mode 100644 Solutions/6_12/stock.py create mode 100644 Solutions/6_12/tableformat.py create mode 100644 Solutions/6_12/ticker.py create mode 100644 Solutions/6_15/fileparse.py create mode 100644 Solutions/6_15/follow.py create mode 100644 Solutions/6_15/pcost.py create mode 100644 Solutions/6_15/portfolio.py create mode 100644 Solutions/6_15/report.py create mode 100644 Solutions/6_15/stock.py create mode 100644 Solutions/6_15/tableformat.py create mode 100644 Solutions/6_15/ticker.py create mode 100644 Solutions/6_3/fileparse.py create mode 100644 Solutions/6_3/pcost.py create mode 100644 Solutions/6_3/portfolio.py create mode 100644 Solutions/6_3/report.py create mode 100644 Solutions/6_3/stock.py create mode 100644 Solutions/6_3/tableformat.py create mode 100644 Solutions/6_7/fileparse.py create mode 100644 Solutions/6_7/follow.py create mode 100644 Solutions/6_7/pcost.py create mode 100644 Solutions/6_7/portfolio.py create mode 100644 Solutions/6_7/report.py create mode 100644 Solutions/6_7/stock.py create mode 100644 Solutions/6_7/tableformat.py create mode 100644 Solutions/7_10/timethis.py create mode 100644 Solutions/7_12/fileparse.py create mode 100644 Solutions/7_12/follow.py create mode 100644 Solutions/7_12/pcost.py create mode 100644 Solutions/7_12/portfolio.py create mode 100644 Solutions/7_12/report.py create mode 100644 Solutions/7_12/stock.py create mode 100644 Solutions/7_12/tableformat.py create mode 100644 Solutions/7_12/ticker.py create mode 100644 Solutions/7_12/timethis.py create mode 100644 Solutions/7_12/typedproperty.py create mode 100644 Solutions/7_4/fileparse.py create mode 100644 Solutions/7_4/follow.py create mode 100644 Solutions/7_4/pcost.py create mode 100644 Solutions/7_4/portfolio.py create mode 100644 Solutions/7_4/report.py create mode 100644 Solutions/7_4/stock.py create mode 100644 Solutions/7_4/tableformat.py create mode 100644 Solutions/7_4/ticker.py create mode 100644 Solutions/7_9/fileparse.py create mode 100644 Solutions/7_9/follow.py create mode 100644 Solutions/7_9/pcost.py create mode 100644 Solutions/7_9/portfolio.py create mode 100644 Solutions/7_9/report.py create mode 100644 Solutions/7_9/stock.py create mode 100644 Solutions/7_9/tableformat.py create mode 100644 Solutions/7_9/ticker.py create mode 100644 Solutions/7_9/typedproperty.py create mode 100644 Solutions/8_1/fileparse.py create mode 100644 Solutions/8_1/follow.py create mode 100644 Solutions/8_1/pcost.py create mode 100644 Solutions/8_1/portfolio.py create mode 100644 Solutions/8_1/report.py create mode 100644 Solutions/8_1/stock.py create mode 100644 Solutions/8_1/tableformat.py create mode 100644 Solutions/8_1/test_stock.py create mode 100644 Solutions/8_1/ticker.py create mode 100644 Solutions/8_1/timethis.py create mode 100644 Solutions/8_1/typedproperty.py create mode 100644 Solutions/8_2/fileparse.py create mode 100644 Solutions/8_2/follow.py create mode 100644 Solutions/8_2/pcost.py create mode 100644 Solutions/8_2/portfolio.py create mode 100644 Solutions/8_2/report.py create mode 100644 Solutions/8_2/stock.py create mode 100644 Solutions/8_2/tableformat.py create mode 100644 Solutions/8_2/test_stock.py create mode 100644 Solutions/8_2/ticker.py create mode 100644 Solutions/8_2/timethis.py create mode 100644 Solutions/8_2/typedproperty.py create mode 100644 Solutions/9_3/porty-app/README.txt create mode 100755 Solutions/9_3/porty-app/portfolio.csv create mode 100644 Solutions/9_3/porty-app/porty/__init__.py create mode 100644 Solutions/9_3/porty-app/porty/fileparse.py create mode 100644 Solutions/9_3/porty-app/porty/follow.py create mode 100644 Solutions/9_3/porty-app/porty/pcost.py create mode 100644 Solutions/9_3/porty-app/porty/portfolio.py create mode 100644 Solutions/9_3/porty-app/porty/report.py create mode 100644 Solutions/9_3/porty-app/porty/stock.py create mode 100644 Solutions/9_3/porty-app/porty/tableformat.py create mode 100644 Solutions/9_3/porty-app/porty/test_stock.py create mode 100644 Solutions/9_3/porty-app/porty/ticker.py create mode 100644 Solutions/9_3/porty-app/porty/typedproperty.py create mode 100644 Solutions/9_3/porty-app/prices.csv create mode 100644 Solutions/9_3/porty-app/print-report.py create mode 100644 Solutions/README.md diff --git a/Notes/04_Classes_objects/01_Class.md b/Notes/04_Classes_objects/01_Class.md index e8b4a23..7523c58 100644 --- a/Notes/04_Classes_objects/01_Class.md +++ b/Notes/04_Classes_objects/01_Class.md @@ -136,10 +136,15 @@ If you want to operate on an instance, you always have to refer too it explicitl ## Exercises +Note: For this exercise you want to have fully working code from earlier +exercises. If things are broken look at the solution code for Exercise 3.18. +You can find this code in the `Solutions/3_18` directory. + ### Exercise 4.1: Objects as Data Structures -In section 2 and 3, we worked with data represented as tuples and dictionaries. -For example, a holding of stock could be represented as a tuple like this: +In section 2 and 3, we worked with data represented as tuples and +dictionaries. For example, a holding of stock could be represented as +a tuple like this: ```python s = ('GOOG',100,490.10) @@ -154,116 +159,71 @@ s = { 'name' : 'GOOG', } ``` -You can even write functions for manipulating such data. For example: +You can even write functions for manipulating such data. For example: ```python def cost(s): return s['shares'] * s['price'] ``` -However, as your program gets large, you might want to create a better sense of organization. -Thus, another approach for representing data would be to define a class. - -Create a file called `stock.py` and define a class `Stock` that represents a single holding of stock. -Have the instances of `Stock` have `name`, `shares`, and `price` attributes. +However, as your program gets large, you might want to create a better +sense of organization. Thus, another approach for representing data +would be to define a class. Create a file called `stock.py` and +define a class `Stock` that represents a single holding of stock. +Have the instances of `Stock` have `name`, `shares`, and `price` +attributes. For example: ```python >>> import stock ->>> s = stock.Stock('GOOG',100,490.10) ->>> s.name +>>> a = stock.Stock('GOOG',100,490.10) +>>> a.name 'GOOG' ->>> s.shares +>>> a.shares 100 ->>> s.price +>>> a.price 490.1 >>> ``` -Create a few more `Stock` objects and manipulate them. For example: +Create a few more `Stock` objects and manipulate them. For example: ```python ->>> a = stock.Stock('AAPL',50,122.34) ->>> b = stock.Stock('IBM',75,91.75) ->>> a.shares * a.price -6117.0 +>>> b = stock.Stock('AAPL', 50, 122.34) +>>> c = stock.Stock('IBM', 75, 91.75) >>> b.shares * b.price +6117.0 +>>> c.shares * c.price 6881.25 ->>> stocks = [a,b,s] +>>> stocks = [a, b, c] >>> stocks [, , ] ->>> for t in stocks: - print(f'{t.name:>10s} {t.shares:>10d} {t.price:>10.2f}') +>>> for s in stocks: + print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}') ... look at the output ... >>> ``` -One thing to emphasize here is that the class `Stock` acts like a factory for creating instances of objects. -Basically, you just call it as a function and it creates a new object for you. +One thing to emphasize here is that the class `Stock` acts like a +factory for creating instances of objects. Basically, you call +it as a function and it creates a new object for you. Also, it needs +to be emphasized that each object is distinct---they each have their +own data that is separate from other objects that have been created. -Also, it needs to be emphasized that each object is distinct---they -each have their own data that is separate from other objects that have -been created. An object defined by a class is somewhat similar to a -dictionary, just with somewhat different syntax. -For example, instead of writing `s['name']` or `s['price']`, you now -write `s.name` and `s.price`. +An object defined by a class is somewhat similar to a dictionary--just +with somewhat different syntax. For example, instead of writing +`s['name']` or `s['price']`, you now write `s.name` and `s.price`. -### Exercise 4.2: Reading Data into a List of Objects - -In your `stock.py` program, write a function -`read_portfolio(filename)` that reads portfolio data from a file into -a list of `Stock` objects. This function is going to mimic the -behavior of earlier code you have written. Here’s how your function -will behave: - -```python ->>> import stock ->>> portfolio = stock.read_portfolio('Data/portfolio.csv') ->>> portfolio -[, , , - , , , - ] ->>> -``` - -It is important to emphasize that `read_portfolio()` is a top-level function, not a method of the `Stock` class. -This function is merely creating a list of `Stock` objects; it’s not an operation on an individual `Stock` instance. - -Try performing some calculations with the above data. First, try printing a formatted table: - -```python ->>> for s in portfolio: - print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}') - -... look at the output ... ->>> -``` - -Try a list comprehension: - -```python ->>> more100 = [s for s in portfolio if s.shares > 100] ->>> for s in more100: - print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}') - -... look at the output ... ->>> -``` - -Again, notice the similarity between `Stock` objects and dictionaries. They’re basically the same idea, but the syntax for accessing values differs. - -### Exercise 4.3: Adding some Methods +### Exercise 4.2: Adding some Methods With classes, you can attach functions to your objects. These are -known as methods and are functions that operate on the data stored -inside an object. - -Add a `cost()` and `sell()` method to your `Stock` object. They should -work like this: +known as methods and are functions that operate on the data +stored inside an object. Add a `cost()` and `sell()` method to your +`Stock` object. They should work like this: ```python >>> import stock ->>> s = stock.Stock('GOOG',100,490.10) +>>> s = stock.Stock('GOOG', 100, 490.10) >>> s.cost() 49010.0 >>> s.shares @@ -276,4 +236,54 @@ work like this: >>> ``` +### Exercise 4.3: Creating a list of instances + +Try these steps to make a list of Stock instances and compute the total +cost: + +```python +>>> import fileparse +>>> with open('Data/portfolio.csv') as lines: +... portdicts = fileparse.parse_csv(lines, select=['name','shares','price'], types=[str,int,float]) +... +>>> portfolio = [ stock.Stock(d['name'], d['shares'], d['price']) for d in portdicts] +>>> portfolio +[, , , + , , , + ] +>>> sum([s.cost() for s in portfolio]) +44671.15 +>>> +---- + +### Exercise 4.4: Using your class + +Modify the `read_portfolio()` function in the `report.py` program so that it +reads a portfolio into a list of `Stock` instances. Once you have done that, +fix all of the code in `report.py` and `pcost.py` so that it works with +`Stock` instances instead of dictionaries. + +Hint: You should not have to make major changes to the code. You will mainly +be changing dictionary access such as `s['shares']` into `s.shares`. + +You should be able to run your functions the same as before: + +```python +>>> import pcost +>>> pcost.portfolio_cost('Data/portfolio.csv') +44671.15 +>>> import report +>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') + Name Shares Price Change +---------- ---------- ---------- ---------- + AA 100 9.22 -22.98 + IBM 50 106.28 15.18 + CAT 150 35.46 -47.98 + MSFT 200 20.89 -30.34 + GE 95 13.48 -26.89 + MSFT 50 20.89 -44.21 + IBM 100 106.28 35.84 +>>> +``` + [Contents](../Contents) \| [Previous (3.6 Design discussion)](../03_Program_organization/06_Design_discussion) \| [Next (4.2 Inheritance)](02_Inheritance) diff --git a/Notes/04_Classes_objects/02_Inheritance.md b/Notes/04_Classes_objects/02_Inheritance.md index 59fae09..7b9b5b6 100644 --- a/Notes/04_Classes_objects/02_Inheritance.md +++ b/Notes/04_Classes_objects/02_Inheritance.md @@ -220,63 +220,53 @@ We're not going to explore multiple inheritance further in this course. ## Exercises -### Exercise 4.4: Print Portfolio - -A major use of inheritance is in writing code that’s meant to be extended or customized in various ways—especially in libraries or frameworks. -To illustrate, start by adding the following function to your `stock.py` program: +A major use of inheritance is in writing code that's meant to be +extended or customized in various ways--especially in libraries or +frameworks. To illustrate, consider the `print_report()` function +in your `report.py` program. It should look something like this: ```python -# stock.py -... -def print_portfolio(portfolio): +def print_report(reportdata): ''' - Make a nicely formatted table showing portfolio contents. + Print a nicely formated table from a list of (name, shares, price, change) tuples. ''' - headers = ('Name','Shares','Price') - for h in headers: - print(f'{h:>10s}',end=' ') - print() + headers = ('Name','Shares','Price','Change') + print('%10s %10s %10s %10s' % headers) print(('-'*10 + ' ')*len(headers)) - for s in portfolio: - print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}') + for row in reportdata: + print('%10s %10d %10.2f %10.2f' % row) ``` -Add a little testing section to the bottom of your `stock.py` file that runs the above function: +When you run your report program, you should be getting output like this: -```python -if __name__ == '__main__': - portfolio = read_portfolio('Data/portfolio.csv') - print_portfolio(portfolio) ``` - -When you run your `stock.py`, you should get this output: - -```bash - 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 +>>> import report +>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') + Name Shares Price Change +---------- ---------- ---------- ---------- + AA 100 9.22 -22.98 + IBM 50 106.28 15.18 + CAT 150 35.46 -47.98 + MSFT 200 20.89 -30.34 + GE 95 13.48 -26.89 + MSFT 50 20.89 -44.21 + IBM 100 106.28 35.84 ``` ### Exercise 4.5: An Extensibility Problem -Suppose that you wanted to modify the `print_portfolio()` function to +Suppose that you wanted to modify the `print_report()` function to support a variety of different output formats such as plain-text, HTML, CSV, or XML. To do this, you could try to write one gigantic function that did everything. However, doing so would likely lead to -an unmaintainable mess. Instead, this is a perfect opportunity to use +an unmaintainable mess. Instead, this is a perfect opportunity to use inheritance instead. -To start, focus on the steps that are involved in a creating a -table. At the top of the table is a set of table headers. After that, -rows of table data appear. Let’s take those steps and and put them into their own class. - -Create a file called `tableformat.py` and define the following class: +To start, focus on the steps that are involved in a creating a table. +At the top of the table is a set of table headers. After that, rows +of table data appear. Let's take those steps and and put them into +their own class. Create a file called `tableformat.py` and define the +following class: ```python # tableformat.py @@ -286,53 +276,77 @@ class TableFormatter(object): ''' Emit the table headings. ''' - raise NotImplementedError() + raise NotImplementedError() def row(self, rowdata): ''' Emit a single row of table data. ''' - raise NotImplementedError() + raise NotImplementedError() ``` -This class does nothing, but it serves as a kind of design specification for additional classes that will be defined shortly. +This class does nothing, but it serves as a kind of design specification for +additional classes that will be defined shortly. -Modify the `print_portfolio()` function so that it accepts a `TableFormatter` object as input and invokes methods on it to produce the output. -For example, like this: +Modify the `print_report()` function so that it accepts a `TableFormatter` object +as input and invokes methods on it to produce the output. For example, like this: ```python -# stock.py +# report.py ... -def print_portfolio(portfolio, formatter): + +def print_report(reportdata, formatter): ''' - Make a nicely formatted table showing portfolio contents. + Print a nicely formated table from a list of (name, shares, price, change) tuples. ''' - formatter.headings(['Name', 'Shares', 'Price']) - for s in portfolio: - # Form a row of output data (as strings) - rowdata = [s.name, str(s.shares), f'{s.price:0.2f}' ] + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] formatter.row(rowdata) ``` -Finally, try your new class by modifying the main program like this: +Since you added an argument to print_report(), you're going to need to modify the +`portfolio_report()` function as well. Change it so that it creates a `TableFormatter` +like this: ```python -# stock.py +# report.py + +import tableformat + ... -if __name__ == '__main__': - from tableformat import TableFormatter - portfolio = read_portfolio('Data/portfolio.csv') - formatter = TableFormatter() - print_portfolio(portfolio, formatter) +def portfolio_report(portfoliofile, pricefile): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.TableFormatter() + print_report(report, formatter) ``` -When you run this new code, your program will immediately crash with a `NotImplementedError` exception. -That’s not too exciting, but continue to the next part. +Run this new code: + +```python +>>> ================================ RESTART ================================ +>>> import report +>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') +... crashes ... +``` + +It should immediately crash with a `NotImplementedError` exception. That's not +too exciting, but continue to the next part. ### Exercise 4.6: Using Inheritance to Produce Different Output -The `TableFormatter` class you defined in part (a) is meant to be extended via inheritance. -In fact, that’s the whole idea. To illustrate, define a class `TextTableFormatter` like this: +The `TableFormatter` class you defined in part (a) is meant to be extended via inheritance. +In fact, that's the whole idea. To illustrate, define a class `TextTableFormatter` like this: ```python # tableformat.py @@ -353,33 +367,47 @@ class TextTableFormatter(TableFormatter): print() ``` -Modify your main program in `stock.py` like this and try it: +Modify the `portfolio_report()` function like this and try it: ```python -# stock.py +# report.py ... -if __name__ == '__main__': - from tableformat import TextTableFormatter - portfolio = read_portfolio('Data/portfolio.csv') - formatter = TextTableFormatter() - print_portfolio(portfolio, formatter) +def portfolio_report(portfoliofile, pricefile): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.TextTableFormatter() + print_report(report, formatter) ``` This should produce the same output as before: -```bash - 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 +```python +>>> ================================ RESTART ================================ +>>> import report +>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') + Name Shares Price Change +---------- ---------- ---------- ---------- + AA 100 9.22 -22.98 + IBM 50 106.28 15.18 + CAT 150 35.46 -47.98 + MSFT 200 20.89 -30.34 + GE 95 13.48 -26.89 + MSFT 50 20.89 -44.21 + IBM 100 106.28 35.84 +>>> ``` -However, let’s change the output to something else. Define a new class `CSVTableFormatter` that produces output in CSV format: +However, let's change the output to something else. Define a new +class `CSVTableFormatter` that produces output in CSV format: ```python # tableformat.py @@ -398,105 +426,170 @@ class CSVTableFormatter(TableFormatter): Modify your main program as follows: ```python -# stock.py -... -if __name__ == '__main__': - from tableformat import CSVTableFormatter - portfolio = read_portfolio('Data/portfolio.csv') - formatter = CSVTableFormatter() - print_portfolio(portfolio, formatter) +def portfolio_report(portfoliofile, pricefile): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.CSVTableFormatter() + print_report(report, formatter) ``` You should now see CSV output like this: -```csv -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 +```python +>>> ================================ RESTART ================================ +>>> import report +>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') +Name,Shares,Price,Change +AA,100,9.22,-22.98 +IBM,50,106.28,15.18 +CAT,150,35.46,-47.98 +MSFT,200,20.89,-30.34 +GE,95,13.48,-26.89 +MSFT,50,20.89,-44.21 +IBM,100,106.28,35.84 ``` -Using a similar idea, define a class `HTMLTableFormatter` that produces a table with the following output: +Using a similar idea, define a class `HTMLTableFormatter` +that produces a table with the following output: -```html - Name Shares Price - AA 100 32.20 - IBM 50 91.10 +``` +NameSharesPriceChange +AA1009.22-22.98 +IBM50106.2815.18 +CAT15035.46-47.98 +MSFT20020.89-30.34 +GE9513.48-26.89 +MSFT5020.89-44.21 +IBM100106.2835.84 ``` -Test your code by modifying the main program to create a `HTMLTableFormatter` object instead of a `CSVTableFormatter` object. +Test your code by modifying the main program to create a +`HTMLTableFormatter` object instead of a +`CSVTableFormatter` object. ### Exercise 4.7: Polymorphism in Action -A major feature of object-oriented programming is that you can plug an -object into a program and it will work without having to change any of -the existing code. For example, if you wrote a program that expected -to use a `TableFormatter` object, it would work no matter what kind of -`TableFormatter` you actually gave it. +A major feature of object-oriented programming is that you can +plug an object into a program and it will work without having to +change any of the existing code. For example, if you wrote a program +that expected to use a `TableFormatter` object, it would work no +matter what kind of `TableFormatter` you actually gave it. This +behavior is sometimes referred to as "polymorphism." -This behavior is sometimes referred to as *polymorphism*. - -One potential problem is making it easier for the user to pick the formatter that they want. -This can sometimes be fixed by defining a helper function. - -In the `tableformat.py` file, add a function `create_formatter(name)` -that allows a user to create a formatter given an output name such as -`'txt'`, `'csv'`, or `'html'`. - -For example: +One potential problem is figuring out how to allow a user to pick +out the formatter that they want. Direct use of the class names +such as `TextTableFormatter` is often annoying. Thus, you +might consider some simplified approach. Perhaps you embed an `if-`statement +into the code like this: ```python -# stock.py -... -if __name__ == '__main__': - from tableformat import create_formatter - portfolio = read_portfolio('Data/portfolio.csv') - formatter = create_formatter('csv') - print_portfolio(portfolio, formatter) +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + if fmt == 'txt': + formatter = tableformat.TextTableFormatter() + elif fmt == 'csv': + formatter = tableformat.CSVTableFormatter() + elif fmt == 'html': + formatter = tableformat.HTMLTableFormatter() + else: + raise RuntimeError(f'Unknown format {fmt}') + print_report(report, formatter) ``` -When you run this program, you’ll see output such as this: +In this code, the user specifies a simplified name such as `'txt'` or +`'csv'` to pick a format. However, is putting a big `if`-statement in +the `portfolio_report()` function like that the best idea? It might +be better to move that code to a general purpose function somewhere +else. -```csv -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 -``` - -Try changing the format to `'txt'` and `'html'` just to make sure your -code is working correctly. If the user provides a bad output format -to the `create_formatter()` function, have it raise a `RuntimeError` -exception. For example: +In the `tableformat.py` file, add a +function `create_formatter(name)` that allows a user to create a +formatter given an output name such as `'txt'`, `'csv'`, or `'html'`. +Modify `portfolio_report()` so that it looks like this: ```python ->>> from tableformat import create_formatter ->>> formatter = create_formatter('xls') -Traceback (most recent call last): - File "", line 1, in - File "tableformat.py", line 68, in create_formatter - raise RuntimeError('Unknown table format %s' % name) -RuntimeError: Unknown table format xls +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) +``` + +Try calling the function with different formats to make sure it's working. + +### Exercise 4.8: Putting it all together + +Modify the `report.py` program so that the `portfolio_report()` function takes +an optional argument specifying the output format. For example: + +```python +>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv', 'txt') + Name Shares Price Change +---------- ---------- ---------- ---------- + AA 100 9.22 -22.98 + IBM 50 106.28 15.18 + CAT 150 35.46 -47.98 + MSFT 200 20.89 -30.34 + GE 95 13.48 -26.89 + MSFT 50 20.89 -44.21 + IBM 100 106.28 35.84 >>> ``` -Writing extensible code is one of the most common uses of inheritance in libraries and frameworks. -For example, a framework might instruct you to define your own object that inherits from a provided base class. -You’re then told to fill in various methods that implement various bits of functionality. -That said, designing object oriented programs can be extremely -difficult. For more information, you should probably look for books on -the topic of design patterns. +Modify the main program so that a format can be given on the command line: -That said, understanding what happened in this exercise will take you -pretty far in terms of using most library modules and knowing -what inheritance is good for (extensibility). +```bash +bash $ python3 report.py Data/portfolio.csv Data/prices.csv csv +Name,Shares,Price,Change +AA,100,9.22,-22.98 +IBM,50,106.28,15.18 +CAT,150,35.46,-47.98 +MSFT,200,20.89,-30.34 +GE,95,13.48,-26.89 +MSFT,50,20.89,-44.21 +IBM,100,106.28,35.84 +bash $ +``` + +### Discussion + +Writing extensible code is one of the most common uses of inheritance +in libraries and frameworks. For example, a framework might instruct +you to define your own object that inherits from a provided base +class. You're then told to fill in various methods that implement +various bits of functionality. + +That said, designing object oriented programs can be extremely difficult. +For more information, you should probably look for books on the topic of +design patterns (although understanding what happened in this exercise +will take you pretty far in terms of using most library modules). [Contents](../Contents) \| [Previous (4.1 Classes)](01_Class) \| [Next (4.3 Special methods)](03_Special_methods) diff --git a/Notes/04_Classes_objects/03_Special_methods.md b/Notes/04_Classes_objects/03_Special_methods.md index 9b98626..2f24bbb 100644 --- a/Notes/04_Classes_objects/03_Special_methods.md +++ b/Notes/04_Classes_objects/03_Special_methods.md @@ -202,34 +202,38 @@ x = getattr(obj, 'x', None) ## Exercises -### Exercise 4.8: Better output for printing objects +### Exercise 4.9: Better output for printing objects All Python objects have two string representations. The first -representation is created by string conversion via `str()` (which is -called by `print`). The string representation is usually a nicely -formatted version of the object meant for humans. The second -representation is a code representation of the object created by -`repr()` (or by viewing a value in the interactive shell). The code -representation typically shows you the code that you have to type to -get the object. +representation is created by string conversion via `str()` +(which is called by `print`). The string representation is +usually a nicely formatted version of the object meant for humans. +The second representation is a code representation of the object +created by `repr()` (or by viewing a value in the +interactive shell). The code representation typically shows you the +code that you have to type to get the object. -The two representations of an object are often different. For example, you can see the difference by trying the following: +The two representations of an object are often different. For example, +you can see the difference by trying the following: ```python ->>> s = 'Hello\nWorld' ->>> print(str(s)) # Notice nice output (no quotes) -Hello -World ->>> print(repr(s)) # Notice the added quotes and escape codes -'Hello\nWorld' ->>> print(f'{s!r}') # Alternate way to get repr() string -'Hello\nWorld' +>>> from datetime import date +>>> d = date(2017, 4, 9) +>>> print(d) # Nice output +2017-04-09 +>>> print(repr(d)) # Representation output +datetime.date(2017, 4, 9) +>>> print(f'{d!r}') # Alternate way to get repr() string +datetime.date(2017, 4, 9) >>> ``` -Both kinds of string conversions can be redefined in a class if it defines the `__str__()` and `__repr__()` methods. +Both kinds of string conversions can be redefined in a class if it +defines the `__str__()` and `__repr__()` methods. -Modify the `Stock` object that you defined in Exercise 4.1 so that the `__repr__()` method produces more useful output. +Modify the `Stock` object that you defined in `stock.py` +so that the `__repr__()` method produces more useful output. For +example: ```python >>> goog = Stock('GOOG', 100, 490.1) @@ -238,22 +242,21 @@ Stock('GOOG', 100, 490.1) >>> ``` -See what happens when you read a portfolio of stocks and view the resulting list after you have made these changes. +See what happens when you read a portfolio of stocks and view the +resulting list after you have made these changes. For example: -```python ->>> import stock ->>> portfolio = stock.read_portfolio('Data/portfolio.csv') +``` +>>> import report +>>> portfolio = report.read_portfolio('Data/portfolio.csv') >>> portfolio ... see what the output is ... >>> ``` -### Exercise 4.9: An example of using `getattr()` +### Exercise 4.10: An example of using getattr() -In Exercise 4.2 you worked with a function `print_portfolio()` that made a table for a stock portfolio. -That function was hard-coded to only work with stock data—-how limiting! You can do so much more if you use functions such as `getattr()`. - -To begin, try this little example: +`getattr()` is an alternative mechanism for reading attributes. It can be used to +write extremely flexible code. To begin, try this example: ```python >>> import stock @@ -267,19 +270,19 @@ shares = 100 >>> ``` -Carefully observe that the output data is determined entirely by the attribute names listed in the `columns` variable. +Carefully observe that the output data is determined entirely by the attribute +names listed in the `columns` variable. -In the file `tableformat.py`, take this idea and expand it into a -generalized function `print_table()` that prints a table showing -user-specified attributes of a list of arbitrary objects. - -As with the earlier `print_portfolio()` function, `print_table()` -should also accept a `TableFormatter` instance to control the output -format. Here’s how it should work: +In the file `tableformat.py`, take this idea and expand it into a generalized +function `print_table()` that prints a table showing +user-specified attributes of a list of arbitrary objects. As with the +earlier `print_report()` function, `print_table()` should also accept +a `TableFormatter` instance to control the output format. Here's how +it should work: ```python ->>> import stock ->>> portfolio = stock.read_portfolio('Data/portfolio.csv') +>>> import report +>>> portfolio = report.read_portfolio('Data/portfolio.csv') >>> from tableformat import create_formatter, print_table >>> formatter = create_formatter('txt') >>> print_table(portfolio, ['name','shares'], formatter) @@ -295,7 +298,7 @@ format. Here’s how it should work: >>> print_table(portfolio, ['name','shares','price'], formatter) name shares price ----------- ---------- ---------- +---------- ---------- ---------- AA 100 32.2 IBM 50 91.1 CAT 150 83.44 @@ -303,30 +306,7 @@ format. Here’s how it should work: GE 95 40.37 MSFT 50 65.1 IBM 100 70.44 ->>> -``` - -### Exercise 4.10: Exercise Bonus: Column Formatting - -Modify the `print_table()` function in part (B) so that it also -accepts a list of format specifiers for formatting the contents of -each column. - -```python ->>> print_table(portfolio, - ['name','shares','price'], - ['s','d','0.2f'], - formatter) - 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 ->>> +>>> ``` [Contents](../Contents) \| [Previous (4.2 Inheritance)](02_Inheritance) \| [Next (4.4 Exceptions)](04_Defining_exceptions) diff --git a/Notes/05_Object_model/01_Dicts_revisited.md b/Notes/05_Object_model/01_Dicts_revisited.md index 157edf1..3823cee 100644 --- a/Notes/05_Object_model/01_Dicts_revisited.md +++ b/Notes/05_Object_model/01_Dicts_revisited.md @@ -363,16 +363,22 @@ Frameworks / libraries sometimes use it for advanced features involving composit ## Exercises In Section 4, you defined a class `Stock` that represented a holding of stock. -In this exercise, we will use that class. +In this exercise, we will use that class. Restart the interpreter and make a +few instances: + +```python +>>> ================================ RESTART ================================ +>>> from stock import Stock +>>> goog = Stock('GOOG',100,490.10) +>>> ibm = Stock('IBM',50, 91.23) +>>> +``` ### Exercise 5.1: Representation of Instances At the interactive shell, inspect the underlying dictionaries of the two instances you created: ```python ->>> from stock import Stock ->>> goog = Stock('GOOG',100,490.10) ->>> ibm = Stock('IBM',50, 91.23) >>> goog.__dict__ ... look at the output ... >>> ibm.__dict__ @@ -393,12 +399,14 @@ Try setting a new attribute on one of the above instances: >>> ``` -In the above output, you’ll notice that the `goog` instance has a attribute `date` whereas the `ibm` instance does not. +In the above output, you'll notice that the `goog` instance has a +attribute `date` whereas the `ibm` instance does not. It is important +to note that Python really doesn't place any restrictions on +attributes. For example, the attributes of an instance are not +limited to those set up in the `__init__()` method. -It is important to note that Python really doesn’t place any restrictions on attributes. -The attributes of an instance are not limited to those set up in the `__init__()` method. - -Instead of setting an attribute, try placing a new value directly into the `__dict__` object: +Instead of setting an attribute, try placing a new value directly into +the `__dict__` object: ```python >>> goog.__dict__['time'] = '9:45am' @@ -408,21 +416,22 @@ Instead of setting an attribute, try placing a new value directly into the `__di ``` Here, you really notice the fact that an instance is just a layer on -top of a dictionary. *Note: it should be emphasized that direct -manipulation of the dictionary is uncommon—you should always write -your code to use the (.) syntax.* +top of a dictionary. Note: it should be emphasized that direct +manipulation of the dictionary is uncommon--you should always write +your code to use the (.) syntax. ### Exercise 5.3: The role of classes -The definitions that make up a class definition are shared by all instances of that class. -Notice, that all instances have a link back to their associated class: +The definitions that make up a class definition are shared by all +instances of that class. Notice, that all instances have a link back +to their associated class: ```python >>> goog.__class__ ... look at output ... >>> ibm.__class__ ... look at output ... ->>> +>>> ``` Try calling a method on the instances: @@ -435,7 +444,7 @@ Try calling a method on the instances: >>> ``` -Notice that the name *cost* is not defined in either `goog.__dict__` +Notice that the name 'cost' is not defined in either `goog.__dict__` or `ibm.__dict__`. Instead, it is being supplied by the class dictionary. Try this: @@ -455,7 +464,9 @@ Try calling the `cost()` method directly through the dictionary: >>> ``` -Notice how you are calling the function defined in the class definition and how the `self` argument gets the instance. +Notice how you are calling the function defined in the class +definition and how the `self` argument gets the instance. + Try adding a new attribute to the `Stock` class: ```python @@ -482,21 +493,22 @@ However, notice that it is not part of the instance dictionary: ``` The reason you can access the `foo` attribute on instances is that -Python always checks the class dictionary if it can’t find something -on the instance itself. +Python always checks the class dictionary if it can't find something +on the instance itself. -This part of the exercise illustrates something known as a class +Note: This part of the exercise illustrates something known as a class variable. Suppose, for instance, you have a class like this: ```python class Foo(object): - a = 13 # Class variable - def __init__(self,b): - self.b = b # Instance variable + a = 13 # Class variable + def __init__(self,b): + self.b = b # Instance variable ``` -In this class, the variable `a`, assigned in the body of the class itself, is a *class variable*. -It is shared by all of the instances that get created. +In this class, the variable `a`, assigned in the body of the +class itself, is a "class variable." It is shared by all of the +instances that get created. For example: ```python >>> f = Foo(10) @@ -517,10 +529,10 @@ It is shared by all of the instances that get created. >>> ``` -### Exercise 5.4: Bound Methods +### Exercise 5.4: Bound methods A subtle feature of Python is that invoking a method actually involves -two steps and something known as a bound method. +two steps and something known as a bound method. For example: ```python >>> s = goog.sell @@ -529,11 +541,12 @@ two steps and something known as a bound method. >>> s(25) >>> goog.shares 75 ->>> +>>> ``` -Bound methods actually contain all of the pieces needed to call a method. -For instance, they keep a record of the function implementing the method: +Bound methods actually contain all of the pieces needed to call a +method. For instance, they keep a record of the function implementing +the method: ```python >>> s.__func__ @@ -549,8 +562,8 @@ This is the same value as found in the `Stock` dictionary. >>> ``` -Take a close look at both references do `0x10049af50`. They are both the same in `s` and `Stock.__dict__['sell']`. -Bound methods also record the instance, which is the `self` argument. +Bound methods also record the instance, which is the `self` +argument. ```python >>> s.__self__ @@ -558,8 +571,8 @@ Stock('GOOG',75,490.1) >>> ``` -When you invoke the function using `()` all of the pieces come together. -For example, calling `s(25)` actually does this: +When you invoke the function using `()` all of the pieces come +together. For example, calling `s(25)` actually does this: ```python >>> s.__func__(s.__self__, 25) # Same as s(25) @@ -572,7 +585,7 @@ For example, calling `s(25)` actually does this: Make a new class that inherits from `Stock`. -```python +``` >>> class NewStock(Stock): def yow(self): print('Yow!') @@ -603,7 +616,7 @@ they will be searched for attributes. >>> ``` -Here’s how the `cost()` method of instance `n` above would be found: +Here's how the `cost()` method of instance `n` above would be found: ```python >>> for cls in n.__class__.__mro__: diff --git a/Notes/05_Object_model/02_Classes_encapsulation.md b/Notes/05_Object_model/02_Classes_encapsulation.md index 01db948..8528a16 100644 --- a/Notes/05_Object_model/02_Classes_encapsulation.md +++ b/Notes/05_Object_model/02_Classes_encapsulation.md @@ -252,10 +252,10 @@ day-to-day coding. ## Exercises -### Exercise 5.6: Simple properties +### Exercise 5.6: Simple Properties Properties are a useful way to add "computed attributes" to an object. -In Exercise 4.1, you created an object `Stock`. Notice that on your +In `stock.py`, you created an object `Stock`. Notice that on your object there is a slight inconsistency in how different kinds of data are extracted: @@ -271,17 +271,22 @@ are extracted: >>> ``` -Specifically, notice how you have to add the extra `()` to `cost` because it is a method. -You can get rid of the extra `()` on `cost()` if you turn it into a property. +Specifically, notice how you have to add the extra () to `cost` because it is a method. + +You can get rid of the extra () on `cost()` if you turn it into a property. Take your `Stock` class and modify it so that the cost calculation works like this: ```python +>>> ================================ RESTART ================================ +>>> from stock import Stock +>>> s = Stock('GOOG', 100, 490.1) >>> s.cost 49010.0 >>> ``` -Try calling `s.cost()` as a function and observe that it doesn’t work now that `cost` has been defined as a property. +Try calling `s.cost()` as a function and observe that it +doesn't work now that `cost` has been defined as a property. ```python >>> s.cost() @@ -289,14 +294,19 @@ Try calling `s.cost()` as a function and observe that it doesn’t work now that >>> ``` +Making this change will likely break your earlier `pcost.py` program. +You might need to go back and get rid of the `()` on the `cost()` method. + ### Exercise 5.7: Properties and Setters -Modify the `shares` attribute so that the value is stored in a private -attribute and that a pair of property functions are used to ensure -that it is always set to an integer value. -Here is an example of the expected behavior: +Modify the `shares` attribute so that the value is stored in a +private attribute and that a pair of property functions are used to ensure +that it is always set to an integer value. Here is an example of the expected +behavior: ```python +>>> ================================ RESTART ================================ +>>> from stock import Stock >>> s = Stock('GOOG',100,490.10) >>> s.shares = 50 >>> s.shares = 'a lot' @@ -306,12 +316,13 @@ TypeError: expected an integer >>> ``` -### Exercise 5.8: Adding slots +### Exercise 5.8: Adding slots -Modify the `Stock` class so that it has a `__slots__` attribute. -Then, verify that new attributes can’t be added: +Modify the `Stock` class so that it has a `__slots__` attribute. Then, +verify that new attributes can't be added: ```python +>>> ================================ RESTART ================================ >>> from stock import Stock >>> s = Stock('GOOG', 100, 490.10) >>> s.name @@ -321,8 +332,9 @@ Then, verify that new attributes can’t be added: >>> ``` -When you use `__slots__`, Python actually uses a more efficient internal representation of objects. -What happens if you try to inspect the underlying dictionary of `s` above? +When you use `__slots__`, Python actually uses a more efficient +internal representation of objects. What happens if you try to +inspect the underlying dictionary of `s` above? ```python >>> s.__dict__ diff --git a/Notes/06_Generators/02_Customizing_iteration.md b/Notes/06_Generators/02_Customizing_iteration.md index 275f592..50e7ac4 100644 --- a/Notes/06_Generators/02_Customizing_iteration.md +++ b/Notes/06_Generators/02_Customizing_iteration.md @@ -248,7 +248,7 @@ if __name__ == '__main__': change = float(fields[4]) if name in portfolio: print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') ----- +``` Note: For this to work, your `Portfolio` class must support the `in` operator. See the last exercise and make sure you implement the diff --git a/Notes/07_Advanced_Topics/02_Anonymous_function.md b/Notes/07_Advanced_Topics/02_Anonymous_function.md index 6f8a72d..ccaa7b9 100644 --- a/Notes/07_Advanced_Topics/02_Anonymous_function.md +++ b/Notes/07_Advanced_Topics/02_Anonymous_function.md @@ -158,4 +158,4 @@ Note: `lambda` is a useful shortcut because it allows you to define a special processing function directly in the call to `sort()` as opposed to having to define a separate function first (as in part a). -[Contents](../Contents) \| [Previous (7.1 Variable Arguments)](01_Variable_arguments) \| [Next (7.3 Returning Functions)](03_Returning_function) +[Contents](../Contents) \| [Previous (7.1 Variable Arguments)](01_Variable_arguments) \| [Next (7.3 Returning Functions)](03_Returning_functions) diff --git a/Solutions/1_10/mortgage.py b/Solutions/1_10/mortgage.py new file mode 100644 index 0000000..23ecaca --- /dev/null +++ b/Solutions/1_10/mortgage.py @@ -0,0 +1,28 @@ +# mortgage.py + +principal = 500000.0 +rate = 0.05 +payment = 2684.11 +total_paid = 0.0 +month = 0 + +extra_payment = 1000.0 +extra_payment_start_month = 60 +extra_payment_end_month = 108 + +while principal > 0: + month = month + 1 + principal = principal * (1+rate/12) - payment + total_paid = total_paid + payment + + if month >= extra_payment_start_month and month <= extra_payment_end_month: + principal = principal - extra_payment + total_paid = total_paid + extra_payment + + print(month, round(total_paid,2), round(principal, 2)) + +print('Total paid', round(total_paid, 2)) +print('Months', month) + + + diff --git a/Solutions/1_27/pcost.py b/Solutions/1_27/pcost.py new file mode 100644 index 0000000..8386d53 --- /dev/null +++ b/Solutions/1_27/pcost.py @@ -0,0 +1,13 @@ +# pcost.py + +total_cost = 0.0 + +with open('../../Work/Data/portfolio.csv', 'rt') as f: + headers = next(f) + for line in f: + row = line.split(',') + nshares = int(row[1]) + price = float(row[2]) + total_cost += nshares * price + +print('Total cost', total_cost) diff --git a/Solutions/1_32/pcost.py b/Solutions/1_32/pcost.py new file mode 100644 index 0000000..8ce3a40 --- /dev/null +++ b/Solutions/1_32/pcost.py @@ -0,0 +1,31 @@ +# pcost.py + +import csv +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + total_cost = 0.0 + + with open(filename, 'rt') as f: + rows = csv.reader(f) + headers = next(rows) + for row in rows: + try: + nshares = int(row[1]) + price = float(row[2]) + total_cost += nshares * price + # This catches errors in int() and float() conversions above + except ValueError: + print('Bad row:', row) + + return total_cost + +import sys +if len(sys.argv) == 2: + filename = sys.argv[1] +else: + filename = input('Enter a filename:') + +cost = portfolio_cost(filename) +print('Total cost:', cost) diff --git a/Solutions/1_5/bounce.py b/Solutions/1_5/bounce.py new file mode 100644 index 0000000..1f3fa53 --- /dev/null +++ b/Solutions/1_5/bounce.py @@ -0,0 +1,8 @@ +# bounce.py + +height = 100 +bounce = 1 +while bounce <= 10: + height = height * (3/5) + print(bounce, round(height, 4)) + bounce += 1 diff --git a/Solutions/2_11/report.py b/Solutions/2_11/report.py new file mode 100644 index 0000000..efcb806 --- /dev/null +++ b/Solutions/2_11/report.py @@ -0,0 +1,66 @@ +# report.py +import csv + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + portfolio = [] + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + + for row in rows: + stock = { + 'name' : row[0], + 'shares' : int(row[1]), + 'price' : float(row[2]) + } + portfolio.append(stock) + + return portfolio + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + prices = {} + with open(filename) as f: + rows = csv.reader(f) + for row in rows: + try: + prices[row[0]] = float(row[1]) + except IndexError: + pass + + return prices + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for stock in portfolio: + current_price = prices[stock['name']] + change = current_price - stock['price'] + summary = (stock['name'], stock['shares'], current_price, change) + rows.append(summary) + return rows + +# Read data files and create the report data + +portfolio = read_portfolio('../../Work/Data/portfolio.csv') +prices = read_prices('../../Work/Data/prices.csv') + +# Generate the report data + +report = make_report_data(portfolio, prices) + +# Output the report +headers = ('Name', 'Shares', 'Price', 'Change') +print('%10s %10s %10s %10s' % headers) +print(('-' * 10 + ' ') * len(headers)) +for row in report: + print('%10s %10d %10.2f %10.2f' % row) diff --git a/Solutions/2_16/pcost.py b/Solutions/2_16/pcost.py new file mode 100644 index 0000000..bb0ede9 --- /dev/null +++ b/Solutions/2_16/pcost.py @@ -0,0 +1,32 @@ +# pcost.py + +import csv +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + total_cost = 0.0 + + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + for rowno, row in enumerate(rows, start=1): + record = dict(zip(headers, row)) + try: + nshares = int(record['shares']) + price = float(record['price']) + total_cost += nshares * price + # This catches errors in int() and float() conversions above + except ValueError: + print(f'Row {rowno}: Bad row: {row}') + + return total_cost + +import sys +if len(sys.argv) == 2: + filename = sys.argv[1] +else: + filename = input('Enter a filename:') + +cost = portfolio_cost(filename) +print('Total cost:', cost) diff --git a/Solutions/2_16/report.py b/Solutions/2_16/report.py new file mode 100644 index 0000000..9b74ea8 --- /dev/null +++ b/Solutions/2_16/report.py @@ -0,0 +1,67 @@ +# report.py +import csv + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + portfolio = [] + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + + for row in rows: + record = dict(zip(headers, row)) + stock = { + 'name' : record['name'], + 'shares' : int(record['shares']), + 'price' : float(record['price']) + } + portfolio.append(stock) + + return portfolio + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + prices = {} + with open(filename) as f: + rows = csv.reader(f) + for row in rows: + try: + prices[row[0]] = float(row[1]) + except IndexError: + pass + + return prices + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for stock in portfolio: + current_price = prices[stock['name']] + change = current_price - stock['price'] + summary = (stock['name'], stock['shares'], current_price, change) + rows.append(summary) + return rows + +# Read data files and create the report data + +portfolio = read_portfolio('../../Work/Data/portfolio.csv') +prices = read_prices('../../Work/Data/prices.csv') + +# Generate the report data + +report = make_report_data(portfolio, prices) + +# Output the report +headers = ('Name', 'Shares', 'Price', 'Change') +print('%10s %10s %10s %10s' % headers) +print(('-' * 10 + ' ') * len(headers)) +for row in report: + print('%10s %10d %10.2f %10.2f' % row) diff --git a/Solutions/2_7/report.py b/Solutions/2_7/report.py new file mode 100644 index 0000000..ee670da --- /dev/null +++ b/Solutions/2_7/report.py @@ -0,0 +1,55 @@ +# report.py +import csv + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + portfolio = [] + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + + for row in rows: + stock = { + 'name' : row[0], + 'shares' : int(row[1]), + 'price' : float(row[2]) + } + portfolio.append(stock) + + return portfolio + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + prices = {} + with open(filename) as f: + rows = csv.reader(f) + for row in rows: + try: + prices[row[0]] = float(row[1]) + except IndexError: + pass + + return prices + +portfolio = read_portfolio('../../Work/Data/portfolio.csv') +prices = read_prices('../../Work/Data/prices.csv') + +# Calculate the total cost of the portfolio +total_cost = 0.0 +for s in portfolio: + total_cost += s['shares']*s['price'] + +print('Total cost', total_cost) + +# Compute the current value of the portfolio +total_value = 0.0 +for s in portfolio: + total_value += s['shares']*prices[s['name']] + +print('Current value', total_value) +print('Gain', total_value - total_cost) diff --git a/Solutions/3_10/fileparse.py b/Solutions/3_10/fileparse.py new file mode 100644 index 0000000..00a7c02 --- /dev/null +++ b/Solutions/3_10/fileparse.py @@ -0,0 +1,48 @@ +# fileparse.py +import csv + +def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + with open(filename) as f: + rows = csv.reader(f, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/3_14/fileparse.py b/Solutions/3_14/fileparse.py new file mode 100644 index 0000000..00a7c02 --- /dev/null +++ b/Solutions/3_14/fileparse.py @@ -0,0 +1,48 @@ +# fileparse.py +import csv + +def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + with open(filename) as f: + rows = csv.reader(f, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/3_14/pcost.py b/Solutions/3_14/pcost.py new file mode 100644 index 0000000..5685d01 --- /dev/null +++ b/Solutions/3_14/pcost.py @@ -0,0 +1,19 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return sum([s['shares']*s['price'] for s in portfolio]) + +import sys +if len(sys.argv) == 2: + filename = sys.argv[1] +else: + filename = input('Enter a filename:') + +cost = portfolio_cost(filename) +print('Total cost:', cost) diff --git a/Solutions/3_14/report.py b/Solutions/3_14/report.py new file mode 100644 index 0000000..ce9557f --- /dev/null +++ b/Solutions/3_14/report.py @@ -0,0 +1,56 @@ +# report.py + +import fileparse + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + return fileparse.parse_csv(filename, select=['name','shares','price'], types=[str,int,float]) + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + return dict(fileparse.parse_csv(filename,types=[str,float], has_headers=False)) + +def make_report_data(portfolio,prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for stock in portfolio: + current_price = prices[stock['name']] + change = current_price - stock['price'] + summary = (stock['name'], stock['shares'], current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + headers = ('Name','Shares','Price','Change') + print('%10s %10s %10s %10s' % headers) + print(('-'*10 + ' ')*len(headers)) + for row in reportdata: + print('%10s %10d %10.2f %10.2f' % row) + +def portfolio_report(portfoliofile,pricefile): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + print_report(report) + +portfolio_report('../../Work/Data/portfolio.csv', + '../../Work/Data/prices.csv') diff --git a/Solutions/3_16/fileparse.py b/Solutions/3_16/fileparse.py new file mode 100644 index 0000000..00a7c02 --- /dev/null +++ b/Solutions/3_16/fileparse.py @@ -0,0 +1,48 @@ +# fileparse.py +import csv + +def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + with open(filename) as f: + rows = csv.reader(f, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/3_16/pcost.py b/Solutions/3_16/pcost.py new file mode 100644 index 0000000..b537a45 --- /dev/null +++ b/Solutions/3_16/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return sum([s['shares'] * s['price'] for s in portfolio]) + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/3_16/report.py b/Solutions/3_16/report.py new file mode 100644 index 0000000..aba162b --- /dev/null +++ b/Solutions/3_16/report.py @@ -0,0 +1,62 @@ +# report.py + +import fileparse + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + return fileparse.parse_csv(filename, select=['name','shares','price'], types=[str,int,float]) + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + return dict(fileparse.parse_csv(filename,types=[str,float], has_headers=False)) + +def make_report_data(portfolio,prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for stock in portfolio: + current_price = prices[stock['name']] + change = current_price - stock['price'] + summary = (stock['name'], stock['shares'], current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + headers = ('Name','Shares','Price','Change') + print('%10s %10s %10s %10s' % headers) + print(('-'*10 + ' ')*len(headers)) + for row in reportdata: + print('%10s %10d %10.2f %10.2f' % row) + +def portfolio_report(portfoliofile, pricefile): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + print_report(report) + +def main(args): + if len(args) != 3: + raise SystemExit('Usage: %s portfile pricefile' % args[0]) + portfolio_report(args[1], args[2]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/3_18/fileparse.py b/Solutions/3_18/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/3_18/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/3_18/pcost.py b/Solutions/3_18/pcost.py new file mode 100644 index 0000000..b537a45 --- /dev/null +++ b/Solutions/3_18/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return sum([s['shares'] * s['price'] for s in portfolio]) + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/3_18/report.py b/Solutions/3_18/report.py new file mode 100644 index 0000000..fbf71bd --- /dev/null +++ b/Solutions/3_18/report.py @@ -0,0 +1,64 @@ +# report.py + +import fileparse + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + return fileparse.parse_csv(lines, select=['name','shares','price'], types=[str,int,float]) + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) + +def make_report_data(portfolio,prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for stock in portfolio: + current_price = prices[stock['name']] + change = current_price - stock['price'] + summary = (stock['name'], stock['shares'], current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + headers = ('Name','Shares','Price','Change') + print('%10s %10s %10s %10s' % headers) + print(('-'*10 + ' ')*len(headers)) + for row in reportdata: + print('%10s %10d %10.2f %10.2f' % row) + +def portfolio_report(portfoliofile, pricefile): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + print_report(report) + +def main(args): + if len(args) != 3: + raise SystemExit('Usage: %s portfile pricefile' % args[0]) + portfolio_report(args[1], args[2]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/3_2/report.py b/Solutions/3_2/report.py new file mode 100644 index 0000000..056c48a --- /dev/null +++ b/Solutions/3_2/report.py @@ -0,0 +1,78 @@ +# report.py +import csv + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + portfolio = [] + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + + for row in rows: + record = dict(zip(headers, row)) + stock = { + 'name' : record['name'], + 'shares' : int(record['shares']), + 'price' : float(record['price']) + } + portfolio.append(stock) + + return portfolio + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + prices = {} + with open(filename) as f: + rows = csv.reader(f) + for row in rows: + try: + prices[row[0]] = float(row[1]) + except IndexError: + pass + + return prices + +def make_report_data(portfolio,prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for stock in portfolio: + current_price = prices[stock['name']] + change = current_price - stock['price'] + summary = (stock['name'], stock['shares'], current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + headers = ('Name','Shares','Price','Change') + print('%10s %10s %10s %10s' % headers) + print(('-'*10 + ' ')*len(headers)) + for row in reportdata: + print('%10s %10d %10.2f %10.2f' % row) + +def portfolio_report(portfoliofile,pricefile): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio,prices) + + # Print it out + print_report(report) + +portfolio_report('../../Work/Data/portfolio.csv', + '../../Work/Data/prices.csv') diff --git a/Solutions/3_7/fileparse.py b/Solutions/3_7/fileparse.py new file mode 100644 index 0000000..56605d5 --- /dev/null +++ b/Solutions/3_7/fileparse.py @@ -0,0 +1,39 @@ +# fileparse.py +import csv + +def parse_csv(filename, select=None, types=None, has_headers=True, delimiter=','): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + with open(filename) as f: + rows = csv.reader(f, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for row in rows: + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + row = [func(val) for func, val in zip(types, row)] + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/4_10/fileparse.py b/Solutions/4_10/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/4_10/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/4_10/pcost.py b/Solutions/4_10/pcost.py new file mode 100644 index 0000000..a8e0739 --- /dev/null +++ b/Solutions/4_10/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return sum([s.cost() for s in portfolio]) + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/4_10/report.py b/Solutions/4_10/report.py new file mode 100644 index 0000000..9cf07b3 --- /dev/null +++ b/Solutions/4_10/report.py @@ -0,0 +1,71 @@ +# report.py + +import fileparse +from stock import Stock +import tableformat + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float]) + + portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] + return portfolio + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/4_10/stock.py b/Solutions/4_10/stock.py new file mode 100644 index 0000000..8206801 --- /dev/null +++ b/Solutions/4_10/stock.py @@ -0,0 +1,26 @@ +# stock.py + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares + ''' + self.shares -= nshares + diff --git a/Solutions/4_10/tableformat.py b/Solutions/4_10/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/4_10/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/4_4/fileparse.py b/Solutions/4_4/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/4_4/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/4_4/pcost.py b/Solutions/4_4/pcost.py new file mode 100644 index 0000000..a8e0739 --- /dev/null +++ b/Solutions/4_4/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return sum([s.cost() for s in portfolio]) + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/4_4/report.py b/Solutions/4_4/report.py new file mode 100644 index 0000000..370badd --- /dev/null +++ b/Solutions/4_4/report.py @@ -0,0 +1,70 @@ +# report.py + +import fileparse +from stock import Stock + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float]) + + portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] + return portfolio + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + headers = ('Name','Shares','Price','Change') + print('%10s %10s %10s %10s' % headers) + print(('-'*10 + ' ')*len(headers)) + for row in reportdata: + print('%10s %10d %10.2f %10.2f' % row) + +def portfolio_report(portfoliofile, pricefile): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + print_report(report) + +def main(args): + if len(args) != 3: + raise SystemExit('Usage: %s portfile pricefile' % args[0]) + portfolio_report(args[1], args[2]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/4_4/stock.py b/Solutions/4_4/stock.py new file mode 100644 index 0000000..1f3ad99 --- /dev/null +++ b/Solutions/4_4/stock.py @@ -0,0 +1,23 @@ +# stock.py + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares + ''' + self.shares -= nshares + diff --git a/Solutions/5_8/fileparse.py b/Solutions/5_8/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/5_8/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/5_8/pcost.py b/Solutions/5_8/pcost.py new file mode 100644 index 0000000..a8e0739 --- /dev/null +++ b/Solutions/5_8/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return sum([s.cost() for s in portfolio]) + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/5_8/report.py b/Solutions/5_8/report.py new file mode 100644 index 0000000..9cf07b3 --- /dev/null +++ b/Solutions/5_8/report.py @@ -0,0 +1,71 @@ +# report.py + +import fileparse +from stock import Stock +import tableformat + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float]) + + portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] + return portfolio + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/5_8/stock.py b/Solutions/5_8/stock.py new file mode 100644 index 0000000..99ca129 --- /dev/null +++ b/Solutions/5_8/stock.py @@ -0,0 +1,37 @@ +# stock.py + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + __slots__ = ('name','_shares','price') + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def shares(self): + return self._shares + + @shares.setter + def shares(self, value): + if not isinstance(value,int): + raise TypeError("Must be integer") + self._shares = value + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/5_8/tableformat.py b/Solutions/5_8/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/5_8/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/6_12/fileparse.py b/Solutions/6_12/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/6_12/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/6_12/follow.py b/Solutions/6_12/follow.py new file mode 100644 index 0000000..73b0c22 --- /dev/null +++ b/Solutions/6_12/follow.py @@ -0,0 +1,30 @@ +# follow.py +import os +import time + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + f = open(filename, 'r') + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line == '': + time.sleep(0.1) # Sleep briefly to avoid busy wait + continue + yield line + +# Example use +if __name__ == '__main__': + import report + + portfolio = report.read_portfolio('../../Data/portfolio.csv') + + for line in follow('../../Data/stocklog.csv'): + row = line.split(',') + name = row[0].strip('"') + price = float(row[1]) + change = float(row[4]) + if name in portfolio: + print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') diff --git a/Solutions/6_12/pcost.py b/Solutions/6_12/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/6_12/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_12/portfolio.py b/Solutions/6_12/portfolio.py new file mode 100644 index 0000000..77a1b1f --- /dev/null +++ b/Solutions/6_12/portfolio.py @@ -0,0 +1,31 @@ +# portfolio.py + +class Portfolio(object): + def __init__(self, holdings): + self._holdings = holdings + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any([s.name == name for s in self._holdings]) + + @property + def total_cost(self): + return sum([s.shares * s.price for s in self._holdings]) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + diff --git a/Solutions/6_12/report.py b/Solutions/6_12/report.py new file mode 100644 index 0000000..7c16aad --- /dev/null +++ b/Solutions/6_12/report.py @@ -0,0 +1,72 @@ +# report.py + +import fileparse +from stock import Stock +import tableformat +from portfolio import Portfolio + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float]) + + portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] + return Portfolio(portfolio) + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_12/stock.py b/Solutions/6_12/stock.py new file mode 100644 index 0000000..99ca129 --- /dev/null +++ b/Solutions/6_12/stock.py @@ -0,0 +1,37 @@ +# stock.py + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + __slots__ = ('name','_shares','price') + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def shares(self): + return self._shares + + @shares.setter + def shares(self, value): + if not isinstance(value,int): + raise TypeError("Must be integer") + self._shares = value + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/6_12/tableformat.py b/Solutions/6_12/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/6_12/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/6_12/ticker.py b/Solutions/6_12/ticker.py new file mode 100644 index 0000000..6922632 --- /dev/null +++ b/Solutions/6_12/ticker.py @@ -0,0 +1,50 @@ +# ticker.py + +import csv +import report +import tableformat +from follow import follow +import time + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + +def make_dicts(rows, headers): + for row in rows: + yield dict(zip(headers, row)) + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str,float,float]) + rows = make_dicts(rows, ['name','price','change']) + return rows + +def filter_symbols(rows, names): + for row in rows: + if row['name'] in names: + yield row + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + lines = follow(logfile) + rows = parse_stock_data(lines) + rows = filter_symbols(rows, portfolio) + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name','Price','Change']) + for row in rows: + formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) + ticker(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_15/fileparse.py b/Solutions/6_15/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/6_15/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/6_15/follow.py b/Solutions/6_15/follow.py new file mode 100644 index 0000000..1dec7bc --- /dev/null +++ b/Solutions/6_15/follow.py @@ -0,0 +1,17 @@ +# follow.py + +import time +import os + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + with open(filename,'r') as f: + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line != '': + yield line + else: + time.sleep(0.1) # Sleep briefly to avoid busy wait diff --git a/Solutions/6_15/pcost.py b/Solutions/6_15/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/6_15/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_15/portfolio.py b/Solutions/6_15/portfolio.py new file mode 100644 index 0000000..b738d26 --- /dev/null +++ b/Solutions/6_15/portfolio.py @@ -0,0 +1,31 @@ +# portfolio.py + +class Portfolio(object): + def __init__(self, holdings): + self._holdings = holdings + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any(s.name == name for s in self._holdings) + + @property + def total_cost(self): + return sum(s.shares * s.price for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + diff --git a/Solutions/6_15/report.py b/Solutions/6_15/report.py new file mode 100644 index 0000000..7c16aad --- /dev/null +++ b/Solutions/6_15/report.py @@ -0,0 +1,72 @@ +# report.py + +import fileparse +from stock import Stock +import tableformat +from portfolio import Portfolio + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float]) + + portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] + return Portfolio(portfolio) + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_15/stock.py b/Solutions/6_15/stock.py new file mode 100644 index 0000000..99ca129 --- /dev/null +++ b/Solutions/6_15/stock.py @@ -0,0 +1,37 @@ +# stock.py + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + __slots__ = ('name','_shares','price') + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def shares(self): + return self._shares + + @shares.setter + def shares(self, value): + if not isinstance(value,int): + raise TypeError("Must be integer") + self._shares = value + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/6_15/tableformat.py b/Solutions/6_15/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/6_15/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/6_15/ticker.py b/Solutions/6_15/ticker.py new file mode 100644 index 0000000..67171e0 --- /dev/null +++ b/Solutions/6_15/ticker.py @@ -0,0 +1,44 @@ +# ticker.py + +import csv +import report +import tableformat +from follow import follow +import time + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + +def make_dicts(rows, headers): + return (dict(zip(headers,row)) for row in rows) + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str,float,float]) + rows = make_dicts(rows, ['name','price','change']) + return rows + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + lines = follow(logfile) + rows = parse_stock_data(lines) + rows = (row for row in rows if row['name'] in portfolio) + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name','Price','Change']) + for row in rows: + formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) + ticker(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_3/fileparse.py b/Solutions/6_3/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/6_3/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/6_3/pcost.py b/Solutions/6_3/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/6_3/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_3/portfolio.py b/Solutions/6_3/portfolio.py new file mode 100644 index 0000000..77a1b1f --- /dev/null +++ b/Solutions/6_3/portfolio.py @@ -0,0 +1,31 @@ +# portfolio.py + +class Portfolio(object): + def __init__(self, holdings): + self._holdings = holdings + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any([s.name == name for s in self._holdings]) + + @property + def total_cost(self): + return sum([s.shares * s.price for s in self._holdings]) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + diff --git a/Solutions/6_3/report.py b/Solutions/6_3/report.py new file mode 100644 index 0000000..7c16aad --- /dev/null +++ b/Solutions/6_3/report.py @@ -0,0 +1,72 @@ +# report.py + +import fileparse +from stock import Stock +import tableformat +from portfolio import Portfolio + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float]) + + portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] + return Portfolio(portfolio) + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_3/stock.py b/Solutions/6_3/stock.py new file mode 100644 index 0000000..99ca129 --- /dev/null +++ b/Solutions/6_3/stock.py @@ -0,0 +1,37 @@ +# stock.py + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + __slots__ = ('name','_shares','price') + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def shares(self): + return self._shares + + @shares.setter + def shares(self, value): + if not isinstance(value,int): + raise TypeError("Must be integer") + self._shares = value + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/6_3/tableformat.py b/Solutions/6_3/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/6_3/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/6_7/fileparse.py b/Solutions/6_7/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/6_7/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/6_7/follow.py b/Solutions/6_7/follow.py new file mode 100644 index 0000000..73b0c22 --- /dev/null +++ b/Solutions/6_7/follow.py @@ -0,0 +1,30 @@ +# follow.py +import os +import time + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + f = open(filename, 'r') + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line == '': + time.sleep(0.1) # Sleep briefly to avoid busy wait + continue + yield line + +# Example use +if __name__ == '__main__': + import report + + portfolio = report.read_portfolio('../../Data/portfolio.csv') + + for line in follow('../../Data/stocklog.csv'): + row = line.split(',') + name = row[0].strip('"') + price = float(row[1]) + change = float(row[4]) + if name in portfolio: + print(f'{name:>10s} {price:>10.2f} {change:>10.2f}') diff --git a/Solutions/6_7/pcost.py b/Solutions/6_7/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/6_7/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_7/portfolio.py b/Solutions/6_7/portfolio.py new file mode 100644 index 0000000..77a1b1f --- /dev/null +++ b/Solutions/6_7/portfolio.py @@ -0,0 +1,31 @@ +# portfolio.py + +class Portfolio(object): + def __init__(self, holdings): + self._holdings = holdings + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any([s.name == name for s in self._holdings]) + + @property + def total_cost(self): + return sum([s.shares * s.price for s in self._holdings]) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + diff --git a/Solutions/6_7/report.py b/Solutions/6_7/report.py new file mode 100644 index 0000000..7c16aad --- /dev/null +++ b/Solutions/6_7/report.py @@ -0,0 +1,72 @@ +# report.py + +import fileparse +from stock import Stock +import tableformat +from portfolio import Portfolio + +def read_portfolio(filename): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float]) + + portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ] + return Portfolio(portfolio) + +def read_prices(filename): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/6_7/stock.py b/Solutions/6_7/stock.py new file mode 100644 index 0000000..99ca129 --- /dev/null +++ b/Solutions/6_7/stock.py @@ -0,0 +1,37 @@ +# stock.py + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + __slots__ = ('name','_shares','price') + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def shares(self): + return self._shares + + @shares.setter + def shares(self, value): + if not isinstance(value,int): + raise TypeError("Must be integer") + self._shares = value + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/6_7/tableformat.py b/Solutions/6_7/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/6_7/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/7_10/timethis.py b/Solutions/7_10/timethis.py new file mode 100644 index 0000000..6cd7057 --- /dev/null +++ b/Solutions/7_10/timethis.py @@ -0,0 +1,21 @@ +# timethis.py + +import time + +def timethis(func): + def wrapper(*args, **kwargs): + start = time.time() + try: + return func(*args,**kwargs) + finally: + end = time.time() + print("%s.%s : %f" % (func.__module__,func.__name__,end-start)) + return wrapper + +if __name__ == '__main__': + @timethis + def countdown(n): + while n > 0: + n-= 1 + + countdown(1000000) diff --git a/Solutions/7_12/fileparse.py b/Solutions/7_12/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/7_12/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/7_12/follow.py b/Solutions/7_12/follow.py new file mode 100644 index 0000000..1dec7bc --- /dev/null +++ b/Solutions/7_12/follow.py @@ -0,0 +1,17 @@ +# follow.py + +import time +import os + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + with open(filename,'r') as f: + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line != '': + yield line + else: + time.sleep(0.1) # Sleep briefly to avoid busy wait diff --git a/Solutions/7_12/pcost.py b/Solutions/7_12/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/7_12/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_12/portfolio.py b/Solutions/7_12/portfolio.py new file mode 100644 index 0000000..e616a0a --- /dev/null +++ b/Solutions/7_12/portfolio.py @@ -0,0 +1,51 @@ +# portfolio.py + +import fileparse +import stock + +class Portfolio(object): + def __init__(self): + self._holdings = [] + + @classmethod + def from_csv(cls, lines, **opts): + self = cls() + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float], + **opts) + + for d in portdicts: + self.append(stock.Stock(**d)) + + return self + + def append(self, holding): + self._holdings.append(holding) + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any(s.name == name for s in self._holdings) + + @property + def total_cost(self): + return sum(s.shares * s.price for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + + diff --git a/Solutions/7_12/report.py b/Solutions/7_12/report.py new file mode 100644 index 0000000..4c74f6e --- /dev/null +++ b/Solutions/7_12/report.py @@ -0,0 +1,67 @@ +# report.py + +import fileparse +from stock import Stock +from portfolio import Portfolio +import tableformat + +def read_portfolio(filename, **opts): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + return Portfolio.from_csv(lines, **opts) + +def read_prices(filename, **opts): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) + +def make_report(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_12/stock.py b/Solutions/7_12/stock.py new file mode 100644 index 0000000..f94e97c --- /dev/null +++ b/Solutions/7_12/stock.py @@ -0,0 +1,32 @@ +# stock.py + +from typedproperty import String, Integer, Float + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + name = String('name') + shares = Integer('shares') + price = Float('price') + + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/7_12/tableformat.py b/Solutions/7_12/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/7_12/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/7_12/ticker.py b/Solutions/7_12/ticker.py new file mode 100644 index 0000000..31f8e67 --- /dev/null +++ b/Solutions/7_12/ticker.py @@ -0,0 +1,43 @@ +# ticker.py + +import csv +import report +import tableformat +from follow import follow + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + +def make_dicts(rows, headers): + return (dict(zip(headers,row)) for row in rows) + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str,float,float]) + rows = make_dicts(rows, ['name','price','change']) + return rows + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + lines = follow(logfile) + rows = parse_stock_data(lines) + rows = (row for row in rows if row['name'] in portfolio) + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name','Price','Change']) + for row in rows: + formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) + ticker(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_12/timethis.py b/Solutions/7_12/timethis.py new file mode 100644 index 0000000..6cd7057 --- /dev/null +++ b/Solutions/7_12/timethis.py @@ -0,0 +1,21 @@ +# timethis.py + +import time + +def timethis(func): + def wrapper(*args, **kwargs): + start = time.time() + try: + return func(*args,**kwargs) + finally: + end = time.time() + print("%s.%s : %f" % (func.__module__,func.__name__,end-start)) + return wrapper + +if __name__ == '__main__': + @timethis + def countdown(n): + while n > 0: + n-= 1 + + countdown(1000000) diff --git a/Solutions/7_12/typedproperty.py b/Solutions/7_12/typedproperty.py new file mode 100644 index 0000000..cda0872 --- /dev/null +++ b/Solutions/7_12/typedproperty.py @@ -0,0 +1,34 @@ +# typedproperty.py + +def typedproperty(name, expected_type): + private_name = '_' + name + @property + def prop(self): + return getattr(self, private_name) + + @prop.setter + def prop(self, value): + if not isinstance(value, expected_type): + raise TypeError(f'Expected {expected_type}') + setattr(self, private_name, value) + + return prop + +String = lambda name: typedproperty(name, str) +Integer = lambda name: typedproperty(name, int) +Float = lambda name: typedproperty(name, float) + +# Example +if __name__ == '__main__': + class Stock(object): + name = typedproperty('name', str) + shares = typedproperty('shares', int) + price = typedproperty('price', float) + + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + + diff --git a/Solutions/7_4/fileparse.py b/Solutions/7_4/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/7_4/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/7_4/follow.py b/Solutions/7_4/follow.py new file mode 100644 index 0000000..1dec7bc --- /dev/null +++ b/Solutions/7_4/follow.py @@ -0,0 +1,17 @@ +# follow.py + +import time +import os + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + with open(filename,'r') as f: + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line != '': + yield line + else: + time.sleep(0.1) # Sleep briefly to avoid busy wait diff --git a/Solutions/7_4/pcost.py b/Solutions/7_4/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/7_4/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_4/portfolio.py b/Solutions/7_4/portfolio.py new file mode 100644 index 0000000..b738d26 --- /dev/null +++ b/Solutions/7_4/portfolio.py @@ -0,0 +1,31 @@ +# portfolio.py + +class Portfolio(object): + def __init__(self, holdings): + self._holdings = holdings + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any(s.name == name for s in self._holdings) + + @property + def total_cost(self): + return sum(s.shares * s.price for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + diff --git a/Solutions/7_4/report.py b/Solutions/7_4/report.py new file mode 100644 index 0000000..789a08e --- /dev/null +++ b/Solutions/7_4/report.py @@ -0,0 +1,73 @@ +# report.py + +import fileparse +from stock import Stock +import tableformat +from portfolio import Portfolio + +def read_portfolio(filename, **opts): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float], + **opts) + + portfolio = [ Stock(**d) for d in portdicts ] + return Portfolio(portfolio) + +def read_prices(filename, **opts): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines,types=[str,float], has_headers=False, **opts)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_4/stock.py b/Solutions/7_4/stock.py new file mode 100644 index 0000000..99ca129 --- /dev/null +++ b/Solutions/7_4/stock.py @@ -0,0 +1,37 @@ +# stock.py + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + __slots__ = ('name','_shares','price') + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def shares(self): + return self._shares + + @shares.setter + def shares(self, value): + if not isinstance(value,int): + raise TypeError("Must be integer") + self._shares = value + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/7_4/tableformat.py b/Solutions/7_4/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/7_4/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/7_4/ticker.py b/Solutions/7_4/ticker.py new file mode 100644 index 0000000..67171e0 --- /dev/null +++ b/Solutions/7_4/ticker.py @@ -0,0 +1,44 @@ +# ticker.py + +import csv +import report +import tableformat +from follow import follow +import time + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + +def make_dicts(rows, headers): + return (dict(zip(headers,row)) for row in rows) + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str,float,float]) + rows = make_dicts(rows, ['name','price','change']) + return rows + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + lines = follow(logfile) + rows = parse_stock_data(lines) + rows = (row for row in rows if row['name'] in portfolio) + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name','Price','Change']) + for row in rows: + formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) + ticker(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_9/fileparse.py b/Solutions/7_9/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/7_9/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/7_9/follow.py b/Solutions/7_9/follow.py new file mode 100644 index 0000000..1dec7bc --- /dev/null +++ b/Solutions/7_9/follow.py @@ -0,0 +1,17 @@ +# follow.py + +import time +import os + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + with open(filename,'r') as f: + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line != '': + yield line + else: + time.sleep(0.1) # Sleep briefly to avoid busy wait diff --git a/Solutions/7_9/pcost.py b/Solutions/7_9/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/7_9/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_9/portfolio.py b/Solutions/7_9/portfolio.py new file mode 100644 index 0000000..b738d26 --- /dev/null +++ b/Solutions/7_9/portfolio.py @@ -0,0 +1,31 @@ +# portfolio.py + +class Portfolio(object): + def __init__(self, holdings): + self._holdings = holdings + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any(s.name == name for s in self._holdings) + + @property + def total_cost(self): + return sum(s.shares * s.price for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + diff --git a/Solutions/7_9/report.py b/Solutions/7_9/report.py new file mode 100644 index 0000000..789a08e --- /dev/null +++ b/Solutions/7_9/report.py @@ -0,0 +1,73 @@ +# report.py + +import fileparse +from stock import Stock +import tableformat +from portfolio import Portfolio + +def read_portfolio(filename, **opts): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float], + **opts) + + portfolio = [ Stock(**d) for d in portdicts ] + return Portfolio(portfolio) + +def read_prices(filename, **opts): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines,types=[str,float], has_headers=False, **opts)) + +def make_report_data(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report_data(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_9/stock.py b/Solutions/7_9/stock.py new file mode 100644 index 0000000..f94e97c --- /dev/null +++ b/Solutions/7_9/stock.py @@ -0,0 +1,32 @@ +# stock.py + +from typedproperty import String, Integer, Float + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + name = String('name') + shares = Integer('shares') + price = Float('price') + + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/7_9/tableformat.py b/Solutions/7_9/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/7_9/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/7_9/ticker.py b/Solutions/7_9/ticker.py new file mode 100644 index 0000000..67171e0 --- /dev/null +++ b/Solutions/7_9/ticker.py @@ -0,0 +1,44 @@ +# ticker.py + +import csv +import report +import tableformat +from follow import follow +import time + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + +def make_dicts(rows, headers): + return (dict(zip(headers,row)) for row in rows) + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str,float,float]) + rows = make_dicts(rows, ['name','price','change']) + return rows + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + lines = follow(logfile) + rows = parse_stock_data(lines) + rows = (row for row in rows if row['name'] in portfolio) + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name','Price','Change']) + for row in rows: + formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) + ticker(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/7_9/typedproperty.py b/Solutions/7_9/typedproperty.py new file mode 100644 index 0000000..cda0872 --- /dev/null +++ b/Solutions/7_9/typedproperty.py @@ -0,0 +1,34 @@ +# typedproperty.py + +def typedproperty(name, expected_type): + private_name = '_' + name + @property + def prop(self): + return getattr(self, private_name) + + @prop.setter + def prop(self, value): + if not isinstance(value, expected_type): + raise TypeError(f'Expected {expected_type}') + setattr(self, private_name, value) + + return prop + +String = lambda name: typedproperty(name, str) +Integer = lambda name: typedproperty(name, int) +Float = lambda name: typedproperty(name, float) + +# Example +if __name__ == '__main__': + class Stock(object): + name = typedproperty('name', str) + shares = typedproperty('shares', int) + price = typedproperty('price', float) + + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + + diff --git a/Solutions/8_1/fileparse.py b/Solutions/8_1/fileparse.py new file mode 100644 index 0000000..8ad4554 --- /dev/null +++ b/Solutions/8_1/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + print(f"Row {rowno}: Couldn't convert {row}") + print(f"Row {rowno}: Reason {e}") + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/8_1/follow.py b/Solutions/8_1/follow.py new file mode 100644 index 0000000..1dec7bc --- /dev/null +++ b/Solutions/8_1/follow.py @@ -0,0 +1,17 @@ +# follow.py + +import time +import os + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + with open(filename,'r') as f: + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line != '': + yield line + else: + time.sleep(0.1) # Sleep briefly to avoid busy wait diff --git a/Solutions/8_1/pcost.py b/Solutions/8_1/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/8_1/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/8_1/portfolio.py b/Solutions/8_1/portfolio.py new file mode 100644 index 0000000..e616a0a --- /dev/null +++ b/Solutions/8_1/portfolio.py @@ -0,0 +1,51 @@ +# portfolio.py + +import fileparse +import stock + +class Portfolio(object): + def __init__(self): + self._holdings = [] + + @classmethod + def from_csv(cls, lines, **opts): + self = cls() + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float], + **opts) + + for d in portdicts: + self.append(stock.Stock(**d)) + + return self + + def append(self, holding): + self._holdings.append(holding) + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any(s.name == name for s in self._holdings) + + @property + def total_cost(self): + return sum(s.shares * s.price for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + + diff --git a/Solutions/8_1/report.py b/Solutions/8_1/report.py new file mode 100644 index 0000000..4c74f6e --- /dev/null +++ b/Solutions/8_1/report.py @@ -0,0 +1,67 @@ +# report.py + +import fileparse +from stock import Stock +from portfolio import Portfolio +import tableformat + +def read_portfolio(filename, **opts): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + return Portfolio.from_csv(lines, **opts) + +def read_prices(filename, **opts): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) + +def make_report(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/8_1/stock.py b/Solutions/8_1/stock.py new file mode 100644 index 0000000..f94e97c --- /dev/null +++ b/Solutions/8_1/stock.py @@ -0,0 +1,32 @@ +# stock.py + +from typedproperty import String, Integer, Float + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + name = String('name') + shares = Integer('shares') + price = Float('price') + + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/8_1/tableformat.py b/Solutions/8_1/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/8_1/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/8_1/test_stock.py b/Solutions/8_1/test_stock.py new file mode 100644 index 0000000..98f6545 --- /dev/null +++ b/Solutions/8_1/test_stock.py @@ -0,0 +1,28 @@ +# test_stock.py + +import unittest +import stock + +class TestStock(unittest.TestCase): + def test_create(self): + s = stock.Stock('GOOG', 100, 490.1) + self.assertEqual(s.name, 'GOOG') + self.assertEqual(s.shares, 100) + self.assertEqual(s.price, 490.1) + + def test_cost(self): + s = stock.Stock('GOOG', 100, 490.1) + self.assertEqual(s.cost, 49010.0) + + def test_sell(self): + s = stock.Stock('GOOG', 100, 490.1) + s.sell(25) + self.assertEqual(s.shares, 75) + + def test_shares_check(self): + s = stock.Stock('GOOG', 100, 490.1) + with self.assertRaises(TypeError): + s.shares = '100' + +if __name__ == '__main__': + unittest.main() diff --git a/Solutions/8_1/ticker.py b/Solutions/8_1/ticker.py new file mode 100644 index 0000000..31f8e67 --- /dev/null +++ b/Solutions/8_1/ticker.py @@ -0,0 +1,43 @@ +# ticker.py + +import csv +import report +import tableformat +from follow import follow + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + +def make_dicts(rows, headers): + return (dict(zip(headers,row)) for row in rows) + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str,float,float]) + rows = make_dicts(rows, ['name','price','change']) + return rows + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + lines = follow(logfile) + rows = parse_stock_data(lines) + rows = (row for row in rows if row['name'] in portfolio) + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name','Price','Change']) + for row in rows: + formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) + ticker(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/8_1/timethis.py b/Solutions/8_1/timethis.py new file mode 100644 index 0000000..6cd7057 --- /dev/null +++ b/Solutions/8_1/timethis.py @@ -0,0 +1,21 @@ +# timethis.py + +import time + +def timethis(func): + def wrapper(*args, **kwargs): + start = time.time() + try: + return func(*args,**kwargs) + finally: + end = time.time() + print("%s.%s : %f" % (func.__module__,func.__name__,end-start)) + return wrapper + +if __name__ == '__main__': + @timethis + def countdown(n): + while n > 0: + n-= 1 + + countdown(1000000) diff --git a/Solutions/8_1/typedproperty.py b/Solutions/8_1/typedproperty.py new file mode 100644 index 0000000..cda0872 --- /dev/null +++ b/Solutions/8_1/typedproperty.py @@ -0,0 +1,34 @@ +# typedproperty.py + +def typedproperty(name, expected_type): + private_name = '_' + name + @property + def prop(self): + return getattr(self, private_name) + + @prop.setter + def prop(self, value): + if not isinstance(value, expected_type): + raise TypeError(f'Expected {expected_type}') + setattr(self, private_name, value) + + return prop + +String = lambda name: typedproperty(name, str) +Integer = lambda name: typedproperty(name, int) +Float = lambda name: typedproperty(name, float) + +# Example +if __name__ == '__main__': + class Stock(object): + name = typedproperty('name', str) + shares = typedproperty('shares', int) + price = typedproperty('price', float) + + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + + diff --git a/Solutions/8_2/fileparse.py b/Solutions/8_2/fileparse.py new file mode 100644 index 0000000..9cf0de9 --- /dev/null +++ b/Solutions/8_2/fileparse.py @@ -0,0 +1,49 @@ +# fileparse.py +import csv +import logging +log = logging.getLogger(__name__) + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + if select and not has_headers: + raise RuntimeError('select requires column headers') + + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + log.warning("Row %d: Couldn't convert %s", rowno, row) + log.debug("Row %d: Reason %s", rowno, e) + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/8_2/follow.py b/Solutions/8_2/follow.py new file mode 100644 index 0000000..1dec7bc --- /dev/null +++ b/Solutions/8_2/follow.py @@ -0,0 +1,17 @@ +# follow.py + +import time +import os + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + with open(filename,'r') as f: + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line != '': + yield line + else: + time.sleep(0.1) # Sleep briefly to avoid busy wait diff --git a/Solutions/8_2/pcost.py b/Solutions/8_2/pcost.py new file mode 100644 index 0000000..1a5aef3 --- /dev/null +++ b/Solutions/8_2/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/8_2/portfolio.py b/Solutions/8_2/portfolio.py new file mode 100644 index 0000000..e616a0a --- /dev/null +++ b/Solutions/8_2/portfolio.py @@ -0,0 +1,51 @@ +# portfolio.py + +import fileparse +import stock + +class Portfolio(object): + def __init__(self): + self._holdings = [] + + @classmethod + def from_csv(cls, lines, **opts): + self = cls() + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float], + **opts) + + for d in portdicts: + self.append(stock.Stock(**d)) + + return self + + def append(self, holding): + self._holdings.append(holding) + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any(s.name == name for s in self._holdings) + + @property + def total_cost(self): + return sum(s.shares * s.price for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + + diff --git a/Solutions/8_2/report.py b/Solutions/8_2/report.py new file mode 100644 index 0000000..4c74f6e --- /dev/null +++ b/Solutions/8_2/report.py @@ -0,0 +1,67 @@ +# report.py + +import fileparse +from stock import Stock +from portfolio import Portfolio +import tableformat + +def read_portfolio(filename, **opts): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + return Portfolio.from_csv(lines, **opts) + +def read_prices(filename, **opts): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) + +def make_report(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/8_2/stock.py b/Solutions/8_2/stock.py new file mode 100644 index 0000000..f94e97c --- /dev/null +++ b/Solutions/8_2/stock.py @@ -0,0 +1,32 @@ +# stock.py + +from typedproperty import String, Integer, Float + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + name = String('name') + shares = Integer('shares') + price = Float('price') + + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/8_2/tableformat.py b/Solutions/8_2/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/8_2/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/8_2/test_stock.py b/Solutions/8_2/test_stock.py new file mode 100644 index 0000000..98f6545 --- /dev/null +++ b/Solutions/8_2/test_stock.py @@ -0,0 +1,28 @@ +# test_stock.py + +import unittest +import stock + +class TestStock(unittest.TestCase): + def test_create(self): + s = stock.Stock('GOOG', 100, 490.1) + self.assertEqual(s.name, 'GOOG') + self.assertEqual(s.shares, 100) + self.assertEqual(s.price, 490.1) + + def test_cost(self): + s = stock.Stock('GOOG', 100, 490.1) + self.assertEqual(s.cost, 49010.0) + + def test_sell(self): + s = stock.Stock('GOOG', 100, 490.1) + s.sell(25) + self.assertEqual(s.shares, 75) + + def test_shares_check(self): + s = stock.Stock('GOOG', 100, 490.1) + with self.assertRaises(TypeError): + s.shares = '100' + +if __name__ == '__main__': + unittest.main() diff --git a/Solutions/8_2/ticker.py b/Solutions/8_2/ticker.py new file mode 100644 index 0000000..31f8e67 --- /dev/null +++ b/Solutions/8_2/ticker.py @@ -0,0 +1,43 @@ +# ticker.py + +import csv +import report +import tableformat +from follow import follow + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + +def make_dicts(rows, headers): + return (dict(zip(headers,row)) for row in rows) + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str,float,float]) + rows = make_dicts(rows, ['name','price','change']) + return rows + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + lines = follow(logfile) + rows = parse_stock_data(lines) + rows = (row for row in rows if row['name'] in portfolio) + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name','Price','Change']) + for row in rows: + formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) + ticker(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/8_2/timethis.py b/Solutions/8_2/timethis.py new file mode 100644 index 0000000..6cd7057 --- /dev/null +++ b/Solutions/8_2/timethis.py @@ -0,0 +1,21 @@ +# timethis.py + +import time + +def timethis(func): + def wrapper(*args, **kwargs): + start = time.time() + try: + return func(*args,**kwargs) + finally: + end = time.time() + print("%s.%s : %f" % (func.__module__,func.__name__,end-start)) + return wrapper + +if __name__ == '__main__': + @timethis + def countdown(n): + while n > 0: + n-= 1 + + countdown(1000000) diff --git a/Solutions/8_2/typedproperty.py b/Solutions/8_2/typedproperty.py new file mode 100644 index 0000000..cda0872 --- /dev/null +++ b/Solutions/8_2/typedproperty.py @@ -0,0 +1,34 @@ +# typedproperty.py + +def typedproperty(name, expected_type): + private_name = '_' + name + @property + def prop(self): + return getattr(self, private_name) + + @prop.setter + def prop(self, value): + if not isinstance(value, expected_type): + raise TypeError(f'Expected {expected_type}') + setattr(self, private_name, value) + + return prop + +String = lambda name: typedproperty(name, str) +Integer = lambda name: typedproperty(name, int) +Float = lambda name: typedproperty(name, float) + +# Example +if __name__ == '__main__': + class Stock(object): + name = typedproperty('name', str) + shares = typedproperty('shares', int) + price = typedproperty('price', float) + + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + + diff --git a/Solutions/9_3/porty-app/README.txt b/Solutions/9_3/porty-app/README.txt new file mode 100644 index 0000000..4d96adc --- /dev/null +++ b/Solutions/9_3/porty-app/README.txt @@ -0,0 +1,17 @@ +Code from Practical Python. + +The "porty" directory is a Python package of code that's loaded via +import. The "print-report.py" program is a top-level script that +produces a report. Try it: + +shell % python3 print-report.py portfolio.csv prices.csv txt + Name Shares Price Change +---------- ---------- ---------- ---------- + AA 100 9.22 -22.98 + IBM 50 106.28 15.18 + CAT 150 35.46 -47.98 + MSFT 200 20.89 -30.34 + GE 95 13.48 -26.89 + MSFT 50 20.89 -44.21 + IBM 100 106.28 35.84 +shell % diff --git a/Solutions/9_3/porty-app/portfolio.csv b/Solutions/9_3/porty-app/portfolio.csv new file mode 100755 index 0000000..6c16f65 --- /dev/null +++ b/Solutions/9_3/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/Solutions/9_3/porty-app/porty/__init__.py b/Solutions/9_3/porty-app/porty/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Solutions/9_3/porty-app/porty/fileparse.py b/Solutions/9_3/porty-app/porty/fileparse.py new file mode 100644 index 0000000..8c6d5c1 --- /dev/null +++ b/Solutions/9_3/porty-app/porty/fileparse.py @@ -0,0 +1,47 @@ +# fileparse.py +import csv +import logging +log = logging.getLogger(__name__) + +def parse_csv(lines, select=None, types=None, has_headers=True, delimiter=',', silence_errors=False): + ''' + Parse a CSV file into a list of records with type conversion. + ''' + assert not (select and not has_headers), 'select requires column headers' + rows = csv.reader(lines, delimiter=delimiter) + + # Read the file headers (if any) + headers = next(rows) if has_headers else [] + + # If specific columns have been selected, make indices for filtering and set output columns + if select: + indices = [ headers.index(colname) for colname in select ] + headers = select + + records = [] + for rowno, row in enumerate(rows, 1): + if not row: # Skip rows with no data + continue + + # If specific column indices are selected, pick them out + if select: + row = [ row[index] for index in indices] + + # Apply type conversion to the row + if types: + try: + row = [func(val) for func, val in zip(types, row)] + except ValueError as e: + if not silence_errors: + log.warning("Row %d: Couldn't convert %s", rowno, row) + log.debug("Row %d: Reason %s", rowno, e) + continue + + # Make a dictionary or a tuple + if headers: + record = dict(zip(headers, row)) + else: + record = tuple(row) + records.append(record) + + return records diff --git a/Solutions/9_3/porty-app/porty/follow.py b/Solutions/9_3/porty-app/porty/follow.py new file mode 100644 index 0000000..1dec7bc --- /dev/null +++ b/Solutions/9_3/porty-app/porty/follow.py @@ -0,0 +1,17 @@ +# follow.py + +import time +import os + +def follow(filename): + ''' + Generator that produces a sequence of lines being written at the end of a file. + ''' + with open(filename,'r') as f: + f.seek(0,os.SEEK_END) + while True: + line = f.readline() + if line != '': + yield line + else: + time.sleep(0.1) # Sleep briefly to avoid busy wait diff --git a/Solutions/9_3/porty-app/porty/pcost.py b/Solutions/9_3/porty-app/porty/pcost.py new file mode 100644 index 0000000..8f43a3e --- /dev/null +++ b/Solutions/9_3/porty-app/porty/pcost.py @@ -0,0 +1,20 @@ +# pcost.py + +from . import report + +def portfolio_cost(filename): + ''' + Computes the total cost (shares*price) of a portfolio file + ''' + portfolio = report.read_portfolio(filename) + return portfolio.total_cost + +def main(args): + if len(args) != 2: + raise SystemExit('Usage: %s portfoliofile' % args[0]) + filename = args[1] + print('Total cost:', portfolio_cost(filename)) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/9_3/porty-app/porty/portfolio.py b/Solutions/9_3/porty-app/porty/portfolio.py new file mode 100644 index 0000000..0c9fbb1 --- /dev/null +++ b/Solutions/9_3/porty-app/porty/portfolio.py @@ -0,0 +1,51 @@ +# portfolio.py + +from . import fileparse +from . import stock + +class Portfolio(object): + def __init__(self): + self._holdings = [] + + @classmethod + def from_csv(cls, lines, **opts): + self = cls() + portdicts = fileparse.parse_csv(lines, + select=['name','shares','price'], + types=[str,int,float], + **opts) + + for d in portdicts: + self.append(stock.Stock(**d)) + + return self + + def append(self, holding): + self._holdings.append(holding) + + def __iter__(self): + return self._holdings.__iter__() + + def __len__(self): + return len(self._holdings) + + def __getitem__(self, index): + return self._holdings[index] + + def __contains__(self, name): + return any(s.name == name for s in self._holdings) + + @property + def total_cost(self): + return sum(s.shares * s.price for s in self._holdings) + + def tabulate_shares(self): + from collections import Counter + total_shares = Counter() + for s in self._holdings: + total_shares[s.name] += s.shares + return total_shares + + + + diff --git a/Solutions/9_3/porty-app/porty/report.py b/Solutions/9_3/porty-app/porty/report.py new file mode 100644 index 0000000..0131aad --- /dev/null +++ b/Solutions/9_3/porty-app/porty/report.py @@ -0,0 +1,67 @@ +# report.py + +from . import fileparse +from .stock import Stock +from .portfolio import Portfolio +from . import tableformat + +def read_portfolio(filename, **opts): + ''' + Read a stock portfolio file into a list of dictionaries with keys + name, shares, and price. + ''' + with open(filename) as lines: + return Portfolio.from_csv(lines, **opts) + +def read_prices(filename, **opts): + ''' + Read a CSV file of price data into a dict mapping names to prices. + ''' + with open(filename) as lines: + return dict(fileparse.parse_csv(lines, types=[str,float], has_headers=False, **opts)) + +def make_report(portfolio, prices): + ''' + Make a list of (name, shares, price, change) tuples given a portfolio list + and prices dictionary. + ''' + rows = [] + for s in portfolio: + current_price = prices[s.name] + change = current_price - s.price + summary = (s.name, s.shares, current_price, change) + rows.append(summary) + return rows + +def print_report(reportdata, formatter): + ''' + Print a nicely formated table from a list of (name, shares, price, change) tuples. + ''' + formatter.headings(['Name','Shares','Price','Change']) + for name, shares, price, change in reportdata: + rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] + formatter.row(rowdata) + +def portfolio_report(portfoliofile, pricefile, fmt='txt'): + ''' + Make a stock report given portfolio and price data files. + ''' + # Read data files + portfolio = read_portfolio(portfoliofile) + prices = read_prices(pricefile) + + # Create the report data + report = make_report(portfolio, prices) + + # Print it out + formatter = tableformat.create_formatter(fmt) + print_report(report, formatter) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfile pricefile format' % args[0]) + portfolio_report(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/9_3/porty-app/porty/stock.py b/Solutions/9_3/porty-app/porty/stock.py new file mode 100644 index 0000000..976d5d7 --- /dev/null +++ b/Solutions/9_3/porty-app/porty/stock.py @@ -0,0 +1,33 @@ +# stock.py + +from .typedproperty import String, Integer, Float + +class Stock(object): + ''' + An instance of a stock holding consisting of name, shares, and price. + ''' + if __debug__: + name = String('name') + shares = Integer('shares') + price = Float('price') + + def __init__(self,name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})' + + @property + def cost(self): + ''' + Return the cost as shares*price + ''' + return self.shares * self.price + + def sell(self, nshares): + ''' + Sell a number of shares and return the remaining number. + ''' + self.shares -= nshares diff --git a/Solutions/9_3/porty-app/porty/tableformat.py b/Solutions/9_3/porty-app/porty/tableformat.py new file mode 100644 index 0000000..df3d102 --- /dev/null +++ b/Solutions/9_3/porty-app/porty/tableformat.py @@ -0,0 +1,81 @@ +# tableformat.py + +class TableFormatter(object): + def headings(self, headers): + ''' + Emit the table headers + ''' + raise NotImplementedError() + + def row(self, rowdata): + ''' + Emit a single row of table data + ''' + raise NotImplementedError() + +class TextTableFormatter(TableFormatter): + ''' + Output data in plain-text format. + ''' + def headings(self, headers): + for h in headers: + print(f'{h:>10s}', end=' ') + print() + print(('-'*10 + ' ')*len(headers)) + + def row(self, rowdata): + for d in rowdata: + print(f'{d:>10s}', end=' ') + print() + +class CSVTableFormatter(TableFormatter): + ''' + Output data in CSV format. + ''' + def headings(self, headers): + print(','.join(headers)) + + def row(self, rowdata): + print(','.join(rowdata)) + +class HTMLTableFormatter(TableFormatter): + ''' + Output data in HTML format. + ''' + def headings(self, headers): + print('', end='') + for h in headers: + print(f'{h}', end='') + print('') + + def row(self, rowdata): + print('', end='') + for d in rowdata: + print(f'{d}', end='') + print('') + +class FormatError(Exception): + pass + +def create_formatter(name): + ''' + Create an appropriate formatter given an output format name + ''' + if name == 'txt': + return TextTableFormatter() + elif name == 'csv': + return CSVTableFormatter() + elif name == 'html': + return HTMLTableFormatter() + else: + raise FormatError(f'Unknown table format {name}') + +def print_table(objects, columns, formatter): + ''' + Make a nicely formatted table from a list of objects and attribute names. + ''' + formatter.headings(columns) + for obj in objects: + rowdata = [ str(getattr(obj, name)) for name in columns ] + formatter.row(rowdata) + diff --git a/Solutions/9_3/porty-app/porty/test_stock.py b/Solutions/9_3/porty-app/porty/test_stock.py new file mode 100644 index 0000000..b7bf691 --- /dev/null +++ b/Solutions/9_3/porty-app/porty/test_stock.py @@ -0,0 +1,28 @@ +# test_stock.py + +import unittest +from . import stock + +class TestStock(unittest.TestCase): + def test_create(self): + s = stock.Stock('GOOG', 100, 490.1) + self.assertEqual(s.name, 'GOOG') + self.assertEqual(s.shares, 100) + self.assertEqual(s.price, 490.1) + + def test_cost(self): + s = stock.Stock('GOOG', 100, 490.1) + self.assertEqual(s.cost, 49010.0) + + def test_sell(self): + s = stock.Stock('GOOG', 100, 490.1) + s.sell(25) + self.assertEqual(s.shares, 75) + + def test_shares_check(self): + s = stock.Stock('GOOG', 100, 490.1) + with self.assertRaises(TypeError): + s.shares = '100' + +if __name__ == '__main__': + unittest.main() diff --git a/Solutions/9_3/porty-app/porty/ticker.py b/Solutions/9_3/porty-app/porty/ticker.py new file mode 100644 index 0000000..4ac26bc --- /dev/null +++ b/Solutions/9_3/porty-app/porty/ticker.py @@ -0,0 +1,43 @@ +# ticker.py + +import csv +from . import report +from . import tableformat +from .follow import follow + +def select_columns(rows, indices): + for row in rows: + yield [row[index] for index in indices] + +def convert_types(rows, types): + for row in rows: + yield [func(val) for func, val in zip(types, row)] + +def make_dicts(rows, headers): + return (dict(zip(headers,row)) for row in rows) + +def parse_stock_data(lines): + rows = csv.reader(lines) + rows = select_columns(rows, [0, 1, 4]) + rows = convert_types(rows, [str,float,float]) + rows = make_dicts(rows, ['name','price','change']) + return rows + +def ticker(portfile, logfile, fmt): + portfolio = report.read_portfolio(portfile) + lines = follow(logfile) + rows = parse_stock_data(lines) + rows = (row for row in rows if row['name'] in portfolio) + formatter = tableformat.create_formatter(fmt) + formatter.headings(['Name','Price','Change']) + for row in rows: + formatter.row([ row['name'], f"{row['price']:0.2f}", f"{row['change']:0.2f}"] ) + +def main(args): + if len(args) != 4: + raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) + ticker(args[1], args[2], args[3]) + +if __name__ == '__main__': + import sys + main(sys.argv) diff --git a/Solutions/9_3/porty-app/porty/typedproperty.py b/Solutions/9_3/porty-app/porty/typedproperty.py new file mode 100644 index 0000000..cda0872 --- /dev/null +++ b/Solutions/9_3/porty-app/porty/typedproperty.py @@ -0,0 +1,34 @@ +# typedproperty.py + +def typedproperty(name, expected_type): + private_name = '_' + name + @property + def prop(self): + return getattr(self, private_name) + + @prop.setter + def prop(self, value): + if not isinstance(value, expected_type): + raise TypeError(f'Expected {expected_type}') + setattr(self, private_name, value) + + return prop + +String = lambda name: typedproperty(name, str) +Integer = lambda name: typedproperty(name, int) +Float = lambda name: typedproperty(name, float) + +# Example +if __name__ == '__main__': + class Stock(object): + name = typedproperty('name', str) + shares = typedproperty('shares', int) + price = typedproperty('price', float) + + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + + diff --git a/Solutions/9_3/porty-app/prices.csv b/Solutions/9_3/porty-app/prices.csv new file mode 100644 index 0000000..6bbcb20 --- /dev/null +++ b/Solutions/9_3/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/Solutions/9_3/porty-app/print-report.py b/Solutions/9_3/porty-app/print-report.py new file mode 100644 index 0000000..afe330a --- /dev/null +++ b/Solutions/9_3/porty-app/print-report.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +# print-report.py + +import sys +from porty.report import main +main(sys.argv) diff --git a/Solutions/README.md b/Solutions/README.md new file mode 100644 index 0000000..d3d8625 --- /dev/null +++ b/Solutions/README.md @@ -0,0 +1,7 @@ +# Solutions + +This directory contains solutions to selected exercises. The code is +written to run within this directory and has file paths set +accordingly. If you copy any of the code to the `Work/` directory, +you might need to adjust filenames. +