├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ └── code-issue.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── ch01 └── ch01.py ├── ch02 ├── decomposition.py ├── imports_practice.py ├── receipt.py ├── rock_paper_scissors.py └── sales_tax.py ├── ch03 ├── composition.py ├── declarative.py ├── functional.py ├── greeter.py ├── procedural.py └── reviews.py ├── ch04 ├── colors.py ├── counting.py ├── cpu_profiling.py ├── for_loops.py ├── generators.py └── timing.py ├── ch05 ├── cart.py ├── product.py ├── tax.py ├── test_cart.py ├── test_product.py └── test_tax.py ├── ch06 ├── bark.py ├── commands.py └── database.py ├── ch07 ├── bark.py ├── bicycle.py ├── bicycle_improved.py ├── commands.py └── database.py ├── ch08 ├── banking.py ├── bark.py ├── bicycle.py ├── birds.py ├── cats.py ├── commands.py ├── database.py ├── gastropods.py └── predators.py ├── ch09 ├── bark.py ├── book.py ├── commands.py ├── complexity.py ├── configuration.py └── database.py ├── ch10 ├── bark.py ├── book.py ├── bookmarks.db ├── commands.py ├── commands_coupled.py ├── commands_decoupled_messaging.py ├── database.py ├── persistence.py ├── search.py ├── search_reduced_coupling.py └── search_revisited.py └── cover.png /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Practices of the Python Pro 2 | 3 | Thank you for your interest in improving _Practices of the Python Pro_! 4 | Please read these guidelines to see how best to contribute to the book's success. 5 | 6 | ## Source code issues 7 | 8 | If you've discovered an issue with the code in this repository, please [open an issue](https://github.com/daneah/practices-of-the-python-pro/issues/new/choose). 9 | Or, if you have time, please consider forking this repo and opening a pull request with the fix! 10 | 11 | ## Errata 12 | 13 | If you've found a typo, inaccuracy, or other issue with the text of the book, please mention it in [the book forum](https://livebook.manning.com/book/practices-of-the-python-pro/). 14 | 15 | ## Questions 16 | 17 | If you just have a question or would like to discuss ideas from the book, [my Twitter account](https://twitter.com/easyaspython) is a good way to reach me! 18 | 19 | ## Code of conduct 20 | 21 | Please read the [code of conduct](../CODE_OF_CONDUCT.md) before contributing to this repository. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/code-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Code issue 3 | about: Identify an issue with the source code for an example or exercise 4 | title: A concise summary of the issue should go here 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | [Line(s) of code in question]() 11 | 12 | ## Issue 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description of issue 2 | 3 | 4 | 5 | ## Description of solution 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # JetBrains 107 | .idea/ 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@danehillard.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dane Hillard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exercises for Practices of the Python Pro 🐍📘 2 | 3 | Practices of the Python Pro, a Manning book by Dane Hillard 4 | 5 | This repository contains the source code for the examples and exercises contained in [Practices of the Python Pro](https://thepythonpro.com). 6 | The repository is a template repository, so if you'd like to follow along with the book you can [make your own copy](https://github.com/daneah/practices-of-the-python-pro/generate). 7 | 8 | Each chapter's examples are in their own directory. 9 | In some chapters, you'll find multiple snippets in a single module. 10 | These won't always produce output when you run them, and are occasionally meant only as snippets to demonstrate a concept. 11 | In later chapters, some modules act as an entrypoint to run a program from the command line, importing other modules along the way. 12 | Follow along in the book for more context! 13 | 14 | 15 | ## Errata and questions 16 | 17 | If you find an error in the code or the book, or if you have a question about the content, please read the [contribution guidelines](.github/CONTRIBUTING.md) to understand the best course of action. 18 | The errata are published on [the book's homepage](https://thepythonpro.com). 19 | -------------------------------------------------------------------------------- /ch01/ch01.py: -------------------------------------------------------------------------------- 1 | print(col1_name + ',' + col2_name + ',' + col3_name + ',' + col4_name) 2 | print(first_val + ',' + second_val + ',' + third_val + ',' + fourth_val) 3 | 4 | 5 | DELIMITER = '\t' 6 | print(DELIMITER.join([col1_name, col2_name, col3_name, col4_name])) 7 | print(DELIMITER.join([first_val, second_val, third_val, fourth_val])) 8 | 9 | 10 | """ 11 | >>> us_capitals_by_state = { # <1> 12 | 'Alabama': 'Montgomery', 13 | 'Alaska': 'Juneau', 14 | ... 15 | } 16 | >>> capitals = us_capitals_by_state.values() # <2> 17 | dict_values(['Montgomery', 'Juneau']) 18 | >>> capitals.sort() # <3> 19 | Traceback (most recent call last): 20 | File "", line 1, in 21 | AttributeError: 'dict_values' object has no attribute 'sort' 22 | >>> sorted(capitals) # <4> 23 | ['Albany', 'Annapolis', ...] 24 | """ 25 | 26 | 27 | def get_united_states_capitals(): # <1> 28 | us_capitals_by_state = {'Alabama': ...} 29 | capitals = us_capitals_by_state.values() 30 | return sorted(capitals) 31 | 32 | 33 | US_CAPITALS_BY_STATE = {'Alabama': 'Montgomery', ...} # <1> 34 | US_CAPITALS = sorted(US_CAPITALS_BY_STATE.values()) # <2> 35 | -------------------------------------------------------------------------------- /ch02/decomposition.py: -------------------------------------------------------------------------------- 1 | # Take 1 2 | names = ['Larry', 'Curly', 'Moe'] 3 | message = 'The Three Stooges: ' 4 | for index, name in enumerate(names): 5 | if index > 0: 6 | message += ', ' 7 | if index == len(names) - 1: 8 | message += 'and ' 9 | message += name 10 | print(message) 11 | 12 | 13 | # Repetition 14 | names = ['Moe', 'Larry', 'Shemp'] 15 | message = 'The Three Stooges: ' 16 | for index, name in enumerate(names): 17 | if index > 0: 18 | message += ', ' 19 | if index == len(names) - 1: 20 | message += 'and ' 21 | message += name 22 | print(message) 23 | 24 | names = ['Larry', 'Curly', 'Moe'] 25 | message = 'The Three Stooges: ' 26 | for index, name in enumerate(names): 27 | if index > 0: 28 | message += ', ' 29 | if index == len(names) - 1: 30 | message += 'and ' 31 | message += name 32 | print(message) 33 | 34 | 35 | # Extract function 36 | def introduce_stooges(names): # <1> 37 | message = 'The Three Stooges: ' 38 | for index, name in enumerate(names): 39 | if index > 0: 40 | message += ', ' 41 | if index == len(names) - 1: 42 | message += 'and ' 43 | message += name 44 | print(message) 45 | 46 | 47 | introduce_stooges(['Moe', 'Larry', 'Shemp']) # <2> 48 | introduce_stooges(['Larry', 'Curly', 'Moe']) 49 | 50 | 51 | # Generalize function 52 | def introduce(title, names): # <1> 53 | message = f'{title}: ' 54 | for index, name in enumerate(names): 55 | if index > 0: 56 | message += ', ' 57 | if index == len(names) - 1: 58 | message += 'and ' 59 | message += name 60 | print(message) 61 | 62 | 63 | introduce('The Three Stooges', ['Moe', 'Larry', 'Shemp']) # <2> 64 | introduce('The Three Stooges', ['Larry', 'Curly', 'Moe']) 65 | 66 | introduce( # <3> 67 | 'Teenage Mutant Ninja Turtles', 68 | ['Donatello', 'Raphael', 'Michaelangelo', 'Leonardo'] 69 | ) 70 | 71 | introduce('The Chipmunks', ['Alvin', 'Simon', 'Theodore']) 72 | 73 | 74 | # Extract further to separate concerns 75 | def join_names(names): # <1> 76 | name_string = '' 77 | 78 | for index, name in enumerate(names): 79 | if index > 0 and len(names) > 2: 80 | name_string += ',' 81 | if index > 0: 82 | name_string += ' ' 83 | if index == len(names) - 1 and len(names) > 1: 84 | name_string += 'and ' 85 | name_string += name 86 | return name_string 87 | 88 | 89 | def introduce(title, names): # <2> 90 | print(f'{title}: {join_names(names)}') 91 | -------------------------------------------------------------------------------- /ch02/imports_practice.py: -------------------------------------------------------------------------------- 1 | # Single import 2 | from time import time 3 | print(time()) 4 | 5 | 6 | # Different import 7 | from datetime import time 8 | print(time()) 9 | 10 | 11 | # Both imports at once 12 | from time import time 13 | from datetime import time 14 | print(time()) # <1> 15 | 16 | 17 | # Disambiguiation by importing whole modules 18 | import time 19 | import datetime 20 | now = time.time() # <1> 21 | midnight = datetime.time() # <2> 22 | 23 | 24 | # Disambiguation via renaming 25 | import datetime 26 | from mycoollibrary import datetime as cooldatetime 27 | -------------------------------------------------------------------------------- /ch02/receipt.py: -------------------------------------------------------------------------------- 1 | # Importing a single name 2 | from sales_tax import add_sales_tax # <1> 3 | 4 | 5 | def print_receipt(): 6 | total = ... 7 | state = ... 8 | print(f'TOTAL: {total}') 9 | print(f'AFTER TAX: {add_sales_tax(total, state)}') # <2> 10 | 11 | 12 | # Importing the full module 13 | import sales_tax 14 | 15 | 16 | def print_receipt(): 17 | total = ... 18 | locale = ... 19 | ... 20 | print(f'AFTER MILLAGE: {sales_tax.add_local_millage_tax(total, locale)}') 21 | -------------------------------------------------------------------------------- /ch02/rock_paper_scissors.py: -------------------------------------------------------------------------------- 1 | # Shoddy procedural code 2 | import random 3 | 4 | options = ['rock', 'paper', 'scissors'] 5 | print('(1) Rock\n(2) Paper\n(3) Scissors') 6 | human_choice = options[int(input('Enter the number of your choice: ')) - 1] 7 | print(f'You chose {human_choice}') 8 | computer_choice = random.choice(options) 9 | print(f'The computer chose {computer_choice}') 10 | if human_choice == 'rock': 11 | if computer_choice == 'paper': 12 | print('Sorry, paper beat rock') 13 | elif computer_choice == 'scissors': 14 | print('Yes, rock beat scissors!') 15 | else: 16 | print('Draw!') 17 | elif human_choice == 'paper': 18 | if computer_choice == 'scissors': 19 | print('Sorry, scissors beat paper') 20 | elif computer_choice == 'rock': 21 | print('Yes, paper beat rock!') 22 | else: 23 | print('Draw!') 24 | elif human_choice == 'scissors': 25 | if computer_choice == 'rock': 26 | print('Sorry, rock beat scissors') 27 | elif computer_choice == 'paper': 28 | print('Yes, scissors beat paper!') 29 | else: 30 | print('Draw!') 31 | 32 | 33 | # Code with extracted functions 34 | import random 35 | 36 | OPTIONS = ['rock', 'paper', 'scissors'] 37 | 38 | 39 | def get_computer_choice(): 40 | return random.choice(OPTIONS) 41 | 42 | 43 | def get_human_choice(): 44 | choice_number = int(input('Enter the number of your choice: ')) 45 | return OPTIONS[choice_number - 1] 46 | 47 | 48 | def print_options(): 49 | print('\n'.join(f'({i}) {option.title()}' for i, option in enumerate(OPTIONS, 1))) 50 | 51 | 52 | def print_choices(human_choice, computer_choice): 53 | print(f'You chose {human_choice}') 54 | print(f'The computer chose {computer_choice}') 55 | 56 | 57 | def print_win_lose(human_choice, computer_choice, human_beats, human_loses_to): 58 | if computer_choice == human_loses_to: 59 | print(f'Sorry, {computer_choice} beats {human_choice}') 60 | elif computer_choice == human_beats: 61 | print(f'Yes, {human_choice} beats {computer_choice}!') 62 | 63 | 64 | def print_result(human_choice, computer_choice): 65 | if human_choice == computer_choice: 66 | print('Draw!') 67 | 68 | if human_choice == 'rock': 69 | print_win_lose('rock', computer_choice, 'scissors', 'paper') 70 | elif human_choice == 'paper': 71 | print_win_lose('paper', computer_choice, 'rock', 'scissors') 72 | elif human_choice == 'scissors': 73 | print_win_lose('scissors', computer_choice, 'paper', 'rock') 74 | 75 | 76 | print_options() 77 | human_choice = get_human_choice() 78 | computer_choice = get_computer_choice() 79 | print_choices(human_choice, computer_choice) 80 | print_result(human_choice, computer_choice) 81 | 82 | 83 | # Code with extracted functions reprise 84 | import random 85 | 86 | OPTIONS = ['rock', 'paper', 'scissors'] 87 | 88 | 89 | def get_computer_choice(): # <1> 90 | return random.choice(OPTIONS) 91 | 92 | 93 | def get_human_choice(): 94 | choice_number = int(input('Enter the number of your choice: ')) 95 | return OPTIONS[choice_number - 1] 96 | 97 | 98 | def print_options(): 99 | print('\n'.join(f'({i}) {option.title()}' for i, option in enumerate(OPTIONS, 1))) 100 | 101 | 102 | def print_choices(human_choice, computer_choice): # <2> 103 | print(f'You chose {human_choice}') 104 | print(f'The computer chose {computer_choice}') 105 | 106 | 107 | def print_win_lose(human_choice, computer_choice, human_beats, human_loses_to): 108 | if computer_choice == human_loses_to: 109 | print(f'Sorry, {computer_choice} beats {human_choice}') 110 | elif computer_choice == human_beats: 111 | print(f'Yes, {human_choice} beats {computer_choice}!') 112 | 113 | 114 | def print_result(human_choice, computer_choice): # <3> 115 | if human_choice == computer_choice: 116 | print('Draw!') 117 | 118 | if human_choice == 'rock': 119 | print_win_lose('rock', computer_choice, 'scissors', 'paper') 120 | elif human_choice == 'paper': 121 | print_win_lose('paper', computer_choice, 'rock', 'scissors') 122 | elif human_choice == 'scissors': 123 | print_win_lose('scissors', computer_choice, 'paper', 'rock') 124 | 125 | 126 | # Moving functions into a class as methods 127 | import random 128 | 129 | OPTIONS = ['rock', 'paper', 'scissors'] 130 | 131 | 132 | class RockPaperScissorsSimulator: 133 | def __init__(self): 134 | self.computer_choice = None 135 | self.human_choice = None 136 | 137 | def get_computer_choice(self): # <1> 138 | return random.choice(OPTIONS) 139 | 140 | def get_human_choice(self): 141 | choice_number = int(input('Enter the number of your choice: ')) 142 | return OPTIONS[choice_number - 1] 143 | 144 | def print_options(self): 145 | print('\n'.join(f'({i}) {option.title()}' for i, option in enumerate(OPTIONS, 1))) 146 | 147 | def print_choices(self, human_choice, computer_choice): # <2> 148 | print(f'You chose {human_choice}') 149 | print(f'The computer chose {computer_choice}') 150 | 151 | def print_win_lose(self, human_choice, computer_choice, human_beats, human_loses_to): 152 | if computer_choice == human_loses_to: 153 | print(f'Sorry, {computer_choice} beats {human_choice}') 154 | elif computer_choice == human_beats: 155 | print(f'Yes, {human_choice} beats {computer_choice}!') 156 | 157 | def print_result(self, human_choice, computer_choice): 158 | if human_choice == computer_choice: 159 | print('Draw!') 160 | 161 | if human_choice == 'rock': 162 | self.print_win_lose('rock', computer_choice, 'scissors', 'paper') 163 | elif human_choice == 'paper': 164 | self.print_win_lose('paper', computer_choice, 'rock', 'scissors') 165 | elif human_choice == 'scissors': 166 | self.print_win_lose('scissors', computer_choice, 'paper', 'rock') 167 | 168 | def simulate(self): 169 | self.print_options() 170 | human_choice = self.get_human_choice() 171 | computer_choice = self.get_computer_choice() 172 | self.print_choices(human_choice, computer_choice) 173 | self.print_result(human_choice, computer_choice) 174 | 175 | 176 | # Using self to access attributes 177 | import random 178 | 179 | OPTIONS = ['rock', 'paper', 'scissors'] 180 | 181 | 182 | class RockPaperScissorsSimulator: 183 | def __init__(self): 184 | self.computer_choice = None 185 | self.human_choice = None 186 | 187 | def get_computer_choice(self): # <1> 188 | self.computer_choice = random.choice(OPTIONS) 189 | 190 | def get_human_choice(self): 191 | choice_number = int(input('Enter the number of your choice: ')) 192 | self.human_choice = OPTIONS[choice_number - 1] 193 | 194 | def print_options(self): 195 | print('\n'.join(f'({i}) {option.title()}' for i, option in enumerate(OPTIONS, 1))) 196 | 197 | def print_choices(self): # <2> 198 | print(f'You chose {self.human_choice}') # <3> 199 | print(f'The computer chose {self.computer_choice}') 200 | 201 | def print_win_lose(self, human_beats, human_loses_to): 202 | if self.computer_choice == human_loses_to: 203 | print(f'Sorry, {self.computer_choice} beats {self.human_choice}') 204 | elif self.computer_choice == human_beats: 205 | print(f'Yes, {self.human_choice} beats {self.computer_choice}!') 206 | 207 | def print_result(self): 208 | if self.human_choice == self.computer_choice: 209 | print('Draw!') 210 | 211 | if self.human_choice == 'rock': 212 | self.print_win_lose('scissors', 'paper') 213 | elif self.human_choice == 'paper': 214 | self.print_win_lose('rock', 'scissors') 215 | elif self.human_choice == 'scissors': 216 | self.print_win_lose('paper', 'rock') 217 | 218 | def simulate(self): 219 | self.print_options() 220 | self.get_human_choice() 221 | self.get_computer_choice() 222 | self.print_choices() 223 | self.print_result() 224 | 225 | 226 | # Running the simulation 227 | RPS = RockPaperScissorsSimulator() 228 | RPS.simulate() 229 | -------------------------------------------------------------------------------- /ch02/sales_tax.py: -------------------------------------------------------------------------------- 1 | # Take 1 2 | def add_sales_tax(total, tax_rate): 3 | return total * tax_rate 4 | 5 | 6 | # Take 2 7 | TAX_RATES_BY_STATE = { # <1> 8 | 'MI': 1.06, 9 | # ... 10 | } 11 | 12 | def add_sales_tax(total, state): 13 | return total * TAX_RATES_BY_STATE[state] # <2> 14 | 15 | 16 | # Take 3 17 | TAX_RATES_BY_STATE = { 18 | 'MI': 1.06, 19 | ... 20 | } 21 | 22 | def add_sales_tax(total, state): 23 | tax_rate = TAX_RATES_BY_STATE[state] # <1> 24 | return total * tax_rate # <2> -------------------------------------------------------------------------------- /ch03/composition.py: -------------------------------------------------------------------------------- 1 | # Creating a composable mixin 2 | class SpeakMixin: # <1> 3 | def speak(self): 4 | name = self.__class__.__name__.lower() 5 | print(f'The {name} says, "Hello!"') 6 | 7 | 8 | class RollOverMixin: # <2> 9 | def roll_over(self): 10 | print('Did a barrel roll!') 11 | 12 | 13 | class Dog(SpeakMixin, RollOverMixin): # <3> 14 | pass 15 | 16 | 17 | # Using the Dog class 18 | dog = Dog() 19 | dog.speak() 20 | dog.roll_over() 21 | -------------------------------------------------------------------------------- /ch03/declarative.py: -------------------------------------------------------------------------------- 1 | # Defining a scatterplot in a declarative style 2 | import plotly.graph_objects as go 3 | 4 | trace1 = go.Scatter( # <1> 5 | x=[1, 2, 3], # <2> 6 | y=[4, 5, 6], # <3> 7 | marker={'color': 'red', 'symbol': 104}, # <4> 8 | mode='markers+lines', # <5> 9 | text=['one', 'two', 'three'], # <6> 10 | name='1st Trace', 11 | ) 12 | 13 | 14 | # Defining a scatterplot in a procedural style 15 | trace1 = go.Scatter() 16 | trace1.set_x_data([1, 2, 3]) # <1> 17 | trace1.set_y_data([4, 5, 6]) 18 | trace1.set_marker_config({'color': 'red', 'symbol': 104, 'size': '10'}) 19 | trace1.set_mode('markers+lines') 20 | -------------------------------------------------------------------------------- /ch03/functional.py: -------------------------------------------------------------------------------- 1 | # Python for loop 2 | numbers = [1, 2, 3, 4, 5] 3 | for i in numbers: 4 | print(i * i) 5 | 6 | 7 | # Functional style 8 | from functools import reduce 9 | 10 | squares = map(lambda x: x * x, [1, 2, 3, 4, 5]) 11 | should = reduce(lambda x, y: x and y, [True, True, False]) 12 | evens = filter(lambda x: x % 2 == 0, [1, 2, 3, 4, 5]) 13 | 14 | 15 | # List comprehension style 16 | squares = [x * x for x in [1, 2, 3, 4, 5]] 17 | should = all([True, True, False]) 18 | evens = [x for x in [1, 2, 3, 4, 5] if x % 2 == 0] 19 | 20 | 21 | # Partial functions 22 | from functools import partial 23 | 24 | 25 | def pow(x, power=1): 26 | return x ** power 27 | 28 | 29 | square = partial(pow, power=2) # <1> 30 | cube = partial(pow, power=3) # <2> 31 | -------------------------------------------------------------------------------- /ch03/greeter.py: -------------------------------------------------------------------------------- 1 | # Class with somewhat unrelated methods 2 | from datetime import datetime 3 | 4 | 5 | class Greeter: 6 | def __init__(self, name): 7 | self.name = name 8 | 9 | def _day(self): # <1> 10 | return datetime.now().strftime('%A') 11 | 12 | def _part_of_day(self): # <2> 13 | current_hour = datetime.now().hour 14 | 15 | if current_hour < 12: 16 | part_of_day = 'morning' 17 | elif 12 <= current_hour < 17: 18 | part_of_day = 'afternoon' 19 | else: 20 | part_of_day = 'evening' 21 | 22 | return part_of_day 23 | 24 | def greet(self, store): # <3> 25 | print(f'Hi, my name is {self.name}, and welcome to {store}!') 26 | print(f'How\'s your {self._day()} {self._part_of_day()} going?') 27 | print('Here\'s a coupon for 20% off!') 28 | 29 | ... 30 | 31 | 32 | # Methods extracted as functions outside the class 33 | def day(): 34 | return datetime.now().strftime('%A') 35 | 36 | 37 | def part_of_day(): 38 | current_hour = datetime.now().hour 39 | 40 | if current_hour < 12: 41 | part_of_day = 'morning' 42 | elif 12 <= current_hour < 17: 43 | part_of_day = 'afternoon' 44 | else: 45 | part_of_day = 'evening' 46 | 47 | return part_of_day 48 | 49 | 50 | # Referencing extracted functions inside the class 51 | class Greeter: 52 | ... 53 | 54 | def greet(self, store): 55 | print(f'Hi, my name is {self.name}, and welcome to {store}!') 56 | print(f'How\'s your {day()} {part_of_day()} going?') 57 | print('Here\'s a coupon for 20% off!') 58 | -------------------------------------------------------------------------------- /ch03/procedural.py: -------------------------------------------------------------------------------- 1 | NAMES = ['Abby', 'Dave', 'Keira'] 2 | 3 | 4 | def print_greetings(): # <1> 5 | greeting_pattern = 'Say hi to {name}!' 6 | nice_person_pattern = '{name} is a nice person!' 7 | for name in NAMES: 8 | print(greeting_pattern.format(name=name)) 9 | print(nice_person_pattern.format(name=name)) 10 | -------------------------------------------------------------------------------- /ch03/reviews.py: -------------------------------------------------------------------------------- 1 | # A single procedure for splitting a paragraph into sentences and tokens 2 | import re 3 | 4 | product_review = '''This is a fine milk, but the product 5 | line appears to be limited in available colors. I 6 | could only find white.''' # <1> 7 | 8 | sentence_pattern = re.compile(r'(.*?\.)(\s|$)', re.DOTALL) # <2> 9 | matches = sentence_pattern.findall(product_review) # <3> 10 | sentences = [match[0] for match in matches] # <4> 11 | 12 | word_pattern = re.compile(r"([\w\-']+)([\s,.])?") # <5> 13 | for sentence in sentences: 14 | matches = word_pattern.findall(sentence) 15 | words = [match[0] for match in matches] # <6> 16 | print(words) 17 | 18 | 19 | # Sentence parsing with the pattern matching factored into a function 20 | import re 21 | 22 | 23 | def get_matches_for_pattern(pattern, string): # <1> 24 | matches = pattern.findall(string) 25 | return [match[0] for match in matches] 26 | 27 | 28 | product_review = '...' 29 | 30 | sentence_pattern = re.compile(r'(.*?\.)(\s|$)', re.DOTALL) 31 | sentences = get_matches_for_pattern( # <2> 32 | sentence_pattern, 33 | product_review, 34 | ) 35 | 36 | word_pattern = re.compile(r"([\w\-']+)([\s,.])?") 37 | for sentence in sentences: 38 | words = get_matches_for_pattern( # <3> 39 | word_pattern, 40 | sentence 41 | ) 42 | print(words) 43 | -------------------------------------------------------------------------------- /ch04/colors.py: -------------------------------------------------------------------------------- 1 | # Loading all colors into memory at once 2 | color_counts = {} 3 | 4 | with open('all-favorite-colors.txt') as favorite_colors_file: 5 | favorite_colors = favorite_colors_file.read().splitlines() # <1> 6 | 7 | for color in favorite_colors: 8 | if color in color_counts: 9 | color_counts[color] += 1 10 | else: 11 | color_counts[color] = 1 12 | 13 | 14 | # Loading a single color at a time into memory 15 | color_counts = {} 16 | 17 | with open('all-favorite-colors.txt') as favorite_colors_file: 18 | for color in favorite_colors_file: # <1> 19 | color = color.strip() # <2> 20 | 21 | if color in color_counts: 22 | color_counts[color] += 1 23 | else: 24 | color_counts[color] = 1 25 | 26 | 27 | # Using a set to store only unique colors seen 28 | all_colors = set() 29 | 30 | with open('all-favorite-colors.txt') as favorite_colors_file: 31 | for line in favorite_colors_file: # <1> 32 | all_colors.add(line.strip()) # <2> 33 | 34 | print('Amber Waves of Grain' in all_colors) # <3> 35 | -------------------------------------------------------------------------------- /ch04/counting.py: -------------------------------------------------------------------------------- 1 | # Take 1 2 | def get_number_with_highest_count(counts): # <1> 3 | max_count = 0 4 | for number, count in counts.items(): 5 | if count > max_count: 6 | max_count = count 7 | number_with_highest_count = number 8 | return number_with_highest_count 9 | 10 | 11 | def most_frequent(numbers): 12 | counts = {} 13 | for number in numbers: # <2> 14 | if number in counts: 15 | counts[number] += 1 16 | else: 17 | counts[number] = 1 18 | 19 | return get_number_with_highest_count(counts) 20 | 21 | 22 | # Using defaultdict 23 | from collections import defaultdict # <1> 24 | 25 | 26 | def get_number_with_highest_count(counts): 27 | max_count = 0 28 | for number, count in counts.items(): 29 | if count > max_count: 30 | max_count = count 31 | number_with_highest_count = number 32 | return number_with_highest_count 33 | 34 | 35 | def most_frequent(numbers): 36 | counts = defaultdict(int) # <2> 37 | for number in numbers: 38 | counts[number] += 1 # <3> 39 | 40 | return get_number_with_highest_count(counts) 41 | 42 | 43 | # Using Counter 44 | from collections import Counter # <1> 45 | 46 | 47 | def get_number_with_highest_count(counts): 48 | max_count = 0 49 | for number, count in counts.items(): 50 | if count > max_count: 51 | max_count = count 52 | number_with_highest_count = number 53 | return number_with_highest_count 54 | 55 | 56 | def most_frequent(numbers): 57 | counts = Counter(numbers) # <2> 58 | return get_number_with_highest_count(counts) 59 | 60 | 61 | # Using lambda functions 62 | from collections import Counter 63 | 64 | 65 | def get_number_with_highest_count(counts): 66 | return max( # <1> 67 | counts, 68 | key=lambda number: counts[number] 69 | ) 70 | 71 | 72 | def most_frequent(numbers): 73 | counts = Counter(numbers) 74 | return get_number_with_highest_count(counts) 75 | -------------------------------------------------------------------------------- /ch04/cpu_profiling.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | 4 | 5 | def an_expensive_function(): 6 | execution_time = random.random() / 100 # <1> 7 | time.sleep(execution_time) 8 | 9 | 10 | if __name__ == '__main__': 11 | for _ in range(1000): # <2> 12 | an_expensive_function() 13 | -------------------------------------------------------------------------------- /ch04/for_loops.py: -------------------------------------------------------------------------------- 1 | # A single for loop containing one expression 2 | names = ['Aliya', 'Beth', 'David', 'Kareem'] 3 | for name in names: 4 | print(name) 5 | 6 | 7 | # A single for loop containing multiple expressions 8 | names = ['Aliya', 'Beth', 'David', 'Kareem'] 9 | for name in names: 10 | greeting = 'Hi, my name is' 11 | print(f'{greeting} {name}') 12 | 13 | 14 | # Two for loops 15 | names = ['Aliya', 'Beth', 'David', 'Kareem'] 16 | for name in names: 17 | print(f'This is {name}!') 18 | 19 | message = 'Let\'s welcome ' 20 | for name in names: 21 | message += f'{name} ' 22 | print(message) 23 | 24 | 25 | # Nested for loops 26 | def has_duplicates(sequence): 27 | for index1, item1 in enumerate(sequence): # <1> 28 | for index2, item2 in enumerate(sequence): # <2> 29 | if item1 == item2 and index1 != index2: # <3> 30 | return True 31 | return False 32 | -------------------------------------------------------------------------------- /ch04/generators.py: -------------------------------------------------------------------------------- 1 | # How range works under the hood 2 | def range(*args): 3 | if len(args) == 1: # <1> 4 | start = 0 5 | stop = args[0] 6 | else: 7 | start = args[0] 8 | stop = args[1] 9 | 10 | current = start 11 | 12 | while current < stop: 13 | yield current # <2> 14 | current += 1 # <3> 15 | 16 | 17 | # Creating a generator for calculating squares of numbers in a list 18 | def squares(items): 19 | for item in items: 20 | yield item ** 2 21 | -------------------------------------------------------------------------------- /ch04/timing.py: -------------------------------------------------------------------------------- 1 | # Using timeit to measure code execution time 2 | from timeit import timeit 3 | 4 | setup = 'from datetime import datetime' # <1> 5 | statement = 'datetime.now()' # <2> 6 | result = timeit(setup=setup, stmt=statement, number=1_000) # <3> 7 | print(f'Took an average of {result / 1_000}s, or {result}ms') 8 | 9 | 10 | # Functions that need to be profiled to compare their performance 11 | import random 12 | 13 | 14 | def sort_expensive(): 15 | the_list = random.sample(range(1_000_000), 1_000) 16 | the_list.sort() 17 | 18 | 19 | def sort_cheap(): 20 | the_list = random.sample(range(1_000), 10) 21 | the_list.sort() 22 | 23 | 24 | if __name__ == '__main__': 25 | sort_expensive() 26 | for i in range(1000): 27 | sort_cheap() 28 | -------------------------------------------------------------------------------- /ch05/cart.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | 4 | class ShoppingCart: 5 | def __init__(self): 6 | self.products = defaultdict(lambda: defaultdict(int)) # <1> 7 | 8 | def add_product(self, product, quantity=1): # <2> 9 | self.products[product.generate_sku()]['quantity'] += quantity 10 | 11 | def remove_product(self, product, quantity=1): # <3> 12 | sku = product.generate_sku() 13 | self.products[sku]['quantity'] -= quantity 14 | if self.products[sku]['quantity'] == 0: 15 | del self.products[sku] 16 | 17 | """ 18 | To fix the bug: 19 | if self.products[sku]['quantity'] <= 0: 20 | del self.products[sku] 21 | """ 22 | -------------------------------------------------------------------------------- /ch05/product.py: -------------------------------------------------------------------------------- 1 | class Product: 2 | def __init__(self, name, size, color): # <1> 3 | self.name = name 4 | self.size = size 5 | self.color = color 6 | 7 | def transform_name_for_sku(self): 8 | return self.name.upper() 9 | 10 | def transform_color_for_sku(self): 11 | return self.color.upper() 12 | 13 | def generate_sku(self): # <2> 14 | """ 15 | Generates a SKU for this product. 16 | 17 | Example: 18 | >>> small_black_shoes = Product('shoes', 'S', 'black') 19 | >>> small_black_shoes.generate_sku() 20 | 'SHOES-S-BLACK' 21 | """ 22 | name = self.transform_name_for_sku() 23 | color = self.transform_color_for_sku() 24 | return f'{name}-{self.size}-{color}' 25 | -------------------------------------------------------------------------------- /ch05/tax.py: -------------------------------------------------------------------------------- 1 | from urllib.request import urlopen 2 | 3 | 4 | def add_sales_tax(original_amount, country, region): 5 | sales_tax_rate = urlopen(f'https://tax-api.com/{country}/{region}').read().decode() 6 | return original_amount * float(sales_tax_rate) 7 | -------------------------------------------------------------------------------- /ch05/test_cart.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from cart import ShoppingCart 4 | from product import Product 5 | 6 | 7 | class ShoppingCartTestCase(unittest.TestCase): # <1> 8 | def test_add_and_remove_product(self): 9 | cart = ShoppingCart() # <2> 10 | product = Product('shoes', 'S', 'blue') # <3> 11 | 12 | cart.add_product(product) # <4> 13 | cart.remove_product(product) # <5> 14 | 15 | self.assertDictEqual({}, cart.products) # <6> 16 | 17 | 18 | # Full test suite for product and shopping cart 19 | class ProductTestCase(unittest.TestCase): 20 | def test_transform_name_for_sku(self): 21 | small_black_shoes = Product('shoes', 'S', 'black') 22 | self.assertEqual( 23 | 'SHOES', 24 | small_black_shoes.transform_name_for_sku(), 25 | ) 26 | 27 | def test_transform_color_for_sku(self): 28 | small_black_shoes = Product('shoes', 'S', 'black') 29 | self.assertEqual( 30 | 'BLACK', 31 | small_black_shoes.transform_color_for_sku(), 32 | ) 33 | 34 | def test_generate_sku(self): 35 | small_black_shoes = Product('shoes', 'S', 'black') 36 | self.assertEqual( 37 | 'SHOES-S-BLACK', 38 | small_black_shoes.generate_sku(), 39 | ) 40 | 41 | 42 | class ShoppingCartTestCase(unittest.TestCase): 43 | def test_cart_initially_empty(self): 44 | cart = ShoppingCart() 45 | self.assertDictEqual({}, cart.products) 46 | 47 | def test_add_product(self): 48 | cart = ShoppingCart() 49 | product = Product('shoes', 'S', 'blue') 50 | 51 | cart.add_product(product) 52 | 53 | self.assertDictEqual({'SHOES-S-BLUE': {'quantity': 1}}, cart.products) 54 | 55 | def test_add_two_of_a_product(self): 56 | cart = ShoppingCart() 57 | product = Product('shoes', 'S', 'blue') 58 | 59 | cart.add_product(product, quantity=2) 60 | 61 | self.assertDictEqual({'SHOES-S-BLUE': {'quantity': 2}}, cart.products) 62 | 63 | def test_add_two_different_products(self): 64 | cart = ShoppingCart() 65 | product_one = Product('shoes', 'S', 'blue') 66 | product_two = Product('shirt', 'M', 'gray') 67 | 68 | cart.add_product(product_one) 69 | cart.add_product(product_two) 70 | 71 | self.assertDictEqual( 72 | { 73 | 'SHOES-S-BLUE': {'quantity': 1}, 74 | 'SHIRT-M-GRAY': {'quantity': 1} 75 | }, 76 | cart.products 77 | ) 78 | 79 | def test_add_and_remove_product(self): 80 | cart = ShoppingCart() 81 | product = Product('shoes', 'S', 'blue') 82 | 83 | cart.add_product(product) 84 | cart.remove_product(product) 85 | 86 | self.assertDictEqual({}, cart.products) 87 | 88 | def test_remove_too_many_products(self): 89 | cart = ShoppingCart() 90 | product = Product('shoes', 'S', 'blue') 91 | 92 | cart.add_product(product) 93 | cart.remove_product(product, quantity=2) 94 | 95 | self.assertDictEqual({}, cart.products) 96 | 97 | 98 | # Using pytest 99 | class TestProduct: # <1> 100 | def test_transform_name_for_sku(self): 101 | small_black_shoes = Product('shoes', 'S', 'black') 102 | assert small_black_shoes.transform_name_for_sku() == 'SHOES' # <2> 103 | 104 | def test_transform_color_for_sku(self): 105 | small_black_shoes = Product('shoes', 'S', 'black') 106 | assert small_black_shoes.transform_color_for_sku() == 'BLACK' 107 | 108 | def test_generate_sku(self): 109 | small_black_shoes = Product('shoes', 'S', 'black') 110 | assert small_black_shoes.generate_sku() == 'SHOES-S-BLACK' 111 | -------------------------------------------------------------------------------- /ch05/test_product.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from product import Product 4 | 5 | 6 | class ProductTestCase(unittest.TestCase): 7 | def test_working(self): 8 | pass 9 | 10 | def test_transform_name_for_sku(self): 11 | small_black_shoes = Product('shoes', 'S', 'black') # <1> 12 | expected_value = 'SHOES' # <2> 13 | actual_value = small_black_shoes.transform_name_for_sku() # <3> 14 | self.assertEqual(expected_value, actual_value) # <4> 15 | -------------------------------------------------------------------------------- /ch05/test_tax.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | from unittest import mock 4 | 5 | from tax import add_sales_tax 6 | 7 | 8 | class SalesTaxTestCase(unittest.TestCase): 9 | @mock.patch('tax.urlopen') # <1> 10 | def test_get_sales_tax_returns_proper_value_from_api( 11 | self, 12 | mock_urlopen # <2> 13 | ): 14 | test_tax_rate = 1.06 15 | mock_urlopen.return_value = io.BytesIO( # <3> 16 | str(test_tax_rate).encode('utf-8') 17 | ) 18 | 19 | self.assertEqual( 20 | 5 * test_tax_rate, 21 | add_sales_tax(5, 'USA', 'MI') 22 | ) # <4> 23 | -------------------------------------------------------------------------------- /ch06/bark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from collections import OrderedDict 5 | 6 | import commands 7 | 8 | 9 | def print_bookmarks(bookmarks): 10 | for bookmark in bookmarks: 11 | print('\t'.join( 12 | str(field) if field else '' 13 | for field in bookmark 14 | )) 15 | 16 | 17 | class Option: 18 | def __init__(self, name, command, prep_call=None): 19 | self.name = name # <1> 20 | self.command = command # <2> 21 | self.prep_call = prep_call # <3> 22 | 23 | def _handle_message(self, message): 24 | if isinstance(message, list): 25 | print_bookmarks(message) 26 | else: 27 | print(message) 28 | 29 | def choose(self): # <4> 30 | data = self.prep_call() if self.prep_call else None # <5> 31 | message = self.command.execute(data) if data else self.command.execute() # <6> 32 | self._handle_message(message) 33 | 34 | def __str__(self): # <7> 35 | return self.name 36 | 37 | 38 | def clear_screen(): 39 | clear = 'cls' if os.name == 'nt' else 'clear' 40 | os.system(clear) 41 | 42 | 43 | def print_options(options): 44 | for shortcut, option in options.items(): 45 | print(f'({shortcut}) {option}') 46 | print() 47 | 48 | 49 | def option_choice_is_valid(choice, options): 50 | return choice in options or choice.upper() in options # <1> 51 | 52 | 53 | def get_option_choice(options): 54 | choice = input('Choose an option: ') # <2> 55 | while not option_choice_is_valid(choice, options): # <3> 56 | print('Invalid choice') 57 | choice = input('Choose an option: ') 58 | return options[choice.upper()] # <4> 59 | 60 | 61 | def get_user_input(label, required=True): # <1> 62 | value = input(f'{label}: ') or None # <2> 63 | while required and not value: # <3> 64 | value = input(f'{label}: ') or None 65 | return value 66 | 67 | 68 | def get_new_bookmark_data(): # <4> 69 | return { 70 | 'title': get_user_input('Title'), 71 | 'url': get_user_input('URL'), 72 | 'notes': get_user_input('Notes', required=False), # <5> 73 | } 74 | 75 | 76 | def get_bookmark_id_for_deletion(): # <6> 77 | return get_user_input('Enter a bookmark ID to delete') 78 | 79 | 80 | def loop(): # <1> 81 | clear_screen() 82 | 83 | options = OrderedDict({ 84 | 'A': Option('Add a bookmark', commands.AddBookmarkCommand(), prep_call=get_new_bookmark_data), 85 | 'B': Option('List bookmarks by date', commands.ListBookmarksCommand()), 86 | 'T': Option('List bookmarks by title', commands.ListBookmarksCommand(order_by='title')), 87 | 'D': Option('Delete a bookmark', commands.DeleteBookmarkCommand(), prep_call=get_bookmark_id_for_deletion), 88 | 'Q': Option('Quit', commands.QuitCommand()), 89 | }) 90 | print_options(options) 91 | 92 | chosen_option = get_option_choice(options) 93 | clear_screen() 94 | chosen_option.choose() 95 | 96 | _ = input('Press ENTER to return to menu') # <2> 97 | 98 | 99 | if __name__ == '__main__': 100 | commands.CreateBookmarksTableCommand().execute() 101 | 102 | while True: # <3> 103 | loop() 104 | 105 | 106 | def for_listings_only(): 107 | options = { 108 | 'A': Option('Add a bookmark', commands.AddBookmarkCommand()), 109 | 'B': Option('List bookmarks by date', commands.ListBookmarksCommand()), 110 | 'T': Option('List bookmarks by title', commands.ListBookmarksCommand(order_by='title')), 111 | 'D': Option('Delete a bookmark', commands.DeleteBookmarkCommand()), 112 | 'Q': Option('Quit', commands.QuitCommand()), 113 | } 114 | print_options(options) 115 | -------------------------------------------------------------------------------- /ch06/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import datetime 3 | 4 | from database import DatabaseManager 5 | 6 | db = DatabaseManager('bookmarks.db') # <1> 7 | 8 | 9 | class CreateBookmarksTableCommand: 10 | def execute(self): # <2> 11 | db.create_table('bookmarks', { # <3> 12 | 'id': 'integer primary key autoincrement', 13 | 'title': 'text not null', 14 | 'url': 'text not null', 15 | 'notes': 'text', 16 | 'date_added': 'text not null', 17 | }) 18 | 19 | 20 | class AddBookmarkCommand: 21 | def execute(self, data): 22 | data['date_added'] = datetime.utcnow().isoformat() # <1> 23 | db.add('bookmarks', data) # <2> 24 | return 'Bookmark added!' # <3> 25 | 26 | 27 | class ListBookmarksCommand: 28 | def __init__(self, order_by='date_added'): # <1> 29 | self.order_by = order_by 30 | 31 | def execute(self): 32 | return db.select('bookmarks', order_by=self.order_by).fetchall() # <2> 33 | 34 | 35 | class DeleteBookmarkCommand: 36 | def execute(self, data): 37 | db.delete('bookmarks', {'id': data}) # <1> 38 | return 'Bookmark deleted!' 39 | 40 | 41 | class QuitCommand: 42 | def execute(self): 43 | sys.exit() # <1> 44 | -------------------------------------------------------------------------------- /ch06/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class DatabaseManager: 5 | def __init__(self, database_filename): 6 | self.connection = sqlite3.connect(database_filename) # <1> 7 | 8 | def __del__(self): 9 | self.connection.close() # <2> 10 | 11 | def _execute(self, statement, values=None): # <1> 12 | with self.connection: 13 | cursor = self.connection.cursor() 14 | cursor.execute(statement, values or []) # <2> 15 | return cursor 16 | 17 | def create_table(self, table_name, columns): 18 | columns_with_types = [ # <1> 19 | f'{column_name} {data_type}' 20 | for column_name, data_type in columns.items() 21 | ] 22 | self._execute( # <2> 23 | f''' 24 | CREATE TABLE IF NOT EXISTS {table_name} 25 | ({', '.join(columns_with_types)}); 26 | ''' 27 | ) 28 | 29 | def drop_table(self, table_name): 30 | self._execute(f'DROP TABLE {table_name};') 31 | 32 | def add(self, table_name, data): 33 | placeholders = ', '.join('?' * len(data)) 34 | column_names = ', '.join(data.keys()) # <1> 35 | column_values = tuple(data.values()) # <2> 36 | 37 | self._execute( 38 | f''' 39 | INSERT INTO {table_name} 40 | ({column_names}) 41 | VALUES ({placeholders}); 42 | ''', 43 | column_values, # <3> 44 | ) 45 | 46 | def delete(self, table_name, criteria): # <1> 47 | placeholders = [f'{column} = ?' for column in criteria.keys()] 48 | delete_criteria = ' AND '.join(placeholders) 49 | self._execute( 50 | f''' 51 | DELETE FROM {table_name} 52 | WHERE {delete_criteria}; 53 | ''', 54 | tuple(criteria.values()), # <2> 55 | ) 56 | 57 | def select(self, table_name, criteria=None, order_by=None): 58 | criteria = criteria or {} # <1> 59 | 60 | query = f'SELECT * FROM {table_name}' 61 | 62 | if criteria: # <2> 63 | placeholders = [f'{column} = ?' for column in criteria.keys()] 64 | select_criteria = ' AND '.join(placeholders) 65 | query += f' WHERE {select_criteria}' 66 | 67 | if order_by: # <3> 68 | query += f' ORDER BY {order_by}' 69 | 70 | return self._execute( # <4> 71 | query, 72 | tuple(criteria.values()), 73 | ) 74 | -------------------------------------------------------------------------------- /ch07/bark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from collections import OrderedDict 5 | 6 | import commands 7 | 8 | 9 | def print_bookmarks(bookmarks): 10 | for bookmark in bookmarks: 11 | print('\t'.join( 12 | str(field) if field else '' 13 | for field in bookmark 14 | )) 15 | 16 | 17 | class Option: 18 | def __init__(self, name, command, prep_call=None): 19 | self.name = name 20 | self.command = command 21 | self.prep_call = prep_call 22 | 23 | def _handle_message(self, message): 24 | if isinstance(message, list): 25 | print_bookmarks(message) 26 | else: 27 | print(message) 28 | 29 | def choose(self): 30 | data = self.prep_call() if self.prep_call else None 31 | message = self.command.execute(data) if data else self.command.execute() 32 | self._handle_message(message) 33 | 34 | def __str__(self): 35 | return self.name 36 | 37 | 38 | def clear_screen(): 39 | clear = 'cls' if os.name == 'nt' else 'clear' 40 | os.system(clear) 41 | 42 | 43 | def print_options(options): 44 | for shortcut, option in options.items(): 45 | print(f'({shortcut}) {option}') 46 | print() 47 | 48 | 49 | def option_choice_is_valid(choice, options): 50 | return choice in options or choice.upper() in options 51 | 52 | 53 | def get_option_choice(options): 54 | choice = input('Choose an option: ') 55 | while not option_choice_is_valid(choice, options): 56 | print('Invalid choice') 57 | choice = input('Choose an option: ') 58 | return options[choice.upper()] 59 | 60 | 61 | def get_user_input(label, required=True): 62 | value = input(f'{label}: ') or None 63 | while required and not value: 64 | value = input(f'{label}: ') or None 65 | return value 66 | 67 | 68 | def get_new_bookmark_data(): 69 | return { 70 | 'title': get_user_input('Title'), 71 | 'url': get_user_input('URL'), 72 | 'notes': get_user_input('Notes', required=False), 73 | } 74 | 75 | 76 | def get_bookmark_id_for_deletion(): 77 | return get_user_input('Enter a bookmark ID to delete') 78 | 79 | 80 | def get_github_import_options(): # <1> 81 | return { 82 | 'github_username': get_user_input('GitHub username'), 83 | 'preserve_timestamps': # <2> 84 | get_user_input( 85 | 'Preserve timestamps [Y/n]', 86 | required=False 87 | ) in {'Y', 'y', None}, # <3> 88 | } 89 | 90 | 91 | def get_new_bookmark_info(): 92 | bookmark_id = get_user_input('Enter a bookmark ID to edit') 93 | field = get_user_input('Choose a value to edit (title, URL, notes)') 94 | new_value = get_user_input(f'Enter the new value for {field}') 95 | return { 96 | 'id': bookmark_id, 97 | 'update': {field: new_value}, 98 | } 99 | 100 | 101 | def loop(): 102 | clear_screen() 103 | 104 | options = OrderedDict({ 105 | 'A': Option('Add a bookmark', commands.AddBookmarkCommand(), prep_call=get_new_bookmark_data), 106 | 'B': Option('List bookmarks by date', commands.ListBookmarksCommand()), 107 | 'T': Option('List bookmarks by title', commands.ListBookmarksCommand(order_by='title')), 108 | 'E': Option('Edit a bookmark', commands.EditBookmarkCommand(), prep_call=get_new_bookmark_info), 109 | 'D': Option('Delete a bookmark', commands.DeleteBookmarkCommand(), prep_call=get_bookmark_id_for_deletion), 110 | 'G': Option( # <4> 111 | 'Import GitHub stars', 112 | commands.ImportGitHubStarsCommand(), 113 | prep_call=get_github_import_options 114 | ), 115 | 'Q': Option('Quit', commands.QuitCommand()), 116 | }) 117 | print_options(options) 118 | 119 | chosen_option = get_option_choice(options) 120 | clear_screen() 121 | chosen_option.choose() 122 | 123 | _ = input('Press ENTER to return to menu') 124 | 125 | 126 | if __name__ == '__main__': 127 | commands.CreateBookmarksTableCommand().execute() 128 | 129 | while True: 130 | loop() 131 | -------------------------------------------------------------------------------- /ch07/bicycle.py: -------------------------------------------------------------------------------- 1 | class Tire: # <1> 2 | def __repr__(self): 3 | return 'A rubber tire' 4 | 5 | 6 | class Frame: 7 | def __repr__(self): 8 | return 'An aluminum frame' 9 | 10 | 11 | class Bicycle: 12 | def __init__(self): # <2> 13 | self.front_tire = Tire() 14 | self.back_tire = Tire() 15 | self.frame = Frame() 16 | 17 | def print_specs(self): # <3> 18 | print(f'Frame: {self.frame}') 19 | print(f'Front tire: {self.front_tire}, back tire: {self.back_tire}') 20 | 21 | 22 | if __name__ == '__main__': # <4> 23 | bike = Bicycle() 24 | bike.print_specs() 25 | -------------------------------------------------------------------------------- /ch07/bicycle_improved.py: -------------------------------------------------------------------------------- 1 | class Tire: 2 | def __repr__(self): 3 | return 'A rubber tire' 4 | 5 | 6 | class Frame: 7 | def __repr__(self): 8 | return 'An aluminum frame' 9 | 10 | 11 | class CarbonFiberFrame: 12 | def __repr__(self): 13 | return 'A carbon fiber frame' 14 | 15 | 16 | class Bicycle: 17 | def __init__(self, front_tire, back_tire, frame): # <1> 18 | self.front_tire = front_tire 19 | self.back_tire = back_tire 20 | self.frame = frame 21 | 22 | def print_specs(self): 23 | print(f'Frame: {self.frame}') 24 | print(f'Front tire: {self.front_tire}, back tire: {self.back_tire}') 25 | 26 | 27 | if __name__ == '__main__': 28 | bike = Bicycle(Tire(), Tire(), Frame()) # <2> 29 | bike.print_specs() 30 | 31 | bike = Bicycle(Tire(), Tire(), CarbonFiberFrame()) # <1> 32 | bike.print_specs() # <2> 33 | -------------------------------------------------------------------------------- /ch07/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import datetime 3 | 4 | import requests 5 | 6 | from database import DatabaseManager 7 | 8 | db = DatabaseManager('bookmarks.db') 9 | 10 | 11 | class CreateBookmarksTableCommand: 12 | def execute(self): 13 | db.create_table('bookmarks', { 14 | 'id': 'integer primary key autoincrement', 15 | 'title': 'text not null', 16 | 'url': 'text not null', 17 | 'notes': 'text', 18 | 'date_added': 'text not null', 19 | }) 20 | 21 | 22 | class AddBookmarkCommand: 23 | def execute(self, data, timestamp=None): # <1> 24 | data['date_added'] = timestamp or datetime.utcnow().isoformat() # <2> 25 | db.add('bookmarks', data) 26 | return 'Bookmark added!' 27 | 28 | 29 | class ListBookmarksCommand: 30 | def __init__(self, order_by='date_added'): 31 | self.order_by = order_by 32 | 33 | def execute(self): 34 | return db.select('bookmarks', order_by=self.order_by).fetchall() 35 | 36 | 37 | class DeleteBookmarkCommand: 38 | def execute(self, data): 39 | db.delete('bookmarks', {'id': data}) 40 | return 'Bookmark deleted!' 41 | 42 | 43 | class QuitCommand: 44 | def execute(self): 45 | sys.exit() 46 | 47 | 48 | class ImportGitHubStarsCommand: 49 | def _extract_bookmark_info(self, repo): # <1> 50 | return { 51 | 'title': repo['name'], 52 | 'url': repo['html_url'], 53 | 'notes': repo['description'], 54 | } 55 | 56 | def execute(self, data): 57 | bookmarks_imported = 0 58 | 59 | github_username = data['github_username'] 60 | next_page_of_results = f'https://api.github.com/users/{github_username}/starred' # <2> 61 | 62 | while next_page_of_results: # <3> 63 | stars_response = requests.get( # <4> 64 | next_page_of_results, 65 | headers={'Accept': 'application/vnd.github.v3.star+json'}, 66 | ) 67 | next_page_of_results = stars_response.links.get('next', {}).get('url') # <5> 68 | 69 | for repo_info in stars_response.json(): 70 | repo = repo_info['repo'] # <6> 71 | 72 | if data['preserve_timestamps']: 73 | timestamp = datetime.strptime( 74 | repo_info['starred_at'], # <7> 75 | '%Y-%m-%dT%H:%M:%SZ' # <8> 76 | ) 77 | else: 78 | timestamp = None 79 | 80 | bookmarks_imported += 1 81 | AddBookmarkCommand().execute( # <9> 82 | self._extract_bookmark_info(repo), 83 | timestamp=timestamp, 84 | ) 85 | 86 | return f'Imported {bookmarks_imported} bookmarks from starred repos!' # <10> 87 | 88 | 89 | class EditBookmarkCommand: 90 | def execute(self, data): 91 | db.update( 92 | 'bookmarks', 93 | {'id': data['id']}, 94 | data['update'], 95 | ) 96 | return 'Bookmark updated!' 97 | -------------------------------------------------------------------------------- /ch07/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class DatabaseManager: 5 | def __init__(self, database_filename): 6 | self.connection = sqlite3.connect(database_filename) # <1> 7 | 8 | def __del__(self): 9 | self.connection.close() # <2> 10 | 11 | def _execute(self, statement, values=None): # <1> 12 | with self.connection: 13 | cursor = self.connection.cursor() 14 | cursor.execute(statement, values or []) # <2> 15 | return cursor 16 | 17 | def create_table(self, table_name, columns): 18 | columns_with_types = [ # <1> 19 | f'{column_name} {data_type}' 20 | for column_name, data_type in columns.items() 21 | ] 22 | self._execute( # <2> 23 | f''' 24 | CREATE TABLE IF NOT EXISTS {table_name} 25 | ({', '.join(columns_with_types)}); 26 | ''' 27 | ) 28 | 29 | def drop_table(self, table_name): 30 | self._execute(f'DROP TABLE {table_name};') 31 | 32 | def add(self, table_name, data): 33 | placeholders = ', '.join('?' * len(data)) 34 | column_names = ', '.join(data.keys()) # <1> 35 | column_values = tuple(data.values()) # <2> 36 | 37 | self._execute( 38 | f''' 39 | INSERT INTO {table_name} 40 | ({column_names}) 41 | VALUES ({placeholders}); 42 | ''', 43 | column_values, # <3> 44 | ) 45 | 46 | def delete(self, table_name, criteria): # <1> 47 | placeholders = [f'{column} = ?' for column in criteria.keys()] 48 | delete_criteria = ' AND '.join(placeholders) 49 | self._execute( 50 | f''' 51 | DELETE FROM {table_name} 52 | WHERE {delete_criteria}; 53 | ''', 54 | tuple(criteria.values()), # <2> 55 | ) 56 | 57 | def select(self, table_name, criteria=None, order_by=None): 58 | criteria = criteria or {} # <1> 59 | 60 | query = f'SELECT * FROM {table_name}' 61 | 62 | if criteria: # <2> 63 | placeholders = [f'{column} = ?' for column in criteria.keys()] 64 | select_criteria = ' AND '.join(placeholders) 65 | query += f' WHERE {select_criteria}' 66 | 67 | if order_by: # <3> 68 | query += f' ORDER BY {order_by}' 69 | 70 | return self._execute( # <4> 71 | query, 72 | tuple(criteria.values()), 73 | ) 74 | 75 | def update(self, table_name, criteria, data): 76 | update_placeholders = [f'{column} = ?' for column in criteria.keys()] 77 | update_criteria = ' AND '.join(update_placeholders) 78 | 79 | data_placeholders = ', '.join(f'{key} = ?' for key in data.keys()) 80 | 81 | values = tuple(data.values()) + tuple(criteria.values()) 82 | 83 | self._execute( 84 | f''' 85 | UPDATE {table_name} 86 | SET {data_placeholders} 87 | WHERE {update_criteria}; 88 | ''', 89 | values, 90 | ) 91 | -------------------------------------------------------------------------------- /ch08/banking.py: -------------------------------------------------------------------------------- 1 | class Teller: 2 | def deposit(self, amount, account): 3 | account.deposit(amount) 4 | 5 | 6 | class CorruptTeller(Teller): # <1> 7 | def __init__(self): 8 | self.coffers = 0 9 | 10 | def deposit(self, amount, account): # <2> 11 | self.coffers += amount * 0.01 # <3> 12 | super().deposit(amount * 0.99, account) # <4> 13 | -------------------------------------------------------------------------------- /ch08/bark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from collections import OrderedDict 5 | 6 | import commands 7 | 8 | 9 | def print_bookmarks(bookmarks): 10 | for bookmark in bookmarks: 11 | print('\t'.join( 12 | str(field) if field else '' 13 | for field in bookmark 14 | )) 15 | 16 | 17 | class Option: 18 | def __init__(self, name, command, prep_call=None): 19 | self.name = name 20 | self.command = command 21 | self.prep_call = prep_call 22 | 23 | def _handle_message(self, message): 24 | if isinstance(message, list): 25 | print_bookmarks(message) 26 | else: 27 | print(message) 28 | 29 | def choose(self): 30 | data = self.prep_call() if self.prep_call else None 31 | message = self.command.execute(data) # <1> 32 | self._handle_message(message) 33 | 34 | def __str__(self): 35 | return self.name 36 | 37 | 38 | def clear_screen(): 39 | clear = 'cls' if os.name == 'nt' else 'clear' 40 | os.system(clear) 41 | 42 | 43 | def print_options(options): 44 | for shortcut, option in options.items(): 45 | print(f'({shortcut}) {option}') 46 | print() 47 | 48 | 49 | def option_choice_is_valid(choice, options): 50 | return choice in options or choice.upper() in options 51 | 52 | 53 | def get_option_choice(options): 54 | choice = input('Choose an option: ') 55 | while not option_choice_is_valid(choice, options): 56 | print('Invalid choice') 57 | choice = input('Choose an option: ') 58 | return options[choice.upper()] 59 | 60 | 61 | def get_user_input(label, required=True): 62 | value = input(f'{label}: ') or None 63 | while required and not value: 64 | value = input(f'{label}: ') or None 65 | return value 66 | 67 | 68 | def get_new_bookmark_data(): 69 | return { 70 | 'title': get_user_input('Title'), 71 | 'url': get_user_input('URL'), 72 | 'notes': get_user_input('Notes', required=False), 73 | } 74 | 75 | 76 | def get_bookmark_id_for_deletion(): 77 | return get_user_input('Enter a bookmark ID to delete') 78 | 79 | 80 | def get_github_import_options(): 81 | return { 82 | 'github_username': get_user_input('GitHub username'), 83 | 'preserve_timestamps': 84 | get_user_input( 85 | 'Preserve timestamps [Y/n]', 86 | required=False 87 | ) in {'Y', 'y', None}, 88 | } 89 | 90 | 91 | def get_new_bookmark_info(): 92 | bookmark_id = get_user_input('Enter a bookmark ID to edit') 93 | field = get_user_input('Choose a value to edit (title, URL, notes)') 94 | new_value = get_user_input(f'Enter the new value for {field}') 95 | return { 96 | 'id': bookmark_id, 97 | 'update': {field: new_value}, 98 | } 99 | 100 | 101 | def loop(): 102 | clear_screen() 103 | 104 | options = OrderedDict({ 105 | 'A': Option('Add a bookmark', commands.AddBookmarkCommand(), prep_call=get_new_bookmark_data), 106 | 'B': Option('List bookmarks by date', commands.ListBookmarksCommand()), 107 | 'T': Option('List bookmarks by title', commands.ListBookmarksCommand(order_by='title')), 108 | 'E': Option('Edit a bookmark', commands.EditBookmarkCommand(), prep_call=get_new_bookmark_info), 109 | 'D': Option('Delete a bookmark', commands.DeleteBookmarkCommand(), prep_call=get_bookmark_id_for_deletion), 110 | 'G': Option( 111 | 'Import GitHub stars', 112 | commands.ImportGitHubStarsCommand(), 113 | prep_call=get_github_import_options 114 | ), 115 | 'Q': Option('Quit', commands.QuitCommand()), 116 | }) 117 | print_options(options) 118 | 119 | chosen_option = get_option_choice(options) 120 | clear_screen() 121 | chosen_option.choose() 122 | 123 | _ = input('Press ENTER to return to menu') 124 | 125 | 126 | if __name__ == '__main__': 127 | commands.CreateBookmarksTableCommand().execute() 128 | 129 | while True: 130 | loop() 131 | -------------------------------------------------------------------------------- /ch08/bicycle.py: -------------------------------------------------------------------------------- 1 | class Tire: 2 | def __repr__(self): 3 | return 'A rubber tire' 4 | 5 | 6 | class FancyTire(Tire): 7 | def __repr__(self): 8 | return 'A fancy tire' 9 | 10 | 11 | class Frame: 12 | def __repr__(self): 13 | return 'A bicycle frame' 14 | 15 | 16 | class AluminumFrame(Frame): 17 | def __repr__(self): 18 | return 'An aluminum frame' 19 | 20 | 21 | class CarbonFiberFrame(Frame): 22 | def __repr__(self): 23 | return 'A carbon fiber frame' 24 | 25 | 26 | class Bicycle: 27 | def __init__(self, front_tire, back_tire, frame): 28 | self.front_tire = front_tire 29 | self.back_tire = back_tire 30 | self.frame = frame 31 | 32 | def print_specs(self): 33 | print(f'Frame: {self.frame}') 34 | print(f'Front tire: {self.front_tire}, back tire: {self.back_tire}') 35 | 36 | 37 | if __name__ == '__main__': 38 | bike = Bicycle(Tire(), Tire(), AluminumFrame()) 39 | bike.print_specs() 40 | 41 | bike = Bicycle(FancyTire(), FancyTire(), CarbonFiberFrame()) 42 | bike.print_specs() 43 | -------------------------------------------------------------------------------- /ch08/birds.py: -------------------------------------------------------------------------------- 1 | class Bird: 2 | def fly(self): 3 | print('flying!') 4 | 5 | 6 | class Hummingbird(Bird): 7 | def fly(self): 8 | print('zzzzzooommm!') 9 | 10 | 11 | class Penguin(Bird): 12 | def fly(self): 13 | print('no can do.') 14 | -------------------------------------------------------------------------------- /ch08/cats.py: -------------------------------------------------------------------------------- 1 | class BigCat: 2 | def eats(self): 3 | return ['rodents'] 4 | 5 | 6 | class Lion(BigCat): # <1> 7 | def eats(self): 8 | return ['wildebeest'] 9 | # return super().eats() + ['wildebeest'] # cooperative version 10 | 11 | 12 | class Tiger(BigCat): # <2> 13 | def eats(self): 14 | return ['water buffalo'] 15 | # return super().eats() + ['water buffalo'] # cooperative version 16 | 17 | 18 | class Liger(Lion, Tiger): # <3> 19 | def eats(self): 20 | return super().eats() + ['rabbit', 'cow', 'pig', 'chicken'] 21 | 22 | 23 | if __name__ == '__main__': 24 | lion = Lion() 25 | print('The lion eats', lion.eats()) 26 | tiger = Tiger() 27 | print('The tiger eats', tiger.eats()) 28 | liger = Liger() 29 | print('The liger eats', liger.eats()) 30 | -------------------------------------------------------------------------------- /ch08/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from abc import ABC, abstractmethod # <1> 3 | from datetime import datetime 4 | 5 | import requests 6 | 7 | from database import DatabaseManager 8 | 9 | db = DatabaseManager('bookmarks.db') 10 | 11 | 12 | class Command(ABC): # <2> 13 | @abstractmethod 14 | def execute(self, data): # <3> 15 | raise NotImplementedError('Commands must implement an execute method') 16 | 17 | 18 | class CreateBookmarksTableCommand(Command): # <4> 19 | def execute(self, data=None): # <5> 20 | db.create_table('bookmarks', { 21 | 'id': 'integer primary key autoincrement', 22 | 'title': 'text not null', 23 | 'url': 'text not null', 24 | 'notes': 'text', 25 | 'date_added': 'text not null', 26 | }) 27 | 28 | 29 | class AddBookmarkCommand(Command): # <6> 30 | def execute(self, data, timestamp=None): 31 | data['date_added'] = timestamp or datetime.utcnow().isoformat() 32 | db.add('bookmarks', data) 33 | return 'Bookmark added!' 34 | 35 | 36 | class ListBookmarksCommand(Command): 37 | def __init__(self, order_by='date_added'): 38 | self.order_by = order_by 39 | 40 | def execute(self, data=None): 41 | return db.select('bookmarks', order_by=self.order_by).fetchall() 42 | 43 | 44 | class DeleteBookmarkCommand(Command): 45 | def execute(self, data): 46 | db.delete('bookmarks', {'id': data}) 47 | return 'Bookmark deleted!' 48 | 49 | 50 | class QuitCommand(Command): 51 | def execute(self, data=None): 52 | sys.exit() 53 | 54 | 55 | class ImportGitHubStarsCommand(Command): 56 | def _extract_bookmark_info(self, repo): 57 | return { 58 | 'title': repo['name'], 59 | 'url': repo['html_url'], 60 | 'notes': repo['description'], 61 | } 62 | 63 | def execute(self, data): 64 | bookmarks_imported = 0 65 | 66 | github_username = data['github_username'] 67 | next_page_of_results = f'https://api.github.com/users/{github_username}/starred' 68 | 69 | while next_page_of_results: 70 | stars_response = requests.get( 71 | next_page_of_results, 72 | headers={'Accept': 'application/vnd.github.v3.star+json'}, 73 | ) 74 | next_page_of_results = stars_response.links.get('next', {}).get('url') 75 | 76 | for repo_info in stars_response.json(): 77 | repo = repo_info['repo'] 78 | 79 | if data['preserve_timestamps']: 80 | timestamp = datetime.strptime( 81 | repo_info['starred_at'], 82 | '%Y-%m-%dT%H:%M:%SZ' 83 | ) 84 | else: 85 | timestamp = None 86 | 87 | bookmarks_imported += 1 88 | AddBookmarkCommand().execute( 89 | self._extract_bookmark_info(repo), 90 | timestamp=timestamp, 91 | ) 92 | 93 | return f'Imported {bookmarks_imported} bookmarks from starred repos!' # <10> 94 | 95 | 96 | class EditBookmarkCommand(Command): 97 | def execute(self, data): 98 | db.update( 99 | 'bookmarks', 100 | {'id': data['id']}, 101 | data['update'], 102 | ) 103 | return 'Bookmark updated!' 104 | -------------------------------------------------------------------------------- /ch08/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class DatabaseManager: 5 | def __init__(self, database_filename): 6 | self.connection = sqlite3.connect(database_filename) 7 | 8 | def __del__(self): 9 | self.connection.close() 10 | 11 | def _execute(self, statement, values=None): 12 | with self.connection: 13 | cursor = self.connection.cursor() 14 | cursor.execute(statement, values or []) 15 | return cursor 16 | 17 | def create_table(self, table_name, columns): 18 | columns_with_types = [ 19 | f'{column_name} {data_type}' 20 | for column_name, data_type in columns.items() 21 | ] 22 | self._execute( 23 | f''' 24 | CREATE TABLE IF NOT EXISTS {table_name} 25 | ({', '.join(columns_with_types)}); 26 | ''' 27 | ) 28 | 29 | def drop_table(self, table_name): 30 | self._execute(f'DROP TABLE {table_name};') 31 | 32 | def add(self, table_name, data): 33 | placeholders = ', '.join('?' * len(data)) 34 | column_names = ', '.join(data.keys()) 35 | column_values = tuple(data.values()) 36 | 37 | self._execute( 38 | f''' 39 | INSERT INTO {table_name} 40 | ({column_names}) 41 | VALUES ({placeholders}); 42 | ''', 43 | column_values, 44 | ) 45 | 46 | def delete(self, table_name, criteria): 47 | placeholders = [f'{column} = ?' for column in criteria.keys()] 48 | delete_criteria = ' AND '.join(placeholders) 49 | self._execute( 50 | f''' 51 | DELETE FROM {table_name} 52 | WHERE {delete_criteria}; 53 | ''', 54 | tuple(criteria.values()), 55 | ) 56 | 57 | def select(self, table_name, criteria=None, order_by=None): 58 | criteria = criteria or {} 59 | 60 | query = f'SELECT * FROM {table_name}' 61 | 62 | if criteria: 63 | placeholders = [f'{column} = ?' for column in criteria.keys()] 64 | select_criteria = ' AND '.join(placeholders) 65 | query += f' WHERE {select_criteria}' 66 | 67 | if order_by: 68 | query += f' ORDER BY {order_by}' 69 | 70 | return self._execute( 71 | query, 72 | tuple(criteria.values()), 73 | ) 74 | 75 | def update(self, table_name, criteria, data): 76 | update_placeholders = [f'{column} = ?' for column in criteria.keys()] 77 | update_criteria = ' AND '.join(update_placeholders) 78 | 79 | data_placeholders = ', '.join(f'{key} = ?' for key in data.keys()) 80 | 81 | values = tuple(data.values()) + tuple(criteria.values()) 82 | 83 | self._execute( 84 | f''' 85 | UPDATE {table_name} 86 | SET {data_placeholders} 87 | WHERE {update_criteria}; 88 | ''', 89 | values, 90 | ) 91 | -------------------------------------------------------------------------------- /ch08/gastropods.py: -------------------------------------------------------------------------------- 1 | class Slug: 2 | def __init__(self, name): 3 | self.name = name 4 | 5 | def crawl(self): 6 | print('slime trail!') 7 | 8 | 9 | class Snail(Slug): # <1> 10 | def __init__(self, name, shell_size): # <2> 11 | super().__init__(name) 12 | self.name = name 13 | self.shell_size = shell_size 14 | 15 | 16 | def race(gastropod_one, gastropod_two): 17 | gastropod_one.crawl() 18 | gastropod_two.crawl() 19 | 20 | 21 | race(Slug('Geoffrey'), Slug('Ramona')) # <3> 22 | race(Snail('Geoffrey'), Snail('Ramona')) # <4> 23 | -------------------------------------------------------------------------------- /ch08/predators.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Predator(ABC): # <1> 5 | @abstractmethod # <2> 6 | def eat(self, prey): # <3> 7 | pass # <4> 8 | 9 | 10 | class Bear(Predator): # <5> 11 | def eat(self, prey): # <6> 12 | print(f'Mauling {prey}!') 13 | 14 | 15 | class Owl(Predator): 16 | def eat(self, prey): 17 | print(f'Swooping in on {prey}!') 18 | 19 | 20 | class Chameleon(Predator): 21 | def eat(self, prey): 22 | print(f'Shooting tongue at {prey}!') 23 | 24 | 25 | if __name__ == '__main__': 26 | bear = Bear() 27 | bear.eat('deer') 28 | owl = Owl() 29 | owl.eat('mouse') 30 | chameleon = Chameleon() 31 | chameleon.eat('fly') 32 | -------------------------------------------------------------------------------- /ch09/bark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from collections import OrderedDict 5 | 6 | import commands 7 | 8 | 9 | def print_bookmarks(bookmarks): 10 | for bookmark in bookmarks: 11 | print('\t'.join( 12 | str(field) if field else '' 13 | for field in bookmark 14 | )) 15 | 16 | 17 | class Option: 18 | def __init__(self, name, command, prep_call=None): 19 | self.name = name 20 | self.command = command 21 | self.prep_call = prep_call 22 | 23 | def _handle_message(self, message): 24 | if isinstance(message, list): 25 | print_bookmarks(message) 26 | else: 27 | print(message) 28 | 29 | def choose(self): 30 | data = self.prep_call() if self.prep_call else None 31 | message = self.command.execute(data) # <1> 32 | self._handle_message(message) 33 | 34 | def __str__(self): 35 | return self.name 36 | 37 | 38 | def clear_screen(): 39 | clear = 'cls' if os.name == 'nt' else 'clear' 40 | os.system(clear) 41 | 42 | 43 | def print_options(options): 44 | for shortcut, option in options.items(): 45 | print(f'({shortcut}) {option}') 46 | print() 47 | 48 | 49 | def option_choice_is_valid(choice, options): 50 | return choice in options or choice.upper() in options 51 | 52 | 53 | def get_option_choice(options): 54 | choice = input('Choose an option: ') 55 | while not option_choice_is_valid(choice, options): 56 | print('Invalid choice') 57 | choice = input('Choose an option: ') 58 | return options[choice.upper()] 59 | 60 | 61 | def get_user_input(label, required=True): 62 | value = input(f'{label}: ') or None 63 | while required and not value: 64 | value = input(f'{label}: ') or None 65 | return value 66 | 67 | 68 | def get_new_bookmark_data(): 69 | return { 70 | 'title': get_user_input('Title'), 71 | 'url': get_user_input('URL'), 72 | 'notes': get_user_input('Notes', required=False), 73 | } 74 | 75 | 76 | def get_bookmark_id_for_deletion(): 77 | return get_user_input('Enter a bookmark ID to delete') 78 | 79 | 80 | def get_github_import_options(): 81 | return { 82 | 'github_username': get_user_input('GitHub username'), 83 | 'preserve_timestamps': 84 | get_user_input( 85 | 'Preserve timestamps [Y/n]', 86 | required=False 87 | ) in {'Y', 'y', None}, 88 | } 89 | 90 | 91 | def get_new_bookmark_info(): 92 | bookmark_id = get_user_input('Enter a bookmark ID to edit') 93 | field = get_user_input('Choose a value to edit (title, URL, notes)') 94 | new_value = get_user_input(f'Enter the new value for {field}') 95 | return { 96 | 'id': bookmark_id, 97 | 'update': {field: new_value}, 98 | } 99 | 100 | 101 | def loop(): 102 | clear_screen() 103 | 104 | options = OrderedDict({ 105 | 'A': Option('Add a bookmark', commands.AddBookmarkCommand(), prep_call=get_new_bookmark_data), 106 | 'B': Option('List bookmarks by date', commands.ListBookmarksCommand()), 107 | 'T': Option('List bookmarks by title', commands.ListBookmarksCommand(order_by='title')), 108 | 'E': Option('Edit a bookmark', commands.EditBookmarkCommand(), prep_call=get_new_bookmark_info), 109 | 'D': Option('Delete a bookmark', commands.DeleteBookmarkCommand(), prep_call=get_bookmark_id_for_deletion), 110 | 'G': Option( 111 | 'Import GitHub stars', 112 | commands.ImportGitHubStarsCommand(), 113 | prep_call=get_github_import_options 114 | ), 115 | 'Q': Option('Quit', commands.QuitCommand()), 116 | }) 117 | print_options(options) 118 | 119 | chosen_option = get_option_choice(options) 120 | clear_screen() 121 | chosen_option.choose() 122 | 123 | _ = input('Press ENTER to return to menu') 124 | 125 | 126 | if __name__ == '__main__': 127 | commands.CreateBookmarksTableCommand().execute() 128 | 129 | while True: 130 | loop() 131 | -------------------------------------------------------------------------------- /ch09/book.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | # Initial version 5 | class Book: 6 | def __init__(self, data): 7 | self.title = data['title'] # <1> 8 | self.subtitle = data['subtitle'] 9 | 10 | if self.title and self.subtitle: # <2> 11 | self.display_title = f'{self.title}: {self.subtitle}' 12 | elif self.title: 13 | self.display_title = self.title 14 | else: 15 | self.display_title = 'Untitled' 16 | 17 | 18 | # Setter version 19 | class Book: 20 | def __init__(self, data): 21 | self.title = data['title'] 22 | self.subtitle = data['subtitle'] 23 | self.set_display_title() # <1> 24 | 25 | def set_display_title(self): # <2> 26 | if self.title and self.subtitle: 27 | self.display_title = f'{self.title}: {self.subtitle}' 28 | elif self.title: 29 | self.display_title = self.title 30 | else: 31 | self.display_title = 'Untitled' 32 | 33 | 34 | # @property version 35 | class Book: 36 | def __init__(self, data): 37 | self.title = data['title'] 38 | self.subtitle = data['subtitle'] 39 | 40 | @property 41 | def display_title(self): # <1> 42 | if self.title and self.subtitle: 43 | return f'{self.title}: {self.subtitle}' 44 | elif self.title: 45 | return self.title 46 | else: 47 | return 'Untitled' 48 | 49 | 50 | # With embedded author data 51 | class Book: 52 | def __init__(self, data): 53 | # ... 54 | 55 | self.author_data = data['author'] # <1> 56 | 57 | @property 58 | def author_for_display(self): # <2> 59 | return f'{self.author_data["first_name"]} {self.author_data["last_name"]}' 60 | 61 | @property 62 | def author_for_citation(self): # <3> 63 | return f'{self.author_data["last_name"]}, {self.author_data["first_name"][0]}.' 64 | 65 | 66 | # With extracted author class 67 | class Author: 68 | def __init__(self, author_data): # <1> 69 | self.first_name = author_data['first_name'] 70 | self.last_name = author_data['last_name'] 71 | 72 | @property 73 | def for_display(self): # <2> 74 | return f'{self.first_name} {self.last_name}' 75 | 76 | @property 77 | def for_citation(self): 78 | return f'{self.last_name}, {self.first_name[0]}.' 79 | 80 | 81 | class Book: 82 | def __init__(self, data): 83 | # ... 84 | 85 | self.author_data = data['author'] # <3> 86 | self.author = Author(self.author_data) # <4> 87 | 88 | @property 89 | def author_for_display(self): # <5> 90 | warnings.warn('Use book.author.for_display instead', DeprecationWarning) 91 | return self.author.for_display 92 | 93 | @property 94 | def author_for_citation(self): 95 | warnings.warn('Use book.author.for_citation instead', DeprecationWarning) 96 | return self.author.for_citation 97 | -------------------------------------------------------------------------------- /ch09/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from abc import ABC, abstractmethod 3 | from datetime import datetime 4 | 5 | import requests 6 | 7 | from database import DatabaseManager 8 | 9 | db = DatabaseManager('bookmarks.db') 10 | 11 | 12 | class Command(ABC): 13 | @abstractmethod 14 | def execute(self, data): 15 | raise NotImplementedError('Commands must implement an execute method') 16 | 17 | 18 | class CreateBookmarksTableCommand(Command): 19 | def execute(self, data=None): 20 | db.create_table('bookmarks', { 21 | 'id': 'integer primary key autoincrement', 22 | 'title': 'text not null', 23 | 'url': 'text not null', 24 | 'notes': 'text', 25 | 'date_added': 'text not null', 26 | }) 27 | 28 | 29 | class AddBookmarkCommand(Command): 30 | def execute(self, data, timestamp=None): 31 | data['date_added'] = timestamp or datetime.utcnow().isoformat() 32 | db.add('bookmarks', data) 33 | return 'Bookmark added!' 34 | 35 | 36 | class ListBookmarksCommand(Command): 37 | def __init__(self, order_by='date_added'): 38 | self.order_by = order_by 39 | 40 | def execute(self, data=None): 41 | return db.select('bookmarks', order_by=self.order_by).fetchall() 42 | 43 | 44 | class DeleteBookmarkCommand(Command): 45 | def execute(self, data): 46 | db.delete('bookmarks', {'id': data}) 47 | return 'Bookmark deleted!' 48 | 49 | 50 | class QuitCommand(Command): 51 | def execute(self, data=None): 52 | sys.exit() 53 | 54 | 55 | class ImportGitHubStarsCommand(Command): 56 | def _extract_bookmark_info(self, repo): 57 | return { 58 | 'title': repo['name'], 59 | 'url': repo['html_url'], 60 | 'notes': repo['description'], 61 | } 62 | 63 | def execute(self, data): 64 | bookmarks_imported = 0 65 | 66 | github_username = data['github_username'] 67 | next_page_of_results = f'https://api.github.com/users/{github_username}/starred' 68 | 69 | while next_page_of_results: # <1> 70 | stars_response = requests.get( 71 | next_page_of_results, 72 | headers={'Accept': 'application/vnd.github.v3.star+json'}, 73 | ) 74 | next_page_of_results = stars_response.links.get('next', {}).get('url') 75 | 76 | for repo_info in stars_response.json(): # <2> 77 | repo = repo_info['repo'] 78 | 79 | if data['preserve_timestamps']: # <3> 80 | timestamp = datetime.strptime( 81 | repo_info['starred_at'], 82 | '%Y-%m-%dT%H:%M:%SZ' 83 | ) 84 | else: # <4> 85 | timestamp = None 86 | 87 | bookmarks_imported += 1 88 | AddBookmarkCommand().execute( 89 | self._extract_bookmark_info(repo), 90 | timestamp=timestamp, 91 | ) # <5> 92 | 93 | return f'Imported {bookmarks_imported} bookmarks from starred repos!' 94 | 95 | 96 | class EditBookmarkCommand(Command): 97 | def execute(self, data): 98 | db.update( 99 | 'bookmarks', 100 | {'id': data['id']}, 101 | data['update'], 102 | ) 103 | return 'Bookmark updated!' 104 | -------------------------------------------------------------------------------- /ch09/complexity.py: -------------------------------------------------------------------------------- 1 | def has_long_words(sentence): 2 | if isinstance(sentence, str): # <1> 3 | sentence = sentence.split(' ') 4 | 5 | for word in sentence: # <2> 6 | if len(word) > 10: # <3> 7 | return True 8 | 9 | return False # <4> 10 | -------------------------------------------------------------------------------- /ch09/configuration.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | 4 | FOODS = [ # <1> 5 | 'pizza', 6 | 'burgers', 7 | 'salad', 8 | 'soup', 9 | ] 10 | 11 | 12 | # 1st version 13 | def random_food(request): 14 | food = random.choice(FOODS) # <1> 15 | 16 | if request.headers.get('Accept') == 'application/json': # <2> 17 | return json.dumps({'food': food}) 18 | else: 19 | return food # <3> 20 | 21 | 22 | # 2nd version 23 | def random_food(request): 24 | food = random.choice(FOODS) 25 | 26 | if request.headers.get('Accept') == 'application/json': 27 | return json.dumps({'food': food}) 28 | elif request.headers.get('Accept') == 'application/xml': # <1> 29 | return f'{food}' 30 | else: 31 | return food 32 | 33 | 34 | # 3rd version 35 | def random_food(request): 36 | food = random.choice(FOODS) 37 | 38 | formats = { # <1> 39 | 'application/json': json.dumps({'food': food}), 40 | 'application/xml': f'{food}', 41 | } 42 | 43 | return formats.get(request.headers.get('Accept'), food) # <2> 44 | 45 | 46 | # Initial function 47 | def random_food(request): # <2> 48 | return random.choice(FOODS) # <3> 49 | 50 | 51 | # Extract functions 52 | def to_json(food): # <1> 53 | return json.dumps({'food': food}) 54 | 55 | 56 | def to_xml(food): 57 | return f'{food}' 58 | 59 | 60 | def random_food(request): 61 | food = random.choice(FOODS) 62 | 63 | formats = { # <2> 64 | 'application/json': to_json, 65 | 'application/xml': to_xml, 66 | } 67 | 68 | format_function = formats.get( # <3> 69 | request.headers.get('Accept'), 70 | lambda val: val # <4> 71 | ) 72 | return format_function(food) # <5> 73 | 74 | 75 | # Full separation 76 | def get_format_function(accept=None): # <1> 77 | formats = { 78 | 'application/json': to_json, 79 | 'application/xml': to_xml, 80 | } 81 | 82 | return formats.get(accept, lambda val: val) 83 | 84 | 85 | def random_food(request): # <2> 86 | food = random.choice(FOODS) 87 | format_function = get_format_function(request.headers.get('Accept')) # <3> 88 | return format_function(food) 89 | -------------------------------------------------------------------------------- /ch09/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class DatabaseManager: 5 | def __init__(self, database_filename): 6 | self.connection = sqlite3.connect(database_filename) 7 | 8 | def __del__(self): 9 | self.connection.close() 10 | 11 | def _execute(self, statement, values=None): 12 | with self.connection: 13 | cursor = self.connection.cursor() 14 | cursor.execute(statement, values or []) 15 | return cursor 16 | 17 | def create_table(self, table_name, columns): 18 | columns_with_types = [ 19 | f'{column_name} {data_type}' 20 | for column_name, data_type in columns.items() 21 | ] 22 | self._execute( 23 | f''' 24 | CREATE TABLE IF NOT EXISTS {table_name} 25 | ({', '.join(columns_with_types)}); 26 | ''' 27 | ) 28 | 29 | def drop_table(self, table_name): 30 | self._execute(f'DROP TABLE {table_name};') 31 | 32 | def add(self, table_name, data): 33 | placeholders = ', '.join('?' * len(data)) 34 | column_names = ', '.join(data.keys()) 35 | column_values = tuple(data.values()) 36 | 37 | self._execute( 38 | f''' 39 | INSERT INTO {table_name} 40 | ({column_names}) 41 | VALUES ({placeholders}); 42 | ''', 43 | column_values, 44 | ) 45 | 46 | def delete(self, table_name, criteria): 47 | placeholders = [f'{column} = ?' for column in criteria.keys()] 48 | delete_criteria = ' AND '.join(placeholders) 49 | self._execute( 50 | f''' 51 | DELETE FROM {table_name} 52 | WHERE {delete_criteria}; 53 | ''', 54 | tuple(criteria.values()), 55 | ) 56 | 57 | def select(self, table_name, criteria=None, order_by=None): 58 | criteria = criteria or {} 59 | 60 | query = f'SELECT * FROM {table_name}' 61 | 62 | if criteria: 63 | placeholders = [f'{column} = ?' for column in criteria.keys()] 64 | select_criteria = ' AND '.join(placeholders) 65 | query += f' WHERE {select_criteria}' 66 | 67 | if order_by: 68 | query += f' ORDER BY {order_by}' 69 | 70 | return self._execute( 71 | query, 72 | tuple(criteria.values()), 73 | ) 74 | 75 | def update(self, table_name, criteria, data): 76 | update_placeholders = [f'{column} = ?' for column in criteria.keys()] 77 | update_criteria = ' AND '.join(update_placeholders) 78 | 79 | data_placeholders = ', '.join(f'{key} = ?' for key in data.keys()) 80 | 81 | values = tuple(data.values()) + tuple(criteria.values()) 82 | 83 | self._execute( 84 | f''' 85 | UPDATE {table_name} 86 | SET {data_placeholders} 87 | WHERE {update_criteria}; 88 | ''', 89 | values, 90 | ) 91 | -------------------------------------------------------------------------------- /ch10/bark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from collections import OrderedDict 5 | 6 | import commands 7 | 8 | 9 | def format_bookmark(bookmark): 10 | return '\t'.join( 11 | str(field) if field else '' 12 | for field in bookmark 13 | ) 14 | 15 | 16 | class Option: 17 | def __init__(self, name, command, prep_call=None, success_message='{result}'): # <1> 18 | self.name = name 19 | self.command = command 20 | self.prep_call = prep_call 21 | self.success_message = success_message # <2> 22 | 23 | def choose(self): 24 | data = self.prep_call() if self.prep_call else None 25 | success, result = self.command.execute(data) # <3> 26 | 27 | formatted_result = '' 28 | 29 | if isinstance(result, list): # <4> 30 | for bookmark in result: 31 | formatted_result += '\n' + format_bookmark(bookmark) 32 | else: 33 | formatted_result = result 34 | 35 | if success: 36 | print(self.success_message.format(result=formatted_result)) # <5> 37 | 38 | def __str__(self): 39 | return self.name 40 | 41 | 42 | def clear_screen(): 43 | clear = 'cls' if os.name == 'nt' else 'clear' 44 | os.system(clear) 45 | 46 | 47 | def print_options(options): 48 | for shortcut, option in options.items(): 49 | print(f'({shortcut}) {option}') 50 | print() 51 | 52 | 53 | def option_choice_is_valid(choice, options): 54 | return choice in options or choice.upper() in options 55 | 56 | 57 | def get_option_choice(options): 58 | choice = input('Choose an option: ') 59 | while not option_choice_is_valid(choice, options): 60 | print('Invalid choice') 61 | choice = input('Choose an option: ') 62 | return options[choice.upper()] 63 | 64 | 65 | def get_user_input(label, required=True): 66 | value = input(f'{label}: ') or None 67 | while required and not value: 68 | value = input(f'{label}: ') or None 69 | return value 70 | 71 | 72 | def get_new_bookmark_data(): 73 | return { 74 | 'title': get_user_input('Title'), 75 | 'url': get_user_input('URL'), 76 | 'notes': get_user_input('Notes', required=False), 77 | } 78 | 79 | 80 | def get_bookmark_id_for_deletion(): 81 | return get_user_input('Enter a bookmark ID to delete') 82 | 83 | 84 | def get_github_import_options(): 85 | return { 86 | 'github_username': get_user_input('GitHub username'), 87 | 'preserve_timestamps': 88 | get_user_input( 89 | 'Preserve timestamps [Y/n]', 90 | required=False 91 | ) in {'Y', 'y', None}, 92 | } 93 | 94 | 95 | def get_new_bookmark_info(): 96 | bookmark_id = get_user_input('Enter a bookmark ID to edit') 97 | field = get_user_input('Choose a value to edit (title, URL, notes)') 98 | new_value = get_user_input(f'Enter the new value for {field}') 99 | return { 100 | 'id': bookmark_id, 101 | 'update': {field: new_value}, 102 | } 103 | 104 | 105 | def loop(): 106 | clear_screen() 107 | 108 | options = OrderedDict({ 109 | 'A': Option( 110 | 'Add a bookmark', 111 | commands.AddBookmarkCommand(), 112 | prep_call=get_new_bookmark_data, 113 | success_message='Bookmark added!', # <6> 114 | ), 115 | 'B': Option( 116 | 'List bookmarks by date', 117 | commands.ListBookmarksCommand(), # <7> 118 | ), 119 | 'T': Option( 120 | 'List bookmarks by title', 121 | commands.ListBookmarksCommand(order_by='title'), 122 | ), 123 | 'E': Option( 124 | 'Edit a bookmark', 125 | commands.EditBookmarkCommand(), 126 | prep_call=get_new_bookmark_info, 127 | success_message='Bookmark updated!' 128 | ), 129 | 'D': Option( 130 | 'Delete a bookmark', 131 | commands.DeleteBookmarkCommand(), 132 | prep_call=get_bookmark_id_for_deletion, 133 | success_message='Bookmark deleted!', 134 | ), 135 | 'G': Option( 136 | 'Import GitHub stars', 137 | commands.ImportGitHubStarsCommand(), 138 | prep_call=get_github_import_options, 139 | success_message='Imported {result} bookmarks from starred repos!', # <8> 140 | ), 141 | 'Q': Option( 142 | 'Quit', 143 | commands.QuitCommand() 144 | ), 145 | }) 146 | print_options(options) 147 | 148 | chosen_option = get_option_choice(options) 149 | clear_screen() 150 | chosen_option.choose() 151 | 152 | _ = input('Press ENTER to return to menu') 153 | 154 | 155 | if __name__ == '__main__': 156 | while True: 157 | loop() 158 | -------------------------------------------------------------------------------- /ch10/book.py: -------------------------------------------------------------------------------- 1 | # A function tightly coupled to an object 2 | class Book: 3 | def __init__(self, title, subtitle, author): # <1> 4 | self.title = title 5 | self.subtitle = subtitle 6 | self.author = author 7 | 8 | 9 | def display_book_info(book): 10 | print(f'{book.title}: {book.subtitle} by {book.author}') # <2> 11 | 12 | 13 | # Coupling addressed by listening to the cohesion 14 | class Book: 15 | def __init__(self, title, subtitle, author): 16 | self.title = title 17 | self.subtitle = subtitle 18 | self.author = author 19 | 20 | def display_info(self): # <1> 21 | print(f'{self.title}: {self.subtitle} by {self.author}') # <2> 22 | -------------------------------------------------------------------------------- /ch10/bookmarks.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daneah/practices-of-the-python-pro/98bd0a1273d3a3d75f20069cc38d112ea09e6cec/ch10/bookmarks.db -------------------------------------------------------------------------------- /ch10/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from abc import ABC, abstractmethod 3 | from datetime import datetime 4 | 5 | import requests 6 | 7 | from persistence import BookmarkDatabase # <1> 8 | 9 | persistence = BookmarkDatabase() # <2> 10 | 11 | 12 | class Command(ABC): 13 | @abstractmethod 14 | def execute(self, data): 15 | raise NotImplementedError('Commands must implement an execute method') 16 | 17 | 18 | class AddBookmarkCommand(Command): 19 | def execute(self, data, timestamp=None): 20 | data['date_added'] = timestamp or datetime.utcnow().isoformat() 21 | persistence.create(data) # <3> 22 | return True, None 23 | 24 | 25 | class ListBookmarksCommand(Command): 26 | def __init__(self, order_by='date_added'): 27 | self.order_by = order_by 28 | 29 | def execute(self, data=None): 30 | return True, persistence.list(order_by=self.order_by) # <4> 31 | 32 | 33 | class DeleteBookmarkCommand(Command): 34 | def execute(self, data): 35 | persistence.delete(data) # <5> 36 | return True, None 37 | 38 | 39 | class QuitCommand(Command): 40 | def execute(self, data=None): 41 | sys.exit() 42 | 43 | 44 | class ImportGitHubStarsCommand(Command): 45 | def _extract_bookmark_info(self, repo): 46 | return { 47 | 'title': repo['name'], 48 | 'url': repo['html_url'], 49 | 'notes': repo['description'], 50 | } 51 | 52 | def execute(self, data): 53 | bookmarks_imported = 0 54 | 55 | github_username = data['github_username'] 56 | next_page_of_results = f'https://api.github.com/users/{github_username}/starred' 57 | 58 | while next_page_of_results: 59 | stars_response = requests.get( 60 | next_page_of_results, 61 | headers={'Accept': 'application/vnd.github.v3.star+json'}, 62 | ) 63 | next_page_of_results = stars_response.links.get('next', {}).get('url') 64 | 65 | for repo_info in stars_response.json(): 66 | repo = repo_info['repo'] 67 | 68 | if data['preserve_timestamps']: 69 | timestamp = datetime.strptime( 70 | repo_info['starred_at'], 71 | '%Y-%m-%dT%H:%M:%SZ' 72 | ) 73 | else: 74 | timestamp = None 75 | 76 | bookmarks_imported += 1 77 | AddBookmarkCommand().execute( 78 | self._extract_bookmark_info(repo), 79 | timestamp=timestamp, 80 | ) 81 | 82 | return True, bookmarks_imported 83 | 84 | 85 | class EditBookmarkCommand(Command): 86 | def execute(self, data): 87 | persistence.edit(data['id'], data['update']) # <6> 88 | return True, None 89 | -------------------------------------------------------------------------------- /ch10/commands_coupled.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from abc import ABC, abstractmethod 3 | from datetime import datetime 4 | 5 | import requests 6 | 7 | from database import DatabaseManager 8 | 9 | db = DatabaseManager('bookmarks.db') 10 | 11 | 12 | class Command(ABC): 13 | @abstractmethod 14 | def execute(self, data): 15 | raise NotImplementedError('Commands must implement an execute method') 16 | 17 | 18 | class CreateBookmarksTableCommand(Command): 19 | def execute(self, data=None): 20 | db.create_table('bookmarks', { 21 | 'id': 'integer primary key autoincrement', 22 | 'title': 'text not null', 23 | 'url': 'text not null', 24 | 'notes': 'text', 25 | 'date_added': 'text not null', 26 | }) 27 | 28 | 29 | class AddBookmarkCommand(Command): 30 | def execute(self, data, timestamp=None): # <1> 31 | data['date_added'] = timestamp or datetime.utcnow().isoformat() # <2> 32 | db.add('bookmarks', data) # <3> 33 | return 'Bookmark added!' # <4> 34 | 35 | 36 | class ListBookmarksCommand(Command): 37 | def __init__(self, order_by='date_added'): 38 | self.order_by = order_by 39 | 40 | def execute(self, data=None): 41 | return db.select('bookmarks', order_by=self.order_by).fetchall() 42 | 43 | 44 | class DeleteBookmarkCommand(Command): 45 | def execute(self, data): 46 | db.delete('bookmarks', {'id': data}) 47 | return 'Bookmark deleted!' 48 | 49 | 50 | class QuitCommand(Command): 51 | def execute(self, data=None): 52 | sys.exit() 53 | 54 | 55 | class ImportGitHubStarsCommand(Command): 56 | def _extract_bookmark_info(self, repo): 57 | return { 58 | 'title': repo['name'], 59 | 'url': repo['html_url'], 60 | 'notes': repo['description'], 61 | } 62 | 63 | def execute(self, data): 64 | bookmarks_imported = 0 65 | 66 | github_username = data['github_username'] 67 | next_page_of_results = f'https://api.github.com/users/{github_username}/starred' 68 | 69 | while next_page_of_results: 70 | stars_response = requests.get( 71 | next_page_of_results, 72 | headers={'Accept': 'application/vnd.github.v3.star+json'}, 73 | ) 74 | next_page_of_results = stars_response.links.get('next', {}).get('url') 75 | 76 | for repo_info in stars_response.json(): 77 | repo = repo_info['repo'] 78 | 79 | if data['preserve_timestamps']: 80 | timestamp = datetime.strptime( 81 | repo_info['starred_at'], 82 | '%Y-%m-%dT%H:%M:%SZ' 83 | ) 84 | else: 85 | timestamp = None 86 | 87 | bookmarks_imported += 1 88 | AddBookmarkCommand().execute( 89 | self._extract_bookmark_info(repo), 90 | timestamp=timestamp, 91 | ) 92 | 93 | return f'Imported {bookmarks_imported} bookmarks from starred repos!' 94 | 95 | 96 | class EditBookmarkCommand(Command): 97 | def execute(self, data): 98 | db.update( 99 | 'bookmarks', 100 | {'id': data['id']}, 101 | data['update'], 102 | ) 103 | return 'Bookmark updated!' 104 | -------------------------------------------------------------------------------- /ch10/commands_decoupled_messaging.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from abc import ABC, abstractmethod 3 | from datetime import datetime 4 | 5 | import requests 6 | 7 | from database import DatabaseManager 8 | 9 | db = DatabaseManager('bookmarks.db') 10 | 11 | 12 | class Command(ABC): 13 | @abstractmethod 14 | def execute(self, data): 15 | raise NotImplementedError('Commands must implement an execute method') 16 | 17 | 18 | class CreateBookmarksTableCommand(Command): 19 | def execute(self, data=None): 20 | db.create_table('bookmarks', { 21 | 'id': 'integer primary key autoincrement', 22 | 'title': 'text not null', 23 | 'url': 'text not null', 24 | 'notes': 'text', 25 | 'date_added': 'text not null', 26 | }) 27 | 28 | 29 | class AddBookmarkCommand(Command): # <1> 30 | def execute(self, data, timestamp=None): 31 | data['date_added'] = timestamp or datetime.utcnow().isoformat() 32 | db.add('bookmarks', data) 33 | return True, None # <2> 34 | 35 | 36 | class ListBookmarksCommand(Command): # <3> 37 | def __init__(self, order_by='date_added'): 38 | self.order_by = order_by 39 | 40 | def execute(self, data=None): 41 | return True, db.select('bookmarks', order_by=self.order_by).fetchall() # <4> 42 | 43 | 44 | class DeleteBookmarkCommand(Command): 45 | def execute(self, data): 46 | db.delete('bookmarks', {'id': data}) 47 | return True, None 48 | 49 | 50 | class QuitCommand(Command): 51 | def execute(self, data=None): 52 | sys.exit() 53 | 54 | 55 | class ImportGitHubStarsCommand(Command): 56 | def _extract_bookmark_info(self, repo): 57 | return { 58 | 'title': repo['name'], 59 | 'url': repo['html_url'], 60 | 'notes': repo['description'], 61 | } 62 | 63 | def execute(self, data): 64 | bookmarks_imported = 0 65 | 66 | github_username = data['github_username'] 67 | next_page_of_results = f'https://api.github.com/users/{github_username}/starred' 68 | 69 | while next_page_of_results: 70 | stars_response = requests.get( 71 | next_page_of_results, 72 | headers={'Accept': 'application/vnd.github.v3.star+json'}, 73 | ) 74 | next_page_of_results = stars_response.links.get('next', {}).get('url') 75 | 76 | for repo_info in stars_response.json(): 77 | repo = repo_info['repo'] 78 | 79 | if data['preserve_timestamps']: 80 | timestamp = datetime.strptime( 81 | repo_info['starred_at'], 82 | '%Y-%m-%dT%H:%M:%SZ' 83 | ) 84 | else: 85 | timestamp = None 86 | 87 | bookmarks_imported += 1 88 | AddBookmarkCommand().execute( 89 | self._extract_bookmark_info(repo), 90 | timestamp=timestamp, 91 | ) 92 | 93 | return True, bookmarks_imported 94 | 95 | 96 | class EditBookmarkCommand(Command): 97 | def execute(self, data): 98 | db.update( 99 | 'bookmarks', 100 | {'id': data['id']}, 101 | data['update'], 102 | ) 103 | return True, None 104 | -------------------------------------------------------------------------------- /ch10/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class DatabaseManager: 5 | def __init__(self, database_filename): 6 | self.connection = sqlite3.connect(database_filename) 7 | 8 | def __del__(self): 9 | self.connection.close() 10 | 11 | def _execute(self, statement, values=None): 12 | with self.connection: 13 | cursor = self.connection.cursor() 14 | cursor.execute(statement, values or []) 15 | return cursor 16 | 17 | def create_table(self, table_name, columns): 18 | columns_with_types = [ 19 | f'{column_name} {data_type}' 20 | for column_name, data_type in columns.items() 21 | ] 22 | self._execute( 23 | f''' 24 | CREATE TABLE IF NOT EXISTS {table_name} 25 | ({', '.join(columns_with_types)}); 26 | ''' 27 | ) 28 | 29 | def drop_table(self, table_name): 30 | self._execute(f'DROP TABLE {table_name};') 31 | 32 | def add(self, table_name, data): 33 | placeholders = ', '.join('?' * len(data)) 34 | column_names = ', '.join(data.keys()) 35 | column_values = tuple(data.values()) 36 | 37 | self._execute( 38 | f''' 39 | INSERT INTO {table_name} 40 | ({column_names}) 41 | VALUES ({placeholders}); 42 | ''', 43 | column_values, 44 | ) 45 | 46 | def delete(self, table_name, criteria): 47 | placeholders = [f'{column} = ?' for column in criteria.keys()] 48 | delete_criteria = ' AND '.join(placeholders) 49 | self._execute( 50 | f''' 51 | DELETE FROM {table_name} 52 | WHERE {delete_criteria}; 53 | ''', 54 | tuple(criteria.values()), 55 | ) 56 | 57 | def select(self, table_name, criteria=None, order_by=None): 58 | criteria = criteria or {} 59 | 60 | query = f'SELECT * FROM {table_name}' 61 | 62 | if criteria: 63 | placeholders = [f'{column} = ?' for column in criteria.keys()] 64 | select_criteria = ' AND '.join(placeholders) 65 | query += f' WHERE {select_criteria}' 66 | 67 | if order_by: 68 | query += f' ORDER BY {order_by}' 69 | 70 | return self._execute( 71 | query, 72 | tuple(criteria.values()), 73 | ) 74 | 75 | def update(self, table_name, criteria, data): 76 | update_placeholders = [f'{column} = ?' for column in criteria.keys()] 77 | update_criteria = ' AND '.join(update_placeholders) 78 | 79 | data_placeholders = ', '.join(f'{key} = ?' for key in data.keys()) 80 | 81 | values = tuple(data.values()) + tuple(criteria.values()) 82 | 83 | self._execute( 84 | f''' 85 | UPDATE {table_name} 86 | SET {data_placeholders} 87 | WHERE {update_criteria}; 88 | ''', 89 | values, 90 | ) 91 | -------------------------------------------------------------------------------- /ch10/persistence.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from database import DatabaseManager 4 | 5 | 6 | class PersistenceLayer(ABC): # <1> 7 | @abstractmethod 8 | def create(self, data): # <2> 9 | raise NotImplementedError('Persistence layers must implement a create method') 10 | 11 | @abstractmethod 12 | def list(self, order_by=None): 13 | raise NotImplementedError('Persistence layers must implement a list method') 14 | 15 | @abstractmethod 16 | def edit(self, bookmark_id, bookmark_data): 17 | raise NotImplementedError('Persistence layers must implement an edit method') 18 | 19 | @abstractmethod 20 | def delete(self, bookmark_id): 21 | raise NotImplementedError('Persistence layers must implement a delete method') 22 | 23 | 24 | class BookmarkDatabase(PersistenceLayer): # <3> 25 | def __init__(self): 26 | self.table_name = 'bookmarks' # <4> 27 | self.db = DatabaseManager('bookmarks.db') 28 | 29 | self.db.create_table(self.table_name, { 30 | 'id': 'integer primary key autoincrement', 31 | 'title': 'text not null', 32 | 'url': 'text not null', 33 | 'notes': 'text', 34 | 'date_added': 'text not null', 35 | }) 36 | 37 | def create(self, bookmark_data): # <5> 38 | self.db.add(self.table_name, bookmark_data) 39 | 40 | def list(self, order_by=None): 41 | return self.db.select(self.table_name, order_by=order_by).fetchall() 42 | 43 | def edit(self, bookmark_id, bookmark_data): 44 | self.db.update(self.table_name, {'id': bookmark_id}, bookmark_data) 45 | 46 | def delete(self, bookmark_id): 47 | self.db.delete(self.table_name, {'id': bookmark_id}) 48 | -------------------------------------------------------------------------------- /ch10/search.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def remove_spaces(query): # <1> 5 | query = query.strip() 6 | query = re.sub(r'\s+', ' ', query) 7 | return query 8 | 9 | 10 | def normalize(query): # <2> 11 | query = query.casefold() 12 | return query 13 | 14 | 15 | def remove_quotes(query): # <1> 16 | query = re.sub(r'"', '', query) 17 | return query 18 | 19 | 20 | if __name__ == '__main__': 21 | search_query = input('Enter your search query: ') # <3> 22 | search_query = remove_spaces(search_query) # <4> 23 | search_query = remove_quotes(search_query) # <2> 24 | search_query = normalize(search_query) 25 | print(f'Running a search for "{search_query}"') # <5> 26 | -------------------------------------------------------------------------------- /ch10/search_reduced_coupling.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def _remove_spaces(query): # <1> 5 | query = query.strip() 6 | query = re.sub(r'\s+', ' ', query) 7 | return query 8 | 9 | 10 | def _normalize(query): 11 | query = query.casefold() 12 | return query 13 | 14 | 15 | def _remove_quotes(query): 16 | query = re.sub(r'"', '', query) 17 | return query 18 | 19 | 20 | def clean_query(query): # <2> 21 | query = _remove_spaces(query) 22 | query = _remove_quotes(query) 23 | query = _normalize(query) 24 | return query 25 | 26 | 27 | if __name__ == '__main__': 28 | search_query = input('Enter your search query: ') 29 | search_query = clean_query(search_query) # <3> 30 | print(f'Running a search for "{search_query}"') 31 | -------------------------------------------------------------------------------- /ch10/search_revisited.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def remove_spaces(query): 5 | query = query.strip() 6 | query = re.sub(r'\s+', ' ', query) 7 | return query 8 | 9 | 10 | def normalize(query): 11 | query = query.casefold() 12 | return query 13 | 14 | 15 | def remove_quotes(query): 16 | query = re.sub(r'"', '', query) 17 | return query 18 | 19 | 20 | if __name__ == '__main__': 21 | search_query = input('Enter your search query: ') 22 | search_query = remove_spaces(search_query) # <1> 23 | search_query = remove_quotes(search_query) # <2> 24 | search_query = normalize(search_query) # <3> 25 | print(f'Running a search for "{search_query}"') # <4> 26 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daneah/practices-of-the-python-pro/98bd0a1273d3a3d75f20069cc38d112ea09e6cec/cover.png --------------------------------------------------------------------------------