├── .gitignore ├── .python-version ├── .pyup.yml ├── .travis.yml ├── LICENSE ├── README.md ├── __init__.py ├── abstract_factory.py ├── adapter.py ├── borg.py ├── bridge.py ├── builder.py ├── chain_of_responsability.py ├── closure.py ├── command.py ├── composite.py ├── decorator.py ├── facade.py ├── factory_method.py ├── flyweight.py ├── interpreter.py ├── iterator.py ├── mediator.py ├── memento.py ├── mvc ├── __init__.py ├── basic_backend.py ├── dataset_backend.py ├── model_view_controller.py ├── mvc_exceptions.py ├── mvc_mock_objects.py └── sqlite_backend.py ├── null_object.py ├── object_pool.py ├── observer.py ├── poetry.lock ├── prototype.py ├── prototype_class_decorator.py ├── proxy.py ├── pyproject.toml ├── singleton.py ├── state.py ├── strategy.py ├── template_method.py ├── test_design_patterns.py └── visitor.py /.gitignore: -------------------------------------------------------------------------------- 1 | # python virtual environment 2 | venv/ 3 | 4 | # PyCharm project config files and autocompletion 5 | /.idea/* 6 | 7 | .vscode/ 8 | 9 | # compiled bytecode of Python source files 10 | *.pyc 11 | __pycache__ 12 | 13 | # log files 14 | *.log 15 | 16 | # database files 17 | *.db 18 | 19 | # some OS generated files 20 | .DS_Store 21 | 22 | # tests 23 | .pytest_cache/ 24 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.7.9 2 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # See documentation here: https://pyup.io/docs/bot/config/ 2 | 3 | # configure updates globally 4 | # default: all 5 | # allowed: all, insecure, False 6 | update: all 7 | 8 | # configure dependency pinning globally 9 | # default: True 10 | # allowed: True, False 11 | pin: True 12 | 13 | # set the default branch 14 | # default: empty, the default branch on GitHub 15 | branch: master 16 | 17 | # update schedule 18 | # default: empty 19 | # allowed: "every day", "every week", .. 20 | schedule: "every week" 21 | 22 | # search for requirement files 23 | # default: True 24 | # allowed: True, False 25 | search: True 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: xenial 3 | branches: 4 | only: 5 | - master 6 | before_install: 7 | - sudo apt-get update --quiet 8 | - sudo apt-get install graphviz -y 9 | - pip install poetry 10 | language: python 11 | python: 12 | - "3.7" 13 | - "3.8" 14 | install: 15 | - poetry install -v 16 | services: 17 | - postgresql 18 | script: 19 | - poetry run pytest --verbose 20 | notifications: 21 | email: 22 | on_success: change 23 | on_failure: always 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 jackaljack 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 | # design-patterns 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 4 | 5 | Some of the most common design patterns implemented in Python. 6 | 7 | > :warning: **READ THIS** :warning: 8 | > 9 | > *Design patterns are spoonfeed material for brainless programmers incapable of independent thought, who will be resolved to producing code as mediocre as the design patterns they use to create it*. 10 | > 11 | > — [Christer Ericson](https://realtimecollisiondetection.net/blog/?p=81), VP of Technology at Activision Central Tech. 12 | > 13 | > When I started programming, I thought that a serious programmer should have to know all major design patterns described in the book [Design Patterns: Elements of Reusable Object-Oriented Software](https://en.wikipedia.org/wiki/Design_Patterns), and religiously write code that would follow them. 14 | > 15 | > Over time, I realized that only a few of these patterns are actually good, most of them are bad, some others are completely unnecessary, especially in a dynamic language like Python. In fact, I don't recall having to use most of them, except for maybe Decorator, Observer and Strategy. 16 | > 17 | > Before reading the code in this repository, I suggest you have a look at these resources to understand whether a particular design pattern suits your use-case or not: 18 | > - [Design Patterns and Anti-Patterns, Love and Hate](https://www.yegor256.com/2016/02/03/design-patterns-and-anti-patterns.html) — Yegor Bugayenko 19 | > - [Design Patterns in Dynamic Languages](https://norvig.com/design-patterns/) — Peter Norvig 20 | 21 | ## Installation 22 | 23 | This project uses [pyenv](https://github.com/pyenv/pyenv) and [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv) to manage the Python virtual environment, and [poetry](https://poetry.eustace.io/) to manage the project dependencies. 24 | 25 | If you don't have python `3.x.x`, you have to install it. For example, I'm using `3.7.9`. 26 | 27 | ```shell 28 | pyenv install 3.7.9 29 | ``` 30 | 31 | Create a virtual environment and activate it. 32 | 33 | ```shell 34 | pyenv virtualenv 3.7.9 design_patterns 35 | pyenv activate design_patterns 36 | ``` 37 | 38 | Install all the dependencies from the `poetry.lock` file. 39 | 40 | ```shell 41 | poetry install 42 | ``` 43 | 44 | ## Usage 45 | 46 | Every python file contains an implementation of a design pattern and a simple example that can help you understand where the pattern might be useful. 47 | 48 | For example 49 | 50 | ```shell 51 | python observer.py 52 | python strategy.py 53 | # etc... 54 | ``` 55 | 56 | ## Tests 57 | 58 | If you want you can run all tests with: 59 | 60 | ```shell 61 | poetry run pytest --verbose 62 | ``` 63 | 64 | You can also test the MVC pattern with: 65 | 66 | ```shell 67 | cd mvc 68 | poetry run python model_view_controller.py 69 | 70 | # or simply 71 | python mvc/model_view_controller.py 72 | ``` 73 | 74 | ## Troubleshooting 75 | 76 | If you use pyenv and get the error `"No module named '_ctypes'"` on Ubuntu, you are probably missing the `libffi-dev` package. See [this answer](https://stackoverflow.com/a/60374453/3036129). 77 | 78 | If you get `Error: pg_config executable not found.` on Ubuntu, install the `libpq-dev` package. See [here](https://stepupautomation.wordpress.com/2020/06/23/install-psycopg2-with-pg_config-error-in-ubuntu/). 79 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackdbd/design-patterns/662b78098393b71c49bf67bc4ddc45b4c7d48e90/__init__.py -------------------------------------------------------------------------------- /abstract_factory.py: -------------------------------------------------------------------------------- 1 | import random 2 | import inspect 3 | from abc import ABC, abstractmethod 4 | 5 | 6 | class PolygonFactory(ABC): 7 | """Basic abstract Factory class for making polygons (products). 8 | 9 | This class has to be sublassed by a factory class that MUST implement 10 | the "products" method. 11 | A factory class can create many different polygon objects (products) without 12 | exposing the instantiation logic to the client. Infact, since all methods of 13 | this class are abstract, this class can't be instantiated at all! Also, each 14 | subclass of PolygonFactory should implement the "products" method and keep 15 | it abstract, so even that subclass can't be instatiated. 16 | """ 17 | 18 | @classmethod 19 | @abstractmethod 20 | def products(cls): 21 | """Products that the factory can manufacture. Implement in subclass.""" 22 | pass 23 | 24 | @classmethod 25 | @abstractmethod 26 | def make_polygon(cls, color=None): 27 | """Instantiate a random polygon from all the ones that are available. 28 | 29 | This method creates an instance of a product randomly chosen from all 30 | products that the factory class can manufacture. The 'color' property of 31 | the manufactured object is reassigned here. Then the object is returned. 32 | 33 | Parameters 34 | ---------- 35 | color : str 36 | color to assign to the manufactured object. It replaces the color 37 | assigned by the factory class. 38 | 39 | Returns 40 | ------- 41 | polygon : an instance of a class in cls.products() 42 | polygon is the product manufactured by the factory class. It's one 43 | of the products that the factory class can make. 44 | """ 45 | product_name = random.choice(cls.products()) 46 | this_module = __import__(__name__) 47 | polygon_class = getattr(this_module, product_name) 48 | polygon = polygon_class(factory_name=cls.__name__) 49 | if color is not None: 50 | polygon.color = color 51 | return polygon 52 | 53 | @classmethod 54 | @abstractmethod 55 | def color(cls): 56 | return "black" 57 | 58 | 59 | class TriangleFactory(PolygonFactory): 60 | """Abstract Factory class for making triangles.""" 61 | 62 | @classmethod 63 | @abstractmethod 64 | def products(cls): 65 | return tuple(["_TriangleEquilateral", "_TriangleIsosceles", "_TriangleScalene"]) 66 | 67 | 68 | class QuadrilateralFactory(PolygonFactory): 69 | """Abstract Factory class for making quadrilaterals.""" 70 | 71 | @classmethod 72 | @abstractmethod 73 | def products(cls): 74 | return tuple(["_Square", "_Rectangle", "_ConvexQuadrilateral"]) 75 | 76 | 77 | class _Polygon(ABC): 78 | """Basic abstract class for polygons. 79 | 80 | This class is private because the client should not try to instantiate it. 81 | The instantiation process should be carried out by a Factory class. 82 | A _Polygon subclass MUST override ALL _Polygon's abstract methods, otherwise 83 | a TypeError will be raised as soon as we try to instantiate that subclass. 84 | """ 85 | 86 | def __init__(self, factory_name=None): 87 | self._color = "black" 88 | self._manufactured = factory_name 89 | 90 | def __str__(self): 91 | return "{} {} manufactured by {} (perimeter: {}; area: {})".format( 92 | self.color, 93 | self.__class__.__name__, 94 | self.manufactured, 95 | self.perimeter, 96 | self.area, 97 | ) 98 | 99 | @property 100 | @abstractmethod 101 | def family(self): 102 | pass 103 | 104 | @property 105 | @abstractmethod 106 | def perimeter(self): 107 | pass 108 | 109 | @property 110 | @abstractmethod 111 | def area(self): 112 | pass 113 | 114 | @property 115 | def color(self): 116 | return self._color 117 | 118 | @color.setter 119 | def color(self, new_color): 120 | self._color = new_color 121 | 122 | @property 123 | def manufactured(self): 124 | return self._manufactured 125 | 126 | @manufactured.setter 127 | def manufactured(self, factory_name): 128 | self._manufactured = factory_name 129 | 130 | 131 | class _Triangle(_Polygon): 132 | """Basic concrete class for triangles.""" 133 | 134 | @property 135 | def family(self): 136 | return "Triangles" 137 | 138 | @property 139 | def perimeter(self): 140 | return "a+b+c" 141 | 142 | @property 143 | def area(self): 144 | return "base*height/2" 145 | 146 | 147 | class _TriangleEquilateral(_Triangle): 148 | @property 149 | def perimeter(self): 150 | return "3a" 151 | 152 | 153 | class _TriangleIsosceles(_Triangle): 154 | @property 155 | def perimeter(self): 156 | return "2a+b" 157 | 158 | 159 | class _TriangleScalene(_Triangle): 160 | pass 161 | 162 | 163 | class _Quadrilateral(_Polygon): 164 | """Basic concrete class for quadrilaterals.""" 165 | 166 | @property 167 | def family(self): 168 | return "Quadrilaterals" 169 | 170 | @property 171 | def perimeter(self): 172 | return "a+b+c+d" 173 | 174 | @property 175 | def area(self): 176 | return "Bretschneider's formula" 177 | 178 | 179 | class _Square(_Quadrilateral): 180 | @property 181 | def perimeter(self): 182 | return "4a" 183 | 184 | @property 185 | def area(self): 186 | return "a*a" 187 | 188 | 189 | class _Rectangle(_Quadrilateral): 190 | @property 191 | def perimeter(self): 192 | return "2a+2b" 193 | 194 | @property 195 | def area(self): 196 | return "base*height" 197 | 198 | 199 | class _ConvexQuadrilateral(_Quadrilateral): 200 | pass 201 | 202 | 203 | def give_me_some_polygons(factories, color=None): 204 | """Interface between the client and a Factory class. 205 | 206 | Parameters 207 | ---------- 208 | factories : list, or abc.ABCMeta 209 | list of factory classes, or a factory class 210 | color : str 211 | color to pass to the manufacturing method of the factory class. 212 | 213 | Returns 214 | ------- 215 | products : list 216 | a list of objects manufactured by the Factory classes specified 217 | """ 218 | if not hasattr(factories, "__len__"): 219 | factories = [factories] 220 | 221 | products = list() 222 | for factory in factories: 223 | num = random.randint(5, 10) 224 | for i in range(num): 225 | product = factory.make_polygon(color) 226 | products.append(product) 227 | 228 | return products 229 | 230 | 231 | def print_polygon(polygon, show_repr=False, show_hierarchy=False): 232 | print(str(polygon)) 233 | if show_repr: 234 | print(repr(polygon)) 235 | if show_hierarchy: 236 | print(inspect.getmro(polygon.__class__)) 237 | print("\n") 238 | 239 | 240 | def main(): 241 | print("Let's start with something simple: some triangles") 242 | triangles = give_me_some_polygons(TriangleFactory) 243 | print("{} triangles".format(len(triangles))) 244 | for triangle in triangles: 245 | print_polygon(triangle) 246 | 247 | print("\nuse a different factory and add a color") 248 | quadrilaterals = give_me_some_polygons(QuadrilateralFactory, color="blue") 249 | print("{} quadrilaterals".format(len(quadrilaterals))) 250 | for quadrilateral in quadrilaterals: 251 | print_polygon(quadrilateral) 252 | 253 | print("\nand now a mix of everything. And all in red!") 254 | factories = [TriangleFactory, QuadrilateralFactory] 255 | polygons = give_me_some_polygons(factories, color="red") 256 | print("{} polygons".format(len(polygons))) 257 | for polygon in polygons: 258 | print_polygon(polygon) 259 | 260 | print( 261 | "we can still instantiate directly any subclass of _Polygon (but we " 262 | "shouldn't because they are private)" 263 | ) 264 | print_polygon(_Square()) 265 | print("we can't instantiate _Polygon because it has abstract methods.") 266 | 267 | 268 | if __name__ == "__main__": 269 | main() 270 | -------------------------------------------------------------------------------- /adapter.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Adapter pattern is a structural design pattern. It allows a Client to access 3 | functionalities of a Supplier. 4 | Without an Adapter the Client can not access such functionalities. 5 | This pattern can be implemented with an OBJECT approach or a CLASS approach. 6 | """ 7 | 8 | 9 | # Client 10 | 11 | 12 | class Smartphone(object): 13 | 14 | max_input_voltage = 5 15 | 16 | @classmethod 17 | def outcome(cls, input_voltage): 18 | if input_voltage > cls.max_input_voltage: 19 | print("Input voltage: {}V -- BURNING!!!".format(input_voltage)) 20 | else: 21 | print("Input voltage: {}V -- Charging...".format(input_voltage)) 22 | 23 | def charge(self, input_voltage): 24 | """Charge the phone with the given input voltage.""" 25 | self.outcome(input_voltage) 26 | 27 | 28 | # Supplier 29 | 30 | 31 | class Socket(object): 32 | output_voltage = None 33 | 34 | 35 | class EUSocket(Socket): 36 | output_voltage = 230 37 | 38 | 39 | class USSocket(Socket): 40 | output_voltage = 120 41 | 42 | 43 | ################################################################################ 44 | # Approach A: OBJECT Adapter. The adapter encapsulates client and supplier. 45 | ################################################################################ 46 | 47 | 48 | class EUAdapter(object): 49 | """EUAdapter encapsulates client (Smartphone) and supplier (EUSocket).""" 50 | 51 | input_voltage = EUSocket.output_voltage 52 | output_voltage = Smartphone.max_input_voltage 53 | 54 | 55 | class USAdapter(object): 56 | """USAdapter encapsulates client (Smartphone) and supplier (USSocket).""" 57 | 58 | input_voltage = USSocket.output_voltage 59 | output_voltage = Smartphone.max_input_voltage 60 | 61 | 62 | ################################################################################ 63 | # Approach B: CLASS Adapter. Adapt the Client through multiple inheritance. 64 | ################################################################################ 65 | 66 | 67 | class CannotTransformVoltage(Exception): 68 | """Exception raised by the SmartphoneAdapter. 69 | 70 | This exception represents the fact that an adapter could not provide the 71 | right voltage to the Smartphone if the voltage of the Socket is wrong.""" 72 | 73 | pass 74 | 75 | 76 | class SmartphoneAdapter(Smartphone, Socket): 77 | @classmethod 78 | def transform_voltage(cls, input_voltage): 79 | if input_voltage == cls.output_voltage: 80 | return cls.max_input_voltage 81 | 82 | else: 83 | raise CannotTransformVoltage( 84 | "Can't transform {0}-{1}V. This adapter transforms {2}-{1}V.".format( 85 | input_voltage, cls.max_input_voltage, cls.output_voltage 86 | ) 87 | ) 88 | 89 | @classmethod 90 | def charge(cls, input_voltage): 91 | try: 92 | voltage = cls.transform_voltage(input_voltage) 93 | cls.outcome(voltage) 94 | except CannotTransformVoltage as e: 95 | print(e) 96 | 97 | 98 | class SmartphoneEUAdapter(SmartphoneAdapter, EUSocket): 99 | """System (smartphone + adapter) for a European Socket. 100 | 101 | Note: SmartphoneAdapter already inherited from Smartphone and Socket, but by 102 | re-inheriting from EUSocket we redefine all the stuff inherited from Socket. 103 | """ 104 | 105 | pass 106 | 107 | 108 | class SmartphoneUSAdapter(SmartphoneAdapter, USSocket): 109 | """System (smartphone + adapter) for an American Socket.""" 110 | 111 | pass 112 | 113 | 114 | def main(): 115 | 116 | print("Smartphone without adapter") 117 | smartphone = Smartphone() 118 | smartphone.charge(EUSocket.output_voltage) 119 | smartphone.charge(USSocket.output_voltage) 120 | 121 | print("\nSmartphone with EU adapter (object adapter approach)") 122 | smartphone.charge(EUAdapter.output_voltage) 123 | print("\nSmartphone with US adapter (object adapter approach)") 124 | smartphone.charge(USAdapter.output_voltage) 125 | 126 | print("\nSmartphone with EU adapter (class adapter approach)") 127 | smarthone_with_eu_adapter = SmartphoneEUAdapter() 128 | smarthone_with_eu_adapter.charge(EUSocket.output_voltage) 129 | smarthone_with_eu_adapter.charge(USSocket.output_voltage) 130 | print("\nSmartphone with US adapter (class adapter approach)") 131 | smarthone_with_us_adapter = SmartphoneUSAdapter() 132 | smarthone_with_us_adapter.charge(EUSocket.output_voltage) 133 | smarthone_with_us_adapter.charge(USSocket.output_voltage) 134 | 135 | 136 | if __name__ == "__main__": 137 | main() 138 | -------------------------------------------------------------------------------- /borg.py: -------------------------------------------------------------------------------- 1 | """Borg pattern 2 | 3 | Share state among instances. 4 | """ 5 | 6 | 7 | class Borg(object): 8 | """All instances of Borg share state with themselves and with all instances 9 | of Borg's subclasses, unless _shared_state is overriden in that class.""" 10 | 11 | _shared_state = {} 12 | 13 | def __init__(self, name): 14 | self.__dict__ = self._shared_state 15 | if name is not None: 16 | self.name = name 17 | 18 | def __str__(self): 19 | cls = self.__class__.__name__ 20 | return "Class: {}; ID: {}; state: {}".format(cls, id(self), self._shared_state) 21 | 22 | @property 23 | def state(self): 24 | return self._shared_state 25 | 26 | 27 | class ChildShare(Borg): 28 | """All instances of ChildShare share state with themselves and with all 29 | instances of Borg.""" 30 | 31 | def __init__(self, name=None, color=None): 32 | super().__init__(name) # ok in Python 3.x, not in 2.x 33 | 34 | if color is not None: 35 | self.color = color 36 | 37 | 38 | class ChildNotShare(Borg): 39 | """All instances of ChildNotShare share state with themselves, but not with 40 | instances of Borg or any of Borg's subclass. That's because we override 41 | _shared_state = {}. 42 | """ 43 | 44 | _shared_state = {} 45 | 46 | def __init__(self, name=None, age=None): 47 | super(self.__class__, self).__init__(name) # also ok in Python 2.x 48 | 49 | if age is not None: 50 | self.age = age 51 | 52 | 53 | def main(): 54 | print("2 instances of Borg") 55 | a = Borg("Mark") 56 | print(a) 57 | b = Borg("Luke") 58 | print(a) 59 | print(b) 60 | assert a is not b 61 | assert a.state is b.state 62 | 63 | print("\n1 instance of Borg and 1 of ChildShare") 64 | c = ChildShare("Paul", color="red") 65 | print(a) 66 | print(c) 67 | assert a.state is c.state 68 | 69 | print("\n1 instance of Borg, 1 of ChildShare, and 1 of ChildNotShare") 70 | d = ChildNotShare("Andrew", age=5) 71 | print(a) 72 | print(c) 73 | print(d) 74 | assert a.state is not d.state 75 | 76 | print("\n2 instances of ChildNotShare") 77 | e = ChildNotShare("Tom", age=7) 78 | print(d) 79 | print(e) 80 | assert d.state is e.state 81 | 82 | print("\nSet an attribute directly") 83 | a.name = "James" 84 | print(a) 85 | print(b) 86 | print(c) 87 | print(d) 88 | print(e) 89 | assert a.name is c.name 90 | assert a.name is not d.name 91 | 92 | 93 | if __name__ == "__main__": 94 | main() 95 | -------------------------------------------------------------------------------- /bridge.py: -------------------------------------------------------------------------------- 1 | """Bridge pattern 2 | 3 | Bridge is a structural design pattern. It separates responsibilities into two 4 | orthogonal class hierarchies: implementation and interface. Bridge decouples an 5 | abstraction from its implementation so that the two can vary independently. 6 | 7 | The interface class encapsulates an instance of a concrete implementation class. 8 | The client interacts with the interface class, and the interface class in turn 9 | "delegates" all requests to the implementation class. 10 | 11 | Adapter makes things work after they're designed; Bridge makes them work before 12 | they are. 13 | """ 14 | from abc import ABC, abstractmethod 15 | 16 | 17 | # Abstract Interface (aka Handle) used by the client 18 | 19 | 20 | class Website(ABC): 21 | def __init__(self, implementation): 22 | # encapsulate an instance of a concrete implementation class 23 | self._implementation = implementation 24 | 25 | def __str__(self): 26 | return "Interface: {}; Implementation: {}".format( 27 | self.__class__.__name__, self._implementation.__class__.__name__ 28 | ) 29 | 30 | @abstractmethod 31 | def show_page(self): 32 | pass 33 | 34 | 35 | # Concrete Interface 1 36 | 37 | 38 | class FreeWebsite(Website): 39 | def show_page(self): 40 | ads = self._implementation.get_ads() 41 | text = self._implementation.get_excerpt() 42 | call_to_action = self._implementation.get_call_to_action() 43 | print(ads) 44 | print(text) 45 | print(call_to_action) 46 | print("") 47 | 48 | 49 | # Concrete Interface 2 50 | 51 | 52 | class PaidWebsite(Website): 53 | def show_page(self): 54 | text = self._implementation.get_article() 55 | print(text) 56 | print("") 57 | 58 | 59 | # Abstract Implementation (aka Body) decoupled from the client 60 | 61 | 62 | class Implementation(ABC): 63 | def get_excerpt(self): 64 | return "excerpt from the article" 65 | 66 | def get_article(self): 67 | return "full article" 68 | 69 | def get_ads(self): 70 | return "some ads" 71 | 72 | @abstractmethod 73 | def get_call_to_action(self): 74 | pass 75 | 76 | 77 | # Concrete Implementation 1 78 | 79 | 80 | class ImplementationA(Implementation): 81 | def get_call_to_action(self): 82 | return "Pay 10 $ a month to remove ads" 83 | 84 | 85 | # Concrete Implementation 2 86 | 87 | 88 | class ImplementationB(Implementation): 89 | def get_call_to_action(self): 90 | return "Remove ads with just 10 $ a month" 91 | 92 | 93 | # Client 94 | 95 | 96 | def main(): 97 | a_free = FreeWebsite(ImplementationA()) 98 | print(a_free) 99 | a_free.show_page() 100 | 101 | b_free = FreeWebsite(ImplementationB()) 102 | print(b_free) 103 | b_free.show_page() 104 | 105 | a_paid = PaidWebsite(ImplementationA()) 106 | print(a_paid) 107 | a_paid.show_page() 108 | 109 | b_paid = PaidWebsite(ImplementationB()) 110 | print(b_paid) 111 | b_paid.show_page() 112 | 113 | # in a real world scenario, we could perform A/B testing of our website by 114 | # choosing a random implementation 115 | import random 116 | 117 | impl = random.choice([ImplementationA(), ImplementationB()]) 118 | print(FreeWebsite(impl)) 119 | 120 | 121 | if __name__ == "__main__": 122 | main() 123 | -------------------------------------------------------------------------------- /builder.py: -------------------------------------------------------------------------------- 1 | """Builder pattern 2 | 3 | The Builder pattern separates the construction of a complex object from its 4 | representation so that the same construction process can create different 5 | representations. 6 | """ 7 | from abc import ABC, abstractmethod 8 | 9 | 10 | class IceCream(ABC): 11 | """Abstract Product.""" 12 | 13 | @property 14 | def need_spoon(self): 15 | return False 16 | 17 | def __str__(self): 18 | string = self.__class__.__name__ 19 | for key, value in self.__dict__.items(): 20 | string += "\n{}: {}".format(key, value) 21 | string += "\n" 22 | return string 23 | 24 | 25 | class ConeIceCream(IceCream): 26 | """Concrete Product 1.""" 27 | 28 | pass 29 | 30 | 31 | class CupIceCream(IceCream): 32 | """Concrete Product 2.""" 33 | 34 | @property 35 | def need_spoon(self): 36 | return True 37 | 38 | 39 | class Builder(ABC): 40 | """Specify the abstract interface that creates all parts of the product. 41 | 42 | This Abstract interface is used by a Director object. All methods except 43 | "get_product" return self, so this class is a "fluent interface". 44 | """ 45 | 46 | @abstractmethod 47 | def __init__(self): 48 | self.product = None 49 | self.toppings = None 50 | 51 | def set_flavors(self, flavors): 52 | self.product.flavors = flavors 53 | return self 54 | 55 | def set_toppings(self): 56 | if self.toppings is not None: 57 | self.product.toppings = self.toppings 58 | return self 59 | 60 | def add_spoon(self): 61 | if self.product.need_spoon: 62 | self.product.spoon = 1 63 | return self 64 | 65 | def get_product(self): 66 | return self.product 67 | 68 | 69 | class ConeIceCreamBuilder(Builder): 70 | """Concrete Builder 1. 71 | 72 | This class assembles the product by implementing the Builder interface. 73 | It defines and keeps track of the representation it creates. 74 | """ 75 | 76 | def __init__(self): 77 | # super().__init__() # ok in Python 3.x, not in 2.x 78 | super(self.__class__, self).__init__() # also ok in Python 2.x 79 | self.product = ConeIceCream() 80 | self.toppings = "hazelnuts" 81 | 82 | 83 | class CupIceCreamBuilder(Builder): 84 | """Concrete Builder 2. 85 | 86 | This class assembles the product by implementing the Builder interface. 87 | It defines and keeps track of the representation it creates. 88 | """ 89 | 90 | def __init__(self): 91 | # super().__init__() # ok in Python 3.x, not in 2.x 92 | super(self.__class__, self).__init__() # also ok in Python 2.x 93 | self.product = CupIceCream() 94 | self.toppings = "chocolate chips" 95 | 96 | 97 | class Director(object): 98 | """Build an object using the Builder interface.""" 99 | 100 | def __init__(self, builder): 101 | self.builder = builder 102 | 103 | def build_product(self, flavors): 104 | """Prepare the product and finally return it to the client. 105 | 106 | The Builder class defined above is a "fluent interface", so we can use 107 | method chaining. 108 | 109 | Parameters 110 | ---------- 111 | flavors : list 112 | 113 | Returns 114 | ------- 115 | ConeIceCream or CupIceCream 116 | """ 117 | return ( 118 | self.builder.set_flavors(flavors).set_toppings().add_spoon().get_product() 119 | ) 120 | 121 | 122 | # Client: it creates a Director object and configures it with a Builder object. 123 | 124 | 125 | def main(): 126 | director = Director(ConeIceCreamBuilder()) 127 | product = director.build_product(["chocolate", "vanilla", "banana"]) 128 | print(product) 129 | 130 | director = Director(CupIceCreamBuilder()) 131 | product = director.build_product(["lemon", "strawberry"]) 132 | print(product) 133 | 134 | builder = ConeIceCreamBuilder() 135 | director = Director(builder) 136 | builder.toppings = None # the ConeIceCreamBuilder has no more toppings! 137 | product = director.build_product(["chocolate", "vanilla", "banana"]) 138 | print(product) 139 | 140 | 141 | if __name__ == "__main__": 142 | main() 143 | -------------------------------------------------------------------------------- /chain_of_responsability.py: -------------------------------------------------------------------------------- 1 | """Chain of responsability pattern 2 | """ 3 | from abc import ABC 4 | import random 5 | 6 | 7 | class CannotHandleRequest(Exception): 8 | pass 9 | 10 | 11 | class Node(ABC): 12 | def __init__(self): 13 | self._next_node = None 14 | 15 | def __str__(self): 16 | return "{}".format(self.__class__.__name__) 17 | 18 | @property 19 | def next_node(self): 20 | return self._next_node 21 | 22 | @next_node.setter 23 | def next_node(self, node): 24 | self._next_node = node 25 | 26 | def handle(self, request, *args, **kwargs): 27 | req_name = request["name"] 28 | req_args = request["args"] 29 | req_kwargs = request["kwargs"] 30 | method_name = "{}".format(req_name) 31 | 32 | try: 33 | method = getattr(self, method_name) 34 | except AttributeError: 35 | if self._next_node is None: 36 | raise CannotHandleRequest( 37 | 'The request "{}" could not be handled by any node in the ' 38 | "chain".format(req_name) 39 | ) 40 | 41 | else: 42 | print( 43 | 'The request "{}" cannot be handled by {}. Pass request ' 44 | "to {}".format(req_name, str(self), str(self.next_node)) 45 | ) 46 | self._next_node.handle(request, *args, **kwargs) 47 | else: 48 | print( 49 | '{} handles request "{}" with arguments {} and keywords {}'.format( 50 | str(self), req_name, req_args, req_kwargs 51 | ) 52 | ) 53 | method(request, *args, **kwargs) 54 | 55 | 56 | class WatcherNode(Node): 57 | def watch(self, request, *args, **kwargs): 58 | # implement actual behavior here 59 | string = "Process request {}".format(request) 60 | if args: 61 | string += " extra arguments {}".format(args) 62 | if kwargs: 63 | string += " extra keywords {}".format(kwargs) 64 | print(string) 65 | 66 | 67 | class BuyerNode(Node): 68 | def buy(self, request, *args, **kwargs): 69 | # implement actual behavior here 70 | string = "Process request {}".format(request) 71 | if args: 72 | string += " extra arguments {}".format(args) 73 | if kwargs: 74 | string += " extra keywords {}".format(kwargs) 75 | print(string) 76 | 77 | 78 | class EaterNode(Node): 79 | def eat(self, request, *args, **kwargs): 80 | # implement actual behavior here 81 | string = "Process request {}".format(request) 82 | if args: 83 | string += " extra arguments {}".format(args) 84 | if kwargs: 85 | string += " extra keywords {}".format(kwargs) 86 | print(string) 87 | 88 | 89 | def create_chain(*args): 90 | chain = list() 91 | for i, node in enumerate(args): 92 | if i < len(args) - 1: 93 | node.next_node = args[i + 1] 94 | chain.append(node) 95 | return chain 96 | 97 | 98 | def request_generator(): 99 | available_requests = ["buy", "watch", "eat"] 100 | available_args = [(), (123,), (42, 10)] 101 | available_kwargs = [{}, {"movie": "Hero"}, {"apple": 3, "color": "red"}] 102 | req_num = random.choice([3, 4, 5]) 103 | for i in range(req_num): 104 | req_name = random.choice(available_requests) 105 | req_args = random.choice(available_args) 106 | req_kwargs = random.choice(available_kwargs) 107 | request = {"name": req_name, "args": req_args, "kwargs": req_kwargs} 108 | yield request 109 | 110 | 111 | # Client 112 | 113 | 114 | def main(): 115 | # Client (or a third party) defines a chain of nodes (handlers) at runtime 116 | chain = create_chain(EaterNode(), BuyerNode(), WatcherNode()) 117 | root_node = chain[0] 118 | for i, req in enumerate(request_generator()): 119 | print("Request {}".format(i + 1)) 120 | root_node.handle(req) 121 | print("Request {} with extra arguments/keywords".format(i + 1)) 122 | root_node.handle(req, 1, 34, key1=44, greet="hello") 123 | print("") 124 | 125 | 126 | if __name__ == "__main__": 127 | main() 128 | -------------------------------------------------------------------------------- /closure.py: -------------------------------------------------------------------------------- 1 | """Closure pattern 2 | 3 | A closure is a record storing a function together with an environment. 4 | """ 5 | 6 | 7 | def outer(x): 8 | def inner(y): 9 | return x + y 10 | 11 | return inner 12 | 13 | 14 | def outer2(x): 15 | def inner2(y, x=x): 16 | return x + y 17 | 18 | return inner2 19 | 20 | 21 | def main(): 22 | # inner is defined in the local scope of outer, so we can't access it 23 | try: 24 | inner() 25 | except NameError as e: 26 | print(e) 27 | 28 | # a closure 29 | func = outer(3) 30 | print(func(2)) 31 | 32 | # func stores inner and the environment where inner was defined 33 | assert func.__name__ == "inner" 34 | # in inner's scope x was not defined, but it was - and still is - available 35 | # in its environment, so we can access x 36 | assert func.__code__.co_freevars[0] == "x" 37 | # so func is a closure 38 | assert func.__closure__ is not None 39 | 40 | # just a nested function, not a closure 41 | func2 = outer2(3) 42 | print(func2(2)) 43 | 44 | # func2 stores inner2 and the environment where inner2 was defined 45 | assert func2.__name__ == "inner2" 46 | # in inner2's scope x was (re)defined (variable shadowing), so it's not a 47 | # free variable 48 | assert not func2.__code__.co_freevars 49 | # so func2 is NOT a closure 50 | assert func2.__closure__ is None 51 | 52 | 53 | if __name__ == "__main__": 54 | main() 55 | -------------------------------------------------------------------------------- /command.py: -------------------------------------------------------------------------------- 1 | """Command pattern 2 | 3 | The Command pattern is handy when we want to isolate the portion of the code 4 | that executes an action, from the one that requests the execution. Command can 5 | be useful when we want to create a batch of operations and execute them later. 6 | """ 7 | import datetime 8 | from copy import deepcopy 9 | 10 | 11 | def rename_command(x, y, *args, **kwargs): 12 | undo = kwargs.get("undo", False) 13 | if not undo: 14 | print("rename {} into {}".format(x, y)) 15 | else: 16 | print("rename {} into {}".format(y, x)) 17 | 18 | 19 | def move_command(x, source, dest, *args, **kwargs): 20 | undo = kwargs.get("undo", False) 21 | if not undo: 22 | print("move {} from {} to {}".format(x, source, dest)) 23 | else: 24 | print("move {} from {} to {}".format(x, dest, source)) 25 | 26 | 27 | class Queue(object): 28 | def __init__(self): 29 | self._commands = list() 30 | self._history = list() 31 | 32 | def add_command(self, func, *args, **kwargs): 33 | timestamp = datetime.datetime.now().isoformat() 34 | self._commands.append( 35 | {"timestamp": timestamp, "func": func, "args": args, "kwargs": kwargs} 36 | ) 37 | 38 | def execute(self, commands=None): 39 | if commands is None: 40 | commands = self._commands 41 | for cmd in commands: 42 | func = cmd["func"] 43 | args, kwargs = cmd["args"], cmd["kwargs"] 44 | func(*args, **kwargs) 45 | self.update_history(commands) 46 | self.clear_queue() 47 | 48 | def redo(self): 49 | commands = self._history[-1] 50 | self.execute(commands) 51 | 52 | def undo(self): 53 | original_commands = self._history[-1] 54 | commands = deepcopy(original_commands) 55 | for cmd in commands: 56 | func = cmd["func"] 57 | args, kwargs = cmd["args"], cmd["kwargs"] 58 | # we need to store the "undo" within the command (for history) 59 | kwargs.update({"undo": True}) 60 | func(*args, **kwargs) 61 | self.update_history(commands) 62 | 63 | def clear_queue(self): 64 | self._commands = list() 65 | 66 | def update_history(self, commands): 67 | self._history.append(commands) 68 | 69 | def history(self): 70 | for i, commands in enumerate(self._history): 71 | print("Set of commands {}".format(i)) 72 | for cmd in commands: 73 | t = cmd["timestamp"] 74 | f = cmd["func"].__name__ 75 | ar, kw = cmd["args"], cmd["kwargs"] 76 | print(" {} - f: {} args: {} kwargs: {}".format(t, f, ar, kw)) 77 | 78 | 79 | def main(): 80 | queue = Queue() 81 | 82 | queue.add_command(rename_command, "test.py", "hello.py") 83 | queue.add_command(move_command, "hello.py", source="/lib", dest="/home") 84 | queue.add_command(rename_command, x="readme.txt", y="README.txt") 85 | 86 | print("Execute all commands as a single operation") 87 | queue.execute() 88 | 89 | print("\nRedo last operation") 90 | queue.redo() 91 | 92 | print("\nUndo last operation") 93 | queue.undo() 94 | 95 | print("\nExecute a single command") 96 | queue.add_command(move_command, "hello.py", source="/lib", dest="/home") 97 | queue.execute() 98 | 99 | print("\nUndo last operation") 100 | queue.undo() 101 | 102 | print("\nShow history") 103 | queue.history() 104 | 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /composite.py: -------------------------------------------------------------------------------- 1 | """Composite pattern 2 | """ 3 | from abc import ABC, abstractmethod 4 | 5 | 6 | class Component(ABC): 7 | def __init__(self, name): 8 | self.name = name 9 | self.level = 0 10 | self.indentation = "" 11 | 12 | @abstractmethod 13 | def traverse(self): 14 | """Print the name of this component and all of its children. 15 | 16 | Implement in Composite and Leaf 17 | """ 18 | pass 19 | 20 | 21 | class Leaf(Component): 22 | def traverse(self): 23 | print("{}{}".format(self.indentation, self.name)) 24 | 25 | 26 | class Composite(Component): 27 | def __init__(self, name): 28 | # super().__init__(name) # ok in Python 3.x, not in 2.x 29 | super(self.__class__, self).__init__(name) # also ok in Python 2.x 30 | self.children = list() 31 | 32 | # we can design the "child management" interface here (we have safety, 33 | # namely a client cannot append/remove a child from a Leaf), or design such 34 | # interface in the Component class (we have transparency, but a client 35 | # could try to perform meaningless things like appending a node to a Leaf) 36 | 37 | def append_child(self, child): 38 | child.level = self.level + 1 39 | child.indentation = " " * child.level * 2 40 | self.children.append(child) 41 | 42 | def remove_child(self, child): 43 | self.children.remove(child) 44 | 45 | def traverse(self): 46 | print("{}{}".format(self.indentation, self.name)) 47 | [x.traverse() for x in self.children] 48 | 49 | 50 | def main(): 51 | c0 = Composite("/") 52 | l0 = Leaf("hello.txt") 53 | l1 = Leaf("readme.txt") 54 | c1 = Composite("home") 55 | c0.append_child(l0) 56 | c0.append_child(l1) 57 | c0.append_child(c1) 58 | 59 | l2 = Leaf("notes.txt") 60 | l3 = Leaf("todo.txt") 61 | c2 = Composite("documents") 62 | c1.append_child(l2) 63 | c1.append_child(l3) 64 | c1.append_child(c2) 65 | 66 | l4 = Leaf("draft.txt") 67 | c2.append_child(l4) 68 | 69 | print("Traverse the entire directory tree") 70 | c0.traverse() 71 | 72 | print('\nRemove "todo.txt" and traverse the tree once again') 73 | c1.remove_child(l3) 74 | c0.traverse() 75 | 76 | print('\nRemove "home" and traverse the tree once again') 77 | c0.remove_child(c1) 78 | c0.traverse() 79 | 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /decorator.py: -------------------------------------------------------------------------------- 1 | """Decorator pattern 2 | 3 | Decorator is a structural design pattern. It is used to extend (decorate) the 4 | functionality of a certain object at run-time, independently of other instances 5 | of the same class. 6 | The decorator pattern is an alternative to subclassing. Subclassing adds 7 | behavior at compile time, and the change affects all instances of the original 8 | class; decorating can provide new behavior at run-time for selective objects. 9 | """ 10 | 11 | 12 | class Component(object): 13 | 14 | # method we want to decorate 15 | 16 | def whoami(self): 17 | print("I am {}".format(id(self))) 18 | 19 | # method we don't want to decorate 20 | 21 | def another_method(self): 22 | print("I am just another method of {}".format(self.__class__.__name__)) 23 | 24 | 25 | class ComponentDecorator(object): 26 | def __init__(self, decoratee): 27 | self._decoratee = decoratee # reference of the original object 28 | 29 | def whoami(self): 30 | print("start of decorated method") 31 | self._decoratee.whoami() 32 | print("end of decorated method") 33 | 34 | # forward all "Component" methods we don't want to decorate to the 35 | # "Component" pointer 36 | 37 | def __getattr__(self, name): 38 | return getattr(self._decoratee, name) 39 | 40 | 41 | def main(): 42 | a = Component() # original object 43 | b = ComponentDecorator(a) # decorate the original object at run-time 44 | print("Original object") 45 | a.whoami() 46 | a.another_method() 47 | print("\nDecorated object") 48 | b.whoami() 49 | b.another_method() 50 | 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /facade.py: -------------------------------------------------------------------------------- 1 | """ 2 | With the Facade, the external system is our customer; it is better to add 3 | complexity facing inwards if it makes the external interface simpler. 4 | """ 5 | 6 | 7 | # Complex parts 8 | 9 | 10 | class _IgnitionSystem(object): 11 | @staticmethod 12 | def produce_spark(): 13 | return True 14 | 15 | 16 | class _Engine(object): 17 | def __init__(self): 18 | self.revs_per_minute = 0 19 | 20 | def turnon(self): 21 | self.revs_per_minute = 2000 22 | 23 | def turnoff(self): 24 | self.revs_per_minute = 0 25 | 26 | 27 | class _FuelTank(object): 28 | def __init__(self, level=30): 29 | self._level = level 30 | 31 | @property 32 | def level(self): 33 | return self._level 34 | 35 | @level.setter 36 | def level(self, level): 37 | self._level = level 38 | 39 | 40 | class _DashBoardLight(object): 41 | def __init__(self, is_on=False): 42 | self._is_on = is_on 43 | 44 | def __str__(self): 45 | return self.__class__.__name__ 46 | 47 | @property 48 | def is_on(self): 49 | return self._is_on 50 | 51 | @is_on.setter 52 | def is_on(self, status): 53 | self._is_on = status 54 | 55 | def status_check(self): 56 | if self._is_on: 57 | print("{}: ON".format(str(self))) 58 | else: 59 | print("{}: OFF".format(str(self))) 60 | 61 | 62 | class _HandBrakeLight(_DashBoardLight): 63 | pass 64 | 65 | 66 | class _FogLampLight(_DashBoardLight): 67 | pass 68 | 69 | 70 | class _Dashboard(object): 71 | def __init__(self): 72 | self.lights = {"handbreak": _HandBrakeLight(), "fog": _FogLampLight()} 73 | 74 | def show(self): 75 | for light in self.lights.values(): 76 | light.status_check() 77 | 78 | 79 | # Facade 80 | 81 | 82 | class Car(object): 83 | def __init__(self): 84 | self.ignition_system = _IgnitionSystem() 85 | self.engine = _Engine() 86 | self.fuel_tank = _FuelTank() 87 | self.dashboard = _Dashboard() 88 | 89 | @property 90 | def km_per_litre(self): 91 | return 17.0 92 | 93 | def consume_fuel(self, km): 94 | litres = min(self.fuel_tank.level, km / self.km_per_litre) 95 | self.fuel_tank.level -= litres 96 | 97 | def start(self): 98 | print("\nStarting...") 99 | self.dashboard.show() 100 | if self.ignition_system.produce_spark(): 101 | self.engine.turnon() 102 | else: 103 | print("Can't start. Faulty ignition system") 104 | 105 | def has_enough_fuel(self, km, km_per_litre): 106 | litres_needed = km / km_per_litre 107 | if self.fuel_tank.level > litres_needed: 108 | return True 109 | 110 | else: 111 | return False 112 | 113 | def drive(self, km=100): 114 | print("\n") 115 | if self.engine.revs_per_minute > 0: 116 | while self.has_enough_fuel(km, self.km_per_litre): 117 | self.consume_fuel(km) 118 | print("Drove {}km".format(km)) 119 | print("{:.2f}l of fuel still left".format(self.fuel_tank.level)) 120 | else: 121 | print("Can't drive. The Engine is turned off!") 122 | 123 | def park(self): 124 | print("\nParking...") 125 | self.dashboard.lights["handbreak"].is_on = True 126 | self.dashboard.show() 127 | self.engine.turnoff() 128 | 129 | def switch_fog_lights(self, status): 130 | print("\nSwitching {} fog lights...".format(status)) 131 | boolean = True if status == "ON" else False 132 | self.dashboard.lights["fog"].is_on = boolean 133 | self.dashboard.show() 134 | 135 | def fill_up_tank(self): 136 | print("\nFuel tank filled up!") 137 | self.fuel_tank.level = 100 138 | 139 | 140 | # the main function is the Client 141 | 142 | 143 | def main(): 144 | car = Car() 145 | car.start() 146 | car.drive() 147 | 148 | car.switch_fog_lights("ON") 149 | car.switch_fog_lights("OFF") 150 | 151 | car.park() 152 | car.fill_up_tank() 153 | car.drive() 154 | 155 | car.start() 156 | car.drive() 157 | 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /factory_method.py: -------------------------------------------------------------------------------- 1 | """Factory Method pattern 2 | """ 3 | 4 | 5 | class _Car(object): 6 | pass 7 | 8 | 9 | class _Bike(object): 10 | pass 11 | 12 | 13 | def factory_method(product_type): 14 | if product_type == "car": 15 | return _Car() 16 | 17 | elif product_type == "bike": 18 | return _Bike() 19 | 20 | else: 21 | raise ValueError("Cannot make: {}".format(product_type)) 22 | 23 | 24 | def main(): 25 | for product_type in ("car", "bike"): 26 | product = factory_method(product_type) 27 | print(str(product)) 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /flyweight.py: -------------------------------------------------------------------------------- 1 | """Flyweight pattern 2 | 3 | Separate an object into two parts: 4 | 1. an intrinsic, immutable part that can be shared across all instances 5 | (state-independent) 6 | 2. an extrinsic, mutable part that is specific for each instance and cannot 7 | be shared (state-dependent) 8 | """ 9 | import random 10 | from abc import ABC 11 | 12 | 13 | class Model3D(object): 14 | pass 15 | 16 | 17 | class OrkModel(Model3D): 18 | pass 19 | 20 | 21 | class AlienModel(Model3D): 22 | pass 23 | 24 | 25 | class Intelligence(object): 26 | pass 27 | 28 | 29 | class HighIntelligence(Intelligence): 30 | pass 31 | 32 | 33 | class Enemy(object): 34 | 35 | immutables = (Model3D, Intelligence) 36 | 37 | def __init__(self, position=(0, 0)): 38 | self.position = position 39 | 40 | 41 | class Ork(Enemy): 42 | 43 | immutables = (OrkModel, Intelligence) 44 | 45 | 46 | class Alien(Enemy): 47 | 48 | immutables = (AlienModel, Intelligence) 49 | 50 | 51 | class Queen(Alien): 52 | 53 | immutables = (AlienModel, HighIntelligence) 54 | 55 | 56 | class Factory(ABC): 57 | 58 | pool = dict() 59 | 60 | @classmethod 61 | def make_enemy(cls, enemy_type, position): 62 | this_module = __import__(__name__) 63 | enemy_class = getattr(this_module, enemy_type) 64 | 65 | # add immutable objects to the pool if they are not already there 66 | for imm in enemy_class.immutables: 67 | immutable_name = imm.__name__ 68 | obj = cls.pool.get(immutable_name, None) 69 | if obj is None: 70 | obj = object.__new__(imm) 71 | cls.pool[immutable_name] = obj 72 | print("NEW IMMUTABLE in the pool: {}".format(immutable_name)) 73 | 74 | return enemy_class(position=position) 75 | 76 | 77 | if __name__ == "__main__": 78 | ork_model_identities = list() 79 | alien_model_identities = list() 80 | intelligence_identities = list() 81 | high_intelligence_identities = list() 82 | 83 | for i in range(10): 84 | x, y = (random.randint(0, 100), random.randint(0, 100)) 85 | enemy = Factory.make_enemy("Ork", position=(x, y)) 86 | ork_model_identities.append(id(enemy.immutables[0])) 87 | intelligence_identities.append(id(enemy.immutables[1])) 88 | print(enemy) 89 | 90 | print("") 91 | for i in range(10): 92 | x, y = (random.randint(0, 100), random.randint(0, 100)) 93 | enemy = Factory.make_enemy("Alien", position=(x, y)) 94 | alien_model_identities.append(id(enemy.immutables[0])) 95 | intelligence_identities.append(id(enemy.immutables[1])) 96 | print(enemy) 97 | 98 | print("") 99 | for i in range(2): 100 | x, y = (random.randint(0, 100), random.randint(0, 100)) 101 | enemy = Factory.make_enemy("Queen", position=(x, y)) 102 | alien_model_identities.append(id(enemy.immutables[0])) 103 | high_intelligence_identities.append(id(enemy.immutables[1])) 104 | print(enemy) 105 | 106 | print("\nImmutable parts of the same type share the same identity") 107 | print("ork_model_identities:\n{}".format(set(ork_model_identities))) 108 | print("alien_model_identities:\n{}".format(set(alien_model_identities))) 109 | print("intelligence_identities:\n{}".format(set(intelligence_identities))) 110 | print("high_intelligence_identities:\n{}".format(set(high_intelligence_identities))) 111 | assert len(set(ork_model_identities)) == 1 112 | assert len(set(alien_model_identities)) == 1 113 | assert len(set(intelligence_identities)) == 1 114 | assert len(set(high_intelligence_identities)) == 1 115 | -------------------------------------------------------------------------------- /interpreter.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums 3 | 4 | 5 | class DeviceNotAvailable(Exception): 6 | pass 7 | 8 | 9 | class ActionNotAvailable(Exception): 10 | pass 11 | 12 | 13 | class IncorrectAction(Exception): 14 | pass 15 | 16 | 17 | class Device(object): 18 | def __call__(self, *args): 19 | action = args[0] 20 | try: 21 | method = getattr(self, action) 22 | except AttributeError: 23 | raise ActionNotAvailable( 24 | '!!! "{}" not available for {}'.format(action, self.__class__.__name__) 25 | ) 26 | 27 | signature = inspect.signature(method) 28 | # arity of the device's method (excluding self) 29 | arity = len(signature.parameters.keys()) 30 | # or alternatively 31 | # arity = method.__code__.co_argcount -1 32 | # arity = len(inspect.getfullargspec(method)[0]) - 1 33 | 34 | num_args = len([a for a in args if a is not None]) 35 | if arity != num_args - 1: 36 | parameters = list(signature.parameters.keys()) 37 | if parameters: 38 | substring = "these parameters {}".format(parameters) 39 | else: 40 | substring = "no parameters" 41 | err_msg = '!!! "{}" on {} must be defined with {}'.format( 42 | action, self.__class__.__name__, substring 43 | ) 44 | raise IncorrectAction(err_msg) 45 | 46 | else: 47 | if num_args == 1: 48 | method() 49 | elif num_args == 2: 50 | method(int(args[1])) 51 | else: 52 | raise Exception 53 | 54 | 55 | class Garage(Device): 56 | def __init__(self): 57 | self.is_open = False 58 | 59 | def open(self): 60 | print("opening the garage") 61 | self.is_open = True 62 | 63 | def close(self): 64 | print("closing the garage") 65 | self.is_open = False 66 | 67 | 68 | class Boiler(Device): 69 | def __init__(self): 70 | self.temperature = 83 71 | 72 | def heat(self, amount): 73 | print("heat the boiler up by {} degrees".format(amount)) 74 | self.temperature += amount 75 | 76 | def cool(self, amount): 77 | print("cool the boiler down by {} degrees".format(amount)) 78 | self.temperature -= amount 79 | 80 | 81 | class Television(Device): 82 | def __init__(self): 83 | self.is_on = False 84 | 85 | def switch_on(self): 86 | print("switch on the television") 87 | self.is_on = True 88 | 89 | def switch_off(self): 90 | print("switch off the television") 91 | self.is_on = False 92 | 93 | 94 | class Interpreter(object): 95 | 96 | DEVICES = {"boiler": Boiler(), "garage": Garage(), "television": Television()} 97 | 98 | @staticmethod 99 | def parse(input_string): 100 | word = Word(alphanums) 101 | command = Group(OneOrMore(word)) 102 | token = Suppress("->") 103 | device = Group(OneOrMore(word)) 104 | argument = Group(OneOrMore(word)) 105 | event = command + token + device + Optional(token + argument) 106 | parse_results = event.parseString(input_string) 107 | cmd = parse_results[0] 108 | dev = parse_results[1] 109 | cmd_str = "_".join(cmd) 110 | dev_str = "_".join(dev) 111 | try: 112 | val = parse_results[2] 113 | except IndexError: 114 | val_str = None 115 | else: 116 | val_str = "_".join(val) 117 | return cmd_str, dev_str, val_str 118 | 119 | def interpret(self, input_string): 120 | cmd_str, dev_str, val_str = self.parse(input_string) 121 | try: 122 | device = self.DEVICES[dev_str] 123 | except KeyError: 124 | raise DeviceNotAvailable( 125 | "!!! {} is not available an available " "device".format(dev_str) 126 | ) 127 | 128 | else: 129 | device(cmd_str, val_str) 130 | 131 | 132 | def main(): 133 | interpreter = Interpreter() 134 | 135 | valid_inputs = ( 136 | "open -> garage", 137 | "heat -> boiler -> 5", 138 | "cool -> boiler -> 3", 139 | "switch on -> television", 140 | "switch off -> television", 141 | ) 142 | 143 | for valid_input in valid_inputs: 144 | interpreter.interpret(valid_input) 145 | 146 | try: 147 | interpreter.interpret("read -> book") 148 | except DeviceNotAvailable as e: 149 | print(e) 150 | 151 | try: 152 | interpreter.interpret("heat -> boiler") 153 | except IncorrectAction as e: 154 | print(e) 155 | 156 | try: 157 | interpreter.interpret("throw away -> television") 158 | except ActionNotAvailable as e: 159 | print(e) 160 | 161 | 162 | if __name__ == "__main__": 163 | main() 164 | -------------------------------------------------------------------------------- /iterator.py: -------------------------------------------------------------------------------- 1 | """Iterator pattern 2 | """ 3 | 4 | 5 | class MyIterator(object): 6 | def __init__(self, *args): 7 | self.data = args 8 | self.index = 0 9 | 10 | def __iter__(self): 11 | generator = self.generator_function() 12 | return generator 13 | 14 | def __next__(self): 15 | if len(self.data) > self.index: 16 | obj = self.data[self.index] 17 | self.index += 1 18 | else: 19 | raise StopIteration("No more elements!") 20 | 21 | return obj 22 | 23 | def __getitem__(self, index): 24 | return self.data[index] 25 | 26 | def __len__(self): 27 | return len(self.data[self.index :]) 28 | 29 | def generator_function(self): 30 | for d in self.data[self.index :]: 31 | self.index += 1 32 | yield "Item: {} (type: {})".format(d, type(d)) 33 | 34 | 35 | def some_function(a, b, c=123): 36 | print(a, b, c) 37 | 38 | 39 | def main(): 40 | iterator = MyIterator(1, 3.0, None, "hello", some_function) 41 | print("Initial length") 42 | print(len(iterator)) 43 | 44 | index = 3 45 | print("\nGet item at index {}".format(index)) 46 | print(iterator[index]) 47 | 48 | print("\n__next__ calls") 49 | print(next(iterator)) 50 | print(next(iterator)) 51 | 52 | print("\nLength after __next__ calls") 53 | print(len(iterator)) 54 | 55 | print("\nFor loop with the remaining items") 56 | for item in iterator: 57 | print(item) 58 | 59 | print("\nStopIteration") 60 | try: 61 | next(iterator) 62 | except StopIteration as e: 63 | print(e) 64 | 65 | print("But in Python we don't really need any of the above...") 66 | for item in (1, 3.0, None, "hello", some_function): 67 | print(item) 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /mediator.py: -------------------------------------------------------------------------------- 1 | """Mediator pattern 2 | """ 3 | import random 4 | import time 5 | 6 | 7 | class ControlTower(object): 8 | def __init__(self): 9 | self.available_runways = list() 10 | self.engaged_runways = list() 11 | 12 | def authorize_landing(self): 13 | if not self.available_runways: 14 | print("Request denied. No available runways") 15 | return False 16 | 17 | else: 18 | runway = self.available_runways.pop() 19 | self.engaged_runways.append(runway) 20 | print("Request granted. Please land on runway {}".format(runway)) 21 | self.status() 22 | return True 23 | 24 | def authorize_takeoff(self): 25 | # for simplicity, all takeoff requests are granted 26 | time.sleep(random.randint(0, 2)) 27 | runway = self.engaged_runways.pop() 28 | self.available_runways.append(runway) 29 | self.status() 30 | 31 | def status(self): 32 | print( 33 | "The control tower has {} available runway/s".format( 34 | len(self.available_runways) 35 | ) 36 | ) 37 | 38 | 39 | class Airplane(object): 40 | def __init__(self): 41 | self.control_tower = None 42 | 43 | @property 44 | def registered(self): 45 | return True if self.control_tower is not None else False 46 | 47 | def register(self, control_tower): 48 | self.control_tower = control_tower 49 | print("An airplane registers with the control tower") 50 | 51 | def request_landing(self): 52 | is_authorized = self.control_tower.authorize_landing() 53 | if is_authorized: 54 | self.land() 55 | 56 | def land(self): 57 | print("The airplane {} lands".format(self)) 58 | 59 | def takeoff(self): 60 | print("The airplane {} takes off".format(self)) 61 | self.control_tower.authorize_takeoff() 62 | 63 | 64 | class Runway(object): 65 | def register(self, control_tower): 66 | print("A runway has been registered with the control tower") 67 | control_tower.available_runways.append(self) 68 | control_tower.status() 69 | 70 | 71 | def main(): 72 | print("There is an airport with 2 runways and a control tower\n") 73 | r1 = Runway() 74 | r2 = Runway() 75 | ct = ControlTower() 76 | r1.register(ct) 77 | r2.register(ct) 78 | 79 | print("\n3 airplanes approach the airport and register with the tower") 80 | a1 = Airplane() 81 | a2 = Airplane() 82 | a3 = Airplane() 83 | a1.register(ct) 84 | a2.register(ct) 85 | a3.register(ct) 86 | 87 | print( 88 | "\nTwo airplanes request for landing. There are enough runways, so " 89 | "the requests are granted" 90 | ) 91 | a1.request_landing() 92 | a2.request_landing() 93 | 94 | print( 95 | "\nThe third airplane also makes a request for landing. There are no" 96 | " runways available, so the request is denied" 97 | ) 98 | a3.request_landing() 99 | 100 | print( 101 | "\nAfter a while, the first airplane takes off, so now the third " 102 | "airplane can land" 103 | ) 104 | a1.takeoff() 105 | a3.request_landing() 106 | 107 | 108 | if __name__ == "__main__": 109 | main() 110 | -------------------------------------------------------------------------------- /memento.py: -------------------------------------------------------------------------------- 1 | """Memento pattern 2 | """ 3 | 4 | 5 | class Originator(object): 6 | """Originator is some object that has an internal state. 7 | The Originator knows how to save and restore itself, but it's the Caretaker 8 | than controls when to save and restore the Originator.""" 9 | 10 | def __init__(self): 11 | self._state = None 12 | 13 | @property 14 | def state(self): 15 | return self._state 16 | 17 | @state.setter 18 | def state(self, new_state): 19 | self._state = new_state 20 | 21 | def save(self): 22 | """Create a Memento and copy the Originator state in it.""" 23 | return Memento(self.state) 24 | 25 | def restore(self, memento): 26 | """Restore the Originator to a previous state (stored in Memento).""" 27 | self.state = memento.state 28 | 29 | 30 | class Memento(object): 31 | """Memento is an opaque object that holds the state of an Originator.""" 32 | 33 | def __init__(self, state): 34 | self._state = state 35 | 36 | @property 37 | def state(self): 38 | return self._state 39 | 40 | 41 | # Client, the Caretaker of the Memento pattern. 42 | # The Caretaker is going to do something to the Originator, but wants to be 43 | # able to undo the change. 44 | # The Caretaker holds the Memento but cannot change it (Memento is opaque). 45 | # The Caretaker knows when to save and when to restore the Originator. 46 | 47 | 48 | def main(): 49 | originator = Originator() 50 | originator.state = "State1" 51 | memento1 = originator.save() 52 | originator.state = "State2" 53 | memento2 = originator.save() 54 | originator.state = "State3" 55 | originator.state = "State4" 56 | 57 | originator.restore(memento1) 58 | assert originator.state == "State1" 59 | print(originator.state) 60 | 61 | originator.restore(memento2) 62 | assert originator.state == "State2" 63 | print(originator.state) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /mvc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackdbd/design-patterns/662b78098393b71c49bf67bc4ddc45b4c7d48e90/mvc/__init__.py -------------------------------------------------------------------------------- /mvc/basic_backend.py: -------------------------------------------------------------------------------- 1 | import mvc_exceptions as mvc_exc 2 | import mvc_mock_objects as mock 3 | 4 | items = list() 5 | 6 | 7 | def create_item(name, price, quantity): 8 | global items 9 | results = list(filter(lambda x: x["name"] == name, items)) 10 | if results: 11 | raise mvc_exc.ItemAlreadyStored('"{}" already stored!'.format(name)) 12 | 13 | else: 14 | items.append({"name": name, "price": price, "quantity": quantity}) 15 | 16 | 17 | def create_items(app_items): 18 | global items 19 | items = app_items 20 | 21 | 22 | def read_item(name): 23 | global items 24 | myitems = list(filter(lambda x: x["name"] == name, items)) 25 | if myitems: 26 | return myitems[0] 27 | 28 | else: 29 | raise mvc_exc.ItemNotStored( 30 | "Can't read \"{}\" because it's not stored".format(name) 31 | ) 32 | 33 | 34 | def read_items(): 35 | global items 36 | return [item for item in items] 37 | 38 | 39 | def update_item(name, price, quantity): 40 | global items 41 | # Python 3.x removed tuple parameters unpacking (PEP 3113), so we have to do 42 | # it manually (i_x is a tuple, idxs_items is a list of tuples) 43 | idxs_items = list(filter(lambda i_x: i_x[1]["name"] == name, enumerate(items))) 44 | if idxs_items: 45 | i, item_to_update = idxs_items[0][0], idxs_items[0][1] 46 | items[i] = {"name": name, "price": price, "quantity": quantity} 47 | else: 48 | raise mvc_exc.ItemNotStored( 49 | "Can't update \"{}\" because it's not stored".format(name) 50 | ) 51 | 52 | 53 | def delete_item(name): 54 | global items 55 | # Python 3.x removed tuple parameters unpacking (PEP 3113), so we have to do 56 | # it manually (i_x is a tuple, idxs_items is a list of tuples) 57 | idxs_items = list(filter(lambda i_x: i_x[1]["name"] == name, enumerate(items))) 58 | if idxs_items: 59 | i, item_to_delete = idxs_items[0][0], idxs_items[0][1] 60 | del items[i] 61 | else: 62 | raise mvc_exc.ItemNotStored( 63 | "Can't delete \"{}\" because it's not stored".format(name) 64 | ) 65 | 66 | 67 | def main(): 68 | 69 | # CREATE 70 | create_items(mock.items()) 71 | create_item("beer", price=3.0, quantity=15) 72 | # if we try to re-create an object we get an ItemAlreadyStored exception 73 | # create_item('beer', price=2.0, quantity=10) 74 | 75 | # READ 76 | print("READ items") 77 | print(read_items()) 78 | # if we try to read an object not stored we get an ItemNotStored exception 79 | # print('READ chocolate') 80 | # print(read_item('chocolate')) 81 | print("READ bread") 82 | print(read_item("bread")) 83 | 84 | # UPDATE 85 | print("UPDATE bread") 86 | update_item("bread", price=2.0, quantity=30) 87 | print(read_item("bread")) 88 | # if we try to update an object not stored we get an ItemNotStored exception 89 | # print('UPDATE chocolate') 90 | # update_item('chocolate', price=10.0, quantity=20) 91 | 92 | # DELETE 93 | print("DELETE beer") 94 | delete_item("beer") 95 | # if we try to delete an object not stored we get an ItemNotStored exception 96 | # print('DELETE chocolate') 97 | # delete_item('chocolate') 98 | 99 | print("READ items") 100 | print(read_items()) 101 | 102 | 103 | if __name__ == "__main__": 104 | main() 105 | -------------------------------------------------------------------------------- /mvc/dataset_backend.py: -------------------------------------------------------------------------------- 1 | """Some tests with the dataset module. 2 | 3 | https://dataset.readthedocs.io/en/latest/ 4 | """ 5 | import dataset 6 | from sqlalchemy.exc import IntegrityError, NoSuchTableError 7 | import mvc_exceptions as mvc_exc 8 | import mvc_mock_objects as mock 9 | 10 | 11 | DB_name = "myDB" 12 | 13 | 14 | class UnsupportedDatabaseEngine(Exception): 15 | pass 16 | 17 | 18 | def connect_to_db(db_name=None, db_engine="sqlite"): 19 | """Connect to a database. Create the database if there isn't one yet. 20 | 21 | The database can be a SQLite DB (either a DB file or an in-memory DB), or a 22 | PostgreSQL DB. In order to connect to a PostgreSQL DB you have first to 23 | create a database, create a user, and finally grant him all necessary 24 | privileges on that database and tables. 25 | 'postgresql://:@localhost:/' 26 | Note: at the moment it looks it's not possible to close a connection 27 | manually (e.g. like calling conn.close() in sqlite3). 28 | 29 | 30 | Parameters 31 | ---------- 32 | db_name : str or None 33 | database name (without file extension .db) 34 | db_engine : str 35 | database engine ('sqlite' or 'postgres') 36 | 37 | Returns 38 | ------- 39 | dataset.persistence.database.Database 40 | connection to a database 41 | """ 42 | engines = {"sqlite", "postgres"} 43 | if db_name is None: 44 | db_string = "sqlite:///:memory:" 45 | print("New connection to in-memory SQLite DB...") 46 | else: 47 | if db_engine == "sqlite": 48 | db_string = "sqlite:///{}.db".format(DB_name) 49 | print("New connection to SQLite DB...") 50 | elif db_engine == "postgres": 51 | db_string = "postgresql://test_user:test_password@localhost:5432/testdb" 52 | # db_string = \ 53 | # 'postgresql://test_user2:test_password2@localhost:5432/testdb' 54 | print("New connection to PostgreSQL DB...") 55 | else: 56 | raise UnsupportedDatabaseEngine( 57 | "No database engine with this name. " 58 | "Choose one of the following: {}".format(engines) 59 | ) 60 | 61 | return dataset.connect(db_string) 62 | 63 | 64 | def create_table(conn, table_name): 65 | """Load a table or create it if it doesn't exist yet. 66 | 67 | The function load_table can only load a table if exist, and raises a 68 | NoSuchTableError if the table does not already exist in the database. 69 | 70 | The function get_table either loads a table or creates it if it doesn't 71 | exist yet. The new table will automatically have an id column unless 72 | specified via optional parameter primary_id, which will be used as the 73 | primary key of the table. 74 | 75 | Parameters 76 | ---------- 77 | table_name : str 78 | conn : dataset.persistence.database.Database 79 | """ 80 | try: 81 | conn.load_table(table_name) 82 | except NoSuchTableError as e: 83 | print("Table {} does not exist. It will be created now".format(e)) 84 | conn.get_table(table_name, primary_id="name", primary_type="String") 85 | print("Created table {} on database {}".format(table_name, DB_name)) 86 | 87 | 88 | def insert_one(conn, name, price, quantity, table_name): 89 | """Insert a single item in a table. 90 | 91 | Parameters 92 | ---------- 93 | name : str 94 | price : float 95 | quantity : int 96 | table_name : dataset.persistence.table.Table 97 | conn : dataset.persistence.database.Database 98 | 99 | Raises 100 | ------ 101 | mvc_exc.ItemAlreadyStored: if the record is already stored in the table. 102 | """ 103 | table = conn.load_table(table_name) 104 | try: 105 | table.insert(dict(name=name, price=price, quantity=quantity)) 106 | except IntegrityError as e: 107 | raise mvc_exc.ItemAlreadyStored( 108 | '"{}" already stored in table "{}".\nOriginal Exception raised: {}'.format( 109 | name, table.table.name, e 110 | ) 111 | ) 112 | 113 | 114 | def insert_many(conn, items, table_name): 115 | """Insert all items in a table. 116 | 117 | Parameters 118 | ---------- 119 | items : list 120 | list of dictionaries 121 | table_name : str 122 | conn : dataset.persistence.database.Database 123 | """ 124 | # TODO: check what happens if 1+ records can be inserted but 1 fails 125 | table = conn.load_table(table_name) 126 | try: 127 | for x in items: 128 | table.insert(dict(name=x["name"], price=x["price"], quantity=x["quantity"])) 129 | except IntegrityError as e: 130 | print( 131 | 'At least one in {} was already stored in table "{}".\nOriginal ' 132 | "Exception raised: {}".format( 133 | [x["name"] for x in items], table.table.name, e 134 | ) 135 | ) 136 | 137 | 138 | def select_one(conn, name, table_name): 139 | """Select a single item in a table. 140 | 141 | The dataset library returns a result as an OrderedDict. 142 | 143 | Parameters 144 | ---------- 145 | name : str 146 | name of the record to look for in the table 147 | table_name : str 148 | conn : dataset.persistence.database.Database 149 | 150 | Raises 151 | ------ 152 | mvc_exc.ItemNotStored: if the record is not stored in the table. 153 | """ 154 | table = conn.load_table(table_name) 155 | row = table.find_one(name=name) 156 | if row is not None: 157 | return dict(row) 158 | 159 | else: 160 | raise mvc_exc.ItemNotStored( 161 | 'Can\'t read "{}" because it\'s not stored in table "{}"'.format( 162 | name, table.table.name 163 | ) 164 | ) 165 | 166 | 167 | def select_all(conn, table_name): 168 | """Select all items in a table. 169 | 170 | The dataset library returns results as OrderedDicts. 171 | 172 | Parameters 173 | ---------- 174 | table_name : str 175 | conn : dataset.persistence.database.Database 176 | 177 | Returns 178 | ------- 179 | list 180 | list of dictionaries. Each dict is a record. 181 | """ 182 | table = conn.load_table(table_name) 183 | rows = table.all() 184 | return list(map(lambda x: dict(x), rows)) 185 | 186 | 187 | def update_one(conn, name, price, quantity, table_name): 188 | """Update a single item in the table. 189 | 190 | Note: dataset update method is a bit counterintuitive to use. Read the docs 191 | here: https://dataset.readthedocs.io/en/latest/quickstart.html#storing-data 192 | Dataset has also an upsert functionality: if rows with matching keys exist 193 | they will be updated, otherwise a new row is inserted in the table. 194 | 195 | Parameters 196 | ---------- 197 | name : str 198 | price : float 199 | quantity : int 200 | table_name : str 201 | conn : dataset.persistence.database.Database 202 | 203 | Raises 204 | ------ 205 | mvc_exc.ItemNotStored: if the record is not stored in the table. 206 | """ 207 | table = conn.load_table(table_name) 208 | row = table.find_one(name=name) 209 | if row is not None: 210 | item = {"name": name, "price": price, "quantity": quantity} 211 | table.update(item, keys=["name"]) 212 | else: 213 | raise mvc_exc.ItemNotStored( 214 | 'Can\'t update "{}" because it\'s not stored in table "{}"'.format( 215 | name, table.table.name 216 | ) 217 | ) 218 | 219 | 220 | def delete_one(conn, item_name, table_name): 221 | """Delete a single item in a table. 222 | 223 | Parameters 224 | ---------- 225 | item_name : str 226 | table_name : str 227 | conn : dataset.persistence.database.Database 228 | 229 | Raises 230 | ------ 231 | mvc_exc.ItemNotStored: if the record is not stored in the table. 232 | """ 233 | table = conn.load_table(table_name) 234 | row = table.find_one(name=item_name) 235 | if row is not None: 236 | table.delete(name=item_name) 237 | else: 238 | raise mvc_exc.ItemNotStored( 239 | 'Can\'t delete "{}" because it\'s not stored in table "{}"'.format( 240 | item_name, table.table.name 241 | ) 242 | ) 243 | 244 | 245 | def main(): 246 | 247 | conn = connect_to_db() 248 | 249 | table_name = "items" 250 | create_table(conn, table_name) 251 | 252 | # CREATE 253 | insert_many(conn, items=mock.items(), table_name=table_name) 254 | insert_one(conn, "beer", price=2.0, quantity=5, table_name=table_name) 255 | # if we try to insert an object already stored we get an ItemAlreadyStored 256 | # exception 257 | # insert_one(conn, 'beer', 2.0, 5, table_name=table_name) 258 | 259 | # READ 260 | print("SELECT milk") 261 | print(select_one(conn, "milk", table_name=table_name)) 262 | print("SELECT all") 263 | print(select_all(conn, table_name=table_name)) 264 | # if we try to select an object not stored we get an ItemNotStored exception 265 | # print(select_one(conn, 'pizza', table_name=table_name)) 266 | 267 | # UPDATE 268 | print("UPDATE bread, SELECT bread") 269 | update_one(conn, "bread", price=1.5, quantity=5, table_name=table_name) 270 | print(select_one(conn, "bread", table_name=table_name)) 271 | # if we try to update an object not stored we get an ItemNotStored exception 272 | # print('UPDATE pizza') 273 | # update_one(conn, 'pizza', 9.5, 5, table_name=table_name) 274 | 275 | # DELETE 276 | print("DELETE beer, SELECT all") 277 | delete_one(conn, "beer", table_name=table_name) 278 | print(select_all(conn, table_name=table_name)) 279 | 280 | 281 | # if we try to delete an object not stored we get an ItemNotStored exception 282 | # print('DELETE fish') 283 | # delete_one(conn, 'fish', table_name=table_name) 284 | 285 | if __name__ == "__main__": 286 | main() 287 | -------------------------------------------------------------------------------- /mvc/model_view_controller.py: -------------------------------------------------------------------------------- 1 | """Model–view–controller (MVC) is a software architectural pattern. 2 | MVC divides a given software application into three interconnected parts, so as 3 | to separate internal representations of information (Model) from the ways that 4 | information is presented to (View) or accepted from (Controller) the user. 5 | """ 6 | import basic_backend 7 | import sqlite_backend 8 | import dataset_backend 9 | import mvc_exceptions as mvc_exc 10 | import mvc_mock_objects as mock 11 | 12 | 13 | class Model(object): 14 | """The Model class is the business logic of the application. 15 | 16 | The Model class provides methods to access the data of the application and 17 | performs CRUD operations. The data can be stored in the Model itself or in 18 | a database. Only the Model can access the database. A Model never calls 19 | View's methods. 20 | """ 21 | 22 | def __init__(self): 23 | self._item_type = "product" 24 | 25 | @property 26 | def item_type(self): 27 | return self._item_type 28 | 29 | @item_type.setter 30 | def item_type(self, new_item_type): 31 | self._item_type = new_item_type 32 | 33 | def create_item(self, name, price, quantity): 34 | raise NotImplementedError("Implement in subclass") 35 | 36 | def create_items(self, items): 37 | raise NotImplementedError("Implement in subclass") 38 | 39 | def read_item(self, name): 40 | raise NotImplementedError("Implement in subclass") 41 | 42 | def read_items(self): 43 | raise NotImplementedError("Implement in subclass") 44 | 45 | def update_item(self, name, price, quantity): 46 | raise NotImplementedError("Implement in subclass") 47 | 48 | def delete_item(self, name): 49 | raise NotImplementedError("Implement in subclass") 50 | 51 | 52 | class ModelBasic(Model): 53 | def __init__(self, application_items): 54 | # super().__init__() # ok in Python 3.x, not in 2.x 55 | super(self.__class__, self).__init__() # also ok in Python 2.x 56 | self.create_items(application_items) 57 | 58 | def create_item(self, name, price, quantity): 59 | basic_backend.create_item(name, price, quantity) 60 | 61 | def create_items(self, items): 62 | basic_backend.create_items(items) 63 | 64 | def read_item(self, name): 65 | return basic_backend.read_item(name) 66 | 67 | def read_items(self): 68 | return basic_backend.read_items() 69 | 70 | def update_item(self, name, price, quantity): 71 | basic_backend.update_item(name, price, quantity) 72 | 73 | def delete_item(self, name): 74 | basic_backend.delete_item(name) 75 | 76 | 77 | class ModelSQLite(Model): 78 | def __init__(self, application_items): 79 | # super().__init__() # ok in Python 3.x, not in 2.x 80 | super(self.__class__, self).__init__() # also ok in Python 2.x 81 | self._connection = sqlite_backend.connect_to_db(sqlite_backend.DB_name) 82 | sqlite_backend.create_table(self.connection, self._item_type) 83 | self.create_items(application_items) 84 | 85 | @property 86 | def connection(self): 87 | return self._connection 88 | 89 | def create_item(self, name, price, quantity): 90 | sqlite_backend.insert_one( 91 | self.connection, name, price, quantity, table_name=self.item_type 92 | ) 93 | 94 | def create_items(self, items): 95 | sqlite_backend.insert_many(self.connection, items, table_name=self.item_type) 96 | 97 | def read_item(self, name): 98 | return sqlite_backend.select_one( 99 | self.connection, name, table_name=self.item_type 100 | ) 101 | 102 | def read_items(self): 103 | return sqlite_backend.select_all(self.connection, table_name=self.item_type) 104 | 105 | def update_item(self, name, price, quantity): 106 | sqlite_backend.update_one( 107 | self.connection, name, price, quantity, table_name=self.item_type 108 | ) 109 | 110 | def delete_item(self, name): 111 | sqlite_backend.delete_one(self.connection, name, table_name=self.item_type) 112 | 113 | 114 | class ModelDataset(Model): 115 | def __init__(self, application_items): 116 | # super().__init__() # ok in Python 3.x, not in 2.x 117 | super(self.__class__, self).__init__() # also ok in Python 2.x 118 | self._connection = dataset_backend.connect_to_db( 119 | dataset_backend.DB_name, db_engine="postgres" 120 | ) 121 | dataset_backend.create_table(self.connection, self._item_type) 122 | self.create_items(application_items) 123 | 124 | @property 125 | def connection(self): 126 | return self._connection 127 | 128 | def create_item(self, name, price, quantity): 129 | dataset_backend.insert_one( 130 | self.connection, name, price, quantity, table_name=self.item_type 131 | ) 132 | 133 | def create_items(self, items): 134 | dataset_backend.insert_many(self.connection, items, table_name=self.item_type) 135 | 136 | def read_item(self, name): 137 | return dataset_backend.select_one( 138 | self.connection, name, table_name=self.item_type 139 | ) 140 | 141 | def read_items(self): 142 | return dataset_backend.select_all(self.connection, table_name=self.item_type) 143 | 144 | def update_item(self, name, price, quantity): 145 | dataset_backend.update_one( 146 | self.connection, name, price, quantity, table_name=self.item_type 147 | ) 148 | 149 | def delete_item(self, name): 150 | dataset_backend.delete_one(self.connection, name, table_name=self.item_type) 151 | 152 | 153 | class View(object): 154 | """The View class deals with how the data is presented to the user. 155 | 156 | A View should never call its own methods. Only a Controller should do it. 157 | """ 158 | 159 | @staticmethod 160 | def show_bullet_point_list(item_type, items): 161 | print("--- {} LIST ---".format(item_type.upper())) 162 | for item in items: 163 | print("* {}".format(item)) 164 | 165 | @staticmethod 166 | def show_number_point_list(item_type, items): 167 | print("--- {} LIST ---".format(item_type.upper())) 168 | for i, item in enumerate(items): 169 | print("{}. {}".format(i + 1, item)) 170 | 171 | @staticmethod 172 | def show_item(item_type, item, item_info): 173 | print("//////////////////////////////////////////////////////////////") 174 | print("Good news, we have some {}!".format(item.upper())) 175 | print("{} INFO: {}".format(item_type.upper(), item_info)) 176 | print("//////////////////////////////////////////////////////////////") 177 | 178 | @staticmethod 179 | def display_missing_item_error(item, err): 180 | print("**************************************************************") 181 | print("We are sorry, we have no {}!".format(item.upper())) 182 | print("{}".format(err.args[0])) 183 | print("**************************************************************") 184 | 185 | @staticmethod 186 | def display_item_already_stored_error(item, item_type, err): 187 | print("**************************************************************") 188 | print("Hey! We already have {} in our {} list!".format(item.upper(), item_type)) 189 | print("{}".format(err.args[0])) 190 | print("**************************************************************") 191 | 192 | @staticmethod 193 | def display_item_not_yet_stored_error(item, item_type, err): 194 | print("**************************************************************") 195 | print( 196 | "We don't have any {} in our {} list. Please insert it first!".format( 197 | item.upper(), item_type 198 | ) 199 | ) 200 | print("{}".format(err.args[0])) 201 | print("**************************************************************") 202 | 203 | @staticmethod 204 | def display_item_stored(item, item_type): 205 | print("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++") 206 | print( 207 | "Hooray! We have just added some {} to our {} list!".format( 208 | item.upper(), item_type 209 | ) 210 | ) 211 | print("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++") 212 | 213 | @staticmethod 214 | def display_change_item_type(older, newer): 215 | print("--- --- --- --- --- --- --- --- --- --- --") 216 | print('Change item type from "{}" to "{}"'.format(older, newer)) 217 | print("--- --- --- --- --- --- --- --- --- --- --") 218 | 219 | @staticmethod 220 | def display_item_updated(item, o_price, o_quantity, n_price, n_quantity): 221 | print("--- --- --- --- --- --- --- --- --- --- --") 222 | print("Change {} price: {} --> {}".format(item, o_price, n_price)) 223 | print("Change {} quantity: {} --> {}".format(item, o_quantity, n_quantity)) 224 | print("--- --- --- --- --- --- --- --- --- --- --") 225 | 226 | @staticmethod 227 | def display_item_deletion(name): 228 | print("--------------------------------------------------------------") 229 | print("We have just removed {} from our list".format(name)) 230 | print("--------------------------------------------------------------") 231 | 232 | 233 | class Controller(object): 234 | """The Controller class associates the user input to a Model and a View. 235 | 236 | The Controller class handles the user's inputs, invokes Model's methods to 237 | alter the data, and calls specific View's methods to present the data back 238 | to the user. Model and View should be initialized by the Controller. 239 | """ 240 | 241 | def __init__(self, model, view): 242 | self.model = model 243 | self.view = view 244 | 245 | def show_items(self, bullet_points=False): 246 | # items = self.model.get_items_generator() 247 | items = self.model.read_items() 248 | item_type = self.model.item_type 249 | if bullet_points: 250 | self.view.show_bullet_point_list(item_type, items) 251 | else: 252 | self.view.show_number_point_list(item_type, items) 253 | 254 | def show_item(self, item_name): 255 | try: 256 | item = self.model.read_item(item_name) 257 | item_type = self.model.item_type 258 | self.view.show_item(item_type, item_name, item) 259 | except mvc_exc.ItemNotStored as e: 260 | self.view.display_missing_item_error(item_name, e) 261 | 262 | def insert_item(self, name, price, quantity): 263 | assert price > 0, "price must be greater than 0" 264 | assert quantity >= 0, "quantity must be greater than or equal to 0" 265 | item_type = self.model.item_type 266 | try: 267 | self.model.create_item(name, price, quantity) 268 | self.view.display_item_stored(name, item_type) 269 | except mvc_exc.ItemAlreadyStored as e: 270 | self.view.display_item_already_stored_error(name, item_type, e) 271 | 272 | def update_item(self, name, price, quantity): 273 | assert price > 0, "price must be greater than 0" 274 | assert quantity >= 0, "quantity must be greater than or equal to 0" 275 | item_type = self.model.item_type 276 | 277 | try: 278 | older = self.model.read_item(name) 279 | self.model.update_item(name, price, quantity) 280 | self.view.display_item_updated( 281 | name, older["price"], older["quantity"], price, quantity 282 | ) 283 | except mvc_exc.ItemNotStored as e: 284 | self.view.display_item_not_yet_stored_error(name, item_type, e) 285 | 286 | # if the item is not yet stored and we performed an update, we have 287 | # 2 options: do nothing or call insert_item to add it. 288 | # self.insert_item(name, price, quantity) 289 | 290 | def update_item_type(self, new_item_type): 291 | old_item_type = self.model.item_type 292 | self.model.item_type = new_item_type 293 | self.view.display_change_item_type(old_item_type, new_item_type) 294 | 295 | def delete_item(self, name): 296 | item_type = self.model.item_type 297 | try: 298 | self.model.delete_item(name) 299 | self.view.display_item_deletion(name) 300 | except mvc_exc.ItemNotStored as e: 301 | self.view.display_item_not_yet_stored_error(name, item_type, e) 302 | 303 | 304 | if __name__ == "__main__": 305 | 306 | myitems = mock.items() 307 | 308 | c = Controller(ModelBasic(myitems), View()) 309 | # c = Controller(ModelSQLite(myitems), View()) 310 | # c = Controller(ModelDataset(myitems), View()) 311 | 312 | c.show_items() 313 | c.show_items(bullet_points=True) 314 | c.show_item("chocolate") 315 | c.show_item("bread") 316 | 317 | c.insert_item("bread", price=1.0, quantity=5) 318 | c.insert_item("chocolate", price=2.0, quantity=10) 319 | c.show_item("chocolate") 320 | 321 | c.update_item("milk", price=1.2, quantity=20) 322 | c.update_item("ice cream", price=3.5, quantity=20) 323 | 324 | c.delete_item("fish") 325 | c.delete_item("bread") 326 | 327 | c.show_items() 328 | 329 | # we close the current sqlite database connection explicitly 330 | if type(c.model) is ModelSQLite: 331 | sqlite_backend.disconnect_from_db(sqlite_backend.DB_name, c.model.connection) 332 | # the sqlite backend understands that it needs to open a new connection 333 | c.show_items() 334 | -------------------------------------------------------------------------------- /mvc/mvc_exceptions.py: -------------------------------------------------------------------------------- 1 | class ItemAlreadyStored(Exception): 2 | pass 3 | 4 | 5 | class ItemNotStored(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /mvc/mvc_mock_objects.py: -------------------------------------------------------------------------------- 1 | def items(): 2 | """Create some fake items. Useful to populate database tables. 3 | 4 | Returns 5 | ------- 6 | list 7 | """ 8 | return [ 9 | {"name": "bread", "price": 0.5, "quantity": 20}, 10 | {"name": "milk", "price": 1.0, "quantity": 10}, 11 | {"name": "wine", "price": 10.0, "quantity": 5}, 12 | ] 13 | -------------------------------------------------------------------------------- /mvc/sqlite_backend.py: -------------------------------------------------------------------------------- 1 | """SQLite backend (db-sqlite3). 2 | 3 | Each one of the CRUD operations should be able to open a database connection if 4 | there isn't already one available (check if there are any issues with this). 5 | 6 | Documentation: 7 | https://www.sqlite.org/datatype3.html 8 | https://docs.python.org/3/library/sqlite3.html 9 | """ 10 | import sqlite3 11 | from sqlite3 import OperationalError, IntegrityError, ProgrammingError 12 | import mvc_exceptions as mvc_exc 13 | import mvc_mock_objects as mock 14 | 15 | 16 | DB_name = "myDB" 17 | 18 | 19 | def connect_to_db(db=None): 20 | """Connect to a sqlite DB. Create the database if there isn't one yet. 21 | 22 | Opens a connection to a SQLite DB (either a DB file or an in-memory DB). 23 | When a database is accessed by multiple connections, and one of the 24 | processes modifies the database, the SQLite database is locked until that 25 | transaction is committed. 26 | 27 | Parameters 28 | ---------- 29 | db : str 30 | database name (without .db extension). If None, create an In-Memory DB. 31 | 32 | Returns 33 | ------- 34 | connection : sqlite3.Connection 35 | connection object 36 | """ 37 | if db is None: 38 | mydb = ":memory:" 39 | print("New connection to in-memory SQLite DB...") 40 | else: 41 | mydb = "{}.db".format(db) 42 | print("New connection to SQLite DB...") 43 | connection = sqlite3.connect(mydb) 44 | return connection 45 | 46 | 47 | # TODO: use this decorator to wrap commit/rollback in a try/except block ? 48 | # see http://www.kylev.com/2009/05/22/python-decorators-and-database-idioms/ 49 | 50 | 51 | def connect(func): 52 | """Decorator to (re)open a sqlite database connection when needed. 53 | 54 | A database connection must be open when we want to perform a database query 55 | but we are in one of the following situations: 56 | 1) there is no connection 57 | 2) the connection is closed 58 | 59 | Parameters 60 | ---------- 61 | func : function 62 | function which performs the database query 63 | 64 | Returns 65 | ------- 66 | inner func : function 67 | """ 68 | 69 | def inner_func(conn, *args, **kwargs): 70 | try: 71 | # I don't know if this is the simplest and fastest query to try 72 | conn.execute('SELECT name FROM sqlite_temp_master WHERE type="table";') 73 | except (AttributeError, ProgrammingError): 74 | conn = connect_to_db(DB_name) 75 | return func(conn, *args, **kwargs) 76 | 77 | return inner_func 78 | 79 | 80 | def tuple_to_dict(mytuple): 81 | mydict = dict() 82 | mydict["id"] = mytuple[0] 83 | mydict["name"] = mytuple[1] 84 | mydict["price"] = mytuple[2] 85 | mydict["quantity"] = mytuple[3] 86 | return mydict 87 | 88 | 89 | def scrub(input_string): 90 | """Clean an input string (to prevent SQL injection). 91 | 92 | Parameters 93 | ---------- 94 | input_string : str 95 | 96 | Returns 97 | ------- 98 | str 99 | """ 100 | return "".join(k for k in input_string if k.isalnum()) 101 | 102 | 103 | def disconnect_from_db(db=None, conn=None): 104 | if db is not DB_name: 105 | print("You are trying to disconnect from a wrong DB") 106 | if conn is not None: 107 | conn.close() 108 | 109 | 110 | @connect 111 | def create_table(conn, table_name): 112 | table_name = scrub(table_name) 113 | sql = ( 114 | "CREATE TABLE {} (rowid INTEGER PRIMARY KEY AUTOINCREMENT," 115 | "name TEXT UNIQUE, price REAL, quantity INTEGER)".format(table_name) 116 | ) 117 | try: 118 | conn.execute(sql) 119 | except OperationalError as e: 120 | print(e) 121 | 122 | 123 | @connect 124 | def insert_one(conn, name, price, quantity, table_name): 125 | table_name = scrub(table_name) 126 | sql = "INSERT INTO {} ('name', 'price', 'quantity') VALUES (?, ?, ?)".format( 127 | table_name 128 | ) 129 | try: 130 | conn.execute(sql, (name, price, quantity)) 131 | conn.commit() 132 | except IntegrityError as e: 133 | raise mvc_exc.ItemAlreadyStored( 134 | '{}: "{}" already stored in table "{}"'.format(e, name, table_name) 135 | ) 136 | 137 | 138 | @connect 139 | def insert_many(conn, items, table_name): 140 | table_name = scrub(table_name) 141 | sql = "INSERT INTO {} ('name', 'price', 'quantity') VALUES (?, ?, ?)".format( 142 | table_name 143 | ) 144 | entries = list() 145 | for x in items: 146 | entries.append((x["name"], x["price"], x["quantity"])) 147 | try: 148 | conn.executemany(sql, entries) 149 | conn.commit() 150 | except IntegrityError as e: 151 | print( 152 | '{}: at least one in {} was already stored in table "{}"'.format( 153 | e, [x["name"] for x in items], table_name 154 | ) 155 | ) 156 | 157 | 158 | @connect 159 | def select_one(conn, item_name, table_name): 160 | table_name = scrub(table_name) 161 | item_name = scrub(item_name) 162 | sql = 'SELECT * FROM {} WHERE name="{}"'.format(table_name, item_name) 163 | c = conn.execute(sql) 164 | result = c.fetchone() 165 | if result is not None: 166 | return tuple_to_dict(result) 167 | 168 | else: 169 | raise mvc_exc.ItemNotStored( 170 | 'Can\'t read "{}" because it\'s not stored in table "{}"'.format( 171 | item_name, table_name 172 | ) 173 | ) 174 | 175 | 176 | @connect 177 | def select_all(conn, table_name): 178 | table_name = scrub(table_name) 179 | sql = "SELECT * FROM {}".format(table_name) 180 | c = conn.execute(sql) 181 | results = c.fetchall() 182 | return list(map(lambda x: tuple_to_dict(x), results)) 183 | 184 | 185 | @connect 186 | def update_one(conn, name, price, quantity, table_name): 187 | table_name = scrub(table_name) 188 | sql_check = "SELECT EXISTS(SELECT 1 FROM {} WHERE name=? LIMIT 1)".format( 189 | table_name 190 | ) 191 | sql_update = "UPDATE {} SET price=?, quantity=? WHERE name=?".format(table_name) 192 | c = conn.execute(sql_check, (name,)) # we need the comma 193 | result = c.fetchone() 194 | if result[0]: 195 | c.execute(sql_update, (price, quantity, name)) 196 | conn.commit() 197 | else: 198 | raise mvc_exc.ItemNotStored( 199 | 'Can\'t update "{}" because it\'s not stored in table "{}"'.format( 200 | name, table_name 201 | ) 202 | ) 203 | 204 | 205 | @connect 206 | def delete_one(conn, name, table_name): 207 | table_name = scrub(table_name) 208 | sql_check = "SELECT EXISTS(SELECT 1 FROM {} WHERE name=? LIMIT 1)".format( 209 | table_name 210 | ) 211 | table_name = scrub(table_name) 212 | sql_delete = "DELETE FROM {} WHERE name=?".format(table_name) 213 | c = conn.execute(sql_check, (name,)) # we need the comma 214 | result = c.fetchone() 215 | if result[0]: 216 | c.execute(sql_delete, (name,)) # we need the comma 217 | conn.commit() 218 | else: 219 | raise mvc_exc.ItemNotStored( 220 | 'Can\'t delete "{}" because it\'s not stored in table "{}"'.format( 221 | name, table_name 222 | ) 223 | ) 224 | 225 | 226 | def main(): 227 | 228 | table_name = "items" 229 | conn = connect_to_db() # in-memory database 230 | # conn = connect_to_db(DB_name) 231 | 232 | create_table(conn, table_name) 233 | 234 | # CREATE 235 | insert_many(conn, mock.items(), table_name="items") 236 | insert_one(conn, "beer", price=2.0, quantity=5, table_name="items") 237 | # if we try to insert an object already stored we get an ItemAlreadyStored 238 | # exception 239 | # insert_one(conn, 'milk', price=1.0, quantity=3, table_name='items') 240 | 241 | # READ 242 | print("SELECT milk") 243 | print(select_one(conn, "milk", table_name="items")) 244 | print("SELECT all") 245 | print(select_all(conn, table_name="items")) 246 | # if we try to select an object not stored we get an ItemNotStored exception 247 | # print(select_one(conn, 'pizza', table_name='items')) 248 | 249 | # conn.close() # the decorator @connect will reopen the connection 250 | 251 | # UPDATE 252 | print("UPDATE bread, SELECT bread") 253 | update_one(conn, "bread", price=1.5, quantity=5, table_name="items") 254 | print(select_one(conn, "bread", table_name="items")) 255 | # if we try to update an object not stored we get an ItemNotStored exception 256 | # print('UPDATE pizza') 257 | # update_one(conn, 'pizza', price=1.5, quantity=5, table_name='items') 258 | 259 | # DELETE 260 | print("DELETE beer, SELECT all") 261 | delete_one(conn, "beer", table_name="items") 262 | print(select_all(conn, table_name="items")) 263 | # if we try to delete an object not stored we get an ItemNotStored exception 264 | # print('DELETE fish') 265 | # delete_one(conn, 'fish', table_name='items') 266 | 267 | # save (commit) the changes 268 | # conn.commit() 269 | 270 | # close connection 271 | conn.close() 272 | 273 | 274 | if __name__ == "__main__": 275 | main() 276 | -------------------------------------------------------------------------------- /null_object.py: -------------------------------------------------------------------------------- 1 | """Null Object pattern 2 | """ 3 | import random 4 | 5 | 6 | class RealObject(object): 7 | def __init__(self, name): 8 | self.name = name 9 | 10 | def __call__(self, *args, **kwargs): 11 | return print("Called with args {} and kwargs {}".format(args, kwargs)) 12 | 13 | def is_null(self): 14 | return False 15 | 16 | def do_stuff(self): 17 | print("do some real stuff") 18 | 19 | def get_stuff(self): 20 | return "some real stuff" 21 | 22 | 23 | class NullObject(RealObject): 24 | def __init__(self, *args, **kwargs): 25 | pass 26 | 27 | def __call__(self, *args, **kwargs): 28 | return self 29 | 30 | def __repr__(self): 31 | return "" 32 | 33 | def __str__(self): 34 | return "Null" 35 | 36 | def __getattr__(self, attr_name): 37 | return self 38 | 39 | def __setattr__(self, attr_name, attr_value): 40 | return self 41 | 42 | def __delattr__(self, attr_name): 43 | return self 44 | 45 | def is_null(self): 46 | return True 47 | 48 | def do_stuff(self): 49 | pass 50 | 51 | def get_stuff(self): 52 | return None 53 | 54 | 55 | def give_me_an_object(name): 56 | num = random.random() 57 | cls = RealObject if num > 0.5 else NullObject 58 | print("Class: {}".format(cls.__name__)) 59 | print("__init__") 60 | return cls(name) 61 | 62 | 63 | def main(): 64 | name = "Bob" 65 | 66 | # instatiation and representation 67 | obj = give_me_an_object(name) 68 | print("__str__") 69 | print(obj) 70 | print(repr(obj)) 71 | 72 | # attribute handling 73 | print("__getattr__ ") 74 | print(obj.name) 75 | print("__setattr__") 76 | obj.name = "John" 77 | print(obj.name) 78 | print("__delattr__") 79 | del obj.name 80 | 81 | # object calling 82 | print("__call__") 83 | obj("hello", 123, some_key=456) 84 | 85 | # methods for this particular example 86 | print("do_stuff") 87 | obj.do_stuff() 88 | my_stuff = obj.get_stuff() 89 | print(my_stuff) 90 | 91 | 92 | if __name__ == "__main__": 93 | main() 94 | -------------------------------------------------------------------------------- /object_pool.py: -------------------------------------------------------------------------------- 1 | """Singleton Pool 2 | 3 | This is not an Object Pool, but a Singleton... but it's a funny example 4 | """ 5 | import atexit 6 | import pprint 7 | 8 | 9 | class PoolFull(Exception): 10 | pass 11 | 12 | 13 | class PoolMeta(type): 14 | 15 | pool = dict() 16 | 17 | @staticmethod 18 | def serialize_arguments(cls, *args, **kwargs): 19 | """Serialize arguments to a string representation.""" 20 | # cls is the instance's class, not the class's class 21 | serialized_args = [str(arg) for arg in args] 22 | serialized_kwargs = [str(kwargs), cls.__name__] 23 | serialized_args.extend(serialized_kwargs) 24 | return "".join(serialized_args) 25 | 26 | @staticmethod 27 | def delete_instance_func(self): 28 | """Replace the __del__ method of the class that uses this metaclass. 29 | 30 | Parameters 31 | ---------- 32 | self : instance of the class that uses this metaclass 33 | """ 34 | print("Bye bye instance of class {}!".format(self.__class__.__name__)) 35 | 36 | def __init__(cls, *args, **kwargs): 37 | super().__init__(*args, **kwargs) 38 | cls.__del__ = PoolMeta.delete_instance_func 39 | 40 | def __del__(self): 41 | PoolMeta.delete_instance_func(self) 42 | 43 | # the same as assigning the following in the metaclass __init__ 44 | # PoolMeta.__del__ = PoolMeta.delete_instance_func 45 | 46 | def __call__(cls, *args, **kwargs): 47 | print("\nMeta.__call__(cls={}, args={}, kwargs={})".format(cls, args, kwargs)) 48 | key = PoolMeta.serialize_arguments(cls, *args, **kwargs) 49 | 50 | try: 51 | instance = PoolMeta.pool[key] 52 | print("Found in pool (skip __new__ and __init__)") 53 | del PoolMeta.pool[key] # remove from pool 54 | except KeyError: 55 | print("Not found in pool") 56 | instance = super().__call__(*args, **kwargs) 57 | 58 | PoolMeta.pool[key] = instance # insert in pool 59 | return instance 60 | 61 | 62 | class A(object, metaclass=PoolMeta): 63 | def __new__(cls, *args, **kwargs): 64 | print("A.__new__") 65 | return super().__new__(cls) 66 | 67 | def __init__(self, x): 68 | print("A.__init__") 69 | self.x = x 70 | 71 | 72 | class B(object, metaclass=PoolMeta): 73 | def __new__(cls, *args, **kwargs): 74 | print("B.__new__") 75 | return super().__new__(cls) 76 | 77 | def __init__(self, x): 78 | print("B.__init__") 79 | self.x = x 80 | 81 | 82 | class C(B): 83 | def __new__(cls, *args, **kwargs): 84 | print("C.__new__") 85 | return super().__new__(cls) 86 | 87 | def __init__(self, x, y, z=123): 88 | super().__init__(x) 89 | print("C.__init__") 90 | self.x = x 91 | self.y = y 92 | self.z = z 93 | 94 | 95 | def print_pool(): 96 | print("Final state of the Pool (this function was registered with atexit)") 97 | pprint.pprint(PoolMeta.pool) 98 | print("") 99 | 100 | 101 | def main(): 102 | a1 = A(10) 103 | a2 = A(10) 104 | assert id(a1) == id(a2) 105 | a3 = A(42) 106 | assert id(a3) != id(a2) 107 | 108 | b1 = B(10) 109 | b2 = B(10) 110 | assert id(b1) == id(b2) 111 | c1 = C(1, 2, z=3) 112 | c2 = C(1, 2, 42) 113 | assert id(c1) != id(c2) 114 | 115 | assert type(PoolMeta) == type 116 | assert type(A) == PoolMeta 117 | assert type(B) == PoolMeta 118 | assert type(C) == PoolMeta 119 | 120 | 121 | if __name__ == "__main__": 122 | atexit.register(print_pool) 123 | main() 124 | print("") 125 | print(A.__del__.__doc__) 126 | -------------------------------------------------------------------------------- /observer.py: -------------------------------------------------------------------------------- 1 | class Subscriber(object): 2 | """It's the Observer object. It receives messages from the Observable.""" 3 | 4 | def __init__(self, name): 5 | self.name = name 6 | 7 | def receive(self, message): 8 | """Method assigned in, and called by, the Publisher. 9 | 10 | This method is assigned when the Publisher registers a Subscriber to a 11 | newsletter, and it's called when the Publisher dispatches a message. 12 | 13 | Parameters 14 | ---------- 15 | message : str 16 | """ 17 | print("{} received: {}".format(self.name, message)) 18 | 19 | 20 | class Publisher(object): 21 | """It's the Observable object. It dispatches messages to the Observers.""" 22 | 23 | def __init__(self, newsletters): 24 | self.subscriptions = dict() 25 | for newsletter in newsletters: 26 | self.add_newsletter(newsletter) 27 | 28 | def get_subscriptions(self, newsletter): 29 | return self.subscriptions[newsletter] 30 | 31 | def register(self, newsletter, who, callback=None): 32 | """Register a Subscriber to this newsletter. 33 | 34 | Parameters 35 | ---------- 36 | newsletter : str 37 | who : Subscriber 38 | callback : method 39 | callback function bound to the Subscriber object 40 | """ 41 | if callback is None: 42 | callback = getattr(who, "receive") 43 | self.get_subscriptions(newsletter)[who] = callback 44 | 45 | def unregister(self, newsletter, who): 46 | """Remove a Subscriber object from a subscription to a newsletter. 47 | 48 | Parameters 49 | ---------- 50 | newsletter : str 51 | who : Subscriber 52 | """ 53 | try: 54 | del self.get_subscriptions(newsletter)[who] 55 | except KeyError: 56 | print( 57 | "{} is not subscribed to the {} newsletter!".format( 58 | who.name, newsletter 59 | ) 60 | ) 61 | 62 | def dispatch(self, newsletter, message): 63 | """Send a message to all subscribers registered to this newsletter. 64 | 65 | Parameters 66 | ---------- 67 | newsletter : str 68 | message : str 69 | """ 70 | if len(self.get_subscriptions(newsletter).items()) == 0: 71 | print( 72 | "No subscribers for the {} newsletter. Nothing to send!".format( 73 | newsletter 74 | ) 75 | ) 76 | return 77 | 78 | for subscriber, callback in self.get_subscriptions(newsletter).items(): 79 | callback(message) 80 | 81 | def add_newsletter(self, newsletter): 82 | """Add a subscription key-value pair for a new newsletter. 83 | 84 | The key is the name of the new subscription, namely the name of the 85 | newsletter (e.g. 'Tech'). The value is an empty dictionary which will be 86 | populated by subscriber objects willing to register to this newsletter. 87 | 88 | Parameters 89 | ---------- 90 | newsletter : str 91 | """ 92 | self.subscriptions[newsletter] = dict() 93 | 94 | 95 | def main(): 96 | 97 | pub = Publisher(newsletters=["Tech", "Travel"]) 98 | 99 | tom = Subscriber("Tom") 100 | sara = Subscriber("Sara") 101 | john = Subscriber("John") 102 | 103 | pub.register(newsletter="Tech", who=tom) 104 | pub.register(newsletter="Travel", who=tom) 105 | pub.register(newsletter="Travel", who=sara) 106 | pub.register(newsletter="Tech", who=john) 107 | 108 | pub.dispatch(newsletter="Tech", message="Tech Newsletter num 1") 109 | pub.dispatch(newsletter="Travel", message="Travel Newsletter num 1") 110 | 111 | pub.unregister(newsletter="Tech", who=john) 112 | 113 | pub.dispatch(newsletter="Tech", message="Tech Newsletter num 2") 114 | pub.dispatch(newsletter="Travel", message="Travel Newsletter num 2") 115 | 116 | pub.add_newsletter("Fashion") 117 | pub.register(newsletter="Fashion", who=tom) 118 | pub.register(newsletter="Fashion", who=sara) 119 | pub.register(newsletter="Fashion", who=john) 120 | pub.dispatch(newsletter="Fashion", message="Fashion Newsletter num 1") 121 | pub.unregister(newsletter="Fashion", who=tom) 122 | pub.unregister(newsletter="Fashion", who=sara) 123 | pub.dispatch(newsletter="Fashion", message="Fashion Newsletter num 2") 124 | 125 | 126 | if __name__ == "__main__": 127 | main() 128 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alembic" 3 | version = "1.4.3" 4 | description = "A database migration tool for SQLAlchemy." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [package.dependencies] 10 | Mako = "*" 11 | python-dateutil = "*" 12 | python-editor = ">=0.3" 13 | SQLAlchemy = ">=1.1.0" 14 | 15 | [[package]] 16 | name = "appdirs" 17 | version = "1.4.4" 18 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 19 | category = "dev" 20 | optional = false 21 | python-versions = "*" 22 | 23 | [[package]] 24 | name = "atomicwrites" 25 | version = "1.4.0" 26 | description = "Atomic file writes." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 30 | 31 | [[package]] 32 | name = "attrs" 33 | version = "20.2.0" 34 | description = "Classes Without Boilerplate" 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 38 | 39 | [package.extras] 40 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] 41 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 42 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 43 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 44 | 45 | [[package]] 46 | name = "black" 47 | version = "20.8b1" 48 | description = "The uncompromising code formatter." 49 | category = "dev" 50 | optional = false 51 | python-versions = ">=3.6" 52 | 53 | [package.dependencies] 54 | appdirs = "*" 55 | click = ">=7.1.2" 56 | mypy-extensions = ">=0.4.3" 57 | pathspec = ">=0.6,<1" 58 | regex = ">=2020.1.8" 59 | toml = ">=0.10.1" 60 | typed-ast = ">=1.4.0" 61 | typing-extensions = ">=3.7.4" 62 | 63 | [package.extras] 64 | colorama = ["colorama (>=0.4.3)"] 65 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 66 | 67 | [[package]] 68 | name = "boto3" 69 | version = "1.15.5" 70 | description = "The AWS SDK for Python" 71 | category = "main" 72 | optional = false 73 | python-versions = "*" 74 | 75 | [package.dependencies] 76 | botocore = ">=1.18.5,<1.19.0" 77 | jmespath = ">=0.7.1,<1.0.0" 78 | s3transfer = ">=0.3.0,<0.4.0" 79 | 80 | [[package]] 81 | name = "botocore" 82 | version = "1.18.5" 83 | description = "Low-level, data-driven core of boto 3." 84 | category = "main" 85 | optional = false 86 | python-versions = "*" 87 | 88 | [package.dependencies] 89 | jmespath = ">=0.7.1,<1.0.0" 90 | python-dateutil = ">=2.1,<3.0.0" 91 | urllib3 = {version = ">=1.20,<1.26", markers = "python_version != \"3.4\""} 92 | 93 | [[package]] 94 | name = "click" 95 | version = "7.1.2" 96 | description = "Composable command line interface toolkit" 97 | category = "dev" 98 | optional = false 99 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 100 | 101 | [[package]] 102 | name = "colorama" 103 | version = "0.4.3" 104 | description = "Cross-platform colored terminal text." 105 | category = "dev" 106 | optional = false 107 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 108 | 109 | [[package]] 110 | name = "dataset" 111 | version = "1.3.2" 112 | description = "Toolkit for Python-based database access." 113 | category = "main" 114 | optional = false 115 | python-versions = "*" 116 | 117 | [package.dependencies] 118 | alembic = ">=0.6.2" 119 | sqlalchemy = ">=1.3.2" 120 | 121 | [package.extras] 122 | dev = ["pip", "nose", "wheel", "flake8", "coverage", "psycopg2-binary", "pymysql"] 123 | 124 | [[package]] 125 | name = "ddt" 126 | version = "1.4.1" 127 | description = "Data-Driven/Decorated Tests" 128 | category = "main" 129 | optional = false 130 | python-versions = "*" 131 | 132 | [[package]] 133 | name = "importlib-metadata" 134 | version = "2.0.0" 135 | description = "Read metadata from Python packages" 136 | category = "dev" 137 | optional = false 138 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 139 | 140 | [package.dependencies] 141 | zipp = ">=0.5" 142 | 143 | [package.extras] 144 | docs = ["sphinx", "rst.linker"] 145 | testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] 146 | 147 | [[package]] 148 | name = "iniconfig" 149 | version = "1.0.1" 150 | description = "iniconfig: brain-dead simple config-ini parsing" 151 | category = "dev" 152 | optional = false 153 | python-versions = "*" 154 | 155 | [[package]] 156 | name = "jmespath" 157 | version = "0.10.0" 158 | description = "JSON Matching Expressions" 159 | category = "main" 160 | optional = false 161 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 162 | 163 | [[package]] 164 | name = "mako" 165 | version = "1.1.3" 166 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 167 | category = "main" 168 | optional = false 169 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 170 | 171 | [package.dependencies] 172 | MarkupSafe = ">=0.9.2" 173 | 174 | [package.extras] 175 | babel = ["babel"] 176 | lingua = ["lingua"] 177 | 178 | [[package]] 179 | name = "markupsafe" 180 | version = "1.1.1" 181 | description = "Safely add untrusted strings to HTML/XML markup." 182 | category = "main" 183 | optional = false 184 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 185 | 186 | [[package]] 187 | name = "more-itertools" 188 | version = "8.5.0" 189 | description = "More routines for operating on iterables, beyond itertools" 190 | category = "dev" 191 | optional = false 192 | python-versions = ">=3.5" 193 | 194 | [[package]] 195 | name = "mypy-extensions" 196 | version = "0.4.3" 197 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 198 | category = "dev" 199 | optional = false 200 | python-versions = "*" 201 | 202 | [[package]] 203 | name = "packaging" 204 | version = "20.4" 205 | description = "Core utilities for Python packages" 206 | category = "dev" 207 | optional = false 208 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 209 | 210 | [package.dependencies] 211 | pyparsing = ">=2.0.2" 212 | six = "*" 213 | 214 | [[package]] 215 | name = "pastel" 216 | version = "0.2.1" 217 | description = "Bring colors to your terminal." 218 | category = "dev" 219 | optional = false 220 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 221 | 222 | [[package]] 223 | name = "pathspec" 224 | version = "0.8.0" 225 | description = "Utility library for gitignore style pattern matching of file paths." 226 | category = "dev" 227 | optional = false 228 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 229 | 230 | [[package]] 231 | name = "pendulum" 232 | version = "2.1.2" 233 | description = "Python datetimes made easy" 234 | category = "main" 235 | optional = false 236 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 237 | 238 | [package.dependencies] 239 | python-dateutil = ">=2.6,<3.0" 240 | pytzdata = ">=2020.1" 241 | 242 | [[package]] 243 | name = "pluggy" 244 | version = "0.13.1" 245 | description = "plugin and hook calling mechanisms for python" 246 | category = "dev" 247 | optional = false 248 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 249 | 250 | [package.dependencies] 251 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 252 | 253 | [package.extras] 254 | dev = ["pre-commit", "tox"] 255 | 256 | [[package]] 257 | name = "poethepoet" 258 | version = "0.8.0" 259 | description = "A task runner that works well with poetry." 260 | category = "dev" 261 | optional = false 262 | python-versions = ">=3.6,<4.0" 263 | 264 | [package.dependencies] 265 | pastel = ">=0.2.0,<0.3.0" 266 | tomlkit = ">=0.7.0,<0.8.0" 267 | 268 | [[package]] 269 | name = "psycopg2" 270 | version = "2.8.6" 271 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 272 | category = "main" 273 | optional = false 274 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 275 | 276 | [[package]] 277 | name = "py" 278 | version = "1.9.0" 279 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 280 | category = "dev" 281 | optional = false 282 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 283 | 284 | [[package]] 285 | name = "pyparsing" 286 | version = "2.4.7" 287 | description = "Python parsing module" 288 | category = "main" 289 | optional = false 290 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 291 | 292 | [[package]] 293 | name = "pytest" 294 | version = "6.0.2" 295 | description = "pytest: simple powerful testing with Python" 296 | category = "dev" 297 | optional = false 298 | python-versions = ">=3.5" 299 | 300 | [package.dependencies] 301 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 302 | attrs = ">=17.4.0" 303 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 304 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 305 | iniconfig = "*" 306 | more-itertools = ">=4.0.0" 307 | packaging = "*" 308 | pluggy = ">=0.12,<1.0" 309 | py = ">=1.8.2" 310 | toml = "*" 311 | 312 | [package.extras] 313 | checkqa_mypy = ["mypy (0.780)"] 314 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 315 | 316 | [[package]] 317 | name = "python-dateutil" 318 | version = "2.8.1" 319 | description = "Extensions to the standard Python datetime module" 320 | category = "main" 321 | optional = false 322 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 323 | 324 | [package.dependencies] 325 | six = ">=1.5" 326 | 327 | [[package]] 328 | name = "python-editor" 329 | version = "1.0.4" 330 | description = "Programmatically open an editor, capture the result." 331 | category = "main" 332 | optional = false 333 | python-versions = "*" 334 | 335 | [[package]] 336 | name = "pytzdata" 337 | version = "2020.1" 338 | description = "The Olson timezone database for Python." 339 | category = "main" 340 | optional = false 341 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 342 | 343 | [[package]] 344 | name = "regex" 345 | version = "2020.7.14" 346 | description = "Alternative regular expression module, to replace re." 347 | category = "dev" 348 | optional = false 349 | python-versions = "*" 350 | 351 | [[package]] 352 | name = "s3transfer" 353 | version = "0.3.3" 354 | description = "An Amazon S3 Transfer Manager" 355 | category = "main" 356 | optional = false 357 | python-versions = "*" 358 | 359 | [package.dependencies] 360 | botocore = ">=1.12.36,<2.0a.0" 361 | 362 | [[package]] 363 | name = "six" 364 | version = "1.15.0" 365 | description = "Python 2 and 3 compatibility utilities" 366 | category = "main" 367 | optional = false 368 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 369 | 370 | [[package]] 371 | name = "sqlalchemy" 372 | version = "1.3.19" 373 | description = "Database Abstraction Library" 374 | category = "main" 375 | optional = false 376 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 377 | 378 | [package.extras] 379 | mssql = ["pyodbc"] 380 | mssql_pymssql = ["pymssql"] 381 | mssql_pyodbc = ["pyodbc"] 382 | mysql = ["mysqlclient"] 383 | oracle = ["cx-oracle"] 384 | postgresql = ["psycopg2"] 385 | postgresql_pg8000 = ["pg8000"] 386 | postgresql_psycopg2binary = ["psycopg2-binary"] 387 | postgresql_psycopg2cffi = ["psycopg2cffi"] 388 | pymysql = ["pymysql"] 389 | 390 | [[package]] 391 | name = "toml" 392 | version = "0.10.1" 393 | description = "Python Library for Tom's Obvious, Minimal Language" 394 | category = "dev" 395 | optional = false 396 | python-versions = "*" 397 | 398 | [[package]] 399 | name = "tomlkit" 400 | version = "0.7.0" 401 | description = "Style preserving TOML library" 402 | category = "dev" 403 | optional = false 404 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 405 | 406 | [[package]] 407 | name = "transitions" 408 | version = "0.8.2" 409 | description = "A lightweight, object-oriented Python state machine implementation with many extensions." 410 | category = "main" 411 | optional = false 412 | python-versions = "*" 413 | 414 | [package.dependencies] 415 | six = "*" 416 | 417 | [package.extras] 418 | diagrams = ["pygraphviz"] 419 | test = ["pytest"] 420 | 421 | [[package]] 422 | name = "typed-ast" 423 | version = "1.4.1" 424 | description = "a fork of Python 2 and 3 ast modules with type comment support" 425 | category = "dev" 426 | optional = false 427 | python-versions = "*" 428 | 429 | [[package]] 430 | name = "typing-extensions" 431 | version = "3.7.4.3" 432 | description = "Backported and Experimental Type Hints for Python 3.5+" 433 | category = "dev" 434 | optional = false 435 | python-versions = "*" 436 | 437 | [[package]] 438 | name = "urllib3" 439 | version = "1.25.10" 440 | description = "HTTP library with thread-safe connection pooling, file post, and more." 441 | category = "main" 442 | optional = false 443 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 444 | 445 | [package.extras] 446 | brotli = ["brotlipy (>=0.6.0)"] 447 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] 448 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 449 | 450 | [[package]] 451 | name = "zipp" 452 | version = "3.2.0" 453 | description = "Backport of pathlib-compatible object wrapper for zip files" 454 | category = "dev" 455 | optional = false 456 | python-versions = ">=3.6" 457 | 458 | [package.extras] 459 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 460 | testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 461 | 462 | [metadata] 463 | lock-version = "1.1" 464 | python-versions = "^3.7" 465 | content-hash = "8a31f9f42fd7f42faf89012a9e5264d96f3693d9d44e6670baeb693671b6e8a8" 466 | 467 | [metadata.files] 468 | alembic = [ 469 | {file = "alembic-1.4.3-py2.py3-none-any.whl", hash = "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c"}, 470 | {file = "alembic-1.4.3.tar.gz", hash = "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245"}, 471 | ] 472 | appdirs = [ 473 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 474 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 475 | ] 476 | atomicwrites = [ 477 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 478 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 479 | ] 480 | attrs = [ 481 | {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, 482 | {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, 483 | ] 484 | black = [ 485 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 486 | ] 487 | boto3 = [ 488 | {file = "boto3-1.15.5-py2.py3-none-any.whl", hash = "sha256:0c464a7de522f88b581ca0d41ffa71e9be5e17fbb0456c275421f65b7c5f6a55"}, 489 | {file = "boto3-1.15.5.tar.gz", hash = "sha256:0fce548e19d6db8e11fd0e2ae7809e1e3282080636b4062b2452bfa20e4f0233"}, 490 | ] 491 | botocore = [ 492 | {file = "botocore-1.18.5-py2.py3-none-any.whl", hash = "sha256:e3bf44fba058f6df16006b94a67650418a080a525c82521abb3cb516a4cba362"}, 493 | {file = "botocore-1.18.5.tar.gz", hash = "sha256:7ce7a05b98ffb3170396960273383e8aade9be6026d5a762f5f40969d5d6b761"}, 494 | ] 495 | click = [ 496 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 497 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 498 | ] 499 | colorama = [ 500 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 501 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 502 | ] 503 | dataset = [ 504 | {file = "dataset-1.3.2-py2.py3-none-any.whl", hash = "sha256:41e55410f853815753740a84f7dc51b5740266c8bf0d88c7f64d0a83b21d1ae4"}, 505 | {file = "dataset-1.3.2.tar.gz", hash = "sha256:6be387eefcd5a98fe1a68b1403390648c71f2edc5cb271f0e1f1a44ca83409eb"}, 506 | ] 507 | ddt = [ 508 | {file = "ddt-1.4.1-py2.py3-none-any.whl", hash = "sha256:f50469a0695daa770dace20d8973f0e73b0a1e28a5bc951d049add8fc3a07ce6"}, 509 | {file = "ddt-1.4.1.tar.gz", hash = "sha256:0595e70d074e5777771a45709e99e9d215552fb1076443a25fad6b23d8bf38da"}, 510 | ] 511 | importlib-metadata = [ 512 | {file = "importlib_metadata-2.0.0-py2.py3-none-any.whl", hash = "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"}, 513 | {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"}, 514 | ] 515 | iniconfig = [ 516 | {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, 517 | {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, 518 | ] 519 | jmespath = [ 520 | {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, 521 | {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, 522 | ] 523 | mako = [ 524 | {file = "Mako-1.1.3-py2.py3-none-any.whl", hash = "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"}, 525 | {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, 526 | ] 527 | markupsafe = [ 528 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 529 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 530 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 531 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 532 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 533 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 534 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 535 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 536 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 537 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 538 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 539 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 540 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 541 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 542 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 543 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 544 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 545 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 546 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 547 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 548 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 549 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 550 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 551 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 552 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 553 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 554 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 555 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 556 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 557 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 558 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 559 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 560 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 561 | ] 562 | more-itertools = [ 563 | {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"}, 564 | {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"}, 565 | ] 566 | mypy-extensions = [ 567 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 568 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 569 | ] 570 | packaging = [ 571 | {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, 572 | {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, 573 | ] 574 | pastel = [ 575 | {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, 576 | {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, 577 | ] 578 | pathspec = [ 579 | {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, 580 | {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, 581 | ] 582 | pendulum = [ 583 | {file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"}, 584 | {file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"}, 585 | {file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"}, 586 | {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"}, 587 | {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"}, 588 | {file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"}, 589 | {file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"}, 590 | {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"}, 591 | {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"}, 592 | {file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"}, 593 | {file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"}, 594 | {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"}, 595 | {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"}, 596 | {file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"}, 597 | {file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"}, 598 | {file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"}, 599 | {file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"}, 600 | {file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"}, 601 | {file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"}, 602 | {file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"}, 603 | {file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"}, 604 | ] 605 | pluggy = [ 606 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 607 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 608 | ] 609 | poethepoet = [ 610 | {file = "poethepoet-0.8.0-py3-none-any.whl", hash = "sha256:f5984f9bd1cba8719f5befd7461c4bd25dbb2f85539197bb4f0ff7f31e6b1e43"}, 611 | {file = "poethepoet-0.8.0.tar.gz", hash = "sha256:3cf5d4e95a6c763413b451236405aab0ec6b737a00027720b794953867d3329d"}, 612 | ] 613 | psycopg2 = [ 614 | {file = "psycopg2-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725"}, 615 | {file = "psycopg2-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5"}, 616 | {file = "psycopg2-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:b8cae8b2f022efa1f011cc753adb9cbadfa5a184431d09b273fb49b4167561ad"}, 617 | {file = "psycopg2-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:f22ea9b67aea4f4a1718300908a2fb62b3e4276cf00bd829a97ab5894af42ea3"}, 618 | {file = "psycopg2-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:26e7fd115a6db75267b325de0fba089b911a4a12ebd3d0b5e7acb7028bc46821"}, 619 | {file = "psycopg2-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:00195b5f6832dbf2876b8bf77f12bdce648224c89c880719c745b90515233301"}, 620 | {file = "psycopg2-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a49833abfdede8985ba3f3ec641f771cca215479f41523e99dace96d5b8cce2a"}, 621 | {file = "psycopg2-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:f974c96fca34ae9e4f49839ba6b78addf0346777b46c4da27a7bf54f48d3057d"}, 622 | {file = "psycopg2-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:6a3d9efb6f36f1fe6aa8dbb5af55e067db802502c55a9defa47c5a1dad41df84"}, 623 | {file = "psycopg2-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:56fee7f818d032f802b8eed81ef0c1232b8b42390df189cab9cfa87573fe52c5"}, 624 | {file = "psycopg2-2.8.6-cp38-cp38-win32.whl", hash = "sha256:ad2fe8a37be669082e61fb001c185ffb58867fdbb3e7a6b0b0d2ffe232353a3e"}, 625 | {file = "psycopg2-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:56007a226b8e95aa980ada7abdea6b40b75ce62a433bd27cec7a8178d57f4051"}, 626 | {file = "psycopg2-2.8.6.tar.gz", hash = "sha256:fb23f6c71107c37fd667cb4ea363ddeb936b348bbd6449278eb92c189699f543"}, 627 | ] 628 | py = [ 629 | {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, 630 | {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, 631 | ] 632 | pyparsing = [ 633 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 634 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 635 | ] 636 | pytest = [ 637 | {file = "pytest-6.0.2-py3-none-any.whl", hash = "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40"}, 638 | {file = "pytest-6.0.2.tar.gz", hash = "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043"}, 639 | ] 640 | python-dateutil = [ 641 | {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, 642 | {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, 643 | ] 644 | python-editor = [ 645 | {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, 646 | {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, 647 | {file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"}, 648 | {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, 649 | {file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"}, 650 | ] 651 | pytzdata = [ 652 | {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, 653 | {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, 654 | ] 655 | regex = [ 656 | {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, 657 | {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, 658 | {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, 659 | {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, 660 | {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, 661 | {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, 662 | {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, 663 | {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, 664 | {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, 665 | {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, 666 | {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, 667 | {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, 668 | {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, 669 | {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, 670 | {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, 671 | {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, 672 | {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, 673 | {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, 674 | {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, 675 | {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, 676 | {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, 677 | ] 678 | s3transfer = [ 679 | {file = "s3transfer-0.3.3-py2.py3-none-any.whl", hash = "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13"}, 680 | {file = "s3transfer-0.3.3.tar.gz", hash = "sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"}, 681 | ] 682 | six = [ 683 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 684 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 685 | ] 686 | sqlalchemy = [ 687 | {file = "SQLAlchemy-1.3.19-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc"}, 688 | {file = "SQLAlchemy-1.3.19-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36"}, 689 | {file = "SQLAlchemy-1.3.19-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de"}, 690 | {file = "SQLAlchemy-1.3.19-cp27-cp27m-win32.whl", hash = "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338"}, 691 | {file = "SQLAlchemy-1.3.19-cp27-cp27m-win_amd64.whl", hash = "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23"}, 692 | {file = "SQLAlchemy-1.3.19-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f"}, 693 | {file = "SQLAlchemy-1.3.19-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc"}, 694 | {file = "SQLAlchemy-1.3.19-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce"}, 695 | {file = "SQLAlchemy-1.3.19-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804"}, 696 | {file = "SQLAlchemy-1.3.19-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac"}, 697 | {file = "SQLAlchemy-1.3.19-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:b6ff91356354b7ff3bd208adcf875056d3d886ed7cef90c571aef2ab8a554b12"}, 698 | {file = "SQLAlchemy-1.3.19-cp35-cp35m-win32.whl", hash = "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b"}, 699 | {file = "SQLAlchemy-1.3.19-cp35-cp35m-win_amd64.whl", hash = "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d"}, 700 | {file = "SQLAlchemy-1.3.19-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea"}, 701 | {file = "SQLAlchemy-1.3.19-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365"}, 702 | {file = "SQLAlchemy-1.3.19-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02"}, 703 | {file = "SQLAlchemy-1.3.19-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:51064ee7938526bab92acd049d41a1dc797422256086b39c08bafeffb9d304c6"}, 704 | {file = "SQLAlchemy-1.3.19-cp36-cp36m-win32.whl", hash = "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1"}, 705 | {file = "SQLAlchemy-1.3.19-cp36-cp36m-win_amd64.whl", hash = "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0"}, 706 | {file = "SQLAlchemy-1.3.19-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e"}, 707 | {file = "SQLAlchemy-1.3.19-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe"}, 708 | {file = "SQLAlchemy-1.3.19-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d"}, 709 | {file = "SQLAlchemy-1.3.19-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:465c999ef30b1c7525f81330184121521418a67189053bcf585824d833c05b66"}, 710 | {file = "SQLAlchemy-1.3.19-cp37-cp37m-win32.whl", hash = "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba"}, 711 | {file = "SQLAlchemy-1.3.19-cp37-cp37m-win_amd64.whl", hash = "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6"}, 712 | {file = "SQLAlchemy-1.3.19-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37"}, 713 | {file = "SQLAlchemy-1.3.19-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7"}, 714 | {file = "SQLAlchemy-1.3.19-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb"}, 715 | {file = "SQLAlchemy-1.3.19-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:26c5ca9d09f0e21b8671a32f7d83caad5be1f6ff45eef5ec2f6fd0db85fc5dc0"}, 716 | {file = "SQLAlchemy-1.3.19-cp38-cp38-win32.whl", hash = "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86"}, 717 | {file = "SQLAlchemy-1.3.19-cp38-cp38-win_amd64.whl", hash = "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea"}, 718 | {file = "SQLAlchemy-1.3.19.tar.gz", hash = "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e"}, 719 | ] 720 | toml = [ 721 | {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, 722 | {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, 723 | ] 724 | tomlkit = [ 725 | {file = "tomlkit-0.7.0-py2.py3-none-any.whl", hash = "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831"}, 726 | {file = "tomlkit-0.7.0.tar.gz", hash = "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"}, 727 | ] 728 | transitions = [ 729 | {file = "transitions-0.8.2-py2.py3-none-any.whl", hash = "sha256:57ceb045d58e491e676efea306e9c475f112eeb563b6c103eae2ff9bc33759fb"}, 730 | {file = "transitions-0.8.2.tar.gz", hash = "sha256:6ff7a3bfa4ac64b62993bb19dc2bb6a0ccbdf4e70b2cbae8350de6c916d77748"}, 731 | ] 732 | typed-ast = [ 733 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 734 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 735 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 736 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 737 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 738 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 739 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 740 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 741 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 742 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 743 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 744 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 745 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 746 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 747 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 748 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 749 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 750 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 751 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 752 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 753 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 754 | ] 755 | typing-extensions = [ 756 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 757 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 758 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 759 | ] 760 | urllib3 = [ 761 | {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, 762 | {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, 763 | ] 764 | zipp = [ 765 | {file = "zipp-3.2.0-py3-none-any.whl", hash = "sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6"}, 766 | {file = "zipp-3.2.0.tar.gz", hash = "sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"}, 767 | ] 768 | -------------------------------------------------------------------------------- /prototype.py: -------------------------------------------------------------------------------- 1 | """Prototype pattern 2 | """ 3 | import copy 4 | from collections import OrderedDict 5 | 6 | 7 | class Book: 8 | def __init__(self, name, authors, price, **kwargs): 9 | """Examples of kwargs: publisher, length, tags, publication date""" 10 | self.name = name 11 | self.authors = authors 12 | self.price = price 13 | self.__dict__.update(kwargs) 14 | 15 | def __str__(self): 16 | mylist = [] 17 | ordered = OrderedDict(sorted(self.__dict__.items())) 18 | for i in ordered.keys(): 19 | mylist.append("{}: {}".format(i, ordered[i])) 20 | if i == "price": 21 | mylist.append("$") 22 | mylist.append("\n") 23 | return "".join(mylist) 24 | 25 | 26 | class Prototype: 27 | def __init__(self): 28 | self.objects = dict() 29 | 30 | def register(self, identifier, obj): 31 | self.objects[identifier] = obj 32 | 33 | def unregister(self, identifier): 34 | del self.objects[identifier] 35 | 36 | def clone(self, identifier, **attr): 37 | found = self.objects.get(identifier) 38 | if not found: 39 | raise ValueError("Incorrect object identifier: {}".format(identifier)) 40 | 41 | obj = copy.deepcopy(found) 42 | obj.__dict__.update(attr) 43 | return obj 44 | 45 | 46 | def main(): 47 | b1 = Book( 48 | name="The C Programming Language", 49 | authors=("Brian W. Kernighan", "Dennis M.Ritchie"), 50 | price=118, 51 | publisher="Prentice Hall", 52 | length=228, 53 | publication_date="1978-02-22", 54 | tags=("C", "programming", "algorithms", "data structures"), 55 | ) 56 | 57 | prototype = Prototype() 58 | cid = "k&r-first" 59 | prototype.register(cid, b1) 60 | b2 = prototype.clone( 61 | cid, 62 | name="The C Programming Language (ANSI)", 63 | price=48.99, 64 | length=274, 65 | publication_date="1988-04-01", 66 | edition=2, 67 | ) 68 | 69 | for i in (b1, b2): 70 | print(i) 71 | 72 | print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2))) 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /prototype_class_decorator.py: -------------------------------------------------------------------------------- 1 | """Prototype pattern 2 | """ 3 | from copy import deepcopy 4 | 5 | 6 | class InstanceNotAvailable(Exception): 7 | pass 8 | 9 | 10 | def prototype(hash_function=id, auto_register=False, debug=False): 11 | """Implement the Prototype pattern on a class. 12 | 13 | The decorated class gains the following methods: 14 | - register 15 | - unregister 16 | - clone (@classmethod) 17 | - identifier (@property) 18 | - available_identifiers (@staticmethod) 19 | 20 | Parameters 21 | ---------- 22 | hash_function : function 23 | a function 24 | auto_register : bool 25 | if True, automatically add objects to instance pool at instantiation 26 | debug : bool 27 | if True, show some documentation while using this decorator 28 | 29 | Returns 30 | ------- 31 | inner : function 32 | """ 33 | 34 | def inner(klass): 35 | instance_pool = dict() 36 | 37 | class Decorated(klass): 38 | def __init__(self, *args, **kwargs): 39 | """Call __init__ of original class and assign an identifier. 40 | 41 | Parameters 42 | ---------- 43 | args : tuple 44 | args to pass to the __init__ of the original class 45 | kwargs : dict 46 | kwarg to pass to the __init__ of the original class 47 | """ 48 | klass.__init__(self, *args, **kwargs) 49 | self._identifier = hash_function(self) 50 | 51 | def __repr__(self, *args, **kwargs): 52 | klass_repr = klass.__repr__(self, *args, **kwargs) 53 | return "{} (id: {})".format(klass_repr, self.identifier) 54 | 55 | def register(self): 56 | """Add this instance to the instance pool.""" 57 | instance_pool.update({self.identifier: self}) 58 | if debug: 59 | print("{} registered".format(self.identifier)) 60 | 61 | def unregister(self): 62 | """Remove this instance from the instance pool.""" 63 | if debug: 64 | print("{} unregistered".format(self.identifier)) 65 | del instance_pool[self.identifier] 66 | 67 | @property 68 | def identifier(self): 69 | """Return the identifier of this instance.""" 70 | return self._identifier 71 | 72 | @identifier.setter 73 | def identifier(self, value): 74 | self._identifier = value 75 | 76 | Decorated.__name__ = klass.__name__ 77 | 78 | class ClassObject: 79 | def __repr__(self): 80 | return klass.__name__ 81 | 82 | __str__ = __repr__ 83 | 84 | def __call__(self, *args, **kwargs): 85 | if debug: 86 | print("{}.__call__ (prototype)".format(str(self))) 87 | decorated_instance = Decorated(*args, **kwargs) 88 | if auto_register: 89 | decorated_instance.register() 90 | return decorated_instance 91 | 92 | @classmethod 93 | def clone(cls, identifier): 94 | """Get an instance from the pool and return it to the caller. 95 | 96 | Parameters 97 | ---------- 98 | identifier : int 99 | identifier for an instance 100 | 101 | Raises 102 | ------ 103 | InstanceNotAvailable 104 | if the instance is not available in the instance pool 105 | 106 | Returns 107 | ------- 108 | cloned_obj : decorated class 109 | instance of the decorated class 110 | """ 111 | try: 112 | original_object = instance_pool[identifier] 113 | cloned_obj = deepcopy(original_object) 114 | return cloned_obj 115 | 116 | except KeyError: 117 | raise InstanceNotAvailable( 118 | "Instance with identifier {} not found.\nWas it " 119 | "registered?\nThe available identifiers are: {}".format( 120 | identifier, cls.available_identifiers() 121 | ) 122 | ) 123 | 124 | @staticmethod 125 | def available_identifiers(): 126 | """Return the identifiers stored in the instance pool. 127 | 128 | Returns 129 | ------- 130 | list 131 | identifiers of all instances available in instance pool. 132 | """ 133 | return list(instance_pool.keys()) 134 | 135 | return ClassObject() 136 | 137 | return inner 138 | 139 | 140 | @prototype(hash_function=id) 141 | class Point(object): 142 | def __init__(self, x, y): 143 | print("{}__init__ (original class)".format(self.__class__.__name__)) 144 | self.x = x 145 | self.y = y 146 | 147 | def __repr__(self): 148 | return "{}({}, {})".format(self.__class__.__name__, self.x, self.y) 149 | 150 | def move(self, x, y): 151 | self.x += x 152 | self.y += y 153 | 154 | 155 | # TODO: how can we inherit from Point? 156 | 157 | # we will decorate this class later 158 | 159 | 160 | class Stuff(object): 161 | pass 162 | 163 | 164 | class MoreStuff(Stuff): 165 | pass 166 | 167 | 168 | def main(): 169 | print("\nCreate 2 points") 170 | print("p1") 171 | p1 = Point(x=3, y=5) 172 | print(p1) 173 | print("p2") 174 | p2 = Point(x=100, y=150) 175 | print(p2) 176 | print("p1.identifier != p2.identifier") 177 | assert p1.identifier != p2.identifier 178 | 179 | print("\nIdentifiers in the instance pool") 180 | print(Point.available_identifiers()) 181 | 182 | print( 183 | "\nThe instance pool is empty because we didn't register any " 184 | "instance. Let's fix this" 185 | ) 186 | p1.register() 187 | p2.register() 188 | print(Point.available_identifiers()) 189 | 190 | print("\nCreate a point by cloning p1 (__init__ is not called)") 191 | p3 = Point.clone(p1.identifier) 192 | print(p3) 193 | print("Create a point by cloning p3 (which is a clone of p1)") 194 | p4 = Point.clone(p3.identifier) 195 | print(p4) 196 | print("p1.identifier == p3.identifier == p4.identifier") 197 | assert p1.identifier == p3.identifier == p4.identifier 198 | 199 | print("\nIdentifiers in the instance pool") 200 | print(Point.available_identifiers()) 201 | 202 | print("\nmove p1") 203 | p1.move(5, 7) 204 | print(p1) 205 | # p3 and p4 are not weak references of p1, they are deep copies 206 | print("if p1 moves, p3 and p4 are unaffected") 207 | print(p3) 208 | print(p4) 209 | 210 | print("\nunregister p1") 211 | p1.unregister() 212 | print("p1 cannot be cloned because it was unregistered") 213 | try: 214 | Point.clone(p1.identifier) 215 | except InstanceNotAvailable as e: 216 | print(e) 217 | print("but p1 still exists") 218 | print(p1) 219 | 220 | # TODO: this behavior might be undesirable 221 | print( 222 | "\nEven if we destroy p2, it's not removed from the instance pool, " 223 | "so if we know the identifier we can still clone it" 224 | ) 225 | identifier = deepcopy(p2.identifier) 226 | del p2 227 | print(Point.available_identifiers()) 228 | Point.clone(identifier) 229 | 230 | print("\nwith a wrong identifier we get a ValueError exception") 231 | wrong_identifier = 123456789 232 | try: 233 | Point.clone(wrong_identifier) 234 | except InstanceNotAvailable as e: 235 | print(e) 236 | 237 | print("\nDecorate a new class") 238 | proto = prototype(auto_register=True, debug=True) 239 | StuffDecorated = proto(Stuff) 240 | 241 | s1 = StuffDecorated() 242 | StuffDecorated.clone(s1.identifier) 243 | print("\nInstance pools are different for each class") 244 | print("StuffDecorated.available_identifiers") 245 | print(StuffDecorated.available_identifiers()) 246 | print("Point.available_identifiers") 247 | print(Point.available_identifiers()) 248 | 249 | proto = prototype(auto_register=True) 250 | MoreStuffDecorated = proto(MoreStuff) 251 | MoreStuffDecorated() 252 | print("MoreStuffDecorated.available_identifiers") 253 | print(MoreStuffDecorated.available_identifiers()) 254 | 255 | 256 | if __name__ == "__main__": 257 | main() 258 | -------------------------------------------------------------------------------- /proxy.py: -------------------------------------------------------------------------------- 1 | """Proxy pattern 2 | 3 | Proxy is a structural design pattern. A proxy is a surrogate object which can 4 | communicate with the real object (aka implementation). Whenever a method in the 5 | surrogate is called, the surrogate simply calls the corresponding method in 6 | the implementation. The real object is encapsulated in the surrogate object when 7 | the latter is instantiated. It's NOT mandatory that the real object class and 8 | the surrogate object class share the same common interface. 9 | """ 10 | from abc import ABC, abstractmethod 11 | 12 | 13 | class CommonInterface(ABC): 14 | """Common interface for Implementation (real obj) and Proxy (surrogate).""" 15 | 16 | @abstractmethod 17 | def load(self): 18 | pass 19 | 20 | @abstractmethod 21 | def do_stuff(self): 22 | pass 23 | 24 | 25 | class Implementation(CommonInterface): 26 | def __init__(self, filename): 27 | self.filename = filename 28 | 29 | def load(self): 30 | print("load {}".format(self.filename)) 31 | 32 | def do_stuff(self): 33 | print("do stuff on {}".format(self.filename)) 34 | 35 | 36 | class Proxy(CommonInterface): 37 | def __init__(self, implementation): 38 | self.__implementation = implementation # the real object 39 | self.__cached = False 40 | 41 | def load(self): 42 | self.__implementation.load() 43 | self.__cached = True 44 | 45 | def do_stuff(self): 46 | if not self.__cached: 47 | self.load() 48 | self.__implementation.do_stuff() 49 | 50 | 51 | def main(): 52 | p1 = Proxy(Implementation("RealObject1")) 53 | p2 = Proxy(Implementation("RealObject2")) 54 | 55 | p1.do_stuff() # loading necessary 56 | p1.do_stuff() # loading unnecessary (use cached object) 57 | p2.do_stuff() # loading necessary 58 | p2.do_stuff() # loading unnecessary (use cached object) 59 | p1.do_stuff() # loading unnecessary (use cached object) 60 | 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "design-patterns" 3 | version = "0.1.0" 4 | description = "Some of the most common design patterns implemented in Python." 5 | license = "MIT" 6 | authors = ["Giacomo Debidda "] 7 | repository = "https://github.com/jackdbd/design-patterns" 8 | homepage = "https://github.com/jackdbd/design-patterns" 9 | keywords = ['design-patterns', 'python', 'mvc'] 10 | 11 | [tool.poetry.dependencies] 12 | alembic = "^1.4.3" 13 | boto3 = "^1.15.5" 14 | botocore = "^1.18.5" 15 | dataset = "^1.3.2" 16 | ddt = "^1.4.1" 17 | pendulum = "^2.1.2" 18 | psycopg2 = "^2.8.6" 19 | pyparsing = "^2.4.7" 20 | python = "^3.7" 21 | SQLAlchemy = "^1.3.19" 22 | transitions = "^0.8.2" 23 | 24 | [tool.poetry.dev-dependencies] 25 | black = "^20.8b1" 26 | poethepoet = "^0.8.0" 27 | pytest = "^6.0.2" 28 | 29 | [tool.poe.tasks] 30 | format = "poetry run black ." 31 | test = "pytest --verbose" 32 | 33 | [build-system] 34 | requires = ["poetry>=0.12"] 35 | build-backend = "poetry.masonry.api" 36 | -------------------------------------------------------------------------------- /singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton(object): 2 | 3 | _instance = None 4 | 5 | def __init__(self, name): 6 | self.name = name 7 | 8 | def __new__(cls, *args): 9 | if getattr(cls, "_instance") is None or cls != cls._instance.__class__: 10 | cls._instance = object.__new__(cls) 11 | return cls._instance 12 | 13 | 14 | class Child(Singleton): 15 | def childmethod(self): 16 | pass 17 | 18 | 19 | class GrandChild(Child): 20 | def grandchildmethod(self): 21 | pass 22 | 23 | 24 | def main(): 25 | # The __new__ method creates a new instance and returns it. 26 | # The __init__ method initializes the instance. 27 | s1 = Singleton("Sam") 28 | # The __new__ method does NOT create an instance and returns the first one. 29 | # The __init__ method re-initializes the instance (the first one). 30 | # The instance is the same, so the effects of the first __init__ are lost. 31 | s2 = Singleton("Tom") 32 | # The __new__ method creates a new instance because it's a different class. 33 | c1 = Child("John") 34 | c2 = Child("Andy") 35 | g1 = GrandChild("Bob") 36 | print(s1.name, id(s1), s1) 37 | print(s2.name, id(s2), s2) 38 | print(c1.name, id(c1), c1) 39 | print(c2.name, id(c2), c2) 40 | print(g1.name, id(g1), g1) 41 | print("s1 is s2?") 42 | print(s1 is s2) 43 | print("s1 is c1?") 44 | print(s1 is c1) 45 | print("c1 is c2?") 46 | print(c1 is c2) 47 | print("c1 is g1?") 48 | print(c1 is g1) 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /state.py: -------------------------------------------------------------------------------- 1 | """State pattern 2 | 3 | A state machine is an abstract machine with two main components: 4 | - states. A state is the current status of a system. A state machine can have 5 | only one active state at any point in time. 6 | - transitions. A transition is a switch from the current state to a new one. 7 | 8 | Basic example with the transitions library 9 | https://github.com/tyarkoni/transitions 10 | """ 11 | import random 12 | from transitions import MachineError 13 | from transitions.extensions import GraphMachine 14 | 15 | 16 | class Process(object): 17 | 18 | states = ["sleeping", "waiting", "running", "terminated"] 19 | 20 | def __init__(self, name): 21 | self.name = name 22 | 23 | # initialize the state machine 24 | self.machine = GraphMachine(model=self, states=self.states, initial="sleeping") 25 | 26 | # add transitions 27 | self.machine.add_transition( 28 | trigger="wake_up", source="sleeping", dest="waiting" 29 | ) 30 | self.machine.add_transition( 31 | trigger="start", source="waiting", dest="running", before="display_message" 32 | ) 33 | self.machine.add_transition( 34 | trigger="interrupt", source="*", dest="terminated", after="display_warning" 35 | ) 36 | self.machine.add_transition( 37 | trigger="random_trigger", 38 | source="*", 39 | dest="terminated", 40 | conditions=["is_valid"], 41 | ) 42 | 43 | # create image of the state machine (requires GraphViz and pygraphviz) 44 | self.graph.draw("my_state_diagram.png", prog="dot") 45 | 46 | def is_valid(self): 47 | return random.random() < 0.5 48 | 49 | def display_message(self): 50 | print("I'm starting...") 51 | 52 | def display_warning(self): 53 | print("I've just got an interrupt!") 54 | 55 | def random_termination(self): 56 | print("terminated") 57 | 58 | 59 | def main(): 60 | p = Process("p1") 61 | print("initial state: {}".format(p.state)) 62 | 63 | old = p.state 64 | print("\nwake_up trigger") 65 | p.wake_up() 66 | print("{} -> {}".format(old, p.state)) 67 | 68 | old = p.state 69 | print("\nstart trigger") 70 | p.start() 71 | print("{} -> {}".format(old, p.state)) 72 | 73 | old = p.state 74 | print("\nrandom trigger (stay in current state or go to terminated 50/50)") 75 | p.random_trigger() 76 | print("{} -> {}".format(old, p.state)) 77 | 78 | old = p.state 79 | print("\ninterrupt trigger") 80 | p.interrupt() 81 | print("{} -> {}".format(old, p.state)) 82 | 83 | print('\nFrom "terminated" we cannot trigger a "start" event') 84 | try: 85 | p.start() 86 | except MachineError as e: 87 | print(e) 88 | 89 | 90 | if __name__ == "__main__": 91 | main() 92 | -------------------------------------------------------------------------------- /strategy.py: -------------------------------------------------------------------------------- 1 | """Strategy pattern 2 | 3 | Strategy is a behavioral design pattern. It enables an algorithm's behavior to 4 | be selected at runtime. We can implement it by creating a common (abstract) 5 | interface and subclassing it with a new class for each strategy, how it's done 6 | in [1], or by creating a single class and replacing a method of that class with 7 | a different function, how it's done in [2]. The latter implementation is 8 | possible because in Python functions are first class objects. 9 | """ 10 | import types 11 | 12 | 13 | class Strategy(object): 14 | def __init__(self, func=None): 15 | 16 | if func is not None: 17 | # replace the default bound method 'execute' with a simple function. 18 | # The new 'execute' method will be a static method (no self). 19 | # self.execute = func 20 | # take a function, bind it to this instance, and replace the default 21 | # bound method 'execute' with this new bound method. 22 | # The new 'execute' will be a normal method (self available). 23 | self.execute = types.MethodType(func, self) 24 | self.name = "{}_{}".format(self.__class__.__name__, func.__name__) 25 | else: 26 | self.name = "{}_default".format(self.__class__.__name__) 27 | 28 | def execute(self): 29 | print("Default method") 30 | print("{}\n".format(self.name)) 31 | 32 | 33 | # Replacement strategies for the default method 'execute'. These ones are 34 | # defined as normal functions, so we will need to bind them to an instance when 35 | # the object is instatiated (we can use types.MethodType). 36 | 37 | 38 | def execute_replacement1(self): 39 | print("Replacement1 method") 40 | print("{}\n".format(self.name)) 41 | 42 | 43 | def execute_replacement2(self): 44 | print("Replacement2 method") 45 | print("{}\n".format(self.name)) 46 | 47 | 48 | def main(): 49 | 50 | # This part of the program is the Context: it decides which strategy to use. 51 | 52 | s0 = Strategy() 53 | s0.execute() 54 | 55 | s1 = Strategy(execute_replacement1) 56 | s1.execute() 57 | 58 | s2 = Strategy(execute_replacement2) 59 | s2.execute() 60 | 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /template_method.py: -------------------------------------------------------------------------------- 1 | """Template method pattern 2 | 3 | Template Method is a behavioral design pattern. It defines an algorithm's 4 | skeleton in a Base class, but lets subclasses redefine certain steps of the 5 | algorithm. The Base class declares some placeholder methods, and derived classes 6 | implement them. 7 | """ 8 | import sys 9 | from abc import ABC, abstractmethod 10 | 11 | 12 | class Algorithm(ABC): 13 | def template_method(self): 14 | """Skeleton of operations to perform. DON'T override me. 15 | 16 | The Template Method defines a skeleton of an algorithm in an operation, 17 | and defers some steps to subclasses. 18 | """ 19 | self.__do_absolutely_this() 20 | self.do_step_1() 21 | self.do_step_2() 22 | self.do_something() 23 | 24 | def __do_absolutely_this(self): 25 | """Protected operation. DON'T override me.""" 26 | this_method_name = sys._getframe().f_code.co_name 27 | print("{}.{}".format(self.__class__.__name__, this_method_name)) 28 | 29 | @abstractmethod 30 | def do_step_1(self): 31 | """Primitive operation. You HAVE TO override me, I'm a placeholder.""" 32 | pass 33 | 34 | @abstractmethod 35 | def do_step_2(self): 36 | """Primitive operation. You HAVE TO override me, I'm a placeholder.""" 37 | pass 38 | 39 | def do_something(self): 40 | """Hook. You CAN override me, I'm NOT a placeholder.""" 41 | print("do something") 42 | 43 | 44 | class AlgorithmA(Algorithm): 45 | def do_step_1(self): 46 | print("do step 1 for Algorithm A") 47 | 48 | def do_step_2(self): 49 | print("do step 2 for Algorithm A") 50 | 51 | 52 | class AlgorithmB(Algorithm): 53 | def do_step_1(self): 54 | print("do step 1 for Algorithm B") 55 | 56 | def do_step_2(self): 57 | print("do step 2 for Algorithm B") 58 | 59 | def do_something(self): 60 | print("do something else") 61 | 62 | 63 | def main(): 64 | print("Algorithm A") 65 | a = AlgorithmA() 66 | a.template_method() 67 | 68 | print("\nAlgorithm B") 69 | b = AlgorithmB() 70 | b.template_method() 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /test_design_patterns.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | from io import StringIO 4 | from ddt import ddt, data 5 | from contextlib import contextmanager 6 | from borg import Borg, ChildShare, ChildNotShare 7 | from interpreter import ( 8 | Interpreter, 9 | DeviceNotAvailable, 10 | ActionNotAvailable, 11 | IncorrectAction, 12 | ) 13 | from factory_method import factory_method 14 | from abstract_factory import ( 15 | TriangleFactory, 16 | QuadrilateralFactory, 17 | give_me_some_polygons, 18 | ) 19 | from memento import Originator 20 | from null_object import NullObject 21 | from observer import Publisher, Subscriber 22 | from proxy import Proxy, Implementation 23 | from singleton import Singleton, Child, GrandChild 24 | from strategy import Strategy, execute_replacement1, execute_replacement2 25 | 26 | 27 | @contextmanager 28 | def captured_output(): 29 | new_out, new_err = StringIO(), StringIO() 30 | old_out, old_err = sys.stdout, sys.stderr 31 | try: 32 | sys.stdout, sys.stderr = new_out, new_err 33 | yield sys.stdout, sys.stderr 34 | 35 | finally: 36 | sys.stdout, sys.stderr = old_out, old_err 37 | 38 | 39 | class TestBorg(unittest.TestCase): 40 | def test_two_borgs_have_different_identity(self): 41 | a = Borg("Mark") 42 | b = Borg("Luke") 43 | self.assertIsNot(a, b) 44 | 45 | def test_two_borgs_share_common_state(self): 46 | a = Borg("Mark") 47 | b = Borg("Luke") 48 | self.assertEqual(a.state, b.state) 49 | 50 | def test_borg_and_childshare_share_common_state(self): 51 | a = Borg("Mark") 52 | c = ChildShare("Paul", color="red") 53 | self.assertEqual(a.state, c.state) 54 | 55 | def test_borg_and_childnotshare_do_not_share_common_state(self): 56 | a = Borg("Mark") 57 | d = ChildNotShare("Andrew", age=5) 58 | self.assertNotEqual(a.state, d.state) 59 | 60 | def test_two_childnotshare_share_common_state(self): 61 | d = ChildNotShare("Andrew", age=5) 62 | e = ChildNotShare("Tom", age=7) 63 | self.assertEqual(d.state, e.state) 64 | 65 | def test_update_state(self): 66 | a = Borg("Mark") 67 | c = ChildShare("Paul", color="red") 68 | self.assertIn("color", a.state) 69 | d = ChildNotShare("Andrew", age=5) 70 | a.name = "James" 71 | self.assertEqual(a.name, c.name) 72 | self.assertNotEqual(a.name, d.name) 73 | 74 | 75 | @ddt 76 | class TestInterpreter(unittest.TestCase): 77 | @classmethod 78 | def setUpClass(cls): 79 | cls.interpreter = Interpreter() 80 | 81 | def test_opening_the_garage(self): 82 | with captured_output() as (out, err): 83 | self.interpreter.interpret("open -> garage") 84 | output = out.getvalue().strip() 85 | self.assertEqual(output, "opening the garage") 86 | 87 | def test_heat_the_boiler_up(self): 88 | with captured_output() as (out, err): 89 | self.interpreter.interpret("heat -> boiler -> 5") 90 | output = out.getvalue().strip() 91 | self.assertEqual(output, "heat the boiler up by 5 degrees") 92 | 93 | def test_cool_the_boiler_down(self): 94 | with captured_output() as (out, err): 95 | self.interpreter.interpret("cool -> boiler -> 3") 96 | output = out.getvalue().strip() 97 | self.assertEqual(output, "cool the boiler down by 3 degrees") 98 | 99 | def test_switch_the_television_on(self): 100 | with captured_output() as (out, err): 101 | self.interpreter.interpret("switch on -> television") 102 | output = out.getvalue().strip() 103 | self.assertEqual(output, "switch on the television") 104 | 105 | def test_switch_the_television_off(self): 106 | with captured_output() as (out, err): 107 | self.interpreter.interpret("switch off -> television") 108 | output = out.getvalue().strip() 109 | self.assertEqual(output, "switch off the television") 110 | 111 | @data("cool -> boiler", "switch off -> television -> 4") 112 | def test_raise_incorrect_action(self, val): 113 | self.assertRaises(IncorrectAction, self.interpreter.interpret, val) 114 | 115 | @data("break -> garage", "smash -> television") 116 | def test_raise_action_not_available(self, val): 117 | self.assertRaises(ActionNotAvailable, self.interpreter.interpret, val) 118 | 119 | @data("read -> book", "open -> gate") 120 | def test_raise_device_not_available(self, val): 121 | self.assertRaises(DeviceNotAvailable, self.interpreter.interpret, val) 122 | 123 | 124 | class TestMemento(unittest.TestCase): 125 | def test_restore_state(self): 126 | originator = Originator() 127 | originator.state = "State1" 128 | memento1 = originator.save() 129 | originator.state = "State2" 130 | originator.restore(memento1) 131 | self.assertEqual(originator.state, "State1") 132 | 133 | 134 | class TestNullObject(unittest.TestCase): 135 | @classmethod 136 | def setUpClass(cls): 137 | cls.null = NullObject(name="Bob") 138 | 139 | def test_null_object_is_null(self): 140 | self.assertTrue(self.null.is_null) 141 | 142 | def test_null_has_no_name(self): 143 | self.assertIsNot(self.null.name, "Bob") 144 | 145 | def test_repr_of_null_object(self): 146 | self.assertEqual(repr(self.null), "") 147 | 148 | def test_do_stuff_does_nothing(self): 149 | self.assertIsNone(self.null.do_stuff()) 150 | 151 | def test_get_stuff_gets_nothing(self): 152 | self.assertIsNone(self.null.get_stuff()) 153 | 154 | 155 | class TestFactoryMethod(unittest.TestCase): 156 | def test_factory_cannot_manufacture_a_train(self): 157 | self.assertRaises(ValueError, factory_method, "train") 158 | 159 | 160 | class TestAbstractFactory(unittest.TestCase): 161 | def test_factories_are_abstract_and_cannot_be_instantiated(self): 162 | with self.assertRaises(TypeError): 163 | TriangleFactory() 164 | with self.assertRaises(TypeError): 165 | QuadrilateralFactory() 166 | 167 | def test_triangle_factory_produces_triangles(self): 168 | triangle = TriangleFactory.make_polygon() 169 | self.assertIn(triangle.__class__.__name__, TriangleFactory.products()) 170 | 171 | def test_polygons_produced_are_subset_of_all_available_polygons(self): 172 | all_available_polygons = set(TriangleFactory.products()).union( 173 | QuadrilateralFactory.products() 174 | ) 175 | polygons = give_me_some_polygons([TriangleFactory, QuadrilateralFactory]) 176 | polygons_produced = set([p.__class__.__name__ for p in polygons]) 177 | self.assertTrue(polygons_produced.issubset(all_available_polygons)) 178 | 179 | 180 | class TestObserver(unittest.TestCase): 181 | def setUp(self): 182 | self.newsletters = ["Tech", "Travel", "Fashion"] 183 | self.pub = Publisher(self.newsletters) 184 | subscribers = [("s0", "Tom"), ("s1", "Sara")] 185 | for sub, name in subscribers: 186 | setattr(self, sub, Subscriber(name)) 187 | 188 | # before each test case, set some subscriptions 189 | self.pub.register("Tech", self.s0) 190 | self.pub.register("Travel", self.s0) 191 | self.pub.register("Travel", self.s1) 192 | 193 | def tearDown(self): 194 | # after each test case, reset the subscriptions 195 | for newsletter in self.newsletters: 196 | if self.s0 in self.pub.subscriptions[newsletter]: 197 | self.pub.unregister(newsletter, self.s0) 198 | if self.s1 in self.pub.subscriptions[newsletter]: 199 | self.pub.unregister(newsletter, self.s1) 200 | 201 | def test_register_subscriber(self): 202 | john = Subscriber("John") 203 | self.pub.register(newsletter="Tech", who=john) 204 | self.assertEqual(self.pub.subscriptions["Tech"][john], john.receive) 205 | self.pub.unregister(newsletter="Tech", who=john) # cleanup 206 | 207 | def test_unregister_subscriber(self): 208 | self.assertIn(self.s0, self.pub.get_subscriptions("Tech")) 209 | self.pub.unregister("Tech", self.s0) 210 | self.assertNotIn(self.s0, self.pub.get_subscriptions("Tech")) 211 | 212 | def test_dispatch_newsletter(self): 213 | with captured_output() as (out, err): 214 | self.pub.dispatch(newsletter="Tech", message="Tech Newsletter num 1") 215 | output = out.getvalue().strip() 216 | self.assertEqual(output, "Tom received: Tech Newsletter num 1") 217 | 218 | def test_get_subscription_without_subscribers(self): 219 | self.assertEqual(self.pub.get_subscriptions("Fashion"), {}) 220 | 221 | def test_get_subscription_with_subscribers(self): 222 | self.assertIn(self.s0, self.pub.get_subscriptions("Tech")) 223 | 224 | def test_add_newsletter(self): 225 | self.assertNotIn("Videogames", self.pub.subscriptions.keys()) 226 | self.pub.add_newsletter("Videogames") 227 | self.assertIn("Videogames", self.pub.subscriptions.keys()) 228 | 229 | def test_subscription_does_not_exist(self): 230 | with self.assertRaises(KeyError): 231 | self.pub.subscriptions["Videogames"] 232 | 233 | 234 | class TestProxy(unittest.TestCase): 235 | def test_load_real_or_cached_object(self): 236 | p1 = Proxy(Implementation("RealObject1")) 237 | 238 | # the first time we call do_stuff we need to load the real object 239 | with captured_output() as (out, err): 240 | p1.do_stuff() 241 | output = out.getvalue().strip() 242 | self.assertEqual(output, "load RealObject1\ndo stuff on RealObject1") 243 | 244 | # after that, loading is unnecessary (we use the cached object) 245 | with captured_output() as (out, err): 246 | p1.do_stuff() 247 | output = out.getvalue().strip() 248 | self.assertEqual(output, "do stuff on RealObject1") 249 | 250 | 251 | class TestSingleton(unittest.TestCase): 252 | def test_two_singletons_have_same_identity(self): 253 | s1 = Singleton("Sam") 254 | s2 = Singleton("Tom") 255 | self.assertIs(s1, s2) 256 | 257 | def test_singleton_and_child_have_different_identity(self): 258 | s1 = Singleton("Sam") 259 | c1 = Child("John") 260 | self.assertIsNot(s1, c1) 261 | 262 | def test_two_children_have_same_identity(self): 263 | c1 = Child("John") 264 | c2 = Child("Andy") 265 | self.assertIs(c1, c2) 266 | 267 | def test_child_and_grandchild_have_different_identity(self): 268 | c1 = Child("John") 269 | g1 = GrandChild("Bob") 270 | self.assertIsNot(c1, g1) 271 | 272 | 273 | class TestStrategy(unittest.TestCase): 274 | def test_default_strategy(self): 275 | self.assertEqual(Strategy().name, "Strategy_default") 276 | 277 | def test_replacement_strategy_one(self): 278 | self.assertEqual( 279 | Strategy(execute_replacement1).name, "Strategy_execute_replacement1" 280 | ) 281 | 282 | def test_replacement_strategy_two(self): 283 | self.assertEqual( 284 | Strategy(execute_replacement2).name, "Strategy_execute_replacement2" 285 | ) 286 | 287 | 288 | if __name__ == "__main__": 289 | unittest.main() 290 | -------------------------------------------------------------------------------- /visitor.py: -------------------------------------------------------------------------------- 1 | """Visitor pattern 2 | 3 | Visitor gives us the ability to add new operations to existing objects without 4 | modifying these objects. It's one way to follow the open/closed principle. 5 | """ 6 | 7 | 8 | # classes that we cannot change (e.g. a fairly stable class hierarchy) 9 | 10 | 11 | class Element(object): 12 | pass 13 | 14 | 15 | class ElementOne(Element): 16 | pass 17 | 18 | 19 | class ElementTwo(Element): 20 | pass 21 | 22 | 23 | class ElementThree(ElementOne, ElementTwo): 24 | pass 25 | 26 | 27 | class ElementFour(ElementThree): 28 | pass 29 | 30 | 31 | # Extrinsic Visitor 32 | 33 | 34 | class Visitor(object): 35 | 36 | operations = { 37 | "ElementTwo": "custom_operation", 38 | "ElementFour": "another_custom_operation", 39 | } 40 | 41 | def visit(self, element, *args, **kwargs): 42 | """Perform an operation specific for the passed element. 43 | 44 | Steps: 45 | 1. traverse the class hierarchy of an Element object 46 | 2. discover what operation to perform on the Element object 47 | 3. perform the specific operation on the Element object 48 | 49 | Parameters 50 | ---------- 51 | element : Element 52 | object whose behavior must be implemented here 53 | 54 | Returns 55 | ------- 56 | return value/s of the method chosen at runtime 57 | """ 58 | method_name = "default_operation" 59 | for cls in element.__class__.__mro__: 60 | try: 61 | method_name = self.operations[cls.__name__] 62 | break # we found out a custom operation to perform, so we exit 63 | 64 | except KeyError: 65 | pass # keep default_operation if there isn't a custom one 66 | method = getattr(self, method_name) 67 | return method(element, *args, **kwargs) 68 | 69 | # implement the behaviors for the Element objects 70 | 71 | @staticmethod 72 | def default_operation(elem, *args, **kwargs): 73 | print( 74 | "No custom operation defined for {} or its class hierarchy".format( 75 | elem.__class__.__name__ 76 | ) 77 | ) 78 | print( 79 | "default_operation on {} with args {} and kwargs {}".format( 80 | elem.__class__.__name__, args, kwargs 81 | ) 82 | ) 83 | 84 | @staticmethod 85 | def custom_operation(elem, *args, **kwargs): 86 | print( 87 | "custom_operation on {} with args {} and kwargs {}".format( 88 | elem.__class__.__name__, args, kwargs 89 | ) 90 | ) 91 | 92 | @staticmethod 93 | def another_custom_operation(elem, *args, **kwargs): 94 | print( 95 | "another_custom_operation on {} with args {} and kwargs {}".format( 96 | elem.__class__.__name__, args, kwargs 97 | ) 98 | ) 99 | 100 | 101 | def main(): 102 | elements = [ElementOne(), ElementTwo(), ElementThree(), ElementFour()] 103 | visitor = Visitor() 104 | for elem in elements: 105 | visitor.visit(elem) 106 | 107 | 108 | if __name__ == "__main__": 109 | main() 110 | --------------------------------------------------------------------------------