├── requirements.txt ├── .gitignore ├── .devcontainer ├── startup.sh ├── devcontainer.json └── Dockerfile ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── main.yml └── ISSUE_TEMPLATE.md ├── Ch02 ├── 02_04 │ ├── error_cost.png │ ├── plugin.py │ └── plugin_abc.py ├── 02_02 │ └── vm.py ├── 02_05 │ └── writer.py ├── challenge │ └── ingest.py ├── 02_03 │ └── logger.py ├── 02_01 │ └── user.py └── solution │ └── ingest.py ├── Ch04 ├── challenge │ └── singleton.py ├── solution │ └── singleton.py ├── 04_01 │ └── robot.py ├── 04_02 │ └── robot.py ├── 04_03 │ └── robot.py └── 04_04 │ └── robot.py ├── Ch01 ├── challenge │ └── colors.py ├── 01_01 │ ├── game.py │ └── attr.py ├── 01_03 │ └── proxy.py ├── solution │ └── colors.py ├── 01_04 │ ├── car.py │ └── compact_car.py ├── 01_05 │ ├── worker.py │ └── worker_mangled.py ├── 01_02 │ └── line_item.py └── 01_06 │ └── validators.py ├── Ch03 ├── 03_05 │ └── norm.py ├── 03_03 │ ├── traces.py │ └── headers.py ├── 03_04 │ └── duration.py ├── 03_01 │ └── currency.py ├── 03_02 │ └── stack.py ├── challenge │ └── kv.py └── solution │ └── kv.py ├── Ch05 ├── challenge │ └── vm.py ├── 05_04 │ └── event.py ├── 05_02 │ └── game.py ├── 05_03 │ └── bookmark.py ├── solution │ └── vm.py └── 05_01 │ └── serialize.py ├── CONTRIBUTING.md ├── NOTICE ├── .vscode └── settings.json ├── README.md └── LICENSE /requirements.txt: -------------------------------------------------------------------------------- 1 | mypy ~= 1.5 2 | 3 | jupyter ~= 1.0 # For VSCode 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .tmp 4 | npm-debug.log 5 | .mypy_cache 6 | -------------------------------------------------------------------------------- /.devcontainer/startup.sh: -------------------------------------------------------------------------------- 1 | if [ -f requirements.txt ]; then 2 | pip install --user -r requirements.txt 3 | fi -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Codeowners for these exercise files: 2 | # * (asterisk) denotes "all files and folders" 3 | # Example: * @producer @instructor 4 | -------------------------------------------------------------------------------- /Ch02/02_04/error_cost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInLearning/advanced-python-object-oriented-programming-4510177/main/Ch02/02_04/error_cost.png -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Ch04/challenge/singleton.py: -------------------------------------------------------------------------------- 1 | # %% Singleton 2 | class Singleton: 3 | # TODO 4 | pass 5 | 6 | 7 | # %% Test 8 | 9 | class Driver(Singleton): 10 | pass 11 | 12 | d1 = Driver() 13 | d2 = Driver() 14 | print(d1 is d2) 15 | -------------------------------------------------------------------------------- /Ch01/challenge/colors.py: -------------------------------------------------------------------------------- 1 | # %% Color database 2 | color_db = { 3 | 'red': 0xFF0000, 4 | 'green': 0x00FF00, 5 | 'blue': 0x0000FF, 6 | } 7 | 8 | 9 | class Colors: 10 | """Dynamically get color from color_db""" 11 | # FIXME 12 | 13 | 14 | # %% Test 15 | colors = Colors() 16 | 17 | val = colors.green 18 | print(f'green: {val:06X}') # 00FF00 19 | -------------------------------------------------------------------------------- /Ch01/01_01/game.py: -------------------------------------------------------------------------------- 1 | # %% Player 2 | class Player: 3 | num_players = 0 # Class attribute 4 | 5 | def __init__(self, name): 6 | self.name = name # Instance attribute 7 | self.mana = 100 8 | self.num_players += 1 9 | print('self:', self.num_players) 10 | 11 | 12 | # %% Test 13 | p1 = Player('Parzival') 14 | print('Player:', Player.num_players) -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Copy To Branches 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | copy-to-branches: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - name: Copy To Branches Action 12 | uses: planetoftheweb/copy-to-branches@v1.2 13 | env: 14 | key: main 15 | -------------------------------------------------------------------------------- /Ch01/01_03/proxy.py: -------------------------------------------------------------------------------- 1 | # %% Proxy 2 | class Proxy: 3 | def __init__(self, obj): 4 | self._obj = obj 5 | 6 | def __getattr__(self, attr): 7 | value = getattr(self._obj, attr) 8 | print(f'{attr} -> {value!r}') 9 | return value 10 | 11 | # %% Test 12 | import sqlite3 13 | 14 | conn = sqlite3.connect(':memory:') 15 | proxy = Proxy(conn) 16 | n = proxy.total_changes 17 | print(n) 18 | 19 | # %% Method 20 | proxy.close() -------------------------------------------------------------------------------- /Ch02/02_02/vm.py: -------------------------------------------------------------------------------- 1 | # %% VM 2 | from random import choice 3 | 4 | adjectives = ['cool', 'funny', 'strong'] 5 | names = ['bruce', 'carol', 'natasha'] 6 | 7 | 8 | class VM: 9 | def __init__(self): 10 | self.name = VM.random_name() 11 | 12 | 13 | @staticmethod 14 | def random_name(): 15 | adjective, name = choice(adjectives), choice(names) 16 | return f'{adjective}_{name}' 17 | 18 | 19 | # %% Test 20 | vm = VM() 21 | print(vm.name) 22 | -------------------------------------------------------------------------------- /Ch03/03_05/norm.py: -------------------------------------------------------------------------------- 1 | # %% normalize 2 | def normalize(value): 3 | return value * .9 4 | 5 | 6 | # %% Norm 7 | class Norm: 8 | def __init__(self, factor): 9 | self.factor = factor 10 | 11 | def __call__(self, value): 12 | return self.factor * value 13 | 14 | 15 | # %% Test 16 | n93 = Norm(.93) 17 | print(n93(100)) 18 | 19 | # %% 20 | def make_norm(factor): 21 | return lambda value: factor * value 22 | 23 | n93 = make_norm(.93) 24 | print(n93(100)) 25 | -------------------------------------------------------------------------------- /Ch04/solution/singleton.py: -------------------------------------------------------------------------------- 1 | # %% Singleton 2 | class SingletonMeta(type): 3 | def __call__(cls, *args, **kw): 4 | inst = getattr(cls, '_instance', None) 5 | if inst is None: 6 | inst = cls._instance = type.__call__(cls, *args, **kw) 7 | return inst 8 | 9 | 10 | class Singleton(metaclass=SingletonMeta): 11 | pass 12 | 13 | 14 | # %% Test 15 | 16 | class Driver(Singleton): 17 | pass 18 | 19 | d1 = Driver() 20 | d2 = Driver() 21 | print(d1 is d2) 22 | -------------------------------------------------------------------------------- /Ch01/solution/colors.py: -------------------------------------------------------------------------------- 1 | # %% Color database 2 | color_db = { 3 | 'red': 0xFF0000, 4 | 'green': 0x00FF00, 5 | 'blue': 0x0000FF, 6 | } 7 | 8 | 9 | class Colors: 10 | """Dynamically get color from color_db""" 11 | def __getattr__(self, attr): 12 | val = color_db.get(attr) 13 | if val is None: 14 | raise AttributeError(attr) 15 | return val 16 | 17 | # %% Test 18 | colors = Colors() 19 | 20 | val = colors.green 21 | print(f'green: {val:06X}') # 00FF00 22 | -------------------------------------------------------------------------------- /Ch05/challenge/vm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use a dataclass, write a VM class that has the following fields: 3 | - id: str, no default 4 | - cpus: int, default to 2 5 | - memory: int, default to 512 (in MB) 6 | - state: one of 'starting', 'running', 'stopped' 7 | - tags: list of str, default to empty 8 | 9 | After the VM is created, check that 10 | - id is not empty 11 | - cpus >= 1 12 | - memory >= 256 13 | """ 14 | 15 | # %% test 16 | vm = VM( 17 | id='i-0af01c0123456789a', 18 | cpus=4, 19 | memory=2048, 20 | tags=['db', 'env:qa'] 21 | ) 22 | print(vm) -------------------------------------------------------------------------------- /Ch05/05_04/event.py: -------------------------------------------------------------------------------- 1 | # %% Event 2 | from dataclasses import dataclass 3 | 4 | @dataclass 5 | class Event: 6 | uri: str 7 | action: str 8 | 9 | 10 | events = [ 11 | Event(uri='file:///etc/passwd', action='READ'), 12 | Event(uri='file:///etc/passwd', action='WRITE'), 13 | Event(uri='file:///var/log/httpd', action='WRITE'), 14 | Event(uri='file:///etc/passwd', action='READ'), 15 | ] 16 | 17 | # %% count 18 | from collections import Counter 19 | 20 | counts = Counter() 21 | for evt in events: 22 | counts[evt] += 1 23 | print(counts) -------------------------------------------------------------------------------- /Ch05/05_02/game.py: -------------------------------------------------------------------------------- 1 | # %% Room 2 | from collections import namedtuple 3 | 4 | Room = namedtuple('Room', 'x y') 5 | 6 | r1 = Room(1, 2) 7 | print(r1) 8 | 9 | # %% Attributes 10 | print('len:', len(r1)) 11 | print('x:', r1.x) 12 | print('[0]:', r1[0]) 13 | 14 | # %% attributes 15 | from collections import defaultdict 16 | 17 | players = defaultdict(list) 18 | players[r1].append('amy') 19 | print(players) 20 | 21 | # %% replace 22 | r2 = r1._replace(x=3) 23 | print(r2) 24 | 25 | # %% asdict 26 | print(r2._asdict()) 27 | 28 | # %% compare 29 | Range = namedtuple('Range', 'low high') 30 | 31 | rng = Range(1, 2) 32 | print(rng == r1) 33 | -------------------------------------------------------------------------------- /Ch01/01_04/car.py: -------------------------------------------------------------------------------- 1 | # %% Car 2 | class Car: 3 | def __init__(self, id, lat, lng): 4 | self.id = id 5 | self.lat = lat 6 | self.lng = lng 7 | 8 | 9 | # %% Memory 10 | import tracemalloc 11 | from random import random 12 | 13 | def rand_lat(): 14 | return random()*180 - 90 15 | 16 | def rand_lng(): 17 | return random()*360 - 180 18 | 19 | def mb(bytes): 20 | return bytes/1_000_000 21 | 22 | size = 100_000 23 | 24 | tracemalloc.start() 25 | cars = [Car(i, rand_lat(), rand_lng()) for i in range(size)] 26 | current, peak = tracemalloc.get_traced_memory() 27 | print('current:', mb(current), 'peak:', mb(peak)) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | Contribution Agreement 3 | ====================== 4 | 5 | This repository does not accept pull requests (PRs). All pull requests will be closed. 6 | 7 | However, if any contributions (through pull requests, issues, feedback or otherwise) are provided, as a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code (or otherwise providing feedback), you (and, if applicable, your employer) are licensing the submitted code (and/or feedback) to LinkedIn and the open source community subject to the BSD 2-Clause license. 8 | -------------------------------------------------------------------------------- /Ch02/02_05/writer.py: -------------------------------------------------------------------------------- 1 | # %% Writer 2 | from typing import Protocol 3 | 4 | 5 | class Writer(Protocol): 6 | def write(self, data: bytes) -> None: 7 | ... 8 | 9 | 10 | # %% Store 11 | import json 12 | 13 | def store_json(w: Writer, obj: dict) -> None: 14 | data = json.dumps(obj).encode('utf-8') 15 | w.write(data) 16 | 17 | 18 | # %% S3File 19 | class S3File: 20 | def write(self, data: str) -> None: 21 | # TODO 22 | print(f's3: write: {data!r}') 23 | 24 | 25 | # %% Test 26 | out = S3File() 27 | obj = { 28 | 'id': '007', 29 | 'lat': 51.4871871, 30 | 'lng': -0.1270605, 31 | } 32 | store_json(out, obj) 33 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2024 LinkedIn Corporation 2 | All Rights Reserved. 3 | 4 | Licensed under the LinkedIn Learning Exercise File License (the "License"). 5 | See LICENSE in the project root for license information. 6 | 7 | Please note, this project may automatically load third party code from external 8 | repositories (for example, NPM modules, Composer packages, or other dependencies). 9 | If so, such third party code may be subject to other license terms than as set 10 | forth above. In addition, such third party code may also depend on and load 11 | multiple tiers of dependencies. Please review the applicable licenses of the 12 | additional dependencies. 13 | -------------------------------------------------------------------------------- /Ch01/01_04/compact_car.py: -------------------------------------------------------------------------------- 1 | # %% Car 2 | class Car: 3 | __slots__ = ['id', 'lat', 'lng'] 4 | 5 | def __init__(self, id, lat, lng): 6 | self.id = id 7 | self.lat = lat 8 | self.lng = lng 9 | 10 | 11 | # %% Memory 12 | import tracemalloc 13 | from random import random 14 | 15 | def rand_lat(): 16 | return random()*180 - 90 17 | 18 | def rand_lng(): 19 | return random()*360 - 180 20 | 21 | def mb(bytes): 22 | return bytes/1_000_000 23 | 24 | size = 100_000 25 | 26 | tracemalloc.start() 27 | cars = [Car(i, rand_lat(), rand_lng()) for i in range(size)] 28 | current, peak = tracemalloc.get_traced_memory() 29 | print('current:', mb(current), 'peak:', mb(peak)) -------------------------------------------------------------------------------- /Ch05/05_03/bookmark.py: -------------------------------------------------------------------------------- 1 | # %% Bookmark 2 | from dataclasses import dataclass, field 3 | from datetime import datetime, UTC 4 | 5 | @dataclass 6 | class Bookmark: 7 | url: str 8 | title: str = '' 9 | tags: list[str] = field(default_factory=list) 10 | created: datetime = None 11 | 12 | def __post_init__(self): 13 | if self.created is None: 14 | self.created = datetime.now(tz=UTC) 15 | 16 | b1 = Bookmark( 17 | url='https://fastapi.tiangolo.com/', 18 | title='FastAPI web framework', 19 | tags=['python', 'web', 'server'], 20 | ) 21 | print(b1) 22 | 23 | # %% 24 | b2 = Bookmark( 25 | url='https://python.org', 26 | ) 27 | b2.title = 'Python programming language' 28 | print(b2) 29 | -------------------------------------------------------------------------------- /Ch01/01_05/worker.py: -------------------------------------------------------------------------------- 1 | # %% Worker 2 | from datetime import datetime, timedelta 3 | from time import time, sleep 4 | 5 | 6 | class Worker: 7 | def __init__(self, id): 8 | self.id = id 9 | self._started = time() 10 | 11 | def uptime(self): 12 | return time() - self._started 13 | 14 | 15 | # %% SpotWorker 16 | class SpotWorker(Worker): 17 | def __init__(self, id): 18 | super().__init__(id) 19 | self._started = datetime.now() 20 | 21 | def cost(self): 22 | duration = datetime.now() - self._started 23 | return duration / timedelta(seconds=60) * 0.02 24 | 25 | # %% Test 26 | worker = SpotWorker('769f984') 27 | sleep(0.123) 28 | print(f'uptime: {worker.uptime():.2f}') 29 | -------------------------------------------------------------------------------- /Ch01/01_02/line_item.py: -------------------------------------------------------------------------------- 1 | # %% LineItem 2 | class LineItem: 3 | def __init__(self, sku: str, price: float, amount: int): 4 | self.sku = sku 5 | self.price = price 6 | self.amount = amount 7 | 8 | @property # Computed property. 9 | def value(self): 10 | return self.price * self.amount 11 | 12 | @property # Getter. 13 | def sku(self): 14 | return self._sku 15 | 16 | @sku.setter # Setter. 17 | def sku(self, value): 18 | value = value.strip() 19 | if not value: 20 | raise ValueError(f'empty sku: {value!r}') 21 | self._sku = value 22 | 23 | 24 | # %% Test 25 | li = LineItem('esp32', 1.34, 10) 26 | print(li.value) 27 | 28 | # %% Invalid SKU 29 | li.sku = ' ' -------------------------------------------------------------------------------- /Ch04/04_01/robot.py: -------------------------------------------------------------------------------- 1 | # %% Robot 2 | class Robot: 3 | manufacture = 'BnL' 4 | 5 | def move(self, x, y): 6 | print(f'{self} moving to {x}/{y}') 7 | 8 | 9 | walle = Robot() 10 | walle.move(100, 200) 11 | 12 | 13 | # %% What "class" keyword does 14 | from textwrap import dedent 15 | 16 | class_body = ''' 17 | manufacture = 'BnL' 18 | 19 | def move(self, x, y): 20 | print(f'{self} moving to {x}/{y}') 21 | ''' 22 | 23 | cls_dict = {} 24 | exec(dedent(class_body), None, cls_dict) 25 | print(cls_dict) 26 | move = cls_dict['move'] 27 | print(move.__code__.co_varnames) 28 | move(walle, 10, 20) 29 | 30 | # %% Using type 31 | Robot = type( 32 | 'Robot', 33 | (object,), 34 | cls_dict, 35 | ) 36 | walle = Robot() 37 | walle.move(100, 200) 38 | -------------------------------------------------------------------------------- /Ch01/01_05/worker_mangled.py: -------------------------------------------------------------------------------- 1 | # %% Worker 2 | from datetime import datetime, timedelta 3 | from time import time, sleep 4 | 5 | 6 | class Worker: 7 | def __init__(self, id): 8 | self.id = id 9 | self.__started = time() 10 | 11 | def uptime(self): 12 | return time() - self.__started 13 | 14 | 15 | # %% SpotWorker 16 | class SpotWorker(Worker): 17 | def __init__(self, id): 18 | super().__init__(id) 19 | self._started = datetime.now() 20 | 21 | def cost(self): 22 | duration = datetime.now() - self._started 23 | return duration / timedelta(seconds=60) * 0.02 24 | 25 | # %% Test 26 | worker = SpotWorker('769f984') 27 | sleep(0.123) 28 | print(f'uptime: {worker.uptime():.2f}') 29 | 30 | # %% 31 | print(worker.__dict__) 32 | -------------------------------------------------------------------------------- /Ch02/challenge/ingest.py: -------------------------------------------------------------------------------- 1 | # %% Events 2 | class LoginEvent: 3 | def __init__(self, login): 4 | self.login = login 5 | 6 | def notify_loaded(self): 7 | print('LoginEvent loaded') 8 | 9 | 10 | class AccessEvent: 11 | def __init__(self, login, uri): 12 | self.login = login 13 | self.uri = uri 14 | 15 | def notify_loaded(self): 16 | print(f'AccessEvent loaded (uri={self.uri!r})') 17 | 18 | 19 | # Add an ability to load events from JSON data (str) 20 | # Use a Mixin class 21 | # Make sure that Events have notify_loaded method (ABC) 22 | 23 | 24 | # %% Test 25 | # LoginEvent 26 | login_data = '{"login": "elliot"}' 27 | # AccessEvent 28 | access_data = ''' 29 | { 30 | "login": "elliot", 31 | "uri": "file:///etc/passwd" 32 | } 33 | ''' 34 | -------------------------------------------------------------------------------- /Ch02/02_03/logger.py: -------------------------------------------------------------------------------- 1 | # %% Logger 2 | import logging 3 | 4 | 5 | logging.basicConfig( 6 | level=logging.INFO, 7 | format='%(levelname)s: %(message)s', 8 | ) 9 | 10 | class LoggerMixin: 11 | def log_id(self): 12 | logging.info('%s with id %r', self.name, self.id) 13 | 14 | 15 | # %% User 16 | class User: 17 | def __init__(self, name, id): 18 | self.name = name 19 | self.id = id 20 | 21 | # %% VM 22 | class VM: 23 | def __init__(self, name, id): 24 | self.name = name 25 | self.id = id 26 | 27 | # %% Mixing 28 | class LoggedUser(LoggerMixin, User): 29 | pass 30 | 31 | class LoggedVM(LoggerMixin, VM): 32 | pass 33 | 34 | 35 | # %% Test 36 | user = LoggedUser('root', 1) 37 | user.log_id() 38 | 39 | vm = LoggedVM('m1', '4922a77') 40 | vm.log_id() 41 | -------------------------------------------------------------------------------- /Ch03/03_03/traces.py: -------------------------------------------------------------------------------- 1 | # %% Traces 2 | class TraceIDs(dict): 3 | def __init__(self, *args, **kw): 4 | super().__init__(*args, **kw) 5 | self._counter = 1 6 | 7 | def __missing__(self, key): 8 | val = self._counter 9 | self._counter += 1 10 | self[key] = val 11 | return val 12 | 13 | 14 | # %% Test 15 | trace_ids = TraceIDs() 16 | print('calls ID:', trace_ids['http.calls']) 17 | print('calls ID:', trace_ids['http.calls']) 18 | print('errors ID:', trace_ids['http.errors']) 19 | 20 | 21 | # %% 22 | from collections import defaultdict 23 | from itertools import count 24 | 25 | trace_ids = defaultdict(count(1).__next__) 26 | print('calls ID:', trace_ids['http.calls']) 27 | print('calls ID:', trace_ids['http.calls']) 28 | print('errors ID:', trace_ids['http.errors']) 29 | -------------------------------------------------------------------------------- /Ch02/02_01/user.py: -------------------------------------------------------------------------------- 1 | # %% Auth 2 | class Auth: 3 | def __init__(self, db): 4 | self.db = db 5 | 6 | def from_token(self, token): 7 | return self.db.get(token) 8 | 9 | auth = Auth({ 10 | 'b92d877': 'carly', 11 | '18317ac': 'elliot', 12 | }) 13 | 14 | 15 | # %% User 16 | class User: 17 | def __init__(self, login): 18 | self.login = login 19 | # TODO: More fields 20 | 21 | @classmethod 22 | def from_token(cls, token): 23 | login = auth.from_token(token) 24 | return cls(login) 25 | 26 | # %% Admin 27 | class Admin(User): 28 | ... # TODO 29 | 30 | 31 | # %% Test 32 | u = User('carly') 33 | print(u.login, type(u)) 34 | 35 | u = User.from_token('b92d877') 36 | print(u.login, type(u)) 37 | 38 | a = Admin.from_token('18317ac') 39 | print(a.login, type(a)) 40 | -------------------------------------------------------------------------------- /Ch03/03_04/duration.py: -------------------------------------------------------------------------------- 1 | # %% Duration 2 | 3 | class Duration: 4 | unit_values = { 5 | 'ns': 1, 6 | 'us': 1000, 7 | 'ms': 1_000_000, 8 | } 9 | 10 | def __init__(self, value: float, unit: str): 11 | if value < 0 or unit not in Duration.unit_values: 12 | raise ValueError(f'invalid duration: {value=}, {unit=}') 13 | 14 | self.value = value 15 | self.unit = unit 16 | 17 | def __repr__(self): 18 | return f'{self.value}{self.unit}' 19 | 20 | def __add__(self, other): 21 | v1 = self.value * Duration.unit_values[self.unit] 22 | v2 = other.value * Duration.unit_values[other.unit] 23 | value = (v1 + v2) / Duration.unit_values[self.unit] 24 | return Duration(value, self.unit) 25 | 26 | 27 | # %% Test 28 | u1 = Duration(317, 'us') 29 | u2 = Duration(2.7, 'ms') 30 | print(u1 + u2) 31 | -------------------------------------------------------------------------------- /Ch03/03_03/headers.py: -------------------------------------------------------------------------------- 1 | # %% Headers 2 | from collections.abc import Mapping 3 | 4 | class Headers(Mapping): 5 | def __init__(self, headers: dict): 6 | self._headers = { 7 | key.lower(): value 8 | for key, value in headers.items() 9 | } 10 | 11 | def __len__(self): 12 | return len(self._headers) 13 | 14 | def __getitem__(self, key): 15 | key = key.lower() 16 | return self._headers[key] 17 | 18 | def __iter__(self): 19 | return iter(self._headers) 20 | 21 | 22 | # %% Test 23 | headers = Headers({ 24 | 'Content-Type': 'application/json; charset=utf-8', 25 | 'Content-Length': '1366', 26 | 'Accept-Ranges': 'bytes', 27 | }) 28 | print(len(headers), 'headers') 29 | print('Content Type:', headers['content-type']) 30 | for key in headers: 31 | print('key:', key) 32 | for key, value in headers.items(): 33 | print(key, '->', value) 34 | -------------------------------------------------------------------------------- /Ch02/02_04/plugin.py: -------------------------------------------------------------------------------- 1 | # %% Plugins 2 | 3 | class LoggingPlugin: 4 | def notify(self, event): 5 | print(f'got {event}') 6 | 7 | def shutdown(self): 8 | print('logger shutting down') 9 | 10 | 11 | class SecurityPlugin: 12 | def notify(self, event): 13 | if event.action == 'login' and event.user == 'elliot': 14 | print(f'WARNING: {event.user} has logged in') 15 | 16 | def shutdwon(self): 17 | print('security shutting down') 18 | 19 | 20 | def notify(plugins, event): 21 | for plugin in plugins: 22 | plugin.notify(event) 23 | 24 | 25 | def shutdown(plugins): 26 | for plugin in plugins: 27 | plugin.shutdown() 28 | 29 | 30 | 31 | # %% Test 32 | class Event: 33 | def __init__(self, user, action): 34 | self.user = user 35 | self.action = action 36 | 37 | plugins = [LoggingPlugin(), SecurityPlugin()] 38 | event = Event('elliot', 'login') 39 | notify(plugins, event) 40 | shutdown(plugins) 41 | -------------------------------------------------------------------------------- /Ch03/03_01/currency.py: -------------------------------------------------------------------------------- 1 | # %% Payment 2 | import re 3 | 4 | 5 | class Payment: 6 | def __init__(self, amount, currency): 7 | self.amount = amount 8 | self.currency = currency 9 | 10 | def __str__(self): 11 | return f'{self.currency}{self.amount:.2f}' 12 | 13 | def __repr__(self): 14 | name = self.__class__.__name__ # Support inheritance. 15 | return f'{name}({self.amount!r}, {self.currency!r})' 16 | 17 | def _replace(self, match): 18 | if match[1] == 'a': 19 | return f'{self.amount:.2f}' 20 | if match[1] == 'c': 21 | return self.currency 22 | raise ValueError(f'unknown format: {match.group()}') 23 | 24 | def __format__(self, spec): 25 | if not spec: 26 | return str(self) 27 | return re.sub(r'(? 10: 18 | raise ValueError(f'{name} has too many methods ({count})') 19 | print(f'[checker] {name} with {count} methods') 20 | return type.__init__(cls, name, bases, mapping) 21 | 22 | 23 | # %% Checker 24 | class Checker(metaclass=CheckerMeta): 25 | pass 26 | 27 | # %% Robot 28 | class Robot(Checker): 29 | manufacture = 'BnL' 30 | 31 | def move(self, x, y): 32 | print(f'{self} moving to {x}/{y}') 33 | 34 | 35 | walle = Robot() 36 | walle.move(100, 200) 37 | -------------------------------------------------------------------------------- /Ch05/solution/vm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use a dataclass, write a VM class that has the following fields: 3 | - id: str, no default 4 | - cpus: int, default to 2 5 | - memory: int, default to 512 (in MB) 6 | - state: one of 'starting', 'running', 'stopped' 7 | - tags: list of str, default to empty 8 | 9 | After the VM is created, check that 10 | - id is not empty 11 | - cpus >= 1 12 | - memory >= 256 13 | """ 14 | # %% VM 15 | from dataclasses import dataclass, field 16 | 17 | @dataclass 18 | class VM: 19 | id: str 20 | cpus: int = 2 21 | memory: int = 512 # MB 22 | state: str = 'starting' 23 | tags: list[str] = field(default_factory=list) 24 | 25 | def __post_init__(self): 26 | if not self.id: 27 | raise ValueError('empty ID') 28 | if self.cpus < 1: 29 | raise ValueError(f'cpus ({self.cpus}) < 1') 30 | if self.memory < 256: 31 | raise ValueError(f'memory ({self.memory}) < 256') 32 | 33 | # %% test 34 | vm = VM( 35 | id='i-0af01c0123456789a', 36 | cpus=4, 37 | memory=2048, 38 | tags=['db', 'env:qa'] 39 | ) 40 | print(vm) 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Issue Overview 9 | 10 | 11 | ## Describe your environment 12 | 13 | 14 | ## Steps to Reproduce 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 4. 20 | 21 | ## Expected Behavior 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | ## Possible Solution 28 | 29 | 30 | ## Screenshots / Video 31 | 32 | 33 | ## Related Issues 34 | 35 | -------------------------------------------------------------------------------- /Ch03/03_02/stack.py: -------------------------------------------------------------------------------- 1 | # %% Stack 2 | from collections.abc import Sequence 3 | 4 | class Node: 5 | def __init__(self, value, next): 6 | self.value = value 7 | self.next = next 8 | 9 | 10 | class Stack(Sequence): 11 | def __init__(self): 12 | self._head = None 13 | 14 | def push(self, value): 15 | self._head = Node(value, self._head) 16 | 17 | def pop(self): 18 | if self._head is None: 19 | raise ValueError('pop from empty stack') 20 | 21 | value = self._head.value 22 | self._head = self._head.next 23 | return value 24 | 25 | def __len__(self): 26 | count = 0 27 | node = self._head 28 | while node: 29 | count += 1 30 | node = node.next 31 | return count 32 | 33 | def __getitem__(self, index): 34 | node = self._head 35 | while index > 0 and node: 36 | index -= 1 37 | node = node.next 38 | if not node: 39 | raise IndexError(index) 40 | return node.value 41 | 42 | 43 | 44 | # %% Test 45 | s = Stack() 46 | for c in 'Python': 47 | s.push(c) 48 | print('len:', len(s)) 49 | print('s[2]:', s[2]) 50 | print('t' in s) 51 | -------------------------------------------------------------------------------- /Ch02/02_04/plugin_abc.py: -------------------------------------------------------------------------------- 1 | # %% Plugin 2 | from abc import ABC, abstractmethod 3 | 4 | 5 | class Plugin(ABC): 6 | @abstractmethod 7 | def notify(self, event): 8 | pass 9 | 10 | @abstractmethod 11 | def shutdown(self): 12 | pass 13 | 14 | # %% Plugins 15 | 16 | class LoggingPlugin(Plugin): 17 | def notify(self, event): 18 | print(f'got {event}') 19 | 20 | def shutdown(self): 21 | print('logger shutting down') 22 | 23 | 24 | class SecurityPlugin(Plugin): 25 | def notify(self, event): 26 | if event.action == 'login' and event.user == 'elliot': 27 | print(f'WARNING: {event.user} has logged in') 28 | 29 | def shutdwon(self): 30 | print('security shutting down') 31 | 32 | 33 | def notify(plugins, event): 34 | for plugin in plugins: 35 | plugin.notify(event) 36 | 37 | 38 | def shutdown(plugins): 39 | for plugin in plugins: 40 | plugin.shutdown() 41 | 42 | 43 | 44 | # %% Test 45 | class Event: 46 | def __init__(self, user, action): 47 | self.user = user 48 | self.action = action 49 | 50 | plugins = [LoggingPlugin(), SecurityPlugin()] 51 | event = Event('elliot', 'login') 52 | notify(plugins, event) 53 | shutdown(plugins) 54 | -------------------------------------------------------------------------------- /Ch01/01_01/attr.py: -------------------------------------------------------------------------------- 1 | # %% Emulate build-in "getattr" 2 | def find_attribute(obj, attr): 3 | if attr in obj.__dict__: 4 | print(f'found {attr} in instance') 5 | return obj.__dict__[attr] 6 | 7 | if attr in obj.__class__.__dict__: 8 | print(f'found {attr} in class') 9 | return obj.__class__.__dict__[attr] 10 | 11 | for cls in obj.__class__.__mro__: 12 | if attr not in cls.__dict__: 13 | continue 14 | print(f'found {attr} in parent {cls.__name__!r}') 15 | return cls.__dict__[attr] 16 | 17 | # TODO: __getattr__, descriptors ... 18 | 19 | raise AttributeError(attr) 20 | 21 | 22 | # %% VM 23 | class VM: 24 | version = '1.2.3' # Class attribute. 25 | 26 | 27 | class A1(VM): 28 | cpu_family = 'arm64' 29 | 30 | def __init__(self, id): 31 | self.id = id # Instance attribute. 32 | self.state = 'running' 33 | 34 | def shutdown(self): 35 | # TODO 36 | self.state = 'stopped' 37 | 38 | 39 | a1 = A1('9e99929') 40 | 41 | #%% a1.id 42 | print(find_attribute(a1, 'id')) 43 | 44 | #%% a1.cpu_family 45 | print(find_attribute(a1, 'cpu_family')) 46 | 47 | #%% a1.version 48 | print(find_attribute(a1, 'version')) 49 | 50 | #%% a1.nic 51 | print(find_attribute(a1, 'nic')) -------------------------------------------------------------------------------- /Ch04/04_03/robot.py: -------------------------------------------------------------------------------- 1 | # %% 2 | from datetime import datetime 3 | 4 | 5 | class CheckerMeta(type): 6 | """ 7 | If you have a procedure with 10 parameters, you probably missed some. 8 | - Alan J. Perlis 9 | """ 10 | def __new__(mclass, name, bases, mapping): 11 | print(f'[checker] Creating class {name} with {bases}') 12 | mapping['created'] = datetime.now() 13 | return type.__new__(mclass, name, bases, mapping) 14 | 15 | def __init__(cls, name, bases, mapping): 16 | count = sum(1 for v in mapping.values() if callable(v)) 17 | if count > 10: 18 | raise ValueError(f'{name} has too many methods ({count})') 19 | print(f'[checker] {name} with {count} methods') 20 | return type.__init__(cls, name, bases, mapping) 21 | 22 | def __call__(cls, *args, **kw): 23 | print(f'[checker] instance of {cls.__name__} created') 24 | if (count := len(args) + len(kw) > 10): 25 | name = cls.__name__ 26 | raise ValueError(f'{name} instance with too many arguments ({count})') 27 | return type.__call__(cls, *args, **kw) 28 | 29 | 30 | class Checker(metaclass=CheckerMeta): 31 | pass 32 | 33 | 34 | class Robot(Checker): 35 | manufacture = 'BnL' 36 | 37 | def move(self, x, y): 38 | print(f'{self} moving to {x}/{y}') 39 | 40 | 41 | walle = Robot() 42 | walle.move(100, 200) 43 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "context": "..", 6 | "args": { 7 | "VARIANT": "3.11", // Set Python version here 8 | "NODE_VERSION": "lts/*" 9 | } 10 | }, 11 | "settings": { 12 | "python.defaultInterpreterPath": "/usr/local/bin/python", 13 | "python.linting.enabled": true, 14 | "python.linting.pylintEnabled": false, 15 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 16 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 17 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 18 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 19 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 20 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 21 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", 22 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 23 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", 24 | "python.linting.pylintArgs": ["--disable=C0111"] 25 | }, 26 | "extensions": [ 27 | "ms-python.python", 28 | "ms-python.vscode-pylance", 29 | "ms-toolsai.jupyter" 30 | ], 31 | "remoteUser": "vscode", 32 | "onCreateCommand": "echo PS1='\"$ \"' >> ~/.bashrc", //Set Terminal Prompt to $ 33 | "postCreateCommand": "sh .devcontainer/startup.sh" 34 | } 35 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster 4 | ARG VARIANT="3.11" 5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 6 | 7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 8 | ARG NODE_VERSION="none" 9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 10 | 11 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 12 | # COPY requirements.txt /tmp/pip-tmp/ 13 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 14 | # && rm -rf /tmp/pip-tmp 15 | 16 | # [Optional] Uncomment this section to install additional OS packages. 17 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 18 | # && apt-get -y install --no-install-recommends 19 | 20 | # [Optional] Uncomment this line to install global node packages. 21 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 22 | -------------------------------------------------------------------------------- /Ch02/solution/ingest.py: -------------------------------------------------------------------------------- 1 | # %% Events 2 | class LoginEvent: 3 | def __init__(self, login): 4 | self.login = login 5 | 6 | def notify_loaded(self): 7 | print('LoginEvent loaded') 8 | 9 | 10 | class AccessEvent: 11 | def __init__(self, login, uri): 12 | self.login = login 13 | self.uri = uri 14 | 15 | def notify_loaded(self): 16 | print(f'AccessEvent loaded (uri={self.uri!r})') 17 | 18 | 19 | # Add an ability to load events from JSON data (str) 20 | # Use a Mixin class 21 | # Make sure that Events have notify_loaded method (ABC) 22 | 23 | 24 | # %% Serialized 25 | from abc import ABC, abstractmethod 26 | import json 27 | 28 | class Serialized(ABC): 29 | @classmethod 30 | def from_json(cls, data): 31 | params = json.loads(data) 32 | inst = cls(**params) 33 | inst.notify_loaded() 34 | return inst 35 | 36 | @abstractmethod 37 | def notify_loaded(self): 38 | pass 39 | 40 | 41 | # %% MixIns 42 | class SerializeLogin(LoginEvent, Serialized): 43 | pass 44 | 45 | class SerializedAccess(AccessEvent, Serialized): 46 | pass 47 | 48 | 49 | # %% Test 50 | # LoginEvent 51 | login_data = '{"login": "elliot"}' 52 | # AccessEvent 53 | access_data = ''' 54 | { 55 | "login": "elliot", 56 | "uri": "file:///etc/passwd" 57 | } 58 | ''' 59 | 60 | event = SerializeLogin.from_json(login_data) 61 | event = SerializedAccess.from_json(access_data) 62 | -------------------------------------------------------------------------------- /Ch01/01_06/validators.py: -------------------------------------------------------------------------------- 1 | # %% Validator 2 | class Validator: 3 | attr_name = '_validators' 4 | 5 | def __set_name__(self, cls, name): 6 | print(f'__set_name__: {name=}') 7 | validator_name = self.__class__.__name__ 8 | self._key = f'{validator_name}_{name}' 9 | 10 | def __get__(self, inst, cls): 11 | print(f'__get__: {inst=}, {cls=}') 12 | if inst is None: # Access via class. 13 | return self 14 | 15 | values = getattr(inst, Validator.attr_name, {}) 16 | return values.get(self._key) 17 | 18 | def __set__(self, inst, value): 19 | print(f'__set__: {inst=}, {value=}') 20 | self.validate(value) 21 | values = getattr(inst, Validator.attr_name, None) 22 | if values is None: 23 | values = {} 24 | setattr(inst, Validator.attr_name, values) 25 | values[self._key] = value 26 | 27 | def validate(self, value): 28 | raise NotImplementedError() 29 | 30 | 31 | # %% Price 32 | class Price(Validator): 33 | def validate(self, value): 34 | if value <= 0: 35 | raise ValueError(f'negtaive price: {value!r}') 36 | 37 | # %% Trade 38 | class Trade: 39 | open = Price() 40 | close = Price() 41 | 42 | def __init__(self, open, close): 43 | self.open = open 44 | self.close = close 45 | 46 | 47 | t1 = Trade(133.2, 147.5) 48 | print(t1.__dict__) 49 | 50 | # %% Getting 51 | print(Trade.open) # Class level. 52 | print(t1.open) # Instance level. 53 | 54 | # %% Setting 55 | t1.close = 147.2 56 | 57 | # %% Error 58 | 59 | t1.close = -2 -------------------------------------------------------------------------------- /Ch05/05_01/serialize.py: -------------------------------------------------------------------------------- 1 | # %% serializer 2 | serializers = {} # media_type -> class 3 | 4 | 5 | class serializer: 6 | def __init__(self, media_type): 7 | self.media_type = media_type 8 | 9 | def __call__(self, cls): 10 | if (other := serializers.get(self.media_type)): 11 | name = other.__name__ 12 | msg = f'{self.media_type} already registered to {name}' 13 | raise ValueError(msg) 14 | 15 | dump = getattr(cls, 'dump', None) 16 | if not callable(dump): 17 | name = cls.__name__ 18 | raise ValueError(f'{name} does not have a "dump" method') 19 | 20 | serializers[self.media_type] = cls 21 | return cls 22 | 23 | def serialize(out, media_type, objects): 24 | cls = serializers.get(media_type) 25 | if cls is None: 26 | raise ValueError('unknown media type: {media_type!r}') 27 | serializer = cls(out) 28 | for obj in objects: 29 | serializer.dump(obj) 30 | 31 | 32 | # %% JSON 33 | import json 34 | 35 | 36 | @serializer('application/json') 37 | class JSONSerializer: 38 | def __init__(self, out): 39 | self.out = out 40 | 41 | def dump(self, obj): 42 | json.dump(obj, self.out) 43 | self.out.write('\n') 44 | 45 | # %% Test 46 | import sys 47 | 48 | events = [ 49 | { 50 | 'login': 'elliot', 51 | 'action': 'logout', 52 | }, 53 | { 54 | 'login': 'elliot', 55 | 'action': 'access', 56 | 'uri': 'file:///etc/passwd', 57 | }, 58 | ] 59 | serialize(sys.stdout, 'application/json', events) 60 | -------------------------------------------------------------------------------- /Ch04/04_04/robot.py: -------------------------------------------------------------------------------- 1 | # %% 2 | from datetime import datetime 3 | 4 | 5 | class CheckerMeta(type): 6 | """ 7 | If you have a procedure with 10 parameters, you probably missed some. 8 | - Alan J. Perlis 9 | """ 10 | def __new__(mclass, name, bases, mapping): 11 | print(f'[checker] Creating class {name} with {bases}') 12 | mapping['created'] = datetime.now() 13 | return type.__new__(mclass, name, bases, mapping) 14 | 15 | def __init__(cls, name, bases, mapping): 16 | count = sum(1 for v in mapping.values() if callable(v)) 17 | if count > 10: 18 | raise ValueError(f'{name} has too many methods ({count})') 19 | print(f'[checker] {name} with {count} methods') 20 | return type.__init__(cls, name, bases, mapping) 21 | 22 | def __call__(cls, *args, **kw): 23 | print(f'[checker] instance of {cls.__name__} created') 24 | if (count := len(args) + len(kw) > 10): 25 | name = cls.__name__ 26 | raise ValueError(f'{name} instance with too many arguments ({count})') 27 | return type.__call__(cls, *args, **kw) 28 | 29 | def __setattr__(cls, attr, value): 30 | old = getattr(cls, attr, None) 31 | name = cls.__name__ 32 | print(f'[checker] {name}:{attr} {old!r} -> {value!r}') 33 | if value is None: 34 | raise ValueError(f'{name} sets {attr} to None') 35 | type.__setattr__(cls, attr, value) 36 | 37 | 38 | class Checker(metaclass=CheckerMeta): 39 | pass 40 | 41 | 42 | class Robot(Checker): 43 | manufacture = 'BnL' 44 | 45 | def move(self, x, y): 46 | print(f'{self} moving to {x}/{y}') 47 | 48 | 49 | walle = Robot() 50 | walle.move(100, 200) 51 | Robot.manufacture = 'Boston Dynamics' 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Python: Object-Oriented Programming 2 | This is the repository for the LinkedIn Learning course `Advanced Python: Object-Oriented Programming`. The full course is available from [LinkedIn Learning][lil-course-url]. 3 | 4 | ![lil-thumbnail-url] 5 | 6 | In this advanced course, Miki Tebeka, the CEO at 353Solutions, guides you through object-oriented programming features in Python. Learn about attribute access and how to use properties, name mangling, descriptors, and more. Explore using class methods, static methods, mixin classes, and abstract base classes. Dive into sequences, mappings, numbers, and callable types. Find out what metaclasses are and how you can use them. Plus, go over class creation utilities that you’ll need to use. Each chapter comes with a hands-on challenge to practice and reinforce what you learn.

