├── ERRATUM-PYTHON-DESIGN-PATTERN-PROXY.pdf ├── LICENSE ├── README.md ├── chapter01 ├── abstract_factory.py ├── data │ ├── movies.json │ └── person.xml ├── factory_method.py └── id.py ├── chapter02 ├── apple_factory.py ├── builder.py ├── computer_builder.py └── exercise_fluent_builder.py ├── chapter03 ├── prototype.py └── singleton.py ├── chapter04 ├── adapter.py └── external.py ├── chapter05 ├── mymath.py ├── number_sum.py └── number_sum_naive.py ├── chapter06 ├── bridge.py └── file.txt ├── chapter07 └── facade.py ├── chapter08 ├── flyweight.py ├── lazy.py ├── mvc.py └── proxy.py ├── chapter09 └── chain.py ├── chapter10 ├── command.py └── first-class.py ├── chapter11 └── observer.py ├── chapter12 └── state.py ├── chapter13 ├── boiler.py ├── interpreter.py ├── iterator.py ├── memento.py ├── strategy.py └── template.py ├── chapter14 ├── peoplelist.py ├── rx_example1.py ├── rx_example2.py ├── rx_example3.py ├── rx_peoplelist_1.py ├── rx_peoplelist_2.py └── rx_peoplelist_3.py └── chapter15 ├── cache_aside ├── cache_aside.py ├── data │ ├── quotes.sqlite3 │ └── quotes_cache.csv └── populate_db.py ├── circuit_breaker.py ├── circuit_breaker_in_flaskapp.py ├── retry_write_file.py ├── retry_write_file_retrying_module.py ├── retry_write_file_tenacity_module.py ├── service_first.py ├── service_second.py ├── test_service_first.py ├── test_service_second.py └── throttling_in_flaskapp.py /ERRATUM-PYTHON-DESIGN-PATTERN-PROXY.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Python-Design-Patterns-Second-Edition/639c6712e0c5cf1cfd9576026d722b46e2bed09e/ERRATUM-PYTHON-DESIGN-PATTERN-PROXY.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Packt 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 | # Mastering Python Design Patterns, Second Edition 2 | 3 | 4 | 5 | This is the code repository for [Mastering Python Design Patterns, Second Edition](https://www.packtpub.com/application-development/mastering-python-design-patterns-second-edition?utm_source=github&utm_medium=repository&utm_campaign=), published by Packt. 6 | 7 | **A guide to creating smart, efficient, and reusable software** 8 | 9 | ## What is this book about? 10 | Python is an object-oriented scripting language that is used in a wide range of categories. In software engineering, a design pattern is an elected solution for solving software design problems. Although they have been around for a while, design patterns remain one of the top topics in software engineering, and are a ready source for software developers to solve the problems they face on a regular basis. This book takes you through a variety of design patterns and explains them with real-world examples. You will get to grips with low-level details and concepts that show you how to write Python code, without focusing on common solutions as enabled in Java and C++. You'll also find sections on corrections, best practices, system architecture, and its designing aspects. 11 | This book covers the following exciting features: 12 | * Explore Factory Method and Abstract Factory for object creation 13 | * Clone objects using the Prototype pattern 14 | * Make incompatible interfaces compatible using the Adapter pattern 15 | * Secure an interface using the Proxy pattern 16 | * Choose an algorithm dynamically using the Strategy pattern 17 | 18 | 19 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1788837487) today! 20 | 21 | https://www.packtpub.com/ 23 | 24 | ## Errata 25 | 26 | * Chapter 9, Implementation of Proxy Pattern section: The code block following the sentence "Let's recapitulate the full code of the proxy.py file:" is incorrect. The correction can be found at https://github.com/PacktPublishing/Mastering-Python-Design-Patterns-Second-Edition/blob/master/ERRATUM-PYTHON-DESIGN-PATTERN-PROXY.pdf. 27 | 28 | ## Instructions and Navigations 29 | All of the code is organized into folders. For example, Chapter02. 30 | 31 | The code will look like the following: 32 | ``` 33 | class Musician: 34 | def __init__(self, name): 35 | self.name = name 36 | def __str__(self): 37 | return f'the musician {self.name}' 38 | def play(self): 39 | return 'plays music' 40 | ``` 41 | 42 | **Following is what you need for this book:** 43 | This book is for intermediate Python developers. Prior knowledge of design patterns is not required to enjoy this book. 44 | 45 | With the following software and hardware list you can run all code files present in the book (Chapter 1-15). 46 | ### Software and Hardware List 47 | | Chapter | Software required | OS required | 48 | | -------- | ------------------------------------ | ----------------------------------- | 49 | |All chapters |Python 3.6.x | Windows, Mac OS X, and Linux (Any) | 50 | |Chapter 15 |SQLite 3.22.0 or later | Windows, Mac OS X, and Linux (Any) | 51 | |Chapter 15 |RabbitMQ 3.7.7 | Windows, Mac OS X, and Linux (Any) | 52 | 53 | 54 | ### Related products 55 | * Learn Python Programming - Second Edition [[Packt]](https://www.packtpub.com/application-development/learn-python-programming-second-edition?utm_source=github&utm_medium=repository&utm_campaign=1-788-99666-6) [[Amazon]](https://www.amazon.com/dp/1788996666) 56 | 57 | * Clean Code in Python [[Packt]](https://www.packtpub.com/application-development/clean-code-python?utm_source=github&utm_medium=repository&utm_campaign=978-1-78883-583-1) [[Amazon]](https://www.amazon.com/dp/1788835832) 58 | 59 | 60 | ## Get to Know the Author 61 | **Kamon Ayeva** 62 | is a web developer/DevOps engineer working with a variety of tools. He spends most of his time building projects using Python's powerful scripting capabilities, add-on libraries, and web frameworks such as Django or Flask. Kamon has been using Python in professional contexts for more than 12 years. He is also a Python instructor with a passion for teaching how to use Python features to quickly produce results. 63 | 64 | 65 | **Sakis Kasampalis** 66 | is a software engineer living in the Netherlands. He is not dogmatic about particular programming languages and tools; his principle is that the right tool should be used for the right job. One of his favorite tools is Python because he finds it very productive. Sakis was also the technical reviewer of Mastering Object-oriented Python and Learning Python Design Patterns, published by Packt Publishing. 67 | 68 | 69 | ### Suggestions and Feedback 70 | [Click here](https://docs.google.com/forms/d/e/1FAIpQLSdy7dATC6QmEL81FIUuymZ0Wy9vH1jHkvpY57OiMeKGqib_Ow/viewform) if you have any feedback or suggestions. 71 | 72 | 73 | -------------------------------------------------------------------------------- /chapter01/abstract_factory.py: -------------------------------------------------------------------------------- 1 | 2 | # Frog game 3 | 4 | class Frog: 5 | def __init__(self, name): 6 | self.name = name 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | def interact_with(self, obstacle): 12 | act = obstacle.action() 13 | msg = f'{self} the Frog encounters {obstacle} and {act}!' 14 | print(msg) 15 | 16 | class Bug: 17 | def __str__(self): 18 | return 'a bug' 19 | 20 | def action(self): 21 | return 'eats it' 22 | 23 | class FrogWorld: 24 | def __init__(self, name): 25 | print(self) 26 | self.player_name = name 27 | 28 | def __str__(self): 29 | return '\n\n\t------ Frog World -------' 30 | 31 | def make_character(self): 32 | return Frog(self.player_name) 33 | 34 | def make_obstacle(self): 35 | return Bug() 36 | 37 | 38 | # Wizard game 39 | 40 | class Wizard: 41 | def __init__(self, name): 42 | self.name = name 43 | 44 | def __str__(self): 45 | return self.name 46 | 47 | def interact_with(self, obstacle): 48 | act = obstacle.action() 49 | msg = f'{self} the Wizard battles against {obstacle} and {act}!' 50 | print(msg) 51 | 52 | class Ork: 53 | def __str__(self): 54 | return 'an evil ork' 55 | 56 | def action(self): 57 | return 'kills it' 58 | 59 | class WizardWorld: 60 | def __init__(self, name): 61 | print(self) 62 | self.player_name = name 63 | 64 | def __str__(self): 65 | return '\n\n\t------ Wizard World -------' 66 | 67 | def make_character(self): 68 | return Wizard(self.player_name) 69 | 70 | def make_obstacle(self): 71 | return Ork() 72 | 73 | # Game environment 74 | class GameEnvironment: 75 | def __init__(self, factory): 76 | self.hero = factory.make_character() 77 | self.obstacle = factory.make_obstacle() 78 | 79 | def play(self): 80 | self.hero.interact_with(self.obstacle) 81 | 82 | def validate_age(name): 83 | try: 84 | age = input(f'Welcome {name}. How old are you? ') 85 | age = int(age) 86 | except ValueError as err: 87 | print(f"Age {age} is invalid, please try again...") 88 | return (False, age) 89 | return (True, age) 90 | 91 | def main(): 92 | name = input("Hello. What's your name? ") 93 | valid_input = False 94 | while not valid_input: 95 | valid_input, age = validate_age(name) 96 | game = FrogWorld if age < 18 else WizardWorld 97 | environment = GameEnvironment(game(name)) 98 | environment.play() 99 | 100 | 101 | if __name__ == '__main__': 102 | main() 103 | -------------------------------------------------------------------------------- /chapter01/data/movies.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"title":"After Dark in Central Park", 3 | "year":1900, 4 | "director":null, "cast":null, "genre":null}, 5 | {"title":"Boarding School Girls' Pajama Parade", 6 | "year":1900, 7 | "director":null, "cast":null, "genre":null}, 8 | {"title":"Buffalo Bill's Wild West Parad", 9 | "year":1900, 10 | "director":null, "cast":null, "genre":null}, 11 | {"title":"Caught", 12 | "year":1900, 13 | "director":null, "cast":null, "genre":null}, 14 | {"title":"Clowns Spinning Hats", 15 | "year":1900, 16 | "director":null, "cast":null, "genre":null}, 17 | {"title":"Capture of Boer Battery by British", 18 | "year":1900, 19 | "director":"James H. White", "cast":null, "genre":"Short documentary"}, 20 | {"title":"The Enchanted Drawing", 21 | "year":1900, 22 | "director":"J. Stuart Blackton", "cast":null,"genre":null}, 23 | {"title":"Family Troubles", 24 | "year":1900, 25 | "director":null, "cast":null, "genre":null}, 26 | {"title":"Feeding Sea Lions", 27 | "year":1900, 28 | "director":null, "cast":"Paul Boyton", "genre":null} 29 | ] -------------------------------------------------------------------------------- /chapter01/data/person.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | John 4 | Smith 5 | 25 6 |
7 | 21 2nd Street 8 | New York 9 | NY 10 | 10021 11 |
12 | 13 | 212 555-1234 14 | 646 555-4567 15 | 16 | 17 | male 18 | 19 |
20 | 21 | Jimy 22 | Liar 23 | 19 24 |
25 | 18 2nd Street 26 | New York 27 | NY 28 | 10021 29 |
30 | 31 | 212 555-1234 32 | 33 | 34 | male 35 | 36 |
37 | 38 | Patty 39 | Liar 40 | 20 41 |
42 | 18 2nd Street 43 | New York 44 | NY 45 | 10021 46 |
47 | 48 | 212 555-1234 49 | 001 452-8819 50 | 51 | 52 | female 53 | 54 |
55 |
56 | -------------------------------------------------------------------------------- /chapter01/factory_method.py: -------------------------------------------------------------------------------- 1 | import json 2 | import xml.etree.ElementTree as etree 3 | 4 | 5 | class JSONDataExtractor: 6 | 7 | def __init__(self, filepath): 8 | self.data = dict() 9 | with open(filepath, mode='r', encoding='utf-8') as f: 10 | self.data = json.load(f) 11 | 12 | @property 13 | def parsed_data(self): 14 | return self.data 15 | 16 | 17 | class XMLDataExtractor: 18 | 19 | def __init__(self, filepath): 20 | self.tree = etree.parse(filepath) 21 | 22 | @property 23 | def parsed_data(self): 24 | return self.tree 25 | 26 | 27 | def dataextraction_factory(filepath): 28 | if filepath.endswith('json'): 29 | extractor = JSONDataExtractor 30 | elif filepath.endswith('xml'): 31 | extractor = XMLDataExtractor 32 | else: 33 | raise ValueError('Cannot extract data from {}'.format(filepath)) 34 | return extractor(filepath) 35 | 36 | 37 | def extract_data_from(filepath): 38 | factory_obj = None 39 | try: 40 | factory_obj = dataextraction_factory(filepath) 41 | except ValueError as e: 42 | print(e) 43 | return factory_obj 44 | 45 | 46 | def main(): 47 | sqlite_factory = extract_data_from('data/person.sq3') 48 | print() 49 | 50 | json_factory = extract_data_from('data/movies.json') 51 | json_data = json_factory.parsed_data 52 | print(f'Found: {len(json_data)} movies') 53 | for movie in json_data: 54 | print(f"Title: {movie['title']}") 55 | year = movie['year'] 56 | if year: 57 | print(f"Year: {year}") 58 | director = movie['director'] 59 | if director: 60 | print(f"Director: {director}") 61 | genre = movie['genre'] 62 | if genre: 63 | print(f"Genre: {genre}") 64 | print() 65 | 66 | xml_factory = extract_data_from('data/person.xml') 67 | xml_data = xml_factory.parsed_data 68 | liars = xml_data.findall(f".//person[lastName='Liar']") 69 | print(f'found: {len(liars)} persons') 70 | for liar in liars: 71 | firstname = liar.find('firstName').text 72 | print(f'first name: {firstname}') 73 | lastname = liar.find('lastName').text 74 | print(f'last name: {lastname}') 75 | [print(f"phone number ({p.attrib['type']}):", p.text) 76 | for p in liar.find('phoneNumbers')] 77 | print() 78 | print() 79 | 80 | 81 | if __name__ == '__main__': 82 | main() 83 | -------------------------------------------------------------------------------- /chapter01/id.py: -------------------------------------------------------------------------------- 1 | 2 | class A: 3 | pass 4 | 5 | if __name__ == '__main__': 6 | a = A() 7 | b = A() 8 | 9 | print(id(a) == id(b)) 10 | print(a, b) 11 | -------------------------------------------------------------------------------- /chapter02/apple_factory.py: -------------------------------------------------------------------------------- 1 | 2 | MINI14 = '1.4GHz Mac mini' 3 | 4 | class AppleFactory: 5 | class MacMini14: 6 | def __init__(self): 7 | self.memory = 4 # in gigabytes 8 | self.hdd = 500 # in gigabytes 9 | self.gpu = 'Intel HD Graphics 5000' 10 | 11 | def __str__(self): 12 | info = (f'Model: {MINI14}', 13 | f'Memory: {self.memory}GB', 14 | f'Hard Disk: {self.hdd}GB', 15 | f'Graphics Card: {self.gpu}') 16 | return '\n'.join(info) 17 | 18 | def build_computer(self, model): 19 | if model == MINI14: 20 | return self.MacMini14() 21 | else: 22 | print(f"I don't know how to build {model}") 23 | 24 | if __name__ == '__main__': 25 | afac = AppleFactory() 26 | mac_mini = afac.build_computer(MINI14) 27 | print(mac_mini) 28 | -------------------------------------------------------------------------------- /chapter02/builder.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | import time 4 | 5 | PizzaProgress = Enum('PizzaProgress', 'queued preparation baking ready') 6 | PizzaDough = Enum('PizzaDough', 'thin thick') 7 | PizzaSauce = Enum('PizzaSauce', 'tomato creme_fraiche') 8 | PizzaTopping = Enum('PizzaTopping', 9 | 'mozzarella double_mozzarella bacon ham mushrooms red_onion oregano') 10 | STEP_DELAY = 3 # in seconds for the sake of the example 11 | 12 | 13 | class Pizza: 14 | def __init__(self, name): 15 | self.name = name 16 | self.dough = None 17 | self.sauce = None 18 | self.topping = [] 19 | 20 | def __str__(self): 21 | return self.name 22 | 23 | def prepare_dough(self, dough): 24 | self.dough = dough 25 | print(f'preparing the {self.dough.name} dough of your {self}...') 26 | time.sleep(STEP_DELAY) 27 | print(f'done with the {self.dough.name} dough') 28 | 29 | 30 | class MargaritaBuilder: 31 | def __init__(self): 32 | self.pizza = Pizza('margarita') 33 | self.progress = PizzaProgress.queued 34 | self.baking_time = 5 # in seconds for the sake of the example 35 | 36 | def prepare_dough(self): 37 | self.progress = PizzaProgress.preparation 38 | self.pizza.prepare_dough(PizzaDough.thin) 39 | 40 | def add_sauce(self): 41 | print('adding the tomato sauce to your margarita...') 42 | self.pizza.sauce = PizzaSauce.tomato 43 | time.sleep(STEP_DELAY) 44 | print('done with the tomato sauce') 45 | 46 | def add_topping(self): 47 | topping_desc = 'double mozzarella, oregano' 48 | topping_items = (PizzaTopping.double_mozzarella, PizzaTopping.oregano) 49 | print(f'adding the topping ({topping_desc}) to your margarita') 50 | self.pizza.topping.append([t for t in topping_items]) 51 | time.sleep(STEP_DELAY) 52 | print(f'done with the topping ({topping_desc})') 53 | 54 | def bake(self): 55 | self.progress = PizzaProgress.baking 56 | print(f'baking your margarita for {self.baking_time} seconds') 57 | time.sleep(self.baking_time) 58 | self.progress = PizzaProgress.ready 59 | print('your margarita is ready') 60 | 61 | 62 | class CreamyBaconBuilder: 63 | def __init__(self): 64 | self.pizza = Pizza('creamy bacon') 65 | self.progress = PizzaProgress.queued 66 | self.baking_time = 7 # in seconds for the sake of the example 67 | 68 | def prepare_dough(self): 69 | self.progress = PizzaProgress.preparation 70 | self.pizza.prepare_dough(PizzaDough.thick) 71 | 72 | def add_sauce(self): 73 | print('adding the crème fraîche sauce to your creamy bacon') 74 | self.pizza.sauce = PizzaSauce.creme_fraiche 75 | time.sleep(STEP_DELAY) 76 | print('done with the crème fraîche sauce') 77 | 78 | def add_topping(self): 79 | topping_desc = 'mozzarella, bacon, ham, mushrooms, red onion, oregano' 80 | topping_items = (PizzaTopping.mozzarella, 81 | PizzaTopping.bacon, 82 | PizzaTopping.ham, 83 | PizzaTopping.mushrooms, 84 | PizzaTopping.red_onion, 85 | PizzaTopping.oregano) 86 | print(f'adding the topping ({topping_desc}) to your creamy bacon') 87 | self.pizza.topping.append([t for t in topping_items]) 88 | time.sleep(STEP_DELAY) 89 | print(f'done with the topping ({topping_desc})') 90 | 91 | def bake(self): 92 | self.progress = PizzaProgress.baking 93 | print(f'baking your creamy bacon for {self.baking_time} seconds') 94 | time.sleep(self.baking_time) 95 | self.progress = PizzaProgress.ready 96 | print('your creamy bacon is ready') 97 | 98 | 99 | class Waiter: 100 | def __init__(self): 101 | self.builder = None 102 | 103 | def construct_pizza(self, builder): 104 | self.builder = builder 105 | steps = (builder.prepare_dough, 106 | builder.add_sauce, 107 | builder.add_topping, 108 | builder.bake) 109 | [step() for step in steps] 110 | 111 | @property 112 | def pizza(self): 113 | return self.builder.pizza 114 | 115 | 116 | def validate_style(builders): 117 | try: 118 | input_msg = 'What pizza would you like, [m]argarita or [c]reamy bacon? ' 119 | pizza_style = input(input_msg) 120 | builder = builders[pizza_style]() 121 | valid_input = True 122 | except KeyError: 123 | error_msg = 'Sorry, only margarita (key m) and creamy bacon (key c) are available' 124 | print(error_msg) 125 | return (False, None) 126 | return (True, builder) 127 | 128 | 129 | def main(): 130 | builders = dict(m=MargaritaBuilder, c=CreamyBaconBuilder) 131 | valid_input = False 132 | while not valid_input: 133 | valid_input, builder = validate_style(builders) 134 | print() 135 | waiter = Waiter() 136 | waiter.construct_pizza(builder) 137 | pizza = waiter.pizza 138 | print() 139 | print(f'Enjoy your {pizza}!') 140 | 141 | 142 | if __name__ == '__main__': 143 | main() 144 | -------------------------------------------------------------------------------- /chapter02/computer_builder.py: -------------------------------------------------------------------------------- 1 | 2 | class Computer: 3 | def __init__(self, serial_number): 4 | self.serial = serial_number 5 | self.memory = None # in gigabytes 6 | self.hdd = None # in gigabytes 7 | self.gpu = None 8 | 9 | def __str__(self): 10 | info = (f'Memory: {self.memory}GB', 11 | f'Hard Disk: {self.hdd}GB', 12 | f'Graphics Card: {self.gpu}') 13 | return '\n'.join(info) 14 | 15 | class ComputerBuilder: 16 | def __init__(self): 17 | self.computer = Computer('AG23385193') 18 | 19 | def configure_memory(self, amount): 20 | self.computer.memory = amount 21 | 22 | def configure_hdd(self, amount): 23 | self.computer.hdd = amount 24 | 25 | def configure_gpu(self, gpu_model): 26 | self.computer.gpu = gpu_model 27 | 28 | class HardwareEngineer: 29 | def __init__(self): 30 | self.builder = None 31 | 32 | def construct_computer(self, memory, hdd, gpu): 33 | self.builder = ComputerBuilder() 34 | steps = (self.builder.configure_memory(memory), 35 | self.builder.configure_hdd(hdd), 36 | self.builder.configure_gpu(gpu)) 37 | [step for step in steps] 38 | 39 | @property 40 | def computer(self): 41 | return self.builder.computer 42 | 43 | def main(): 44 | engineer = HardwareEngineer() 45 | engineer.construct_computer(hdd=500, 46 | memory=8, 47 | gpu='GeForce GTX 650 Ti') 48 | computer = engineer.computer 49 | print(computer) 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /chapter02/exercise_fluent_builder.py: -------------------------------------------------------------------------------- 1 | class Pizza: 2 | def __init__(self, builder): 3 | self.garlic = builder.garlic 4 | self.extra_cheese = builder.extra_cheese 5 | 6 | def __str__(self): 7 | garlic = 'yes' if self.garlic else 'no' 8 | cheese = 'yes' if self.extra_cheese else 'no' 9 | info = (f'Garlic: {garlic}', f'Extra cheese: {cheese}') 10 | return '\n'.join(info) 11 | 12 | class PizzaBuilder: 13 | def __init__(self): 14 | self.extra_cheese = False 15 | self.garlic = False 16 | 17 | def add_garlic(self): 18 | self.garlic = True 19 | return self 20 | 21 | def add_extra_cheese(self): 22 | self.extra_cheese = True 23 | return self 24 | 25 | def build(self): 26 | return Pizza(self) 27 | 28 | if __name__ == '__main__': 29 | pizza = Pizza.PizzaBuilder().add_garlic().add_extra_cheese().build() 30 | print(pizza) -------------------------------------------------------------------------------- /chapter03/prototype.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | class Website: 4 | def __init__(self, name, domain, description, author, **kwargs): 5 | '''Examples of optional attributes (kwargs): 6 | category, creation_date, technologies, keywords. 7 | ''' 8 | self.name = name 9 | self.domain = domain 10 | self.description = description 11 | self.author = author 12 | 13 | for key in kwargs: 14 | setattr(self, key, kwargs[key]) 15 | 16 | def __str__(self): 17 | summary = [f'Website "{self.name}"\n',] 18 | 19 | infos = vars(self).items() 20 | ordered_infos = sorted(infos) 21 | for attr, val in ordered_infos: 22 | if attr == 'name': 23 | continue 24 | summary.append(f'{attr}: {val}\n') 25 | 26 | return ''.join(summary) 27 | 28 | 29 | class Prototype: 30 | def __init__(self): 31 | self.objects = dict() 32 | 33 | def register(self, identifier, obj): 34 | self.objects[identifier] = obj 35 | 36 | def unregister(self, identifier): 37 | del self.objects[identifier] 38 | 39 | def clone(self, identifier, **attrs): 40 | found = self.objects.get(identifier) 41 | if not found: 42 | raise ValueError(f'Incorrect object identifier: {identifier}') 43 | obj = copy.deepcopy(found) 44 | for key in attrs: 45 | setattr(obj, key, attrs[key]) 46 | 47 | return obj 48 | 49 | def main(): 50 | keywords = ('python', 'data', 'apis', 'automation') 51 | site1 = Website('ContentGardening', 52 | domain='contentgardening.com', 53 | description='Automation and data-driven apps', 54 | author='Kamon Ayeva', 55 | category='Blog', 56 | keywords=keywords) 57 | 58 | prototype = Prototype() 59 | identifier = 'ka-cg-1' 60 | prototype.register(identifier, site1) 61 | 62 | site2 = prototype.clone(identifier, 63 | name='ContentGardeningPlayground', 64 | domain='play.contentgardening.com', 65 | description='Experimentation for techniques featured on the blog', 66 | category='Membership site', 67 | creation_date='2018-08-01') 68 | 69 | for site in (site1, site2): 70 | print(site) 71 | print(f'ID site1 : {id(site1)} != ID site2 : {id(site2)}') 72 | 73 | if __name__ == '__main__': 74 | main() 75 | -------------------------------------------------------------------------------- /chapter03/singleton.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import urllib.parse 4 | import urllib.request 5 | 6 | 7 | class SingletonType(type): 8 | _instances = {} 9 | def __call__(cls, *args, **kwargs): 10 | if cls not in cls._instances: 11 | cls._instances[cls] = super(SingletonType, cls).__call__(*args, **kwargs) 12 | return cls._instances[cls] 13 | 14 | 15 | class URLFetcher(metaclass=SingletonType): 16 | 17 | def __init__(self): 18 | self.urls = [] 19 | 20 | def fetch(self, url): 21 | req = urllib.request.Request(url) 22 | with urllib.request.urlopen(req) as response: 23 | if response.code == 200: 24 | the_page = response.read() 25 | print(the_page) 26 | 27 | urls = self.urls 28 | urls.append(url) 29 | self.urls = urls 30 | 31 | def dump_url_registry(self): 32 | return ', '.join(self.urls) 33 | 34 | 35 | def main(): 36 | 37 | MY_URLS = ['http://www.voidspace.org.uk', 38 | 'http://google.com', 39 | 'http://python.org', 40 | 'https://www.python.org/error', 41 | ] 42 | 43 | print(URLFetcher() is URLFetcher()) 44 | 45 | fetcher = URLFetcher() 46 | for url in MY_URLS: 47 | try: 48 | fetcher.fetch(url) 49 | except Exception as e: 50 | print(e) 51 | 52 | print('-------') 53 | done_urls = fetcher.dump_url_registry() 54 | print(f'Done URLs: {done_urls}') 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /chapter04/adapter.py: -------------------------------------------------------------------------------- 1 | 2 | from external import Musician, Dancer 3 | 4 | 5 | class Club: 6 | def __init__(self, name): 7 | self.name = name 8 | 9 | def __str__(self): 10 | return f'the club {self.name}' 11 | 12 | def organize_event(self): 13 | return 'hires an artist to perform for the people' 14 | 15 | 16 | class Adapter: 17 | def __init__(self, obj, adapted_methods): 18 | self.obj = obj 19 | self.__dict__.update(adapted_methods) 20 | 21 | def __str__(self): 22 | return str(self.obj) 23 | 24 | 25 | def main(): 26 | 27 | objects = [Club('Jazz Cafe'), Musician('Roy Ayers'), Dancer('Shane Sparks')] 28 | 29 | for obj in objects: 30 | if hasattr(obj, 'play') or hasattr(obj, 'dance'): 31 | if hasattr(obj, 'play'): 32 | adapted_methods = dict(organize_event=obj.play) 33 | elif hasattr(obj, 'dance'): 34 | adapted_methods = dict(organize_event=obj.dance) 35 | 36 | # referencing the adapted object here 37 | obj = Adapter(obj, adapted_methods) 38 | 39 | print(f'{obj} {obj.organize_event()}') 40 | 41 | 42 | if __name__ == "__main__": 43 | main() -------------------------------------------------------------------------------- /chapter04/external.py: -------------------------------------------------------------------------------- 1 | class Musician: 2 | def __init__(self, name): 3 | self.name = name 4 | 5 | def __str__(self): 6 | return f'the musician {self.name}' 7 | 8 | def play(self): 9 | return 'plays music' 10 | 11 | class Dancer: 12 | def __init__(self, name): 13 | self.name = name 14 | 15 | def __str__(self): 16 | return f'the dancer {self.name}' 17 | 18 | def dance(self): 19 | return 'does a dance performance' 20 | -------------------------------------------------------------------------------- /chapter05/mymath.py: -------------------------------------------------------------------------------- 1 | 2 | import functools 3 | 4 | def memoize(fn): 5 | cache = dict() 6 | 7 | @functools.wraps(fn) 8 | def memoizer(*args): 9 | if args not in cache: 10 | cache[args] = fn(*args) 11 | return cache[args] 12 | 13 | return memoizer 14 | 15 | @memoize 16 | def number_sum(n): 17 | '''Returns the sum of the first n numbers''' 18 | assert(n >= 0), 'n must be >= 0' 19 | if n == 0: 20 | return 0 21 | else: 22 | return n + number_sum(n-1) 23 | 24 | @memoize 25 | def fibonacci(n): 26 | '''Returns the suite of Fibonacci numbers''' 27 | assert(n >= 0), 'n must be >= 0' 28 | if n in (0, 1): 29 | return n 30 | else: 31 | return fibonacci(n-1) + fibonacci(n-2) 32 | 33 | def main(): 34 | from timeit import Timer 35 | 36 | to_execute = [ 37 | (number_sum, 38 | Timer('number_sum(300)', 'from __main__ import number_sum')), 39 | (fibonacci, 40 | Timer('fibonacci(100)', 'from __main__ import fibonacci')) 41 | ] 42 | 43 | for item in to_execute: 44 | fn = item[0] 45 | print(f'Function "{fn.__name__}": {fn.__doc__}') 46 | t = item[1] 47 | print(f'Time: {t.timeit()}') 48 | print() 49 | 50 | if __name__ == '__main__': 51 | main() 52 | -------------------------------------------------------------------------------- /chapter05/number_sum.py: -------------------------------------------------------------------------------- 1 | sum_cache = {0:0} 2 | 3 | def number_sum(n): 4 | '''Returns the sum of the first n numbers''' 5 | assert(n >= 0), 'n must be >= 0' 6 | 7 | if n in sum_cache: 8 | return sum_cache[n] 9 | res = n + number_sum(n-1) 10 | # Add the value to the cache 11 | sum_cache[n] = res 12 | return res 13 | 14 | if __name__ == '__main__': 15 | from timeit import Timer 16 | t = Timer('number_sum(300)', 'from __main__ import number_sum') 17 | print('Time: ', t.timeit()) -------------------------------------------------------------------------------- /chapter05/number_sum_naive.py: -------------------------------------------------------------------------------- 1 | 2 | def number_sum(n): 3 | '''Returns the sum of the first n numbers''' 4 | assert(n >= 0), 'n must be >= 0' 5 | 6 | if n == 0: 7 | return 0 8 | else: 9 | return n + number_sum(n-1) 10 | 11 | if __name__ == '__main__': 12 | from timeit import Timer 13 | t = Timer('number_sum(30)', 'from __main__ import number_sum') 14 | print('Time: ', t.timeit()) -------------------------------------------------------------------------------- /chapter06/bridge.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import abc 4 | import urllib.parse 5 | import urllib.request 6 | 7 | 8 | class ResourceContent: 9 | """ 10 | Define the abstraction's interface. 11 | Maintain a reference to an object which represents the Implementor. 12 | """ 13 | 14 | def __init__(self, imp): 15 | self._imp = imp 16 | 17 | def show_content(self, path): 18 | self._imp.fetch(path) 19 | 20 | 21 | class ResourceContentFetcher(metaclass=abc.ABCMeta): 22 | """ 23 | Define the interface (Implementor) for implementation classes that help fetch content. 24 | """ 25 | 26 | @abc.abstractmethod 27 | def fetch(path): 28 | pass 29 | 30 | 31 | class URLFetcher(ResourceContentFetcher): 32 | """ 33 | Implement the Implementor interface and define its concrete 34 | implementation. 35 | """ 36 | 37 | def fetch(self, path): 38 | # path is an URL 39 | req = urllib.request.Request(path) 40 | with urllib.request.urlopen(req) as response: 41 | if response.code == 200: 42 | the_page = response.read() 43 | print(the_page) 44 | 45 | 46 | class LocalFileFetcher(ResourceContentFetcher): 47 | """ 48 | Implement the Implementor interface and define its concrete 49 | implementation. 50 | """ 51 | 52 | def fetch(self, path): 53 | # path is the filepath to a text file 54 | with open(path) as f: 55 | print(f.read()) 56 | 57 | 58 | def main(): 59 | url_fetcher = URLFetcher() 60 | iface = ResourceContent(url_fetcher) 61 | iface.show_content('http://python.org') 62 | 63 | print('===================') 64 | 65 | localfs_fetcher = LocalFileFetcher() 66 | iface = ResourceContent(localfs_fetcher) 67 | iface.show_content('file.txt') 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | 73 | -------------------------------------------------------------------------------- /chapter06/file.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin in nibh in enim euismod mattis placerat in velit. Donec malesuada risus sed venenatis pharetra. Proin tincidunt porttitor euismod. Etiam non odio sodales, tincidunt elit ac, sodales nisi. Donec massa felis, pharetra ut libero nec, consectetur venenatis dui. Phasellus nec sem non erat ultricies finibus. Donec sed blandit arcu. Aliquam erat volutpat. Donec aliquam ipsum risus, et accumsan nibh faucibus non. Aenean faucibus feugiat diam vitae rutrum. Sed ullamcorper leo sed orci efficitur rhoncus. 2 | 3 | Duis vitae dolor vestibulum nibh semper faucibus. Vivamus libero quam, ultrices quis sapien vel, blandit ultricies purus. Nunc nisl lorem, rutrum id aliquam nec, dictum non ligula. Duis ullamcorper, nulla quis luctus commodo, massa lorem tristique orci, quis aliquam diam est semper nisi. Maecenas tempor odio auctor nulla efficitur, non convallis tellus iaculis. Fusce quis purus nibh. Nulla tempor est vel metus sodales, in dapibus risus molestie. Donec tristique tellus in pretium pulvinar. Pellentesque ut vehicula mauris. Vivamus pellentesque, tellus in dictum vehicula, justo ex volutpat sem, at cursus nisl elit non ex. Sed sed leo eget eros lobortis laoreet. Sed ornare vitae mi a vestibulum. Suspendisse potenti. Donec sed ligula ac enim mattis posuere. -------------------------------------------------------------------------------- /chapter07/facade.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from abc import ABCMeta, abstractmethod 3 | 4 | State = Enum('State', 'new running sleeping restart zombie') 5 | 6 | class User: 7 | pass 8 | 9 | class Process: 10 | pass 11 | 12 | class File: 13 | pass 14 | 15 | class Server(metaclass=ABCMeta): 16 | @abstractmethod 17 | def __init__(self): 18 | pass 19 | 20 | def __str__(self): 21 | return self.name 22 | 23 | @abstractmethod 24 | def boot(self): 25 | pass 26 | 27 | @abstractmethod 28 | def kill(self, restart=True): 29 | pass 30 | 31 | class FileServer(Server): 32 | def __init__(self): 33 | '''actions required for initializing the file server''' 34 | self.name = 'FileServer' 35 | self.state = State.new 36 | 37 | def boot(self): 38 | print(f'booting the {self}') 39 | '''actions required for booting the file server''' 40 | self.state = State.running 41 | 42 | def kill(self, restart=True): 43 | print(f'Killing {self}') 44 | '''actions required for killing the file server''' 45 | self.state = State.restart if restart else State.zombie 46 | 47 | def create_file(self, user, name, permissions): 48 | '''check validity of permissions, user rights, etc.''' 49 | print(f"trying to create the file '{name}' for user '{user}' with permissions {permissions}") 50 | 51 | class ProcessServer(Server): 52 | def __init__(self): 53 | '''actions required for initializing the process server''' 54 | self.name = 'ProcessServer' 55 | self.state = State.new 56 | 57 | def boot(self): 58 | print(f'booting the {self}') 59 | '''actions required for booting the process server''' 60 | self.state = State.running 61 | 62 | def kill(self, restart=True): 63 | print(f'Killing {self}') 64 | '''actions required for killing the process server''' 65 | self.state = State.restart if restart else State.zombie 66 | 67 | def create_process(self, user, name): 68 | '''check user rights, generate PID, etc.''' 69 | print(f"trying to create the process '{name}' for user '{user}'") 70 | 71 | class WindowServer: 72 | pass 73 | 74 | class NetworkServer: 75 | pass 76 | 77 | class OperatingSystem: 78 | '''The Facade''' 79 | def __init__(self): 80 | self.fs = FileServer() 81 | self.ps = ProcessServer() 82 | 83 | def start(self): 84 | [i.boot() for i in (self.fs, self.ps)] 85 | 86 | def create_file(self, user, name, permissions): 87 | return self.fs.create_file(user, name, permissions) 88 | 89 | def create_process(self, user, name): 90 | return self.ps.create_process(user, name) 91 | 92 | def main(): 93 | os = OperatingSystem() 94 | os.start() 95 | os.create_file('foo', 'hello', '-rw-r-r') 96 | os.create_process('bar', 'ls /tmp') 97 | 98 | if __name__ == '__main__': 99 | main() 100 | 101 | -------------------------------------------------------------------------------- /chapter08/flyweight.py: -------------------------------------------------------------------------------- 1 | import random 2 | from enum import Enum 3 | 4 | CarType = Enum('CarType', 'subcompact compact suv') 5 | 6 | class Car: 7 | pool = dict() 8 | 9 | def __new__(cls, car_type): 10 | obj = cls.pool.get(car_type, None) 11 | if not obj: 12 | obj = object.__new__(cls) 13 | cls.pool[car_type] = obj 14 | obj.car_type = car_type 15 | return obj 16 | 17 | def render(self, color, x, y): 18 | type = self.car_type 19 | msg = f'render a car of type {type} and color {color} at ({x}, {y})' 20 | print(msg) 21 | 22 | def main(): 23 | rnd = random.Random() 24 | #age_min, age_max = 1, 30 # in years 25 | colors = 'white black silver gray red blue brown beige yellow green'.split() 26 | min_point, max_point = 0, 100 27 | car_counter = 0 28 | 29 | for _ in range(10): 30 | c1 = Car(CarType.subcompact) 31 | c1.render(random.choice(colors), 32 | rnd.randint(min_point, max_point), 33 | rnd.randint(min_point, max_point)) 34 | car_counter += 1 35 | 36 | for _ in range(3): 37 | c2 = Car(CarType.compact) 38 | c2.render(random.choice(colors), 39 | rnd.randint(min_point, max_point), 40 | rnd.randint(min_point, max_point)) 41 | car_counter += 1 42 | 43 | for _ in range(5): 44 | c3 = Car(CarType.suv) 45 | c3.render(random.choice(colors), 46 | rnd.randint(min_point, max_point), 47 | rnd.randint(min_point, max_point)) 48 | car_counter += 1 49 | 50 | print(f'cars rendered: {car_counter}') 51 | print(f'cars actually created: {len(Car.pool)}') 52 | 53 | c4 = Car(CarType.subcompact) 54 | c5 = Car(CarType.subcompact) 55 | c6 = Car(CarType.suv) 56 | print(f'{id(c4)} == {id(c5)}? {id(c4) == id(c5)}') 57 | print(f'{id(c5)} == {id(c6)}? {id(c5) == id(c6)}') 58 | 59 | 60 | if __name__ == '__main__': 61 | main() 62 | 63 | -------------------------------------------------------------------------------- /chapter08/lazy.py: -------------------------------------------------------------------------------- 1 | 2 | class LazyProperty: 3 | def __init__(self, method): 4 | self.method = method 5 | self.method_name = method.__name__ 6 | # print(f"function overriden: {self.fget}") 7 | # print(f"function's name: {self.func_name}") 8 | 9 | def __get__(self, obj, cls): 10 | if not obj: 11 | return None 12 | value = self.method(obj) 13 | # print(f'value {value}') 14 | setattr(obj, self.method_name, value) 15 | return value 16 | 17 | 18 | class Test: 19 | def __init__(self): 20 | self.x = 'foo' 21 | self.y = 'bar' 22 | self._resource = None 23 | 24 | @LazyProperty 25 | def resource(self): 26 | print(f'initializing self._resource which is: {self._resource}') 27 | self._resource = tuple(range(5)) # expensive 28 | return self._resource 29 | 30 | 31 | def main(): 32 | t = Test() 33 | print(t.x) 34 | print(t.y) 35 | # do more work... 36 | print(t.resource) 37 | print(t.resource) 38 | 39 | 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /chapter08/mvc.py: -------------------------------------------------------------------------------- 1 | quotes = ( 2 | 'A man is not complete until he is married. Then he is finished.', 3 | 'As I said before, I never repeat myself.', 4 | 'Behind a successful man is an exhausted woman.', 5 | 'Black holes really suck...', 6 | 'Facts are stubborn things.' 7 | ) 8 | 9 | class QuoteModel: 10 | def get_quote(self, n): 11 | try: 12 | value = quotes[n] 13 | except IndexError as err: 14 | value = 'Not found!' 15 | return value 16 | 17 | class QuoteTerminalView: 18 | def show(self, quote): 19 | print(f'And the quote is: "{quote}"') 20 | 21 | def error(self, msg): 22 | print(f'Error: {msg}') 23 | 24 | def select_quote(self): 25 | return input('Which quote number would you like to see? ') 26 | 27 | class QuoteTerminalController: 28 | def __init__(self): 29 | self.model = QuoteModel() 30 | self.view = QuoteTerminalView() 31 | 32 | def run(self): 33 | valid_input = False 34 | while not valid_input: 35 | try: 36 | n = self.view.select_quote() 37 | n = int(n) 38 | valid_input = True 39 | except ValueError as err: 40 | self.view.error(f"Incorrect index '{n}'") 41 | quote = self.model.get_quote(n) 42 | self.view.show(quote) 43 | 44 | def main(): 45 | controller = QuoteTerminalController() 46 | while True: 47 | controller.run() 48 | 49 | if __name__ == '__main__': 50 | main() 51 | -------------------------------------------------------------------------------- /chapter08/proxy.py: -------------------------------------------------------------------------------- 1 | class SensitiveInfo: 2 | def __init__(self): 3 | self.users = ['nick', 'tom', 'ben', 'mike'] 4 | 5 | def read(self): 6 | nb = len(self.users) 7 | print(f"There are {nb} users: {' '.join(self.users)}") 8 | 9 | def add(self, user): 10 | self.users.append(user) 11 | print(f'Added user {user}') 12 | 13 | class Info: 14 | '''protection proxy to SensitiveInfo''' 15 | 16 | def __init__(self): 17 | self.protected = SensitiveInfo() 18 | self.secret = '0xdeadbeef' 19 | 20 | def read(self): 21 | self.protected.read() 22 | 23 | def add(self, user): 24 | sec = input('what is the secret? ') 25 | self.protected.add(user) if sec == self.secret else print("That's wrong!") 26 | 27 | def main(): 28 | info = Info() 29 | 30 | while True: 31 | print('1. read list |==| 2. add user |==| 3. quit') 32 | key = input('choose option: ') 33 | if key == '1': 34 | info.read() 35 | elif key == '2': 36 | name = input('choose username: ') 37 | info.add(name) 38 | elif key == '3': 39 | exit() 40 | else: 41 | print(f'unknown option: {key}') 42 | 43 | if __name__ == '__main__': 44 | main() -------------------------------------------------------------------------------- /chapter09/chain.py: -------------------------------------------------------------------------------- 1 | class Event: 2 | def __init__(self, name): 3 | self.name = name 4 | 5 | def __str__(self): 6 | return self.name 7 | 8 | class Widget: 9 | def __init__(self, parent=None): 10 | self.parent = parent 11 | 12 | def handle(self, event): 13 | handler = f'handle_{event}' 14 | if hasattr(self, handler): 15 | method = getattr(self, handler) 16 | method(event) 17 | elif self.parent is not None: 18 | self.parent.handle(event) 19 | elif hasattr(self, 'handle_default'): 20 | self.handle_default(event) 21 | 22 | class MainWindow(Widget): 23 | def handle_close(self, event): 24 | print(f'MainWindow: {event}') 25 | 26 | def handle_default(self, event): 27 | print(f'MainWindow Default: {event}') 28 | 29 | class SendDialog(Widget): 30 | def handle_paint(self, event): 31 | print(f'SendDialog: {event}') 32 | 33 | class MsgText(Widget): 34 | def handle_down(self, event): 35 | print(f'MsgText: {event}') 36 | 37 | def main(): 38 | mw = MainWindow() 39 | sd = SendDialog(mw) 40 | msg = MsgText(sd) 41 | 42 | for e in ('down', 'paint', 'unhandled', 'close'): 43 | evt = Event(e) 44 | print(f'Sending event -{evt}- to MainWindow') 45 | mw.handle(evt) 46 | print(f'Sending event -{evt}- to SendDialog') 47 | sd.handle(evt) 48 | print(f'Sending event -{evt}- to MsgText') 49 | msg.handle(evt) 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /chapter10/command.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | verbose = True 4 | 5 | class RenameFile: 6 | 7 | def __init__(self, src, dest): 8 | self.src = src 9 | self.dest = dest 10 | 11 | def execute(self): 12 | if verbose: 13 | print(f"[renaming '{self.src}' to '{self.dest}']") 14 | os.rename(self.src, self.dest) 15 | 16 | def undo(self): 17 | if verbose: 18 | print(f"[renaming '{self.dest}' back to '{self.src}']") 19 | os.rename(self.dest, self.src) 20 | 21 | class CreateFile: 22 | 23 | def __init__(self, path, txt='hello world\n'): 24 | self.path = path 25 | self.txt = txt 26 | 27 | def execute(self): 28 | if verbose: 29 | print(f"[creating file '{self.path}']") 30 | with open(self.path, mode='w', encoding='utf-8') as out_file: 31 | out_file.write(self.txt) 32 | 33 | def undo(self): 34 | delete_file(self.path) 35 | 36 | class ReadFile: 37 | 38 | def __init__(self, path): 39 | self.path = path 40 | 41 | def execute(self): 42 | if verbose: 43 | print(f"[reading file '{self.path}']") 44 | with open(self.path, mode='r', encoding='utf-8') as in_file: 45 | print(in_file.read(), end='') 46 | 47 | def delete_file(path): 48 | if verbose: 49 | print(f"deleting file {path}") 50 | os.remove(path) 51 | 52 | def main(): 53 | 54 | orig_name, new_name = 'file1', 'file2' 55 | 56 | commands = (CreateFile(orig_name), 57 | ReadFile(orig_name), 58 | RenameFile(orig_name, new_name)) 59 | 60 | [c.execute() for c in commands] 61 | 62 | answer = input('reverse the executed commands? [y/n] ') 63 | 64 | if answer not in 'yY': 65 | print(f"the result is {new_name}") 66 | exit() 67 | 68 | for c in reversed(commands): 69 | try: 70 | c.undo() 71 | except AttributeError as e: 72 | print("Error", str(e)) 73 | 74 | if __name__ == "__main__": 75 | main() -------------------------------------------------------------------------------- /chapter10/first-class.py: -------------------------------------------------------------------------------- 1 | import os 2 | verbose = True 3 | 4 | class CreateFile: 5 | 6 | def __init__(self, path, txt='hello world\n'): 7 | self.path = path 8 | self.txt = txt 9 | 10 | def execute(self): 11 | if verbose: 12 | print(f"[creating file '{self.path}']") 13 | with open(self.path, mode='w', encoding='utf-8') as out_file: 14 | out_file.write(self.txt) 15 | 16 | def undo(self): 17 | try: 18 | delete_file(self.path) 19 | except: 20 | print('delete action not successful...') 21 | print('... file was probably already deleted.') 22 | 23 | def delete_file(path): 24 | if verbose: 25 | print(f"deleting file {path}...") 26 | os.remove(path) 27 | 28 | def main(): 29 | 30 | orig_name = 'file1' 31 | df=delete_file 32 | 33 | commands = [CreateFile(orig_name),] 34 | commands.append(df) 35 | 36 | for c in commands: 37 | try: 38 | c.execute() 39 | except AttributeError as e: 40 | df(orig_name) 41 | 42 | for c in reversed(commands): 43 | try: 44 | c.undo() 45 | except AttributeError as e: 46 | pass 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /chapter11/observer.py: -------------------------------------------------------------------------------- 1 | 2 | class Publisher: 3 | def __init__(self): 4 | self.observers = [] 5 | 6 | def add(self, observer): 7 | if observer not in self.observers: 8 | self.observers.append(observer) 9 | else: 10 | print(f'Failed to add: {observer}') 11 | 12 | def remove(self, observer): 13 | try: 14 | self.observers.remove(observer) 15 | except ValueError: 16 | print(f'Failed to remove: {observer}') 17 | 18 | def notify(self): 19 | [o.notify(self) for o in self.observers] 20 | 21 | class DefaultFormatter(Publisher): 22 | def __init__(self, name): 23 | Publisher.__init__(self) 24 | self.name = name 25 | self._data = 0 26 | 27 | def __str__(self): 28 | return f"{type(self).__name__}: '{self.name}' has data = {self._data}" 29 | 30 | @property 31 | def data(self): 32 | return self._data 33 | 34 | @data.setter 35 | def data(self, new_value): 36 | try: 37 | self._data = int(new_value) 38 | except ValueError as e: 39 | print(f'Error: {e}') 40 | else: 41 | self.notify() 42 | 43 | class HexFormatterObs: 44 | def notify(self, publisher): 45 | value = hex(publisher.data) 46 | print(f"{type(self).__name__}: '{publisher.name}' has now hex data = {value}") 47 | 48 | class BinaryFormatterObs: 49 | def notify(self, publisher): 50 | value = bin(publisher.data) 51 | print(f"{type(self).__name__}: '{publisher.name}' has now bin data = {value}") 52 | 53 | def main(): 54 | df = DefaultFormatter('test1') 55 | print(df) 56 | 57 | print() 58 | hf = HexFormatterObs() 59 | df.add(hf) 60 | df.data = 3 61 | print(df) 62 | 63 | print() 64 | bf = BinaryFormatterObs() 65 | df.add(bf) 66 | df.data = 21 67 | print(df) 68 | 69 | print() 70 | df.remove(hf) 71 | df.data = 40 72 | print(df) 73 | 74 | print() 75 | df.remove(hf) 76 | df.add(bf) 77 | 78 | df.data = 'hello' 79 | print(df) 80 | 81 | print() 82 | df.data = 15.8 83 | print(df) 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /chapter12/state.py: -------------------------------------------------------------------------------- 1 | 2 | from state_machine import (State, Event, acts_as_state_machine, 3 | after, before, InvalidStateTransition) 4 | 5 | 6 | @acts_as_state_machine 7 | class Process: 8 | created = State(initial=True) 9 | waiting = State() 10 | running = State() 11 | terminated = State() 12 | blocked = State() 13 | swapped_out_waiting = State() 14 | swapped_out_blocked = State() 15 | 16 | wait = Event(from_states=(created, running, blocked, swapped_out_waiting), 17 | to_state=waiting) 18 | run = Event(from_states=waiting, 19 | to_state=running) 20 | terminate = Event(from_states=running, 21 | to_state=terminated) 22 | block = Event(from_states=(running, swapped_out_blocked), 23 | to_state=blocked) 24 | swap_wait = Event(from_states=waiting, 25 | to_state=swapped_out_waiting) 26 | swap_block = Event(from_states=blocked, 27 | to_state=swapped_out_blocked) 28 | 29 | def __init__(self, name): 30 | self.name = name 31 | 32 | @after('wait') 33 | def wait_info(self): 34 | print(f'{self.name} entered waiting mode') 35 | 36 | @after('run') 37 | def run_info(self): 38 | print(f'{self.name} is running') 39 | 40 | @before('terminate') 41 | def terminate_info(self): 42 | print(f'{self.name} terminated') 43 | 44 | @after('block') 45 | def block_info(self): 46 | print(f'{self.name} is blocked') 47 | 48 | @after('swap_wait') 49 | def swap_wait_info(self): 50 | print(f'{self.name} is swapped out and waiting') 51 | 52 | @after('swap_block') 53 | def swap_block_info(self): 54 | print(f'{self.name} is swapped out and blocked') 55 | 56 | def transition(process, event, event_name): 57 | try: 58 | event() 59 | except InvalidStateTransition as err: 60 | print(f'Error: transition of {process.name} from {process.current_state} to {event_name} failed') 61 | 62 | def state_info(process): 63 | print(f'state of {process.name}: {process.current_state}') 64 | 65 | def main(): 66 | RUNNING = 'running' 67 | WAITING = 'waiting' 68 | BLOCKED = 'blocked' 69 | TERMINATED = 'terminated' 70 | 71 | p1, p2 = Process('process1'), Process('process2') 72 | [state_info(p) for p in (p1, p2)] 73 | 74 | print() 75 | transition(p1, p1.wait, WAITING) 76 | transition(p2, p2.terminate, TERMINATED) 77 | [state_info(p) for p in (p1, p2)] 78 | 79 | print() 80 | transition(p1, p1.run, RUNNING) 81 | transition(p2, p2.wait, WAITING) 82 | [state_info(p) for p in (p1, p2)] 83 | 84 | print() 85 | transition(p2, p2.run, RUNNING) 86 | [state_info(p) for p in (p1, p2)] 87 | 88 | print() 89 | [transition(p, p.block, BLOCKED) for p in (p1, p2)] 90 | [state_info(p) for p in (p1, p2)] 91 | 92 | print() 93 | [transition(p, p.terminate, TERMINATED) for p in (p1, p2)] 94 | [state_info(p) for p in (p1, p2)] 95 | 96 | if __name__ == '__main__': 97 | main() 98 | -------------------------------------------------------------------------------- /chapter13/boiler.py: -------------------------------------------------------------------------------- 1 | from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums 2 | 3 | class Boiler: 4 | def __init__(self): 5 | self.temperature = 83# in celsius 6 | 7 | def __str__(self): 8 | return f'boiler temperature: {self.temperature}' 9 | 10 | def increase_temperature(self, amount): 11 | print(f"increasing the boiler's temperature by {amount} degrees") 12 | self.temperature += amount 13 | 14 | def decrease_temperature(self, amount): 15 | print(f"decreasing the boiler's temperature by {amount} degrees") 16 | self.temperature -= amount 17 | 18 | word = Word(alphanums) 19 | command = Group(OneOrMore(word)) 20 | token = Suppress("->") 21 | device = Group(OneOrMore(word)) 22 | argument = Group(OneOrMore(word)) 23 | event = command + token + device + Optional(token + argument) 24 | 25 | boiler = Boiler() 26 | print(boiler) 27 | 28 | cmd, dev, arg = event.parseString('increase -> boiler temperature -> 3 degrees') 29 | cmd_str = ' '.join(cmd) 30 | dev_str = ' '.join(dev) 31 | 32 | if 'increase' in cmd_str and 'boiler' in dev_str: 33 | boiler.increase_temperature(int(arg[0])) 34 | 35 | print(boiler) 36 | -------------------------------------------------------------------------------- /chapter13/interpreter.py: -------------------------------------------------------------------------------- 1 | 2 | from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums 3 | 4 | class Gate: 5 | def __init__(self): 6 | self.is_open = False 7 | 8 | def __str__(self): 9 | return 'open' if self.is_open else 'closed' 10 | 11 | def open(self): 12 | print('opening the gate') 13 | self.is_open = True 14 | 15 | def close(self): 16 | print('closing the gate') 17 | self.is_open = False 18 | 19 | class Garage: 20 | def __init__(self): 21 | self.is_open = False 22 | 23 | def __str__(self): 24 | return 'open' if self.is_open else 'closed' 25 | 26 | def open(self): 27 | print('opening the garage') 28 | self.is_open = True 29 | 30 | def close(self): 31 | print('closing the garage') 32 | self.is_open = False 33 | 34 | class Aircondition: 35 | def __init__(self): 36 | self.is_on = False 37 | 38 | def __str__(self): 39 | return 'on' if self.is_on else 'off' 40 | 41 | def turn_on(self): 42 | print('turning on the air condition') 43 | self.is_on = True 44 | 45 | def turn_off(self): 46 | print('turning off the air condition') 47 | self.is_on = False 48 | 49 | class Heating: 50 | def __init__(self): 51 | self.is_on = False 52 | 53 | def __str__(self): 54 | return 'on' if self.is_on else 'off' 55 | 56 | def turn_on(self): 57 | print('turning on the heating') 58 | self.is_on = True 59 | 60 | def turn_off(self): 61 | print('turning off the heating') 62 | self.is_on = False 63 | 64 | class Boiler: 65 | def __init__(self): 66 | self.temperature = 83# in celsius 67 | 68 | def __str__(self): 69 | return f'boiler temperature: {self.temperature}' 70 | 71 | def increase_temperature(self, amount): 72 | print(f"increasing the boiler's temperature by {amount} degrees") 73 | self.temperature += amount 74 | 75 | def decrease_temperature(self, amount): 76 | print(f"decreasing the boiler's temperature by {amount} degrees") 77 | self.temperature -= amount 78 | 79 | class Fridge: 80 | def __init__(self): 81 | self.temperature = 2 # in celsius 82 | 83 | def __str__(self): 84 | return f'fridge temperature: {self.temperature}' 85 | 86 | def increase_temperature(self, amount): 87 | print(f"increasing the fridge's temperature by {amount} degrees") 88 | self.temperature += amount 89 | 90 | def decrease_temperature(self, amount): 91 | print(f"decreasing the fridge's temperature by {amount} degrees") 92 | self.temperature -= amount 93 | 94 | def main(): 95 | word = Word(alphanums) 96 | command = Group(OneOrMore(word)) 97 | token = Suppress("->") 98 | device = Group(OneOrMore(word)) 99 | argument = Group(OneOrMore(word)) 100 | event = command + token + device + Optional(token + argument) 101 | 102 | gate = Gate() 103 | garage = Garage() 104 | airco = Aircondition() 105 | heating = Heating() 106 | boiler = Boiler() 107 | fridge = Fridge() 108 | 109 | tests = ('open -> gate', 110 | 'close -> garage', 111 | 'turn on -> air condition', 112 | 'turn off -> heating', 113 | 'increase -> boiler temperature -> 5 degrees', 114 | 'decrease -> fridge temperature -> 2 degrees') 115 | 116 | open_actions = {'gate':gate.open, 117 | 'garage':garage.open, 118 | 'air condition':airco.turn_on, 119 | 'heating':heating.turn_on, 120 | 'boiler temperature':boiler.increase_temperature, 121 | 'fridge temperature':fridge.increase_temperature} 122 | close_actions = {'gate':gate.close, 123 | 'garage':garage.close, 124 | 'air condition':airco.turn_off, 125 | 'heating':heating.turn_off, 126 | 'boiler temperature':boiler.decrease_temperature, 127 | 'fridge temperature':fridge.decrease_temperature} 128 | 129 | for t in tests: 130 | if len(event.parseString(t)) == 2: # no argument 131 | cmd, dev = event.parseString(t) 132 | cmd_str, dev_str = ' '.join(cmd), ' '.join(dev) 133 | if 'open' in cmd_str or 'turn on' in cmd_str: 134 | open_actions[dev_str]() 135 | elif 'close' in cmd_str or 'turn off' in cmd_str: 136 | close_actions[dev_str]() 137 | elif len(event.parseString(t)) == 3: # argument 138 | cmd, dev, arg = event.parseString(t) 139 | cmd_str = ' '.join(cmd) 140 | dev_str = ' '.join(dev) 141 | arg_str = ' '.join(cmd) 142 | num_arg = 0 143 | try: 144 | # extract the numeric part 145 | num_arg = int(arg_str.split()[0]) 146 | except ValueError as err: 147 | print(f"expected number but got: '{arg_str[0]}'") 148 | if 'increase' in cmd_str and num_arg > 0: 149 | open_actions[dev_str](num_arg) 150 | elif 'decrease' in cmd_str and num_arg > 0: 151 | close_actions[dev_str](num_arg) 152 | 153 | if __name__ == '__main__': 154 | main() 155 | 156 | -------------------------------------------------------------------------------- /chapter13/iterator.py: -------------------------------------------------------------------------------- 1 | class FootballTeamIterator: 2 | 3 | def __init__(self, members): 4 | # the list of players and coaches 5 | self.members = members 6 | self.index = 0 7 | 8 | def __iter__(self): 9 | return self 10 | 11 | def __next__(self): 12 | if self.index < len(self.members): 13 | val = self.members[self.index] 14 | self.index += 1 15 | return val 16 | else: 17 | raise StopIteration() 18 | 19 | 20 | class FootballTeam: 21 | 22 | def __init__(self, members): 23 | self.members = members 24 | 25 | def __iter__(self): 26 | return FootballTeamIterator(self.members) 27 | 28 | 29 | def main(): 30 | members = [f'player{str(x)}' for x in range(1, 23)] 31 | members = members + ['coach1', 'coach2', 'coach3'] 32 | team = FootballTeam(members) 33 | team_it = iter(team) 34 | 35 | while True: 36 | print(next(team_it)) 37 | 38 | 39 | if __name__ == '__main__': 40 | main() -------------------------------------------------------------------------------- /chapter13/memento.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | class Quote: 4 | 5 | def __init__(self, text, author): 6 | self.text = text 7 | self.author = author 8 | 9 | def save_state(self): 10 | current_state = pickle.dumps(self.__dict__) 11 | 12 | return current_state 13 | 14 | def restore_state(self, memento): 15 | previous_state = pickle.loads(memento) 16 | 17 | self.__dict__.clear() 18 | self.__dict__.update(previous_state) 19 | 20 | def __str__(self): 21 | return f'{self.text} - By {self.author}.' 22 | 23 | def main(): 24 | print('Quote 1') 25 | q1 = Quote("A room without books is like a body without a soul.", 26 | 'Unknown author') 27 | print(f'\nOriginal version:\n{q1}') 28 | q1_mem = q1.save_state() 29 | 30 | # Now, we found the author's name 31 | q1.author = 'Marcus Tullius Cicero' 32 | print(f'\nWe found the author, and did an updated:\n{q1}') 33 | 34 | # Restoring previous state (Undo) 35 | q1.restore_state(q1_mem) 36 | print(f'\nWe had to restore the previous version:\n{q1}') 37 | 38 | print() 39 | print('Quote 2') 40 | q2 = Quote("To be you in a world that is constantly trying to make you be something else is the greatest accomplishment.", 41 | 'Ralph Waldo Emerson') 42 | print(f'\nOriginal version:\n{q2}') 43 | q2_mem1 = q2.save_state() 44 | 45 | # changes to the text 46 | q2.text = "To be yourself in a world that is constantly trying to make you something else is the greatest accomplishment." 47 | print(f'\nWe fixed the text:\n{q2}') 48 | q2_mem2 = q2.save_state() 49 | 50 | q2.text = "To be yourself when the world is constantly trying to make you something else is the greatest accomplishment." 51 | print(f'\nWe fixed the text again:\n{q2}') 52 | 53 | # Restoring previous state (Undo) 54 | q2.restore_state(q2_mem2) 55 | print(f'\nWe had to restore the 2nd version, the correct one:\n{q2}') 56 | 57 | if __name__ == "__main__": 58 | main() 59 | 60 | -------------------------------------------------------------------------------- /chapter13/strategy.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | def pairs(seq): 4 | n = len(seq) 5 | for i in range(n): 6 | yield seq[i], seq[(i + 1) % n] 7 | 8 | SLOW = 3 # in seconds 9 | LIMIT = 5 # in characters 10 | WARNING = 'too bad, you picked the slow algorithm :(' 11 | 12 | def allUniqueSort(s): 13 | if len(s) > LIMIT: 14 | print(WARNING) 15 | time.sleep(SLOW) 16 | srtStr = sorted(s) 17 | for (c1, c2) in pairs(srtStr): 18 | if c1 == c2: 19 | return False 20 | return True 21 | 22 | def allUniqueSet(s): 23 | if len(s) < LIMIT: 24 | print(WARNING) 25 | time.sleep(SLOW) 26 | 27 | return True if len(set(s)) == len(s) else False 28 | 29 | def allUnique(word, strategy): 30 | return strategy(word) 31 | 32 | def main(): 33 | 34 | WORD_IN_DESC = 'Insert word (type quit to exit)> ' 35 | STRAT_IN_DESC = 'Choose strategy: [1] Use a set, [2] Sort and pair> ' 36 | 37 | while True: 38 | word = None 39 | while not word: 40 | word = input(WORD_IN_DESC) 41 | 42 | if word == 'quit': 43 | print('bye') 44 | return 45 | 46 | strategy_picked = None 47 | strategies = { '1': allUniqueSet, '2': allUniqueSort } 48 | while strategy_picked not in strategies.keys(): 49 | strategy_picked = input(STRAT_IN_DESC) 50 | 51 | try: 52 | strategy = strategies[strategy_picked] 53 | result = allUnique(word, strategy) 54 | print(f'allUnique({word}): {result}') 55 | except KeyError as err: 56 | print(f'Incorrect option: {strategy_picked}') 57 | 58 | if __name__ == "__main__": 59 | main() 60 | -------------------------------------------------------------------------------- /chapter13/template.py: -------------------------------------------------------------------------------- 1 | 2 | from cowpy import cow 3 | 4 | def generate_banner(msg, style): 5 | print('-- start of banner --') 6 | print(style(msg)) 7 | print('-- end of banner --nn') 8 | 9 | def dots_style(msg): 10 | msg = msg.capitalize() 11 | msg = '.' * 10 + msg + '.' * 10 12 | return msg 13 | 14 | def admire_style(msg): 15 | msg = msg.upper() 16 | return '!'.join(msg) 17 | 18 | def cow_style(msg): 19 | msg = cow.milk_random_cow(msg) 20 | return msg 21 | 22 | def main(): 23 | styles = (dots_style, admire_style, cow_style) 24 | msg = 'happy coding' 25 | [generate_banner(msg, style) for style in styles] 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /chapter14/peoplelist.py: -------------------------------------------------------------------------------- 1 | 2 | from faker import Faker 3 | fake = Faker() 4 | 5 | 6 | def populate(): 7 | 8 | persons = [] 9 | for _ in range(0, 20): 10 | p = {'firstname': fake.first_name(), 'lastname': fake.last_name()} 11 | persons.append(p) 12 | 13 | return iter(persons) 14 | 15 | if __name__ == '__main__': 16 | new_persons = populate() 17 | 18 | new_data = [f"{p['firstname']} {p['lastname']}" for p in new_persons] 19 | new_data = ", ".join(new_data) + ", " 20 | 21 | with open('people.txt', 'a') as f: 22 | f.write(new_data) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /chapter14/rx_example1.py: -------------------------------------------------------------------------------- 1 | from rx import Observable, Observer 2 | 3 | def get_quotes(): 4 | import contextlib, io 5 | zen = io.StringIO() 6 | with contextlib.redirect_stdout(zen): 7 | import this 8 | 9 | quotes = zen.getvalue().split('\n')[1:] 10 | return quotes 11 | 12 | def push_quotes(obs): 13 | 14 | quotes = get_quotes() 15 | for q in quotes: 16 | if q: # skip empty 17 | obs.on_next(q) 18 | obs.on_completed() 19 | 20 | class ZenQuotesObserver(Observer): 21 | 22 | def on_next(self, value): 23 | print(f"Received: {value}") 24 | 25 | def on_completed(self): 26 | print("Done!") 27 | 28 | def on_error(self, error): 29 | print(f"Error Occurred: {error}") 30 | 31 | source = Observable.create(push_quotes) 32 | 33 | source.subscribe(ZenQuotesObserver()) 34 | -------------------------------------------------------------------------------- /chapter14/rx_example2.py: -------------------------------------------------------------------------------- 1 | from rx import Observable, Observer 2 | 3 | def get_quotes(): 4 | import contextlib, io 5 | zen = io.StringIO() 6 | with contextlib.redirect_stdout(zen): 7 | import this 8 | 9 | quotes = zen.getvalue().split('\n')[1:] 10 | return enumerate(quotes) 11 | 12 | zen_quotes = get_quotes() 13 | 14 | Observable.from_(zen_quotes) \ 15 | .filter(lambda q: len(q[1]) > 0) \ 16 | .subscribe(lambda value: print(f"Received: {value[0]} - {value[1]}")) 17 | 18 | -------------------------------------------------------------------------------- /chapter14/rx_example3.py: -------------------------------------------------------------------------------- 1 | from rx import Observable, Observer 2 | 3 | def get_quotes(): 4 | import contextlib, io 5 | zen = io.StringIO() 6 | with contextlib.redirect_stdout(zen): 7 | import this 8 | 9 | quotes = zen.getvalue().split('\n')[1:] 10 | return enumerate(quotes) 11 | 12 | zen_quotes = get_quotes() 13 | 14 | Observable.interval(5000) \ 15 | .flat_map(lambda seq: Observable.from_(zen_quotes)) \ 16 | .flat_map(lambda q: Observable.from_(q[1].split())) \ 17 | .filter(lambda s: len(s) > 2) \ 18 | .map(lambda s: s.replace('.', '').replace(',', '').replace('!', '').replace('-', '')) \ 19 | .map(lambda s: s.lower()) \ 20 | .subscribe(lambda value: print(f"Received: {value}")) 21 | 22 | input("Starting... Press any key to quit\n") 23 | 24 | -------------------------------------------------------------------------------- /chapter14/rx_peoplelist_1.py: -------------------------------------------------------------------------------- 1 | from rx import Observable 2 | 3 | def firstnames_from_db(file_name): 4 | file = open(file_name) 5 | 6 | # collect and push stored people firstnames 7 | return Observable.from_(file) \ 8 | .flat_map(lambda content: content.split(', ')) \ 9 | .filter(lambda name: name!='') \ 10 | .map(lambda name: name.split()[0]) \ 11 | .group_by(lambda firstname: firstname) \ 12 | .flat_map(lambda grp: grp.count().map(lambda ct: (grp.key, ct))) 13 | 14 | db_file = "people.txt" 15 | 16 | # Emit data every 5 seconds 17 | Observable.interval(5000) \ 18 | .flat_map(lambda i: firstnames_from_db(db_file)) \ 19 | .subscribe(lambda value: print(str(value))) 20 | 21 | # Keep alive until user presses any key 22 | input("Starting... Press any key to quit\n") 23 | -------------------------------------------------------------------------------- /chapter14/rx_peoplelist_2.py: -------------------------------------------------------------------------------- 1 | from rx import Observable 2 | 3 | 4 | def frequent_firstnames_from_db(file_name): 5 | file = open(file_name) 6 | 7 | # collect and push only the frequent firstnames 8 | return Observable.from_(file) \ 9 | .flat_map(lambda content: content.split(', ')) \ 10 | .filter(lambda name: name!='') \ 11 | .map(lambda name: name.split()[0]) \ 12 | .group_by(lambda firstname: firstname) \ 13 | .flat_map(lambda grp: grp.count().map(lambda ct: (grp.key, ct))) \ 14 | .filter(lambda name_and_ct: name_and_ct[1] > 3) 15 | 16 | db_file = "people.txt" 17 | 18 | # Emit data every 5 seconds 19 | Observable.interval(5000) \ 20 | .flat_map(lambda i: frequent_firstnames_from_db(db_file)) \ 21 | .subscribe(lambda value: print(str(value))) 22 | 23 | # Keep alive until user presses any key 24 | input("Starting... Press any key to quit\n") 25 | -------------------------------------------------------------------------------- /chapter14/rx_peoplelist_3.py: -------------------------------------------------------------------------------- 1 | from rx import Observable 2 | 3 | 4 | def frequent_firstnames_from_db(file_name): 5 | file = open(file_name) 6 | 7 | # collect and push only the frequent firstnames 8 | return Observable.from_(file) \ 9 | .flat_map(lambda content: content.split(', ')) \ 10 | .filter(lambda name: name!='') \ 11 | .map(lambda name: name.split()[0]) \ 12 | .group_by(lambda firstname: firstname) \ 13 | .flat_map(lambda grp: grp.count().map(lambda ct: (grp.key, ct))) \ 14 | .filter(lambda name_and_ct: name_and_ct[1] > 3) 15 | 16 | db_file = "people.txt" 17 | 18 | # Emit data every 5 seconds, but only if it changed 19 | Observable.interval(5000) \ 20 | .flat_map(lambda i: frequent_firstnames_from_db(db_file)) \ 21 | .distinct() \ 22 | .subscribe(lambda value: print(str(value))) 23 | 24 | # Keep alive until user presses any key 25 | input("Starting... Press any key to quit\n") 26 | -------------------------------------------------------------------------------- /chapter15/cache_aside/cache_aside.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import sqlite3 3 | import csv 4 | 5 | cache_key_prefix = "quote" 6 | 7 | 8 | class QuoteCache: 9 | 10 | def __init__(self, filename=""): 11 | self.filename = filename 12 | 13 | def get(self, key): 14 | with open(self.filename) as csv_file: 15 | items = csv.reader(csv_file, delimiter=';') 16 | for item in items: 17 | if item[0] == key.split('.')[1]: 18 | return item[1] 19 | 20 | def set(self, key, quote): 21 | existing = [] 22 | with open(self.filename) as csv_file: 23 | items = csv.reader(csv_file, delimiter=';') 24 | existing = [cache_key_prefix + "." + item[0] for item in items] 25 | 26 | if key in existing: 27 | print("This is weird. The key already exists.") 28 | else: 29 | # save the new data 30 | with open(self.filename, "a", newline="") as csv_file: 31 | writer = csv.DictWriter(csv_file, 32 | fieldnames=['id', 'text'], 33 | delimiter=";") 34 | #print(f"Adding '{q[1]}' to cache") 35 | writer.writerow({'id': key.split('.')[1], 'text': quote}) 36 | 37 | 38 | cache = QuoteCache('data/quotes_cache.csv') 39 | 40 | def get_quote(quote_id): 41 | 42 | # Return the item from cache if found in it. If not found in cache, read from data store. 43 | # Put the read item in cache and return it. 44 | 45 | quote = cache.get(f"quote.{quote_id}") 46 | out = "" 47 | 48 | if quote is None: 49 | try: 50 | db = sqlite3.connect('data/quotes.sqlite3') 51 | cursor = db.cursor() 52 | cursor.execute(f"SELECT text FROM quotes WHERE id = {quote_id}") 53 | for row in cursor: 54 | quote = row[0] 55 | print(f"Got '{quote}' FROM DB") 56 | except Exception as e: 57 | print(e) 58 | finally: 59 | # Close the db connection 60 | db.close() 61 | 62 | # and add it to the cache 63 | key = f"{cache_key_prefix}.{quote_id}" 64 | cache.set(key, quote) 65 | 66 | if quote: 67 | out = f"{quote} (FROM CACHE, with key 'quote.{quote_id}')" 68 | 69 | return out 70 | 71 | 72 | if __name__ == "__main__": 73 | args = sys.argv 74 | 75 | if args[1] == 'fetch': 76 | while True: 77 | quote_id = input('Enter the ID of the quote: ') 78 | q = get_quote(quote_id) 79 | if q: 80 | print(q) 81 | -------------------------------------------------------------------------------- /chapter15/cache_aside/data/quotes.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-Python-Design-Patterns-Second-Edition/639c6712e0c5cf1cfd9576026d722b46e2bed09e/chapter15/cache_aside/data/quotes.sqlite3 -------------------------------------------------------------------------------- /chapter15/cache_aside/data/quotes_cache.csv: -------------------------------------------------------------------------------- 1 | 76;Family card magazine should manage so. 2 | 48;Eight realize third commercial feeling soldier fund. 3 | 83;Establish assume decide myself second increase bar. 4 | 62;Society practice Mrs music admit likely. 5 | 87;Management girl technology summer. 6 | 99;Assume realize fly six. 7 | 82;Account me play figure chance. 8 | 42;Congress cause suffer join either foot. 9 | 5;As for continue collection. 10 | 21;Land talk similar card service. 11 | 100;Try size on change upon. 12 | 31;Born nearly cultural tax drop probably later. 13 | -------------------------------------------------------------------------------- /chapter15/cache_aside/populate_db.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import sqlite3 3 | import csv 4 | from random import randint 5 | 6 | from faker import Faker 7 | fake = Faker() 8 | 9 | 10 | def setup_db(): 11 | 12 | try: 13 | db = sqlite3.connect('data/quotes.sqlite3') 14 | 15 | # Get a cursor object 16 | cursor = db.cursor() 17 | cursor.execute(''' 18 | CREATE TABLE quotes(id INTEGER PRIMARY KEY, text TEXT) 19 | ''') 20 | 21 | db.commit() 22 | except Exception as e: 23 | print(e) 24 | finally: 25 | db.close() 26 | 27 | 28 | def add_quotes(quotes_list): 29 | quotes = [] 30 | try: 31 | db = sqlite3.connect('data/quotes.sqlite3') 32 | 33 | cursor = db.cursor() 34 | 35 | quotes = [] 36 | for quote_text in quotes_list: 37 | quote_id = randint(1, 100) 38 | quote = (quote_id, quote_text) 39 | 40 | try: 41 | cursor.execute('''INSERT INTO quotes(id, text) VALUES(?, ?)''', quote) 42 | quotes.append(quote) 43 | except Exception as e: 44 | print(f"Error with quote id {quote_id}: {e}") 45 | 46 | db.commit() 47 | except Exception as e: 48 | print(e) 49 | finally: 50 | db.close() 51 | 52 | return quotes 53 | 54 | 55 | def main(): 56 | args = sys.argv 57 | 58 | if args[1] == 'init': 59 | setup_db() 60 | 61 | elif args[1] == 'update_db_and_cache': 62 | quotes_list = [fake.sentence() for _ in range(1, 11)] 63 | quotes = add_quotes(quotes_list) 64 | print("New (fake) quotes added to the database:") 65 | for q in quotes: 66 | print(f"Added to DB: {q}") 67 | 68 | # Populate the cache with this content 69 | with open('data/quotes_cache.csv', "a", newline="") as csv_file: 70 | writer = csv.DictWriter(csv_file, 71 | fieldnames=['id', 'text'], 72 | delimiter=";") 73 | for q in quotes: 74 | print(f"Adding '{q[1]}' to cache") 75 | writer.writerow({'id': str(q[0]), 'text': q[1]}) 76 | 77 | elif args[1] == 'update_db_only': 78 | quotes_list = [fake.sentence() for _ in range(1, 11)] 79 | quotes = add_quotes(quotes_list) 80 | print("New (fake) quotes added to the database ONLY:") 81 | for q in quotes: 82 | print(f"Added to DB: {q}") 83 | 84 | 85 | if __name__ == "__main__": 86 | main() 87 | -------------------------------------------------------------------------------- /chapter15/circuit_breaker.py: -------------------------------------------------------------------------------- 1 | 2 | # our version of a script provided in https://github.com/veltra/pybreaker-playground 3 | 4 | import pybreaker 5 | from datetime import datetime 6 | import random 7 | from time import sleep 8 | 9 | 10 | breaker = pybreaker.CircuitBreaker(fail_max=2, reset_timeout=5) 11 | 12 | @breaker 13 | def fragile_function(): 14 | if not random.choice([True, False]): 15 | print(' / OK', end='') 16 | else: 17 | print(' / FAIL', end='') 18 | raise Exception('This is a sample Exception') 19 | 20 | 21 | if __name__ == "__main__": 22 | 23 | while True: 24 | print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), end='') 25 | 26 | try: 27 | fragile_function() 28 | except Exception as e: 29 | print(' / {} {}'.format(type(e), e), end='') 30 | finally: 31 | print('') 32 | sleep(1) 33 | -------------------------------------------------------------------------------- /chapter15/circuit_breaker_in_flaskapp.py: -------------------------------------------------------------------------------- 1 | import pybreaker 2 | import random 3 | 4 | from flask import Flask 5 | app = Flask(__name__) 6 | 7 | breaker = pybreaker.CircuitBreaker(fail_max=2, reset_timeout=5) 8 | 9 | @breaker 10 | def fragile_function(): 11 | if not random.choice([True, False]): 12 | print(' / OK', end='') 13 | else: 14 | print(' / FAIL', end='') 15 | raise Exception('This is a sample Exception') 16 | 17 | @app.route("/") 18 | def hello(): 19 | 20 | fragile_function() 21 | return "Hello World" 22 | 23 | if __name__ == "__main__": 24 | app.run(debug=True) 25 | -------------------------------------------------------------------------------- /chapter15/retry_write_file.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import os 4 | 5 | 6 | def create_file(filename, after_delay=5): 7 | time.sleep(after_delay) 8 | 9 | with open(filename, 'w') as f: 10 | f.write('A file creation test') 11 | 12 | def append_data_to_file(filename): 13 | 14 | if os.path.exists(filename): 15 | with open(filename, 'a') as f: 16 | f.write(' ...Updating the file') 17 | else: 18 | raise OSError 19 | 20 | 21 | FILENAME = 'file1.txt' 22 | 23 | if __name__ == "__main__": 24 | args = sys.argv 25 | 26 | if args[1] == 'create': 27 | create_file(FILENAME) 28 | print(f"Created file '{FILENAME}'") 29 | elif args[1] == 'update': 30 | while True: 31 | try: 32 | append_data_to_file(FILENAME) 33 | print("Success! We are done!") 34 | break 35 | except OSError as e: 36 | print("Error... Try again") 37 | -------------------------------------------------------------------------------- /chapter15/retry_write_file_retrying_module.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import os 4 | from retrying import retry 5 | 6 | 7 | def create_file(filename, after_delay=5): 8 | time.sleep(after_delay) 9 | 10 | with open(filename, 'w') as f: 11 | f.write('A file creation test') 12 | 13 | @retry 14 | def append_data_to_file(filename): 15 | 16 | if os.path.exists(filename): 17 | print("got the file... let's proceed!") 18 | with open(filename, 'a') as f: 19 | f.write(' ...Updating the file') 20 | return "OK" 21 | else: 22 | print("Error: Missing file, so we can't proceed. Retrying...") 23 | raise OSError 24 | 25 | FILENAME = 'file2.txt' 26 | 27 | if __name__ == "__main__": 28 | args = sys.argv 29 | 30 | if args[1] == 'create': 31 | create_file(FILENAME) 32 | print(f"Created file '{FILENAME}'") 33 | elif args[1] == 'update': 34 | while True: 35 | out = append_data_to_file(FILENAME) 36 | if out == "OK": 37 | print("Success! We are done!") 38 | break 39 | -------------------------------------------------------------------------------- /chapter15/retry_write_file_tenacity_module.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import os 4 | import tenacity 5 | 6 | 7 | def create_file(filename, after_delay=5): 8 | time.sleep(after_delay) 9 | 10 | with open(filename, 'w') as f: 11 | f.write('A file creation test') 12 | 13 | #@tenacity.retry(wait=tenacity.wait_fixed(2)) 14 | @tenacity.retry(wait=tenacity.wait_exponential()) 15 | def append_data_to_file(filename): 16 | 17 | if os.path.exists(filename): 18 | print("got the file... let's proceed!") 19 | with open(filename, 'a') as f: 20 | f.write(' ...Updating the file') 21 | return "OK" 22 | else: 23 | print("Error: Missing file, so we can't proceed. Retrying...") 24 | raise OSError 25 | 26 | FILENAME = 'file2.txt' 27 | 28 | if __name__ == "__main__": 29 | args = sys.argv 30 | 31 | if args[1] == 'create': 32 | create_file(FILENAME) 33 | print(f"Created file '{FILENAME}'") 34 | elif args[1] == 'update': 35 | while True: 36 | out = append_data_to_file(FILENAME) 37 | if out == "OK": 38 | print("Success! We are done!") 39 | break 40 | -------------------------------------------------------------------------------- /chapter15/service_first.py: -------------------------------------------------------------------------------- 1 | 2 | from nameko.rpc import rpc 3 | from faker import Faker 4 | 5 | fake = Faker() 6 | 7 | 8 | class PeopleListService: 9 | 10 | name = 'peoplelist' 11 | 12 | @rpc 13 | def populate(self, number=20): 14 | 15 | names = [] 16 | for _ in range(0, number): 17 | n = fake.name() 18 | names.append(n) 19 | 20 | return names -------------------------------------------------------------------------------- /chapter15/service_second.py: -------------------------------------------------------------------------------- 1 | 2 | from nameko.rpc import rpc, RpcProxy 3 | from faker import Faker 4 | import csv 5 | 6 | fake = Faker() 7 | 8 | class PeopleListService: 9 | 10 | name = 'peoplelist' 11 | 12 | @rpc 13 | def populate(self, number=20): 14 | 15 | persons = [] 16 | for _ in range(0, number): 17 | p = {'firstname': fake.first_name(), 18 | 'lastname': fake.last_name(), 19 | 'address': fake.address()} 20 | persons.append(p) 21 | 22 | return persons 23 | 24 | 25 | class PeopleDataPersistenceService: 26 | 27 | name = 'people_data_persistence' 28 | peoplelist_rpc = RpcProxy('peoplelist') 29 | 30 | @rpc 31 | def save(self, filename): 32 | persons = self.peoplelist_rpc.populate(number=25) 33 | 34 | with open(filename, "a", newline="") as csv_file: 35 | fieldnames = ["firstname", "lastname", "address"] 36 | writer = csv.DictWriter(csv_file, 37 | fieldnames=fieldnames, 38 | delimiter=";") 39 | 40 | for p in persons: 41 | writer.writerow(p) 42 | 43 | return f"Saved data for {len(persons)} new people" -------------------------------------------------------------------------------- /chapter15/test_service_first.py: -------------------------------------------------------------------------------- 1 | 2 | from nameko.testing.services import worker_factory 3 | from service_first import PeopleListService 4 | 5 | def test_people(): 6 | service_worker = worker_factory(PeopleListService) 7 | result = service_worker.populate() 8 | for name in result: 9 | print(name) 10 | 11 | if __name__ == "__main__": 12 | test_people() -------------------------------------------------------------------------------- /chapter15/test_service_second.py: -------------------------------------------------------------------------------- 1 | 2 | from nameko.testing.services import worker_factory 3 | from nameko.standalone.rpc import ClusterRpcProxy 4 | from service_second import PeopleDataPersistenceService 5 | 6 | config = { 7 | 'AMQP_URI': "pyamqp://guest:guest@127.0.0.1" 8 | } 9 | 10 | def test_peopledata_persist(): 11 | with ClusterRpcProxy(config) as cluster_rpc: 12 | out = cluster_rpc.people_data_persistence.save.call_async('people.csv') 13 | print(out.result()) 14 | 15 | if __name__ == "__main__": 16 | test_peopledata_persist() -------------------------------------------------------------------------------- /chapter15/throttling_in_flaskapp.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_limiter import Limiter 3 | from flask_limiter.util import get_remote_address 4 | 5 | from flask import Flask 6 | app = Flask(__name__) 7 | limiter = Limiter( 8 | app, 9 | key_func=get_remote_address, 10 | default_limits=["100 per day", "10 per hour"] 11 | ) 12 | 13 | @app.route("/limited") 14 | def limited_api(): 15 | return "Welcome to our API!" 16 | 17 | @app.route("/more_limited") 18 | @limiter.limit("2/minute") 19 | def more_limited_api(): 20 | return "Welcome to our expensive, thus very limited, API!" 21 | 22 | 23 | if __name__ == "__main__": 24 | app.run(debug=True) 25 | --------------------------------------------------------------------------------