7 | 8 |

The best way to learn a language is to use it in practice. That’s why this course is integrated with GitHub Codespaces, an instant cloud developer environment that offers all the functionality of your favorite IDE without the need for any local machine setup. With GitHub Codespaces, you can get hands-on practice from any machine, at any time—all while using a tool that you’ll likely encounter in the workplace. Check out the “Using GitHub Codespaces with this course” video to learn how to get started. 9 | 10 | ### Instructor 11 | 12 | Miki Tebeka 13 | 14 | CEO at 353Solutions 15 | 16 | 17 | Check out my other courses on [LinkedIn Learning](https://www.linkedin.com/learning/instructors/miki-tebeka?u=104). 18 | 19 | [0]: # (Replace these placeholder URLs with actual course URLs) 20 | 21 | [lil-course-url]: https://www.linkedin.com/learning/advanced-python-object-oriented-programming 22 | [lil-thumbnail-url]: https://media.licdn.com/dms/image/D560DAQGj3Mv2DFnQJA/learning-public-crop_675_1200/0/1710976165627?e=2147483647&v=beta&t=dUWSvC-unURI4wEjOIUB7v8zet7idWRUteDo6druIY0 23 | 24 | -------------------------------------------------------------------------------- /Ch03/challenge/kv.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import sqlite3 3 | from collections.abc import MutableMapping 4 | 5 | # %% Create 6 | schema_sql = ''' 7 | CREATE TABLE IF NOT EXISTS kv ( 8 | key TEXT PRIMARY KEY, 9 | value TEXT 10 | ); 11 | ''' 12 | 13 | conn = sqlite3.connect(':memory:') 14 | conn.executescript(schema_sql) 15 | 16 | # %% Set 17 | set_sql = ''' 18 | INSERT INTO kv 19 | (key, value) 20 | VALUES 21 | (:key, :value) 22 | ON CONFLICT (key) DO 23 | UPDATE SET key=:key 24 | ; 25 | ''' 26 | 27 | entry = {'key': 'fish', 'value': 'water'} 28 | with conn: 29 | conn.execute(set_sql, entry) 30 | 31 | # %% Get 32 | get_sql = ''' 33 | SELECT value 34 | FROM kv 35 | WHERE key = :key 36 | ''' 37 | 38 | cur = conn.execute(get_sql, {'key': 'fish'}) 39 | print(cur.fetchone()) 40 | 41 | # %% Delete 42 | del_sql = ''' 43 | DELETE FROM kv 44 | WHERE key = :key 45 | ''' 46 | 47 | with conn: 48 | conn.execute(del_sql, {'key': 'fish'}) 49 | cur = conn.execute(get_sql, {'key': 'fish'}) 50 | print(cur.fetchone()) 51 | 52 | # %% Keys 53 | keys_sql = ''' 54 | SELECT key FROM kv 55 | ''' 56 | 57 | with conn: 58 | entry = {'key': 'fish', 'value': 'water'} 59 | conn.execute(set_sql, entry) 60 | entry = {'key': 'horse', 'value': 'land'} 61 | conn.execute(set_sql, entry) 62 | for row in conn.execute(keys_sql): 63 | print('key:', row[0]) 64 | 65 | # %% Len 66 | len_sql = ''' 67 | SELECT COUNT(key) FROM kv 68 | ''' 69 | 70 | cur = conn.execute(len_sql) 71 | row = cur.fetchone() 72 | print('len:', row[0]) 73 | 74 | # %% DB 75 | class DB(MutableMapping): 76 | """sqlite3 backed mapping""" 77 | def __init__(self, db_file): 78 | # TODO 79 | ... 80 | 81 | # %% Test 82 | db = DB('/tmp/animals.db') 83 | db['fish'] = 'water' 84 | db['horse'] = 'land' 85 | print(' len:', len(db)) 86 | print(' fish:', db['fish']) 87 | print(' keys:') 88 | for key in db: 89 | print(f'- {key}') 90 | print(' len:', len(db)) 91 | del db['fish'] 92 | print(' len (delete):', len(db)) 93 | -------------------------------------------------------------------------------- /Ch03/solution/kv.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import sqlite3 3 | from collections.abc import MutableMapping 4 | 5 | # %% Create 6 | schema_sql = ''' 7 | CREATE TABLE IF NOT EXISTS kv ( 8 | key TEXT PRIMARY KEY, 9 | value TEXT 10 | ); 11 | ''' 12 | 13 | conn = sqlite3.connect(':memory:') 14 | conn.executescript(schema_sql) 15 | 16 | # %% Set 17 | set_sql = ''' 18 | INSERT INTO kv 19 | (key, value) 20 | VALUES 21 | (:key, :value) 22 | ON CONFLICT (key) DO 23 | UPDATE SET key=:key 24 | ; 25 | ''' 26 | 27 | entry = {'key': 'fish', 'value': 'water'} 28 | conn.execute(set_sql, entry) 29 | 30 | # %% Get 31 | get_sql = ''' 32 | SELECT value 33 | FROM kv 34 | WHERE key = :key 35 | ''' 36 | 37 | cur = conn.execute(get_sql, {'key': 'fish'}) 38 | print(cur.fetchone()) 39 | 40 | # %% Delete 41 | del_sql = ''' 42 | DELETE FROM kv 43 | WHERE key = :key 44 | ''' 45 | 46 | conn.execute(del_sql, {'key': 'fish'}) 47 | cur = conn.execute(get_sql, {'key': 'fish'}) 48 | print(cur.fetchone()) 49 | 50 | # %% Keys 51 | keys_sql = ''' 52 | SELECT key FROM kv 53 | ''' 54 | 55 | entry = {'key': 'fish', 'value': 'water'} 56 | conn.execute(set_sql, entry) 57 | entry = {'key': 'horse', 'value': 'land'} 58 | conn.execute(set_sql, entry) 59 | for row in conn.execute(keys_sql): 60 | print('key:', row[0]) 61 | 62 | # %% Len 63 | len_sql = ''' 64 | SELECT COUNT(key) FROM kv 65 | ''' 66 | 67 | cur = conn.execute(len_sql) 68 | row = cur.fetchone() 69 | print('len:', row[0]) 70 | 71 | # %% DB 72 | class DB(MutableMapping): 73 | """sqlite3 backed mapping""" 74 | def __init__(self, db_file): 75 | self.conn = sqlite3.connect(db_file) 76 | self.conn.executescript(schema_sql) 77 | 78 | def __getitem__(self, key): 79 | cur = self.conn.execute(get_sql, {'key': key}) 80 | row = cur.fetchone() 81 | if row is None: 82 | raise KeyError(key) 83 | return row[0] 84 | 85 | def __setitem__(self, key, value): 86 | with self.conn: 87 | entry = {'key': key, 'value': value} 88 | self.conn.execute(set_sql, entry) 89 | 90 | def __delitem__(self, key): 91 | with self.conn: 92 | self.conn.execute(del_sql, {'key': key}) 93 | 94 | def __iter__(self): 95 | for row in self.conn.execute(keys_sql): 96 | yield row[0] 97 | 98 | def __len__(self): 99 | return self.conn.execute(len_sql).fetchone()[0] 100 | 101 | 102 | # %% Test 103 | db = DB('/tmp/animals.db') 104 | db['fish'] = 'water' 105 | db['horse'] = 'land' 106 | print(' len:', len(db)) 107 | print(' fish:', db['fish']) 108 | print(' keys:') 109 | for key in db: 110 | print(f'- {key}') 111 | print(' len:', len(db)) 112 | del db['fish'] 113 | print(' len (delete):', len(db)) 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LinkedIn Learning Exercise Files License Agreement 2 | ================================================== 3 | 4 | This License Agreement (the "Agreement") is a binding legal agreement 5 | between you (as an individual or entity, as applicable) and LinkedIn 6 | Corporation (“LinkedIn”). By downloading or using the LinkedIn Learning 7 | exercise files in this repository (“Licensed Materials”), you agree to 8 | be bound by the terms of this Agreement. If you do not agree to these 9 | terms, do not download or use the Licensed Materials. 10 | 11 | 1. License. 12 | - a. Subject to the terms of this Agreement, LinkedIn hereby grants LinkedIn 13 | members during their LinkedIn Learning subscription a non-exclusive, 14 | non-transferable copyright license, for internal use only, to 1) make a 15 | reasonable number of copies of the Licensed Materials, and 2) make 16 | derivative works of the Licensed Materials for the sole purpose of 17 | practicing skills taught in LinkedIn Learning courses. 18 | - b. Distribution. Unless otherwise noted in the Licensed Materials, subject 19 | to the terms of this Agreement, LinkedIn hereby grants LinkedIn members 20 | with a LinkedIn Learning subscription a non-exclusive, non-transferable 21 | copyright license to distribute the Licensed Materials, except the 22 | Licensed Materials may not be included in any product or service (or 23 | otherwise used) to instruct or educate others. 24 | 25 | 2. Restrictions and Intellectual Property. 26 | - a. You may not to use, modify, copy, make derivative works of, publish, 27 | distribute, rent, lease, sell, sublicense, assign or otherwise transfer the 28 | Licensed Materials, except as expressly set forth above in Section 1. 29 | - b. Linkedin (and its licensors) retains its intellectual property rights 30 | in the Licensed Materials. Except as expressly set forth in Section 1, 31 | LinkedIn grants no licenses. 32 | - c. You indemnify LinkedIn and its licensors and affiliates for i) any 33 | alleged infringement or misappropriation of any intellectual property rights 34 | of any third party based on modifications you make to the Licensed Materials, 35 | ii) any claims arising from your use or distribution of all or part of the 36 | Licensed Materials and iii) a breach of this Agreement. You will defend, hold 37 | harmless, and indemnify LinkedIn and its affiliates (and our and their 38 | respective employees, shareholders, and directors) from any claim or action 39 | brought by a third party, including all damages, liabilities, costs and 40 | expenses, including reasonable attorneys’ fees, to the extent resulting from, 41 | alleged to have resulted from, or in connection with: (a) your breach of your 42 | obligations herein; or (b) your use or distribution of any Licensed Materials. 43 | 44 | 3. Open source. This code may include open source software, which may be 45 | subject to other license terms as provided in the files. 46 | 47 | 4. Warranty Disclaimer. LINKEDIN PROVIDES THE LICENSED MATERIALS ON AN “AS IS” 48 | AND “AS AVAILABLE” BASIS. LINKEDIN MAKES NO REPRESENTATION OR WARRANTY, 49 | WHETHER EXPRESS OR IMPLIED, ABOUT THE LICENSED MATERIALS, INCLUDING ANY 50 | REPRESENTATION THAT THE LICENSED MATERIALS WILL BE FREE OF ERRORS, BUGS OR 51 | INTERRUPTIONS, OR THAT THE LICENSED MATERIALS ARE ACCURATE, COMPLETE OR 52 | OTHERWISE VALID. TO THE FULLEST EXTENT PERMITTED BY LAW, LINKEDIN AND ITS 53 | AFFILIATES DISCLAIM ANY IMPLIED OR STATUTORY WARRANTY OR CONDITION, INCLUDING 54 | ANY IMPLIED WARRANTY OR CONDITION OF MERCHANTABILITY OR FITNESS FOR A 55 | PARTICULAR PURPOSE, AVAILABILITY, SECURITY, TITLE AND/OR NON-INFRINGEMENT. 56 | YOUR USE OF THE LICENSED MATERIALS IS AT YOUR OWN DISCRETION AND RISK, AND 57 | YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE THAT RESULTS FROM USE OF THE 58 | LICENSED MATERIALS TO YOUR COMPUTER SYSTEM OR LOSS OF DATA. NO ADVICE OR 59 | INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM US OR THROUGH OR 60 | FROM THE LICENSED MATERIALS WILL CREATE ANY WARRANTY OR CONDITION NOT 61 | EXPRESSLY STATED IN THESE TERMS. 62 | 63 | 5. Limitation of Liability. LINKEDIN SHALL NOT BE LIABLE FOR ANY INDIRECT, 64 | INCIDENTAL, SPECIAL, PUNITIVE, CONSEQUENTIAL OR EXEMPLARY DAMAGES, INCLUDING 65 | BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER 66 | INTANGIBLE LOSSES . IN NO EVENT WILL LINKEDIN'S AGGREGATE LIABILITY TO YOU 67 | EXCEED $100. THIS LIMITATION OF LIABILITY SHALL: 68 | - i. APPLY REGARDLESS OF WHETHER (A) YOU BASE YOUR CLAIM ON CONTRACT, TORT, 69 | STATUTE, OR ANY OTHER LEGAL THEORY, (B) WE KNEW OR SHOULD HAVE KNOWN ABOUT 70 | THE POSSIBILITY OF SUCH DAMAGES, OR (C) THE LIMITED REMEDIES PROVIDED IN THIS 71 | SECTION FAIL OF THEIR ESSENTIAL PURPOSE; AND 72 | - ii. NOT APPLY TO ANY DAMAGE THAT LINKEDIN MAY CAUSE YOU INTENTIONALLY OR 73 | KNOWINGLY IN VIOLATION OF THESE TERMS OR APPLICABLE LAW, OR AS OTHERWISE 74 | MANDATED BY APPLICABLE LAW THAT CANNOT BE DISCLAIMED IN THESE TERMS. 75 | 76 | 6. Termination. This Agreement automatically terminates upon your breach of 77 | this Agreement or termination of your LinkedIn Learning subscription. On 78 | termination, all licenses granted under this Agreement will terminate 79 | immediately and you will delete the Licensed Materials. Sections 2-7 of this 80 | Agreement survive any termination of this Agreement. LinkedIn may discontinue 81 | the availability of some or all of the Licensed Materials at any time for any 82 | reason. 83 | 84 | 7. Miscellaneous. This Agreement will be governed by and construed in 85 | accordance with the laws of the State of California without regard to conflict 86 | of laws principles. The exclusive forum for any disputes arising out of or 87 | relating to this Agreement shall be an appropriate federal or state court 88 | sitting in the County of Santa Clara, State of California. If LinkedIn does 89 | not act to enforce a breach of this Agreement, that does not mean that 90 | LinkedIn has waived its right to enforce this Agreement. The Agreement does 91 | not create a partnership, agency relationship, or joint venture between the 92 | parties. Neither party has the power or authority to bind the other or to 93 | create any obligation or responsibility on behalf of the other. You may not, 94 | without LinkedIn’s prior written consent, assign or delegate any rights or 95 | obligations under these terms, including in connection with a change of 96 | control. Any purported assignment and delegation shall be ineffective. The 97 | Agreement shall bind and inure to the benefit of the parties, their respective 98 | successors and permitted assigns. If any provision of the Agreement is 99 | unenforceable, that provision will be modified to render it enforceable to the 100 | extent possible to give effect to the parties’ intentions and the remaining 101 | provisions will not be affected. This Agreement is the only agreement between 102 | you and LinkedIn regarding the Licensed Materials, and supersedes all prior 103 | agreements relating to the Licensed Materials. 104 | 105 | Last Updated: March 2019 106 | --------------------------------------------------------------------------------