diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 78c92c2..112afad 100755 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -41,6 +41,7 @@ RUN curl -k https://packages.microsoft.com/keys/microsoft.asc | apt-key add -\ mssql-tools\ unixodbc-dev\ libgssapi-krb5-2\ + python3-tk\ && apt-get clean\ && rm -rf /var/lib/apt/lists/* @@ -65,4 +66,6 @@ RUN yes Y | sh -c "$(curl -k -fsSL https://raw.githubusercontent.com/ohmyzsh/ohm COPY .devcontainer/config_devcontainer.sh /tmp COPY --chown=$USER:$USER . /home/$USER/python_learning WORKDIR /home/$USER/python_learning -USER $USER \ No newline at end of file +USER $USER + +ARG DEBIAN_FRONTEND=noninteractive \ No newline at end of file diff --git a/environment.yml b/environment.yml index 9ea6f4e..5ec5a37 100644 --- a/environment.yml +++ b/environment.yml @@ -1,16 +1,15 @@ -name: tensorflow-cuda +name: python-bootcamp channels: - defaults - conda-forge dependencies: - python=3.9 - pip - - cudnn=7.6 - - cudatoolkit=10.1 - nb_conda - jupyter - scikit-learn + - numpy - scipy - pandas - pandas-datareader @@ -19,10 +18,5 @@ dependencies: - requests - flask - pip: - - pymc3 - - bayesian-optimization - - gym - kaggle - - kats - - tensorflow==2.3.0 # - -r requirements/dev.txt \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7665a74..f9a3110 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license = "Propietary" keywords = ["cookiecutter"] -repository = "" +repository = "https://github.com/alvaroof/python_learning" readme = ["README.md", "LICENSE", "RELEASE_NOTES.md"] @@ -31,7 +31,8 @@ exclude = ["tests/*"] [tool.poetry.dependencies] click = "~8" loguru = "~0.6" -python = "~3.9" +python = "~3.12" +tk = "^0.1.0" [tool.poetry.group.dev.dependencies] black = {extras = ["jupyter"], version = "22.6.0"} @@ -50,7 +51,6 @@ pylint = "2.15.10" pytest = "7.2.0" pytest-runner = "6.0.0" pytest-xdist = "3.1.0" -pytest-testmon = "1.4.2" python-dotenv = "0.20.0" radon = "5.1.0" semver = "2.13.0" diff --git a/scripts/classes.py b/scripts/classes.py new file mode 100755 index 0000000..4137a98 --- /dev/null +++ b/scripts/classes.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +import random +import time +from enum import Enum + + +class Condition(Enum): + NEW = 0 + GOOD = 1 + OKAY = 2 + BAD = 3 + + +class MethodNowAllowed(Exception): + pass + + +class Bike: + num_wheels = 2 + counter = 0 + + def __init__( + self, + description: str, + condition: Condition = Condition.GOOD, + sale_price: float = 100, + cost: float = 0, + year: int = 2015, + ): + """_summary_ + + :param description: _description_, defaults to "Really Beautiful" + :type description: str, optional + :param condition: _description_, defaults to "Perfect Condition" + :type condition: str, optional + :param sale_price: _description_, defaults to 0 + :type sale_price: float, optional + :param cost: _description_, defaults to 0 + :type cost: float, optional + :param year: _description_, defaults to 2015 + :type year: int, optional + """ + self.description = description + self.condition = condition + self._sale_price = None # Private + self.cost = cost + self._slow_attribute = None + + self.age = 2023 - year + self.sold = False + self.premium = None + self.update_sale_price(sale_price) + Bike.counter += 1 + + def update_sale_price(self, sale_price: float): + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + if sale_price <= 0: + raise ValueError("Sale price must be greater than zero.") + self._sale_price = sale_price + + def is_premium(self): + self.premium = "Yes" + + def sell(self) -> float: + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + self.sold = True + profit = self.sale_price - self.cost + print(f"Sold for a profit: {profit}") + return profit + + def service(self, cost, new_sale_price=None, new_condition=None): + self.cost += cost + if new_sale_price is not None: + self.update_sale_price(new_sale_price) + if new_condition is not None: + self.condition = new_condition + + @property + def sale_price(self): + return self._sale_price + + @sale_price.setter + def sale_price(self, sale_price: float): + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + if sale_price <= 0: + raise ValueError("Sale price must be greater than zero.") + self._sale_price = sale_price + + @property + def profit(self): + return self.sale_price - self.cost + + @property # Used when an attribute is computationally intensive to calculate. Cache version is accessible + def slow_attribute(self): + if self._slow_attribute is not None: + print("Slow Attribute was already cached") + return self._slow_attribute + else: + print("Calculating Slow Attribute") + time.sleep(5) + self._slow_attribute = "Set" + return self._slow_attribute + + @staticmethod + def sing_the_bike_song(): + print("Singing the Bike Song") + + def __add__(self, other): + if isinstance(other, Bike): + self.cost += other.cost + + def __del__(self): + Bike.counter -= 1 + print("Bike deleted") + + def __str__(self): + """Called when str() or print()""" + return f"{self.description}: ${self.sale_price}" + + def __repr__(self): + return f"Bike({self.description}, {self.condition}, {self.sale_price}, {self.cost})" + + @staticmethod + def get_test_bike(): + return Bike(condition=Condition.GOOD, sale_price=1000, cost=0, description=Bike.__name__) + + @classmethod + def get_test_object(cls): + return cls( + condition=random.choice(list(Condition)), + sale_price=1000, + cost=0, + description=f"{cls.__name__}", + ) + + +class Unicycle(Bike): + num_wheels = 1 + + +if __name__ == "__main__": + my_bike = Bike( + description="Black and Beautiful", + condition=Condition.BAD, + sale_price=1000, + cost=100, + year=1965, + ) + test_unicycle = Unicycle.get_test_object() + test_bike = Bike.get_test_bike() + print(test_unicycle) + print(test_bike) + print([test_unicycle, test_bike]) + print(my_bike.slow_attribute) + print(my_bike.slow_attribute) diff --git a/scripts/exercism/robot_name.py b/scripts/exercism/robot_name.py new file mode 100644 index 0000000..035f843 --- /dev/null +++ b/scripts/exercism/robot_name.py @@ -0,0 +1,22 @@ +import random +import string + +class Robot: + robot_names = [] + def __init__(self): + self.name = self.assign_random_name() + self.robot_names.append(self.name) + + def reset(self): + self.name = self.assign_random_name() + + def generate_random_name(self): + letters = ''.join(random.choices(string.ascii_uppercase, k=2)) + digits = ''.join(random.choices(string.digits, k=3)) + return letters + digits + + def assign_random_name(self): + candidate_name = self.generate_random_name() + while candidate_name in Robot.robot_names: + candidate_name = self.generate_random_name() + return candidate_name diff --git a/scripts/freecodecamp/sudoku_oop/main.py b/scripts/freecodecamp/sudoku_oop/main.py new file mode 100644 index 0000000..ca04330 --- /dev/null +++ b/scripts/freecodecamp/sudoku_oop/main.py @@ -0,0 +1,78 @@ +class Board: + def __init__(self, board): + self.board = board + + def __str__(self): + board_str = '' + for row in self.board: + row_str = [str(i) if i else '*' for i in row] + board_str += ' '.join(row_str) + board_str += '\n' + return board_str + + def find_empty_cell(self): + for row, contents in enumerate(self.board): + try: + col = contents.index(0) + return row, col + except ValueError: + pass + return None + + def valid_in_row(self, row, num): + return num not in self.board[row] + + def valid_in_col(self, col, num): + return all(self.board[row][col] != num for row in range(9)) + + def valid_in_square(self, row, col, num): + row_start = (row // 3) * 3 + col_start = (col // 3) * 3 + for row_no in range(row_start, row_start + 3): + for col_no in range(col_start, col_start + 3): + if self.board[row_no][col_no] == num: + return False + return True + + def is_valid(self, empty, num): + row, col = empty + valid_in_row = self.valid_in_row(row, num) + valid_in_col = self.valid_in_col(col, num) + valid_in_square = self.valid_in_square(row, col, num) + return all([valid_in_row, valid_in_col, valid_in_square]) + + def solver(self): + if (next_empty := self.find_empty_cell()) is None: + return True + for guess in range(1, 10): + if self.is_valid(next_empty, guess): + row, col = next_empty + self.board[row][col] = guess + if self.solver(): + return True + self.board[row][col] = 0 + return False + +def solve_sudoku(board): + gameboard = Board(board) + print(f'Puzzle to solve:\n{gameboard}') + if gameboard.solver(): + print(f'Solved puzzle:\n{gameboard}') + else: + print('The provided puzzle is unsolvable.') + return gameboard + +puzzle = [ + [0, 0, 2, 0, 0, 8, 0, 0, 0], + [0, 0, 0, 0, 0, 3, 7, 6, 2], + [4, 3, 0, 0, 0, 0, 8, 0, 0], + [0, 5, 0, 0, 3, 0, 0, 9, 0], + [0, 4, 0, 0, 0, 0, 0, 2, 6], + [0, 0, 0, 4, 6, 7, 0, 0, 0], + [0, 8, 6, 7, 0, 4, 0, 0, 0], + [0, 0, 0, 5, 1, 9, 0, 0, 8], + [1, 7, 0, 0, 0, 6, 0, 0, 5] +] + +if __name__ == "__main__": + solve_sudoku(puzzle) \ No newline at end of file diff --git a/scripts/intermediate_oop/classes.py b/scripts/intermediate_oop/classes.py new file mode 100755 index 0000000..c28ada7 --- /dev/null +++ b/scripts/intermediate_oop/classes.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""Class exercises.""" +import math +from functools import total_ordering + +@total_ordering +class BankAccount: + """Bank account including an account balance.""" + + def __init__(self, balance=0): + self._balance = balance + self.transactions = [] + self.transactions.append(("OPEN", balance, balance)) + + @property + def balance(self): + return self._balance + + def deposit(self, quantity): + self._balance += quantity + self.transactions.append(("DEPOSIT", quantity, self._balance)) + + def withdraw(self, quantity): + self._balance -= quantity + self.transactions.append(("WITHDRAWAL", -quantity, self._balance)) + + def transfer(self, other, quantity): + other._balance += quantity + self._balance -= quantity + + def __repr__(self): + return f"BankAccount(balance={self._balance})" + + def __str__(self): + return f"A Bank Account with balance={self._balance})" + + def __eq__(self, other): + if not isinstance(other, BankAccount): + raise NotImplementedError + return self.balance == other.balance + + def __lt__(self, other): + if not isinstance(other, BankAccount): + raise NotImplementedError + return self.balance < other.balance + +class SuperMap: + """Data structure for quickly finding objects based on their attributes.""" + + +class MinHeap: + """Heap-like data structure.""" + + +class Flavor: + """Flavor of ice cream.""" + + +class Size: + """Ice cream size.""" + + +class IceCream: + """Ice cream to be ordered in our ice cream shop.""" + + +class Month: + """Class representing an entire month.""" + + +class MonthDelta: + """Class representing the difference between months.""" + + +class Row: + """Row class that stores all given arguments as attributes.""" diff --git a/scripts/intermediate_oop/classes_test.py b/scripts/intermediate_oop/classes_test.py new file mode 100755 index 0000000..324069c --- /dev/null +++ b/scripts/intermediate_oop/classes_test.py @@ -0,0 +1,714 @@ +# -*- coding: utf-8 -*- +"""Tests for classes exercises.""" +import random +import unittest +from contextlib import contextmanager +from datetime import date, timedelta +from itertools import cycle, permutations +from locale import LC_TIME, setlocale +from string import ascii_uppercase +from timeit import default_timer + +from classes import BankAccount, Flavor, IceCream, MinHeap, Month, MonthDelta, Row, Size, SuperMap + +random.seed(0) + +names = permutations(ascii_uppercase, 6) +colors = cycle( + [ + "purple", + "green", + "pink", + "blue", + "black", + "orange", + "yellow", + ] +) + + +class BankAccountTests(unittest.TestCase): + """Tests for BankAccount.""" + + def test_new_account_balance_default(self): + account = BankAccount() + self.assertEqual(account.balance, 0) + + def test_opening_balance(self): + account = BankAccount(balance=100) + self.assertEqual(account.balance, 100) + + def test_deposit(self): + account = BankAccount() + account.deposit(100) + self.assertEqual(account.balance, 100) + + def test_withdraw(self): + account = BankAccount(balance=100) + account.withdraw(40) + self.assertEqual(account.balance, 60) + + def test_repr(self): + account = BankAccount() + self.assertEqual(repr(account), "BankAccount(balance=0)") + account.deposit(200) + self.assertEqual(repr(account), "BankAccount(balance=200)") + + def test_transfer(self): + mary_account = BankAccount(balance=100) + dana_account = BankAccount(balance=0) + mary_account.transfer(dana_account, 20) + self.assertEqual(mary_account.balance, 80) + self.assertEqual(dana_account.balance, 20) + + @unittest.skip("BankAccount Transactions") + def test_transactions_open(self): + expected_transactions = [ + ("OPEN", 100, 100), + ] + account = BankAccount(balance=100) + self.assertEqual(account.transactions, expected_transactions) + + @unittest.skip("BankAccount Transactions") + def test_transactions_deposit(self): + expected_transactions = [ + ("OPEN", 0, 0), + ("DEPOSIT", 100, 100), + ] + account = BankAccount() + account.deposit(100) + self.assertEqual(account.transactions, expected_transactions) + + @unittest.skip("BankAccount Transactions") + def test_transactions_withdraw(self): + expected_transactions = [ + ("OPEN", 100, 100), + ("WITHDRAWAL", -40, 60), + ] + account = BankAccount(balance=100) + account.withdraw(40) + self.assertEqual(account.transactions, expected_transactions) + + @unittest.skip("BankAccount Transactions") + def test_transactions_scenario(self): + expected_transactions = [ + ("OPEN", 0, 0), + ("DEPOSIT", 100, 100), + ("WITHDRAWAL", -40, 60), + ("DEPOSIT", 95, 155), + ] + account = BankAccount() + account.deposit(100) + account.withdraw(40) + account.deposit(95) + self.assertEqual(account.transactions, expected_transactions) + + @unittest.skip("Truthy BankAccount") + def test_truthy_accounts(self): + account = BankAccount() + self.assertIs(bool(account), False) + account.deposit(100) + self.assertIs(bool(account), True) + + @unittest.skip("Comparable BankAccount") + def test_account_comparisons(self): + account1 = BankAccount() + account2 = BankAccount() + self.assertTrue(account1 == account2) + self.assertTrue(account1 >= account2) + self.assertTrue(account1 <= account2) + account1.deposit(100) + account2.deposit(10) + self.assertTrue(account1 != account2) + self.assertTrue(account2 < account1) + self.assertTrue(account1 > account2) + self.assertTrue(account2 < account1) + self.assertTrue(account1 >= account2) + self.assertTrue(account2 <= account1) + + @unittest.skip("Read-Only BankAccount") + def test_balance_cannot_be_written(self): + account1 = BankAccount() + account2 = BankAccount(100) + self.assertEqual(account1.balance, 0) + with self.assertRaises(Exception): + account1.balance = 50 + self.assertEqual(account1.balance, 0) + self.assertEqual(account2.balance, 100) + with self.assertRaises(Exception): + account2.balance = 50 + self.assertEqual(account2.balance, 100) + account1.deposit(100) + account2.deposit(10) + self.assertEqual(account1.balance, 100) + self.assertEqual(account2.balance, 110) + with self.assertRaises(Exception): + account2.balance = 500 + self.assertEqual(account2.balance, 110) + account2.transfer(account1, 50) + self.assertEqual(account1.balance, 150) + self.assertEqual(account2.balance, 60) + + @unittest.skip("Hidden Balance") + def test_dir_does_not_show_balance_attribute(self): + account = BankAccount() + account.deposit(100) + self.assertNotIn("_balance", dir(account)) + allowed = { + "accounts", + "balance", + "deposit", + "withdraw", + "transfer", + "transactions", + "name", + } | set(dir(type("", (), {})())) + self.assertEqual(set(dir(account)) - allowed, set()) + + +class Item: + __slots__ = ("id", "name", "color", "version") + + def __init__(self, id, name, color, version): + self.id = id + self.name = name + self.color = color + self.version = version + + @property + def _values(self): + return (self.id, self.name, self.color, self.version) + + def __hash__(self): + return hash(self._values) + + def __eq__(self, other): + if not isinstance(other, Item): + return NotImplemented + return self._values == other._values + + def __repr__(self): + return f"Item{self._values!r}" + + +class SuperMapTests(unittest.TestCase): + """Tests for SuperMap.""" + + def test_where_method(self): + few_items = [ + Item(i, "".join(next(names)), next(colors), random.randint(0, 5)) + for i in range(10_000, 10_100) + ] + mapping = SuperMap(few_items, indexes=("id", "color")) + matches = mapping.where("color", "pink") + self.assertEqual(set(matches), {item for item in few_items if item.color == "pink"}) + matches = mapping.where("id", 4) + self.assertEqual(len(matches), 0) + matches = mapping.where("id", 10_050) + self.assertEqual(len(matches), 1) + + def test_time_efficient_lookups(self): + many_items = [ + Item(i, "".join(next(names)), next(colors), random.randint(0, 5)) for i in range(2_000) + ] + mapping = SuperMap(many_items, indexes=("id", "color")) + with Timer() as manual_lookup: + items1 = {item for item in many_items if item.color == "pink"} + with Timer() as lookup_from_map: + items2 = mapping.where("color", "pink") + self.assertEqual(len(items1), len(items2)) + self.assertEqual(set(items1), items2) + self.assertGreater( + manual_lookup.elapsed, + lookup_from_map.elapsed * 5, + ) + + +class MinHeapTests(unittest.TestCase): + """Tests for MinHeap.""" + + @classmethod + def setUpClass(cls): + cls.big_numbers = [ + 3748, + 7250, + 140, + 7669, + 5711, + 2284, + 3322, + 6435, + 8138, + 6920, + 9634, + 7511, + 5295, + 5456, + 7458, + 5618, + 102, + 7747, + 4638, + 46, + 4532, + 1483, + 944, + 3542, + 6641, + 9091, + 693, + 836, + 3099, + 3385, + 7798, + 758, + 8407, + 4756, + 8801, + 3936, + 5301, + 5744, + 6454, + 1156, + 7686, + 5664, + 2568, + 6414, + 3469, + 2867, + 8875, + 6097, + 2546, + 4658, + 7027, + 9437, + 755, + 8536, + 8186, + 9539, + 661, + 6706, + 265, + 2254, + 2402, + 3355, + 9141, + 5091, + 1727, + 6739, + 4599, + 5599, + 9007, + 2925, + 2894, + 5333, + 9586, + 7409, + 916, + 6420, + 8493, + 9531, + 5083, + 5350, + 3346, + 1378, + 6260, + 3143, + 7216, + 684, + 170, + 6721, + 418, + 7013, + 7729, + 7484, + 5355, + 4850, + 8073, + 1389, + 2084, + 1856, + 9740, + 2747, + ] + + def test_create_heap(self): + MinHeap([322, 76, 4, 7, 2, 123, 47, 1, 18, 3, 29, 199, 11]) + MinHeap(self.big_numbers) + + def test_peek_at_smallest(self): + numbers = [11, 322, 3, 199, 29, 7, 1, 18, 76, 4, 2, 47, 123] + h = MinHeap(numbers) + self.assertEqual(h.peek(), 1) + i = MinHeap(self.big_numbers) + self.assertEqual(i.peek(), 46) + + def test_pop_from_heap(self): + numbers = [11, 322, 3, 199, 29, 7, 1, 18, 76, 4, 2, 47, 123] + h = MinHeap(numbers) + self.assertEqual(h.pop(), 1) + self.assertEqual(h.pop(), 2) + self.assertEqual(h.pop(), 3) + self.assertEqual(h.pop(), 4) + self.assertEqual(h.pop(), 7) + self.assertEqual(h.pop(), 11) + i = MinHeap(self.big_numbers) + self.assertEqual(i.pop(), 46) + + def test_push_onto_heap(self): + numbers = [11, 322, 3, 199, 29, 7, 1, 18, 76, 4, 2, 47, 123] + i = MinHeap(self.big_numbers) + i.push(17) + self.assertEqual(i.peek(), 17) + i.push(24) + self.assertEqual(i.pop(), 17) + self.assertEqual(i.pop(), 24) + self.assertEqual(i.pop(), 46) + h = MinHeap(numbers) + h.push(6) + self.assertEqual(h.pop(), 1) + self.assertEqual(h.pop(), 2) + self.assertEqual(h.pop(), 3) + self.assertEqual(h.pop(), 4) + self.assertEqual(h.pop(), 6) + + def test_faster_than_sorting(self): + many_big_numbers = [random.randint(100, 1000) for n in range(10000)] + with Timer() as sort_timer: + sorted(many_big_numbers) + heap = MinHeap(many_big_numbers) + with Timer() as min_heap_timer: + heap.push(150) + heap.push(950) + heap.push(400) + heap.push(760) + heap.push(280) + heap.push(870) + heap.push(330) + heap.push(1000) + heap.push(50) + heap.push(500) + items = [heap.pop() for _ in range(10)] + self.assertEqual(len(items), 10) + self.assertLess(min_heap_timer.elapsed, sort_timer.elapsed) + + +class FlavorTests(unittest.TestCase): + """Tests for Flavor.""" + + def test_name_attribute(self): + flavor = Flavor("vanilla") + self.assertEqual(flavor.name, "vanilla") + + def test_specifying_ingredients(self): + flavor = Flavor("vanilla", ingredients=["milk", "sugar", "vanilla"]) + self.assertEqual(flavor.ingredients, ["milk", "sugar", "vanilla"]) + flavor = Flavor("chocolate", ingredients=["milk", "sugar", "vanilla", "chocolate"]) + self.assertEqual( + flavor.ingredients, + ["milk", "sugar", "vanilla", "chocolate"], + ) + + def test_modifying_ingredients(self): + original_ingredients = ["milk", "sugar", "vanilla"] + flavor = Flavor("vanilla", ingredients=original_ingredients) + flavor.ingredients.append("red bean") + self.assertEqual(original_ingredients, ["milk", "sugar", "vanilla"]) + + def test_has_dairy_attribute(self): + flavor = Flavor("vanilla") + self.assertIs(flavor.has_dairy, True) + flavor = Flavor("vanilla", has_dairy=True) + self.assertIs(flavor.has_dairy, True) + flavor = Flavor("vanilla", has_dairy=False) + self.assertIs(flavor.has_dairy, False) + + def test_string_representation(self): + flavor = Flavor("chocolate", has_dairy=False) + self.assertEqual(repr(flavor), "Flavor(name='chocolate', ingredients=[], has_dairy=False)") + flavor = Flavor("vanilla", ingredients=["milk", "sugar", "vanilla"]) + self.assertEqual( + repr(flavor), + "Flavor(name='vanilla', ingredients=['milk', 'sugar', 'vanilla'], has_dairy=True)", + ) + + +class SizeTests(unittest.TestCase): + """Tests for Size.""" + + def test_initializer(self): + size = Size(quantity=1, unit="gram", price="5.00") + self.assertEqual(size.quantity, 1) + self.assertEqual(size.unit, "gram") + self.assertEqual(size.price, "5.00") + + def test_human_string_representation(self): + size = Size(quantity=1, unit="gram", price="5.00") + self.assertEqual(str(size), "1 gram") + size = Size(quantity=1, unit="scoop", price="5.00") + self.assertEqual(str(size), "1 scoop") + + def test_pluralization(self): + size = Size(quantity=3, unit="pint", price="9.00") + self.assertEqual(str(size), "3 pints") + size = Size(quantity=3, unit="scoop", price="4.00") + self.assertEqual(str(size), "3 scoops") + + def test_machine_string_representation(self): + size = Size(quantity=1, unit="cup", price="5") + self.assertEqual(repr(size), "Size(quantity=1, unit='cup', price='5')") + + +class IceCreamTests(unittest.TestCase): + """Tests for IceCream.""" + + def test_initializer(self): + one_quart = Size(quantity=1, unit="quart", price="$9") + vanilla = Flavor("vanilla") + quart_of_vanilla = IceCream(flavor=vanilla, size=one_quart) + self.assertEqual(quart_of_vanilla.size, one_quart) + self.assertEqual(quart_of_vanilla.flavor, vanilla) + + def test_string_representation(self): + one_quart = Size(quantity=1, unit="quart", price="$9") + vanilla = Flavor("vanilla") + quart_of_vanilla = IceCream(flavor=vanilla, size=one_quart) + self.assertEqual(str(quart_of_vanilla), "1 quart of vanilla") + self.assertEqual(str(quart_of_vanilla), "1 quart of vanilla") + two_scoops = IceCream( + flavor=Flavor("chocolate"), + size=Size(quantity=2, unit="scoop", price="$3"), + ) + self.assertEqual(str(two_scoops), "2 scoops of chocolate") + + +class MonthTests(unittest.TestCase): + """Tests for Month.""" + + def test_initialization(self): + month = Month(2019, 1) + self.assertEqual(month.year, 2019) + self.assertEqual(month.month, 1) + + def test_machine_readable_representation(self): + month = Month(2019, 1) + self.assertEqual(repr(month), "Month(year=2019, month=1)") + + def test_human_readable_representation(self): + month = Month(2019, 1) + self.assertEqual(str(month), "2019-01") + + def test_string_representations(self): + python2_eol = Month(2020, 1) + self.assertEqual(str(python2_eol), "2020-01") + new_month = eval(repr(python2_eol)) + self.assertEqual(new_month.year, python2_eol.year) + self.assertEqual(new_month.month, python2_eol.month) + + def test_first_method(self): + python2_eol = Month(2020, 1) + eol_date = python2_eol.first() + self.assertEqual(eol_date.year, 2020) + self.assertEqual(eol_date.month, 1) + self.assertEqual(eol_date.day, 1) + self.assertEqual(str(eol_date), "2020-01-01") + self.assertEqual(str(eol_date - timedelta(days=1)), "2019-12-31") + + @unittest.skip("Comparable Month") + def test_equality(self): + python2_eol = Month(2020, 1) + self.assertEqual(python2_eol, Month(2020, 1)) + self.assertNotEqual(python2_eol, Month(2020, 2)) + self.assertNotEqual(python2_eol, Month(2019, 1)) + self.assertFalse(python2_eol != Month(2020, 1)) + self.assertFalse(python2_eol == Month(2020, 2)) + self.assertNotEqual(python2_eol, date(2020, 1, 1)) + self.assertNotEqual(python2_eol, (2020, 1)) + self.assertNotEqual((2020, 1), python2_eol) # tuples aren't months + + @unittest.skip("Comparable Month") + def test_ordering(self): + python2_eol = Month(2020, 1) + pycon_2019 = Month(2019, 5) + self.assertLess(pycon_2019, python2_eol) + self.assertGreater(python2_eol, pycon_2019) + self.assertLessEqual(pycon_2019, python2_eol) + self.assertGreaterEqual(python2_eol, pycon_2019) + self.assertFalse(pycon_2019 > python2_eol) + self.assertFalse(pycon_2019 >= python2_eol) + self.assertFalse(python2_eol < pycon_2019) + self.assertFalse(python2_eol <= pycon_2019) + with self.assertRaises(TypeError): + python2_eol < (2021, 12) # tuples aren't months + with self.assertRaises(TypeError): + python2_eol >= (2021, 12) # tuples aren't months + with self.assertRaises(TypeError): + (2021, 12) < python2_eol # tuples aren't months + + @unittest.skip("Month Formatting") + def test_formatting(self): + python2_eol = Month(2020, 1) + leap_month = Month(2000, 2) + self.assertEqual("{:%Y-%m}".format(python2_eol), "2020-01") + with set_locale("C"): + self.assertEqual("{0:%b %Y}".format(leap_month), "Feb 2000") + self.assertEqual("{:%b %Y}".format(python2_eol), "Jan 2020") + + @unittest.skip("Month from_date") + def test_from_date(self): + python2_eol = Month.from_date(date(2020, 1, 1)) + self.assertEqual(python2_eol, Month(2020, 1)) + leap_month = Month.from_date(date(2000, 2, 29)) + self.assertEqual(leap_month, Month(2000, 2)) + + @unittest.skip("Memory-Efficient Month") + def test_memory_efficient(self): + python2_eol = Month(2020, 1) + with self.assertRaises(Exception): + python2_eol.__dict__ + + +@contextmanager +def set_locale(name): + saved = setlocale(LC_TIME) + try: + yield setlocale(LC_TIME, name) + finally: + setlocale(LC_TIME, saved) + + +class MonthDeltaTests(unittest.TestCase): + """Tests for MonthDelta.""" + + def test_initializer(self): + four_months = MonthDelta(4) + self.assertEqual(four_months.months, 4) + + def test_equality(self): + self.assertEqual(MonthDelta(12), MonthDelta(12)) + self.assertNotEqual(MonthDelta(11), MonthDelta(12)) + self.assertIs(MonthDelta(12) != MonthDelta(12), False) + self.assertIs(MonthDelta(11) == MonthDelta(12), False) + self.assertIs(MonthDelta(0) == timedelta(0), False) + self.assertIs(MonthDelta(0) == 0, False) + self.assertIs(MonthDelta(6) == 6, False) + + def test_adding_month_delta_to_unknown_value(self): + with self.assertRaises(TypeError): + MonthDelta(4) + 8 + with self.assertRaises(TypeError): + 8 + MonthDelta(4) + + def test_adding_and_subtracting_with_monthdeltas(self): + self.assertEqual(MonthDelta(4) + MonthDelta(2), MonthDelta(6)) + self.assertEqual(MonthDelta(4) - MonthDelta(2), MonthDelta(2)) + with self.assertRaises(TypeError): + MonthDelta(4) - 8 + with self.assertRaises(TypeError): + 8 - MonthDelta(4) + + def test_month_arithmetic_with_month_deltas(self): + python2_eol = Month(2020, 1) + python2_release = Month(2000, 10) + python2_lifetime = MonthDelta(231) + self.assertEqual(python2_eol + MonthDelta(4), Month(2020, 5)) + self.assertEqual(MonthDelta(13) + python2_eol, Month(2021, 2)) + self.assertEqual(python2_eol - MonthDelta(4), Month(2019, 9)) + self.assertEqual(python2_eol - MonthDelta(13), Month(2018, 12)) + self.assertEqual(python2_release + python2_lifetime, python2_eol) + + def test_month_subtracting_months(self): + python2_eol = Month(2020, 1) + python2_release = Month(2000, 10) + python2_lifetime = python2_eol - python2_release + self.assertEqual(python2_lifetime, MonthDelta(20 * 12 - 9)) + + def test_month_arithmetic_with_other_types(self): + python2_eol = Month(2020, 1) + python2_release = Month(2000, 10) + python2_lifetime = python2_eol - python2_release + with self.assertRaises(TypeError): + python2_eol + python2_release + with self.assertRaises(TypeError): + python2_eol * python2_release + with self.assertRaises(TypeError): + python2_eol * python2_lifetime + with self.assertRaises(TypeError): + python2_lifetime - python2_eol + with self.assertRaises(TypeError): + python2_eol - date(1999, 12, 1) + + @unittest.skip("MonthDelta Arithmetic") + def test_scaling_and_division(self): + self.assertEqual(MonthDelta(4) * 2, MonthDelta(8)) + self.assertEqual(2 * MonthDelta(4), MonthDelta(8)) + self.assertEqual(MonthDelta(4) / MonthDelta(2), 2) + self.assertEqual(MonthDelta(18) // 12, MonthDelta(1)) + self.assertEqual(MonthDelta(18) // MonthDelta(12), 1) + self.assertEqual(MonthDelta(18) % MonthDelta(12), 6) + self.assertEqual(MonthDelta(18) % 12, MonthDelta(6)) + self.assertEqual(-MonthDelta(18), MonthDelta(-18)) + with self.assertRaises(TypeError): + MonthDelta(4) * "a" + with self.assertRaises(TypeError): + MonthDelta(4) * 2.0 + with self.assertRaises(TypeError): + MonthDelta(4) / 2.0 + with self.assertRaises(TypeError): + MonthDelta(4) * MonthDelta(2) + with self.assertRaises(TypeError): + MonthDelta(4) % 0.5 + + +class RowTests(unittest.TestCase): + """Tests for Row.""" + + def test_no_arguments(self): + row = Row() + attributes = {x for x in dir(row) if not x.startswith("__")} + self.assertEqual(attributes, set()) + + def test_single_argument(self): + row = Row(a=1) + self.assertEqual(row.a, 1) + attributes = {x for x in dir(row) if not x.startswith("__")} + self.assertEqual(attributes, {"a"}) + + def test_two_arguments(self): + row = Row(a=1, b=2) + self.assertEqual(row.a, 1) + self.assertEqual(row.b, 2) + attributes = {x for x in dir(row) if not x.startswith("__")} + self.assertEqual(attributes, {"a", "b"}) + + def test_many_arguments(self): + row = Row(thing="a", item=2, stuff=True) + self.assertEqual(row.thing, "a") + self.assertEqual(row.item, 2) + self.assertEqual(row.stuff, True) + attributes = {x for x in dir(row) if not x.startswith("__")} + self.assertEqual(attributes, {"thing", "item", "stuff"}) + + def test_no_positional_arguments_accepted(self): + with self.assertRaises(Exception): + Row(1, 2) + with self.assertRaises(Exception): + Row(1) + + +class Timer: + """Context manager to time a code block.""" + + def __enter__(self): + self.start = default_timer() + return self + + def __exit__(self, *args): + self.end = default_timer() + self.elapsed = self.end - self.start + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/dunder.py b/scripts/intermediate_oop/dunder.py new file mode 100755 index 0000000..3aba869 --- /dev/null +++ b/scripts/intermediate_oop/dunder.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +"""Dunder exercises.""" + +class ReverseView: + """Lazily operate on a sequence in reverse.""" + def __init__(self, sequence): + self._sequence = sequence + + def __getitem__(self, index): # If it were a dictionary we would use 'key' instead + return self._sequence[-index-1] + + def __str__(self): + return f'{self._sequence[::-1]}' + + def __len__(self): + return len(self._sequence) + +class NotImplemented(BaseException): + pass + +class Comparator: + """Object that is equal to a very small range of numbers.""" + + +class RomanNumeral: + """Class for treating Roman Numerals like numbers.""" + + +class Timer: + """Utility for timing the execution of code.""" + + +class FancyDict: + """Dictioray-like class supporting attribute lookups.""" + + +class reloopable: + """Iterable which resets a file each time it's looped over.""" diff --git a/scripts/intermediate_oop/dunder_test.py b/scripts/intermediate_oop/dunder_test.py new file mode 100755 index 0000000..0c7ffec --- /dev/null +++ b/scripts/intermediate_oop/dunder_test.py @@ -0,0 +1,548 @@ +# -*- coding: utf-8 -*- +"""Tests for dunder exercises.""" +import unittest +from collections.abc import Generator, Iterable, Mapping +from io import StringIO +from sys import getsizeof +from textwrap import dedent +from time import sleep + +from dunder import Comparator, FancyDict, ReverseView, RomanNumeral, Timer, reloopable + + +class ReverseViewTests(unittest.TestCase): + """Tests for ReverseView.""" + + def test_can_iterate_at_least_once(self): + numbers = [2, 1, 3, 4, 7, 11, 18] + view = ReverseView(numbers) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + + def test_can_iterate_more_than_once(self): + numbers = [2, 1, 3, 4, 7, 11, 18] + view = ReverseView(numbers) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + self.assertEqual(list(view), list(view)) + + def test_updating_sequence_updates_view(self): + numbers = [2, 1, 3, 4, 7, 11, 18] + view = ReverseView(numbers) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + numbers.append(29) + self.assertEqual(list(view), [29, 18, 11, 7, 4, 3, 1, 2]) + numbers.pop(0) + self.assertEqual(list(view), [29, 18, 11, 7, 4, 3, 1]) + + def test_no_memory_used(self): + numbers = list(range(10000)) + view = ReverseView(numbers) + next(iter(view)) + if isinstance(view, Generator): + size = sum(get_size(obj) for obj in view.gi_frame.f_locals.values()) + else: + size = get_size(view) + self.assertLess(size, 400000, "Too much memory used") + self.assertNotEqual(type(view), list) + self.assertNotEqual(type(view), tuple) + + def test_does_not_slice_sequence(self): + class UnsliceableList(list): + def __getitem__(self, index): + if not isinstance(index, int): + return NotImplemented("Only indexes accepted") + return super().__getitem__(index) + + numbers = UnsliceableList([2, 1, 3, 4, 7, 11, 18]) + view = ReverseView(numbers) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + + def test_has_length_and_repr_and_is_indexable(self): + numbers = [2, 1, 3, 4, 7, 11, 18] + view = ReverseView(numbers) + + # Has length + self.assertEqual(len(view), 7) + self.assertEqual(len(view), 7) + numbers.append(29) + self.assertEqual(len(view), 8) + numbers.pop() + self.assertEqual(len(view), 7) + + # Is indexable + self.assertEqual(view[0], 18) + self.assertEqual(view[-1], 2) + self.assertEqual(view[2], 7) + self.assertEqual(view[-2], 1) + numbers.append(29) + self.assertEqual(view[0], 29) + self.assertEqual(view[-1], 2) + numbers.pop(0) + self.assertEqual(view[0], 29) + self.assertEqual(view[-1], 1) + + # Has a nice string representation + self.assertEqual(list(view), [29, 18, 11, 7, 4, 3, 1]) + self.assertEqual(str(view), "[29, 18, 11, 7, 4, 3, 1]") + + +class ComparatorTests(unittest.TestCase): + """Tests for Comparator.""" + + def test_equality_with_delta(self): + self.assertEqual(5.5, Comparator(6, delta=0.5)) + self.assertEqual(6.5, Comparator(6, delta=0.5)) + self.assertNotEqual(6.51, Comparator(6, delta=0.5)) + self.assertNotEqual(5.49, Comparator(6, delta=0.5)) + + def test_equality_with_default_delta(self): + self.assertEqual(Comparator(5), 4.99999999) + self.assertEqual(Comparator(5), 5.00000001) + self.assertEqual(5, Comparator(4.99999999)) + self.assertEqual(5, Comparator(5.00000001)) + self.assertNotEqual(Comparator(5), 4.99999) + self.assertNotEqual(Comparator(5), 5.00001) + + def test_negative_numbers(self): + self.assertNotEqual(-5.5, Comparator(-6, delta=0.25)) + self.assertEqual(-5.75, Comparator(-6, delta=0.25)) + self.assertEqual(-6.25, Comparator(-6, delta=0.25)) + self.assertNotEqual(-6.3, Comparator(-6, delta=0.25)) + + def test_very_small_delta(self): + self.assertEqual(-6.000000000000001, Comparator(-6, delta=1e-15)) + self.assertNotEqual(-6.000000000000002, Comparator(-6, delta=1e-15)) + + def test_string_representation(self): + self.assertEqual( + repr(Comparator(5, delta=0.1)), + "Comparator(5, delta=0.1)", + ) + self.assertEqual(repr(Comparator(5)), "Comparator(5, delta=1e-07)") + self.assertEqual(str(Comparator(5)), "Comparator(5, delta=1e-07)") + + def test_addition_and_subtraction(self): + self.assertEqual(Comparator(5, delta=0.1) + 6, 11.1) + self.assertEqual(6 + Comparator(5, delta=0.1), 10.9) + self.assertNotEqual(Comparator(5, delta=0.1) + 6, 11.2) + self.assertNotEqual(6 + Comparator(5, delta=0.1) + 6, 10.8) + self.assertEqual(Comparator(7, delta=0.1) - 6, 1.05) + self.assertNotEqual(Comparator(7, delta=0.1) - 6, 1.2) + self.assertEqual(7 - Comparator(7, delta=0.1), 0.05) + self.assertNotEqual(7 - Comparator(7, delta=0.1), 0.11) + self.assertEqual(6 - Comparator(7, delta=0.1), -1.05) + + def test_arithmetic_and_comparisons_with_comparators(self): + five = Comparator(5, delta=0.1) + six = Comparator(6, delta=0.1) + seven = Comparator(7, delta=0.5) + self.assertEqual(five + six, 11.1) + self.assertNotEqual(five + six, 11.2) + self.assertEqual(five + seven, 12.1) + self.assertEqual(five + seven, 12.5) + self.assertEqual(seven + five, 12.5) + self.assertNotEqual(five + seven, 12.6) + + +class RomanNumeralTests(unittest.TestCase): + """Tests for RomanNumeral.""" + + def verify(self, integer, numeral): + self.assertEqual(int(RomanNumeral(numeral)), integer) + self.assertNotEqual(int(RomanNumeral(numeral)), integer + 1) + self.assertNotEqual(int(RomanNumeral(numeral)), integer - 1) + + def test_single_digit(self): + self.verify(1, "I") + self.verify(5, "V") + self.verify(10, "X") + self.verify(50, "L") + self.verify(100, "C") + self.verify(500, "D") + self.verify(1000, "M") + + def test_two_digits_ascending(self): + self.verify(2, "II") + self.verify(6, "VI") + self.verify(11, "XI") + self.verify(15, "XV") + self.verify(20, "XX") + self.verify(60, "LX") + self.verify(101, "CI") + self.verify(105, "CV") + self.verify(110, "CX") + self.verify(150, "CL") + self.verify(550, "DL") + self.verify(600, "DC") + self.verify(1100, "MC") + self.verify(2000, "MM") + + def test_three_digits_ascending(self): + self.verify(3, "III") + self.verify(7, "VII") + self.verify(12, "XII") + self.verify(16, "XVI") + self.verify(21, "XXI") + self.verify(25, "XXV") + self.verify(30, "XXX") + + def test_four_digits_ascending(self): + self.verify(8, "VIII") + self.verify(13, "XIII") + self.verify(17, "XVII") + self.verify(22, "XXII") + self.verify(26, "XXVI") + self.verify(31, "XXXI") + self.verify(35, "XXXV") + + def test_many_digits(self): + self.verify(1888, "MDCCCLXXXVIII") + + def test_subtractive(self): + self.verify(4, "IV") + self.verify(9, "IX") + self.verify(14, "XIV") + self.verify(19, "XIX") + self.verify(24, "XXIV") + self.verify(29, "XXIX") + self.verify(40, "XL") + self.verify(90, "XC") + self.verify(44, "XLIV") + self.verify(94, "XCIV") + self.verify(49, "XLIX") + self.verify(99, "XCIX") + self.verify(1999, "MCMXCIX") + self.verify(1948, "MCMXLVIII") + + def test_string_representation(self): + self.assertEqual(str(RomanNumeral("I")), "I") + self.assertEqual(repr(RomanNumeral("CD")), "RomanNumeral('CD')") + # Some conversion happens for some numbers + fourteen = RomanNumeral("XIIII") + self.assertEqual(str(fourteen), "XIV") + self.assertEqual(repr(fourteen), "RomanNumeral('XIV')") + + def test_adding(self): + sixty_five = RomanNumeral("LXV") + eighty_seven = RomanNumeral("LXXXVII") + self.assertEqual(int(sixty_five + eighty_seven), 152) + self.assertEqual(type(sixty_five + eighty_seven), RomanNumeral) + self.assertEqual(int(sixty_five + 87), 152) + self.assertEqual(type(sixty_five + 87), RomanNumeral) + self.assertEqual(str(sixty_five + 87), str("CLII")) + + def test_equality_and_ordering(self): + self.assertEqual(RomanNumeral("I"), 1) + self.assertNotEqual(RomanNumeral("I"), 2) + self.assertEqual(RomanNumeral("I"), "I") + self.assertLess(RomanNumeral("MCMXLVIII"), RomanNumeral("MCMXCIX")) + self.assertGreater(RomanNumeral("MCMXCIX"), RomanNumeral("MCMXLVIII")) + self.assertGreaterEqual(RomanNumeral("IX"), RomanNumeral("III")) + self.assertLessEqual(RomanNumeral("III"), RomanNumeral("IX")) + self.assertGreaterEqual(RomanNumeral("X"), RomanNumeral("X")) + self.assertLessEqual(RomanNumeral("IIII"), RomanNumeral("IV")) + self.assertFalse(RomanNumeral("V") < RomanNumeral("IV")) + self.assertFalse(RomanNumeral("V") > RomanNumeral("IX")) + self.assertFalse(RomanNumeral("V") <= RomanNumeral("IV")) + self.assertFalse(RomanNumeral("V") >= RomanNumeral("IX")) + with self.assertRaises(TypeError): + RomanNumeral("X") < "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") <= "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") > "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") >= "XX" + self.assertFalse(RomanNumeral("V") < 4) + self.assertFalse(RomanNumeral("V") > 9) + self.assertFalse(RomanNumeral("V") <= 4) + self.assertFalse(RomanNumeral("V") >= 9) + with self.assertRaises(TypeError): + RomanNumeral("X") < "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") <= "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") > "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") >= "XX" + + @unittest.skip("RomanNumeral from_int") + def test_from_int(self): + numeral = RomanNumeral.from_int(1) + self.assertEqual(numeral, "I") + self.assertEqual(numeral, 1) + self.assertEqual(type(numeral), RomanNumeral) + self.assertEqual(str(RomanNumeral.from_int(10)), "X") + self.assertEqual(str(RomanNumeral.from_int(21)), "XXI") + self.assertEqual(str(RomanNumeral.from_int(600)), "DC") + self.assertEqual(str(RomanNumeral.from_int(2000)), "MM") + self.assertEqual(str(RomanNumeral.from_int(12)), "XII") + self.assertEqual(str(RomanNumeral.from_int(25)), "XXV") + self.assertEqual(str(RomanNumeral.from_int(6)), "VI") + self.assertEqual(str(RomanNumeral.from_int(4)), "IV") + self.assertEqual(str(RomanNumeral.from_int(9)), "IX") + self.assertEqual(str(RomanNumeral.from_int(14)), "XIV") + self.assertEqual(str(RomanNumeral.from_int(1888)), "MDCCCLXXXVIII") + self.assertEqual(str(RomanNumeral.from_int(1999)), "MCMXCIX") + self.assertEqual(str(RomanNumeral.from_int(1948)), "MCMXLVIII") + + +class TimerTests(unittest.TestCase): + """Tests for Timer.""" + + _baseline = None + + @staticmethod + def get_baseline(count=5): + times = 0 + for i in range(count): + with Timer() as timer: + sleep(0) + times += timer.elapsed + return times / count + + def assertTimeEqual(self, actual, expected): + if self._baseline is None: + self._baseline = self.get_baseline() + self.assertAlmostEqual(actual, self._baseline + expected, delta=0.005) + + def test_short_time(self): + with Timer() as timer: + sleep(0.01) + self.assertGreater(timer.elapsed, 0.01) + self.assertLess(timer.elapsed, 1) + + def test_very_short_time(self): + with Timer() as timer: + pass + self.assertTimeEqual(timer.elapsed, 0) + + def test_two_timers(self): + with Timer() as timer1: + sleep(0.005) + with Timer() as timer2: + sleep(0.005) + sleep(0.005) + self.assertLess(timer2.elapsed, timer1.elapsed) + + def test_reusing_same_timer(self): + timer = Timer() + with timer: + sleep(0.0005) + elapsed1 = timer.elapsed + with timer: + sleep(0.004) + self.assertLess(elapsed1, timer.elapsed) + + +class FancyDictTests(unittest.TestCase): + """Tests for FancyDict.""" + + def test_constructor(self): + FancyDict() + FancyDict({"a": 2, "b": 3}) + + def test_key_access(self): + d = FancyDict({"a": 2, "b": 3}) + self.assertEqual(d["a"], 2) + self.assertEqual(d["b"], 3) + + def test_attribute_access(self): + d = FancyDict({"a": 2, "b": 3}) + self.assertEqual(d.a, 2) + self.assertEqual(d.b, 3) + + def test_original_dictionary_unchanged(self): + mapping = {"a": 2, "b": 3} + d = FancyDict(mapping) + d.c = 4 + self.assertEqual(mapping, {"a": 2, "b": 3}) + + def test_allow_setting_keys_and_attributes(self): + d = FancyDict({"a": 2, "b": 3}) + d["a"] = 4 + self.assertEqual(d["a"], 4) + self.assertEqual(d.a, 4) + d.c = 9 + self.assertEqual(d["c"], 9) + self.assertEqual(d.c, 9) + self.assertEqual(d["b"], 3) + x = FancyDict() + y = FancyDict() + x.a = 4 + y.a = 5 + self.assertEqual(x.a, 4) + + def test_keyword_arguments_equality_and_get_method(self): + d = FancyDict(a=2, b=3, c=4, d=5) + self.assertEqual(d.a, 2) + self.assertEqual(d.b, 3) + self.assertEqual(d["c"], 4) + self.assertEqual(d["d"], 5) + x = FancyDict({"a": 2, "b": 3}) + y = FancyDict({"a": 2, "b": 4}) + self.assertNotEqual(x, y) + y.b = 3 + self.assertEqual(x, y) + x.c = 5 + self.assertNotEqual(x, y) + y.c = 5 + self.assertEqual(x, y) + self.assertIsNone(y.get("d")) + self.assertEqual(y.get("c"), 5) + self.assertEqual(y.get("d", 5), 5) + + def test_keys_values_items_containment_and_length(self): + d = FancyDict(a=2, b=3, c=4, d=5) + self.assertEqual(set(d.keys()), {"a", "b", "c", "d"}) + self.assertEqual(set(d.values()), {2, 3, 4, 5}) + self.assertEqual( + set(d.items()), + {("a", 2), ("b", 3), ("c", 4), ("d", 5)}, + ) + self.assertEqual(len(d), 4) + self.assertTrue("a" in d) + self.assertFalse("a" not in d) + self.assertFalse("e" in d) + self.assertTrue("e" not in d) + self.assertNotIn("get", d) + with self.assertRaises(KeyError): + d["get"] + with self.assertRaises(KeyError): + d["keys"] + self.assertNotIn("values", d) + self.assertNotIn("setdefault", d) + self.assertEqual(d.pop("b", None), 3) + self.assertNotIn("b", d) + self.assertEqual(d.pop("b", None), None) + self.assertNotIn("b", d) + with self.assertRaises(KeyError): + d.pop("b") + + def test_normalize_attribute(self): + d = FancyDict({"greeting 1": "hi"}, normalize=True) + self.assertEqual(d["greeting 1"], "hi") + self.assertEqual(d.greeting_1, "hi") + d.greeting_2 = "hello" + self.assertEqual(d["greeting 2"], "hello") + self.assertEqual(d.greeting_2, "hello") + d["greeting 2"] = "hey" + self.assertEqual(d["greeting 2"], "hey") + self.assertEqual(d.get("greeting 2"), "hey") + self.assertEqual(d.greeting_2, "hey") + with self.assertRaises(AttributeError): + d.greeting2 + d = FancyDict({"greeting 1": "hi"}) + self.assertEqual(d["greeting 1"], "hi") + with self.assertRaises(AttributeError): + d.greeting_1 + + def test_dir(self): + d = FancyDict(a=2, b=3, c=4, d=5) + self.assertIn("a", dir(d)) + self.assertIn("b", dir(d)) + self.assertIn("c", dir(d)) + self.assertIn("d", dir(d)) + + +class ReloopableTests(unittest.TestCase): + """Tests for reloopable.""" + + one_line = "hello\n" + + two_lines = "line 1\nline 2\n" + + no_final_newline = "line 1\nline 2\nline 3" + + simone = dedent( + """ + Picket lines, school boycotts + They try to say it's a communist plot + All I want is equality + For my sister, my brother, my people, and me + """.lstrip( + "\n" + ) + ) + + many_lines = "This is a file\n" * 1000 + + empty = "" + + def test_empty_file(self): + f = StringIO(self.empty) + reloop = reloopable(f) + self.assertEqual(list(reloop), []) + self.assertEqual(list(reloop), []) + self.assertEqual(list(reloop), []) + + def test_one_line(self): + f = StringIO(self.one_line) + reloop = reloopable(f) + self.assertEqual(list(reloop), ["hello\n"]) + self.assertEqual(list(reloop), ["hello\n"]) + self.assertEqual(list(reloop), ["hello\n"]) + + def test_two_lines(self): + f = StringIO(self.two_lines) + reloop = reloopable(f) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n"]) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n"]) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n"]) + + def test_no_final_newlines(self): + f = StringIO(self.no_final_newline) + reloop = reloopable(f) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n", "line 3"]) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n", "line 3"]) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n", "line 3"]) + + def test_many_lines(self): + f = StringIO(self.many_lines) + reloop = reloopable(f) + self.assertEqual(list(reloop), ["This is a file\n"] * 1000) + self.assertEqual(list(reloop), ["This is a file\n"] * 1000) + self.assertEqual(list(reloop), ["This is a file\n"] * 1000) + + def test_data_in_file_is_not_stored(self): + f = StringIO(self.many_lines) + reloop = reloopable(f) + + self.assertEqual(list(reloop), ["This is a file\n"] * 1000) + + # Put new contents in the file + f.seek(0) + f.write(self.two_lines) + f.truncate() + + self.assertEqual(list(reloop), ["line 1\n", "line 2\n"]) + + +def get_size(obj, seen=None): + """Return size of any Python object.""" + if seen is None: + seen = set() + size = getsizeof(obj) + if id(obj) in seen: + return 0 + seen.add(id(obj)) + if hasattr(obj, "__dict__"): + size += get_size(obj.__dict__, seen) + if hasattr(obj, "__slots__"): + size += sum( + get_size(getattr(obj, attr), seen) for attr in obj.__slots__ if hasattr(obj, attr) + ) + if isinstance(obj, Mapping): + size += sum(get_size(k, seen) + get_size(v, seen) for k, v in obj.items()) + elif isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)): + size += sum(get_size(item, seen) for item in obj) + return size + + +if __name__ == "__main__": + unittest.main(verbosity=2) + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/helpers.py b/scripts/intermediate_oop/helpers.py new file mode 100755 index 0000000..ae3c71a --- /dev/null +++ b/scripts/intermediate_oop/helpers.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +"""Test helpers.""" +import sys + + +def error_message(): + print("Cannot run {} from the command-line.".format(sys.argv[0])) + print() + print("Run python test.py instead") diff --git a/scripts/intermediate_oop/inheritance.py b/scripts/intermediate_oop/inheritance.py new file mode 100755 index 0000000..faee490 --- /dev/null +++ b/scripts/intermediate_oop/inheritance.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""Inheritance exercises.""" + + +class CyclicList: + """Class with list-like structure that loops cyclicly.""" + + +class EasyDict: + """Class which allows both attribute and get/set item syntax.""" + + +class MinimumBalanceAccount: + """Bank account which does not allow balance to drop below zero.""" + + +class Node: + """Nodes for use in making hierarchies or trees.""" + + def __init__(self, name, *, ancestors=[]): + self.ancestors = list(ancestors) + self.name = name + + def ancestors_and_self(self): + """Return iterable with our ordered ancestors and our own node.""" + return [*self.ancestors, self] + + def make_child(self, *args, **kwargs): + """Create and return a child node of the current node.""" + return type(self)(*args, ancestors=self.ancestors_and_self(), **kwargs) + + def __str__(self): + """Return a slash-delimited ancestors hierarchy for this node.""" + return " / ".join([node.name for node in [*self.ancestors, self]]) + + def __repr__(self): + return self.name + + +class DoublyLinkedNode: + """Class with Nodes that are doubly-linked.""" + + +class Tree: + """Tree-like object.""" + + +class FieldTrackerMixin: + """Mixin for tracking specific attribute changes.""" + + +class LastUpdatedDictionary: + """Dictionary that maintains last-updated order of items.""" + + +class OrderedCounter: + """Counter that maintains last-updated item order.""" + + +class MaxCounter: + """Counter-like class that allows retrieving all maximums.""" diff --git a/scripts/intermediate_oop/inheritance_test.py b/scripts/intermediate_oop/inheritance_test.py new file mode 100755 index 0000000..c727887 --- /dev/null +++ b/scripts/intermediate_oop/inheritance_test.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- +"""Tests for inheritance exercises.""" +import unittest + +from inheritance import ( + CyclicList, + DoublyLinkedNode, + EasyDict, + FieldTrackerMixin, + LastUpdatedDictionary, + MaxCounter, + MinimumBalanceAccount, + Node, + OrderedCounter, + Tree, +) + + +class CyclicListTests(unittest.TestCase): + """Tests for CyclicList.""" + + def test_constructor(self): + CyclicList([1, 2, 3, 4]) + + def test_accepts_non_lists(self): + numbers = CyclicList({1, 2, 3}) + self.assertEqual(next(iter(numbers)), 1) + letters = CyclicList("hello") + self.assertEqual(next(iter(letters)), "h") + + def test_iterate_to_length(self): + numbers = CyclicList([1, 2, 3]) + i = iter(numbers) + self.assertEqual([next(i), next(i), next(i)], [1, 2, 3]) + + def test_iterate_past_length(self): + numbers = CyclicList([1, 2, 3]) + new_list = [x for x, _ in zip(numbers, range(10))] + self.assertEqual(new_list, [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]) + + def test_iterators_are_independent(self): + numbers = CyclicList([1, 2, 3, 4]) + i1 = iter(numbers) + i2 = iter(numbers) + self.assertEqual(next(i1), 1) + self.assertEqual(next(i1), 2) + self.assertEqual(next(i2), 1) + self.assertEqual(next(i2), 2) + + def test_length_append_and_pop(self): + numbers = CyclicList([1, 2, 3]) + self.assertEqual(len(numbers), 3) + numbers.append(4) + self.assertEqual(numbers.pop(), 4) + self.assertEqual(numbers.pop(0), 1) + + def test_supports_indexing(self): + numbers = CyclicList([1, 2, 3, 4]) + self.assertEqual(numbers[2], 3) + numbers = CyclicList([1, 2, 3, 4]) + self.assertEqual(numbers[4], 1) + self.assertEqual(numbers[-1], 4) + numbers[5] = 0 + self.assertEqual(numbers[1], 0) + + +class EasyDictTests(unittest.TestCase): + """Tests for EasyDict.""" + + def test_constructor(self): + EasyDict() + EasyDict({"a": 2, "b": 3}) + + def test_key_access(self): + d = EasyDict({"a": 2, "b": 3}) + self.assertEqual(d["a"], 2) + self.assertEqual(d["b"], 3) + + def test_attribute_access(self): + d = EasyDict({"a": 2, "b": 3}) + self.assertEqual(d.a, 2) + self.assertEqual(d.b, 3) + + def test_keyword_arguments(self): + d = EasyDict(a=2, b=3, c=4, d=5) + self.assertEqual(d.a, 2) + self.assertEqual(d.b, 3) + self.assertEqual(d["c"], 4) + self.assertEqual(d["d"], 5) + + def test_equality(self): + x = EasyDict({"a": 2, "b": 3}) + y = EasyDict({"a": 2, "b": 4}) + self.assertNotEqual(x, y) + self.assertNotEqual(x, {"a": 2, "b": 4}) + self.assertEqual(y, {"a": 2, "b": 4}) + y = EasyDict({"a": 2, "b": 3}) + self.assertEqual(x, y) + x = EasyDict({"a": 2, "b": 3, "c": 5}) + self.assertNotEqual(x, y) + y = EasyDict({"a": 2, "b": 3, "c": 5}) + self.assertEqual(x, y) + self.assertNotEqual(x, (1, 2)) + + def test_get_method(self): + x = EasyDict({"a": 2, "b": 4}) + self.assertIsNone(x.get("d")) + self.assertEqual(x.get("b"), 4) + self.assertEqual(x.get("c", 5), 5) + + def test_original_dictionary_unchanged(self): + mapping = {"a": 2, "b": 3} + d = EasyDict(mapping) + mapping["c"] = 4 + self.assertEqual(d, {"a": 2, "b": 3}) + + +class MinimumBalanceAccountTests(unittest.TestCase): + """Tests for MinimumBalanceAccount.""" + + def test_withdraw_from_new_account(self): + account = MinimumBalanceAccount() + with self.assertRaises(ValueError): + account.withdraw(1) + + def test_exception_message(self): + account = MinimumBalanceAccount() + with self.assertRaises(ValueError) as cm: + account.withdraw(1000) + self.assertEqual(str(cm.exception), "Balance cannot be less than $0") + + def test_withdraw_above_zero(self): + account = MinimumBalanceAccount() + account.deposit(100) + account.withdraw(99) + self.assertEqual(account.balance, 1) + + def test_withdraw_to_exactly_zero(self): + account = MinimumBalanceAccount() + account.deposit(100) + account.withdraw(100) + self.assertEqual(account.balance, 0) + + def test_withdraw_to_below_zero(self): + account = MinimumBalanceAccount() + account.deposit(100) + with self.assertRaises(ValueError): + account.withdraw(101) + + def test_repr(self): + account = MinimumBalanceAccount() + self.assertEqual(repr(account), "MinimumBalanceAccount(balance=0)") + + +class NodeTests(unittest.TestCase): + """Tests for Node.""" + + def test_single_node(self): + self.assertEqual(str(Node("A")), "A") + + def test_multiple_nodes(self): + expected = ( + "Animalia / Chordata / Mammalia / Carnivora / Ailuridae " "/ Ailurus / A. fulgens" + ) + red_panda = ( + Node("Animalia") + .make_child("Chordata") + .make_child("Mammalia") + .make_child("Carnivora") + .make_child("Ailuridae") + .make_child("Ailurus") + .make_child("A. fulgens") + ) + self.assertEqual(str(red_panda), expected) + + +class DoublyLinkedNodeTests(unittest.TestCase): + """Tests for DoublyLinkedNode.""" + + def test_single_node(self): + t = DoublyLinkedNode("A") + leaves = [node.name for node in t.leaves()] + self.assertEqual(leaves, ["A"]) + self.assertIs(t.is_leaf(), True) + + def test_multiple_nodes(self): + root = DoublyLinkedNode("A") + child1 = root.make_child("1") + grandchild1 = child1.make_child("a") + grandchild2 = child1.make_child("b") + child2 = root.make_child("2") + leaves0 = [node.name for node in root.leaves()] + leaves1 = [node.name for node in child1.leaves()] + leaves2 = [node.name for node in child2.leaves()] + self.assertEqual(leaves0, ["a", "b", "2"]) + self.assertEqual(leaves1, ["a", "b"]) + self.assertEqual(leaves2, ["2"]) + self.assertIs(grandchild1.is_leaf(), True) + self.assertIs(grandchild2.is_leaf(), True) + self.assertIs(child1.is_leaf(), False) + self.assertIs(child2.is_leaf(), True) + + +class DBModel: + def __init__(self, **kwargs): + self.id = None + for name, value in kwargs.items(): + setattr(self, name, value) + + def save(self): + self.id = 4 # This would be auto-generated normally + self.stored = True # Pretending to put stuff in a database + + +class TreeTests(unittest.TestCase): + """Tests for Tree.""" + + def test_set_and_delete_item(self): + felidae = Tree() + felidae["panthera"] = ["lion"] + felidae["felis"] = ["cat"] + self.assertEqual(felidae["panthera"], ["lion"]) + self.assertEqual(felidae["felis"], ["cat"]) + del felidae["felis"] + self.assertNotEqual(felidae["felis"], ["cat"]) + + def test_get_missing_item(self): + artiodactyla = Tree() + cetacea = artiodactyla["cetacea"] + self.assertEqual(artiodactyla["cetacea"], cetacea) + self.assertIsNot(artiodactyla["camelids"], cetacea) + + def test_modifying_deeply_nested_items(self): + mammals = Tree() + mammals["carnivora"]["canidae"]["canis"] = ["coyote"] + mammals["carnivora"]["canidae"]["canis"].append("wolf") + self.assertEqual( + mammals["carnivora"]["canidae"]["canis"], + ["coyote", "wolf"], + ) + + def test_repr(self): + mammals = Tree() + mammals["artiodactyla"]["camelidae"]["lama"] = ["Guanaco", "llama"] + dictionary = { + "artiodactyla": { + "camelidae": { + "lama": ["Guanaco", "llama"], + }, + }, + } + self.assertIn("artiodactyla", repr(mammals)) + self.assertIn("camelidae", repr(mammals)) + self.assertIn("lama", repr(mammals)) + self.assertIn("['Guanaco', 'llama']", repr(mammals)) + + def test_getting_and_setting_and_deleting_attributes(self): + mammals = Tree() + + # Accessing as attributes + mammals["artiodactyla"]["camelidae"]["lama"] = ["Guanaco", "llama"] + self.assertEqual( + mammals.artiodactyla.camelidae.lama, + ["Guanaco", "llama"], + ) + + # Assigning as attributes + mammals.carnivora.canidae.canis = ["coyote"] + mammals["carnivora"]["canidae"]["canis"].append("wolf") + self.assertEqual( + mammals["carnivora"]["canidae"]["canis"], + ["coyote", "wolf"], + ) + self.assertEqual( + mammals.carnivora.canidae.canis, + ["coyote", "wolf"], + ) + + def test_initialize_and_update_should_copy(self): + # Initializing tree-of-trees with dict-of-dicts + mammals = Tree( + { + "artiodactyla": { + "camelidae": { + "lama": ["Guanaco", "llama"], + }, + }, + } + ) + self.assertEqual( + mammals.artiodactyla.camelidae.lama, + ["Guanaco", "llama"], + ) + mammals.carnivora.canidae.canis = ["coyote", "wolf"] + self.assertEqual( + mammals["carnivora"]["canidae"]["canis"], + ["coyote", "wolf"], + ) + self.assertEqual( + mammals.carnivora.canidae.canis, + ["coyote", "wolf"], + ) + + # Updating with dict-of-dicts + mammals.update( + { + "carnivora": { + "prionodontidae": {"prionodon": ["pardicolor", "linsang"]}, + "canidae": { + "otocyon": ["megalotis"], + }, + }, + } + ) + self.assertEqual( + mammals.carnivora.prionodontidae.prionodon, + ["pardicolor", "linsang"], + ) + self.assertEqual(mammals.carnivora.canidae.otocyon, ["megalotis"]) + + # Still has the previous values also + self.assertEqual( + mammals.carnivora.canidae.canis, + ["coyote", "wolf"], + ) + + +class FieldTrackerMixinTests(unittest.TestCase): + """Tests for FieldTrackerMixin.""" + + def test_initializer(self): + class Person(FieldTrackerMixin, DBModel): + fields = ("id", "name", "email") + + trey = Person(name="Trey", email="trey@trey.com") + self.assertEqual(trey.name, "Trey") + self.assertEqual(trey.email, "trey@trey.com") + self.assertIsNone(trey.id) + + def test_previous_pre_and_post_save(self): + class Person(FieldTrackerMixin, DBModel): + fields = ("id", "name", "email") + + trey = Person(name="Trey", email="trey@trey.com") + self.assertEqual(trey.previous("email"), "trey@trey.com") + trey.email = "trey@gmail.com" + self.assertEqual(trey.previous("email"), "trey@trey.com") + trey.save() + self.assertEqual(trey.previous("email"), "trey@gmail.com") + self.assertEqual(trey.name, "Trey") + self.assertEqual(trey.email, "trey@gmail.com") + self.assertEqual(trey.id, 4) + + def test_has_changed_pre_and_post_save(self): + class Person(FieldTrackerMixin, DBModel): + fields = ("id", "name", "email") + + trey = Person(name="Trey", email="trey@trey.com") + self.assertFalse(trey.has_changed("email")) + trey.email = "trey@gmail.com" + self.assertTrue(trey.has_changed("email")) + trey.save() + self.assertFalse(trey.has_changed("email")) + self.assertEqual(trey.name, "Trey") + self.assertEqual(trey.email, "trey@gmail.com") + self.assertEqual(trey.id, 4) + + def test_changed_pre_and_post_save(self): + class Person(FieldTrackerMixin, DBModel): + fields = ("id", "name", "email") + + trey = Person(name="Trey", email="trey@trey.com") + self.assertEqual(trey.changed(), {}) + trey.email = "trey@gmail.com" + self.assertEqual(trey.changed(), {"email": "trey@trey.com"}) + trey.save() + self.assertEqual(trey.changed(), {}) + self.assertEqual(trey.name, "Trey") + self.assertEqual(trey.email, "trey@gmail.com") + self.assertEqual(trey.id, 4) + + +class LastUpdatedDictionaryTests(unittest.TestCase): + """Tests for LastUpdatedDictionary.""" + + def test_initial_order(self): + d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)]) + self.assertEqual(list(d.keys()), ["a", "c", "b", "d"]) + self.assertEqual(list(d.values()), [1, 3, 2, 4]) + + def test_order_after_insertion(self): + d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)]) + d["e"] = 5 + self.assertEqual(list(d.keys()), ["a", "c", "b", "d", "e"]) + self.assertEqual(list(d.values()), [1, 3, 2, 4, 5]) + + def test_order_after_update(self): + d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)]) + d["c"] = 0 + self.assertEqual(list(d.keys()), ["a", "b", "d", "c"]) + self.assertEqual(list(d.values()), [1, 2, 4, 0]) + + +class OrderedCounterTests(unittest.TestCase): + """Tests for OrderedCounter.""" + + def test_initial_order(self): + c = OrderedCounter("hello world") + self.assertEqual( + list(c.keys()), + ["h", "e", " ", "w", "o", "r", "l", "d"], + ) + self.assertEqual(list(c.values()), [1, 1, 1, 1, 2, 1, 3, 1]) + + def test_order_after_insertion(self): + c = OrderedCounter("hello world") + c.update("cat") + self.assertEqual( + list(c.keys()), + ["h", "e", " ", "w", "o", "r", "l", "d", "c", "a", "t"], + ) + self.assertEqual(list(c.values()), [1, 1, 1, 1, 2, 1, 3, 1, 1, 1, 1]) + + def test_order_after_update(self): + c = OrderedCounter("hello world") + c.update("hey") + self.assertEqual( + list(c.keys()), + [" ", "w", "o", "r", "l", "d", "h", "e", "y"], + ) + self.assertEqual(list(c.values()), [1, 1, 2, 1, 3, 1, 2, 2, 1]) + + +class MaxCounterTests(unittest.TestCase): + """Tests for MaxCounter.""" + + def test_works_like_counter(self): + counts = MaxCounter("hello") + self.assertEqual(counts, {"h": 1, "e": 1, "l": 2, "o": 1}) + self.assertEqual(counts["h"], 1) + self.assertEqual(counts["!"], 0) + + def test_single_maximum(self): + counts = MaxCounter("hello") + self.assertEqual(set(counts.max_keys()), {"l"}) + + def test_multiple_maximums(self): + counts = MaxCounter("no banana") + self.assertEqual(set(counts.max_keys()), {"a", "n"}) + + def test_all_maximums(self): + counts = MaxCounter("abcd") + self.assertEqual(set(counts.max_keys()), set("abcd")) + + def test_empty(self): + counts = MaxCounter("") + self.assertEqual(set(counts.max_keys()), set()) + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/initial.py b/scripts/intermediate_oop/initial.py new file mode 100755 index 0000000..473dda9 --- /dev/null +++ b/scripts/intermediate_oop/initial.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +"""Function for Initial Framework Test Check.""" + + +def is_ok(): + """Confirm Test Framework.""" + return ( + "Congrats and welcome to the Test Framework! \n" + "The message confirms the Test Framework is working! Yay! \n" + "Pat yourself on the back for successful installation! \n" + "Continue reading this section of the course instructions. \n" + "We will get started in a moment." + ) diff --git a/scripts/intermediate_oop/initial_test.py b/scripts/intermediate_oop/initial_test.py new file mode 100755 index 0000000..a35d58b --- /dev/null +++ b/scripts/intermediate_oop/initial_test.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""Test to confirm that the test framework is working.""" +import unittest + +from initial import is_ok + + +class InitialTests(unittest.TestCase): + """Tests for is_ok.""" + + def test_confirm_test(self): + """Test passed.""" + confirm = is_ok() + print(confirm) + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/main.py b/scripts/intermediate_oop/main.py new file mode 100644 index 0000000..db1473f --- /dev/null +++ b/scripts/intermediate_oop/main.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from classes import BankAccount +from properties import Circle + +trey_account = BankAccount(20) + +print(trey_account._balance) +# 20 + +trey_account.deposit(100) +print(trey_account._balance) + +# trey_account.balance +# 120 + +trey_account.withdraw(40) +print(trey_account._balance) + +# trey_account.balance +# 80 +print(trey_account) +# BankAccount(balance=80) + +print(repr(trey_account)) + +mary_account = BankAccount(100) +print(mary_account._balance) + +dana_account = BankAccount() +print(dana_account._balance) + +mary_account.transfer(dana_account, 20) + +print(mary_account._balance) +print(dana_account._balance) + +print("Print all attributes") +print(trey_account.__dict__) + +my_account = BankAccount(10) + +my_account.deposit(100) + +my_account.withdraw(40) + +my_account.deposit(95) + +print(my_account.transactions) +[("OPEN", 10, 10), ("DEPOSIT", 100, 110), ("WITHDRAWAL", -40, 70), ("DEPOSIT", 95, 165)] + +circle = Circle() + +print(circle.radius) +print(circle.diameter) +print(circle.area) + +circle.radius = 10 +print(circle.radius) +print(circle.diameter) +print(circle.area) + +circle.diameter = 10 +print(circle.radius) +print(circle.diameter) +print(circle.area) + +print(mary_account > dana_account) + diff --git a/scripts/intermediate_oop/main_abstract_classes.py b/scripts/intermediate_oop/main_abstract_classes.py new file mode 100644 index 0000000..3df2b92 --- /dev/null +++ b/scripts/intermediate_oop/main_abstract_classes.py @@ -0,0 +1,31 @@ +from classes import BankAccount +import abc + +class MinimumBankAccount(BankAccount): + + def withdraw(self, amount): + if amount > self.balance: + raise ValueError(f"Can't withdraw {amount} (balance is €{self._balance})") + super().withdraw(amount) + +class Duck(abc.ABC): + @abc.abstractmethod + def quack(self): + pass + +class Goose(Duck): + def quack(self): + print("Quack") + + +if __name__ == '__main__': + account = MinimumBankAccount(10) + try: + account.withdraw(100) + except: + account.withdraw(5) + print(account.balance) + print(account) + + goose = Goose() + goose.quack() \ No newline at end of file diff --git a/scripts/intermediate_oop/main_class_methods.py b/scripts/intermediate_oop/main_class_methods.py new file mode 100644 index 0000000..dc16ac3 --- /dev/null +++ b/scripts/intermediate_oop/main_class_methods.py @@ -0,0 +1,34 @@ + +import math + +class Circle: + """Circle with radius, area, etc.""" + + def __init__(self, radius=1): + self.radius = radius + + @property + def area(self): + area = math.pi * self.radius**2 + return area + + @property + def diameter(self): + diameter = self.radius * 2 + return diameter + + @diameter.setter + def diameter(self, diameter): + self.radius = diameter / 2 + + @classmethod # Typically used Alternative creation class methods, different from __init__ + def from_area(cls, area): + radius = math.sqrt(area / math.pi) + return cls(radius=radius) + + +if __name__ == '__main__': + circle = Circle.from_area(area=100) + print(circle.radius) + print(circle.diameter) + print(circle.area) \ No newline at end of file diff --git a/scripts/intermediate_oop/main_dataclass.py b/scripts/intermediate_oop/main_dataclass.py new file mode 100644 index 0000000..d2c5797 --- /dev/null +++ b/scripts/intermediate_oop/main_dataclass.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass, field +from decimal import Decimal +import math + +# ``dataclass`` provides many functionalities automatically +@dataclass +class Point: + x: float + y: float + + @property + def magnitude(self): + return math.sqrt(self.x**2 + self.y**2) + +@dataclass(frozen=True) +class Item: + """ + frozen: inmutable + """ + name: str + price: Decimal = Decimal(0) + colors: str = field(default_factory=list) + + @property + def amount(self): + return f"€{self.price:,.2f}" + +if __name__ == "__main__": + point = Point(x=1, y=1) + print(point) + print(point.magnitude) + + duck = Item(name='duck', price=5, colors=['purple', 'red']) + + \ No newline at end of file diff --git a/scripts/intermediate_oop/main_dunder.py b/scripts/intermediate_oop/main_dunder.py new file mode 100644 index 0000000..d807694 --- /dev/null +++ b/scripts/intermediate_oop/main_dunder.py @@ -0,0 +1,25 @@ +from dunder import ReverseView + +numbers = [2, 1, 3, 4, 7, 11] +reverse_numbers = ReverseView(numbers) + +print(list(reverse_numbers)) +# [11, 7, 4, 3, 1, 2] + +print(str(reverse_numbers)) +# '[11, 7, 4, 3, 1, 2]' + +print(reverse_numbers[0]) +# 11 + +print(reverse_numbers[-1]) +# 2 + +print(len(reverse_numbers)) +# 6 + + +numbers.append(18) + +print(list(reverse_numbers)) +# [18, 11, 7, 4, 3, 1, 2] diff --git a/scripts/intermediate_oop/properties.py b/scripts/intermediate_oop/properties.py new file mode 100755 index 0000000..260a4cf --- /dev/null +++ b/scripts/intermediate_oop/properties.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +"""Property exercises.""" +import math + + +class Circle: + """Circle with radius, area, etc.""" + + def __init__(self, radius=1): + self.radius = radius + + @property + def area(self): + area = math.pi * self.radius**2 + return area + + @property + def diameter(self): + diameter = self.radius * 2 + return diameter + + @diameter.setter + def diameter(self, diameter): + self.radius = diameter / 2 + + +class Vector: + """Class representing a 3 dimensional vector.""" + + +class Person: + """Person with first and last name.""" diff --git a/scripts/intermediate_oop/properties_test.py b/scripts/intermediate_oop/properties_test.py new file mode 100755 index 0000000..c5f2917 --- /dev/null +++ b/scripts/intermediate_oop/properties_test.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +"""Tests for property exercises.""" +import math +import unittest + +from properties import Circle, Person, Vector + + +class CircleTests(unittest.TestCase): + """Tests for Circle.""" + + def test_radius(self): + circle = Circle(5) + self.assertEqual(circle.radius, 5) + + def test_default_radius(self): + circle = Circle() + self.assertEqual(circle.radius, 1) + + def test_diameter_changes(self): + circle = Circle(2) + self.assertEqual(circle.diameter, 4) + circle.radius = 3 + self.assertEqual(circle.diameter, 6) + + def test_set_diameter(self): + circle = Circle(2) + self.assertEqual(circle.diameter, 4) + circle.diameter = 3 + self.assertEqual(circle.radius, 1.5) + + def test_area(self): + circle = Circle(2) + self.assertEqual(circle.area, math.pi * 4) + + @unittest.skip("Log Radius Changes") + def test_radius_changes_logged(self): + circle = Circle(2) + self.assertEqual(circle.radius_changes, [2]) + circle.radius = 3 + self.assertEqual(circle.radius_changes, [2, 3]) + circle.diameter = 3 + self.assertEqual(circle.radius_changes, [2, 3, 1.5]) + + @unittest.skip("Set Radius Error") + def test_no_negative_radius(self): + circle = Circle(2) + with self.assertRaises(ValueError) as context: + circle.radius = -10 + self.assertEqual(str(context.exception), "Radius cannot be negative!") + + +class VectorTests(unittest.TestCase): + """Tests for Vector.""" + + def test_attributes(self): + v = Vector(1, 2, 3) + self.assertEqual((v.x, v.y, v.z), (1, 2, 3)) + + def test_magnitude_property(self): + v = Vector(2, 3, 6) + self.assertEqual(v.magnitude, 7.0) + try: + v.y = 9 + except AttributeError: + v = Vector(2, 9, 6) + self.assertEqual(v.magnitude, 11.0) + + def test_no_weird_extras(self): + v1 = Vector(1, 2, 3) + v2 = Vector(4, 5, 6) + with self.assertRaises(TypeError): + len(v1) + with self.assertRaises(TypeError): + v1 < v2 + with self.assertRaises(TypeError): + v1 > v2 + with self.assertRaises(TypeError): + v1 <= v2 + with self.assertRaises(TypeError): + v1 >= v2 + with self.assertRaises(TypeError): + v1 + (1, 2, 3) + with self.assertRaises(TypeError): + (1, 2, 3) + v1 + with self.assertRaises(TypeError): + v1 - (1, 2, 3) + with self.assertRaises(TypeError): + v1 * "a" + with self.assertRaises(TypeError): + v1 / v2 + + @unittest.skip("Vector Equality") + def test_equality_and_inequality(self): + self.assertNotEqual(Vector(1, 2, 3), Vector(1, 2, 4)) + self.assertEqual(Vector(1, 2, 3), Vector(1, 2, 3)) + self.assertFalse(Vector(1, 2, 3) != Vector(1, 2, 3)) + v1 = Vector(1, 2, 3) + v2 = Vector(1, 2, 4) + v3 = Vector(1, 2, 3) + self.assertNotEqual(v1, v2) + self.assertEqual(v1, v3) + + @unittest.skip("Vector Adding") + def test_shifting(self): + v1 = Vector(1, 2, 3) + v2 = Vector(4, 5, 6) + v3 = v2 + v1 + v4 = v3 - v1 + self.assertEqual((v3.x, v3.y, v3.z), (5, 7, 9)) + self.assertEqual((v4.x, v4.y, v4.z), (v2.x, v2.y, v2.z)) + + @unittest.skip("Vector Multiplying") + def test_scaling(self): + v1 = Vector(1, 2, 3) + v2 = Vector(4, 5, 6) + v3 = v1 * 4 + v4 = 2 * v2 + self.assertEqual((v3.x, v3.y, v3.z), (4, 8, 12)) + self.assertEqual((v4.x, v4.y, v4.z), (8, 10, 12)) + + @unittest.skip("Vector Iterability") + def test_multiple_assignment(self): + x, y, z = Vector(x=1, y=2, z=3) + self.assertEqual((x, y, z), (1, 2, 3)) + + @unittest.skip("Vector Immutability") + def test_immutability(self): + v1 = Vector(1, 2, 3) + with self.assertRaises(Exception): + v1.x = 4 + self.assertEqual(v1.x, 1) + + +class PersonTests(unittest.TestCase): + """Tests for Person.""" + + def test_construct(self): + Person("Trey", "Hunner") + + def test_first_and_last_name_attributes(self): + trey = Person("Trey", "Hunner") + self.assertEqual(trey.first_name, "Trey") + self.assertEqual(trey.last_name, "Hunner") + + def test_name_attribute(self): + trey = Person("Trey", "Hunner") + self.assertEqual(trey.name, "Trey Hunner") + + def test_change_names(self): + trey = Person("Trey", "Hunner") + trey.last_name = "Smith" + self.assertEqual(trey.name, "Trey Smith") + trey.first_name = "John" + self.assertEqual(trey.name, "John Smith") + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/refactoring.py b/scripts/intermediate_oop/refactoring.py new file mode 100755 index 0000000..2f6b13a --- /dev/null +++ b/scripts/intermediate_oop/refactoring.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +"""Refactoring exercises.""" +from email.parser import Parser + +try: + # Some versions of Anaconda are missing IMAP4_SSL + from imaplib import IMAP4_SSL +except ImportError: + pass + + +class Weekday: + """Class with attributes representing weekdays.""" + + +class NextDate: + """Answers questions about the next Monday/Tuesday/etc.""" + + +def next_date(): + """Returns next Monday/Tuesday/etc.""" + + +def days_until(): + """Returns days until next Monday/Tuesday/etc.""" + + +def next_tuesday(): + """Returns date of next Tuesday.""" + + +def days_to_tuesday(): + """Returns days until next Tuesday.""" + + +class IMAPChecker: + """Facilitate connection to IMAP server.""" + + +# Refactor the below functions into IMAPChecker methods + + +def get_connection(host, username, password): + """Initialize IMAP server and login.""" + server = IMAP4_SSL(host) + server.login(username, password) + server.select("inbox") + return server + + +def close_connection(server): + server.close() + server.logout() + + +def get_message_uids(server): + """Return unique identifiers for each message.""" + return server.uid("search", None, "ALL")[1][0].split() + + +def get_message(server, uid): + """Get email message identified by given UID.""" + result, data = server.uid("fetch", uid, "(RFC822)") + (_, message_text), _ = data + message = Parser().parsestr(message_text) + return message diff --git a/scripts/intermediate_oop/refactoring_test.py b/scripts/intermediate_oop/refactoring_test.py new file mode 100755 index 0000000..9cd0dab --- /dev/null +++ b/scripts/intermediate_oop/refactoring_test.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +"""Tests for refactoring exercises.""" +import unittest +from contextlib import contextmanager +from datetime import date +from unittest.mock import call, patch, sentinel + +from refactoring import IMAPChecker, Weekday + + +class NextDateTests(unittest.TestCase): + """Tests for NextDate.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_date_for_changing_time(self): + monday = NextDate(Weekday.MONDAY) + tuesday = NextDate(Weekday.TUESDAY) + wednesday = NextDate(Weekday.WEDNESDAY) + thursday = NextDate(Weekday.THURSDAY) + friday = NextDate(Weekday.FRIDAY) + saturday = NextDate(Weekday.SATURDAY) + sunday = NextDate(Weekday.SUNDAY) + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(monday.date(), date(2019, 9, 9)) + self.assertEqual(tuesday.date(), date(2019, 9, 3)) + self.assertEqual(wednesday.date(), date(2019, 9, 4)) + self.assertEqual(thursday.date(), date(2019, 9, 5)) + self.assertEqual(friday.date(), date(2019, 9, 6)) + self.assertEqual(saturday.date(), date(2019, 9, 7)) + self.assertEqual(sunday.date(), date(2019, 9, 8)) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(monday.date(), date(2019, 9, 9)) + self.assertEqual(tuesday.date(), date(2019, 9, 10)) + self.assertEqual(wednesday.date(), date(2019, 9, 11)) + self.assertEqual(thursday.date(), date(2019, 9, 5)) + self.assertEqual(friday.date(), date(2019, 9, 6)) + self.assertEqual(saturday.date(), date(2019, 9, 7)) + self.assertEqual(sunday.date(), date(2019, 9, 8)) + + def test_days_until(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(NextDate(Weekday.MONDAY).days_until(), 6) + self.assertEqual(NextDate(Weekday.TUESDAY).days_until(), 0) + self.assertEqual(NextDate(Weekday.WEDNESDAY).days_until(), 1) + + +class NextDateFunctionTests(unittest.TestCase): + """Tests for next_date.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_next_date(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(next_date(Weekday.MONDAY), date(2019, 9, 9)) + self.assertEqual(next_date(Weekday.TUESDAY), date(2019, 9, 3)) + self.assertEqual(next_date(Weekday.WEDNESDAY), date(2019, 9, 4)) + self.assertEqual(next_date(Weekday.THURSDAY), date(2019, 9, 5)) + self.assertEqual(next_date(Weekday.FRIDAY), date(2019, 9, 6)) + self.assertEqual(next_date(Weekday.SATURDAY), date(2019, 9, 7)) + self.assertEqual(next_date(Weekday.SUNDAY), date(2019, 9, 8)) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(next_date(Weekday.MONDAY), date(2019, 9, 9)) + self.assertEqual(next_date(Weekday.TUESDAY), date(2019, 9, 10)) + self.assertEqual(next_date(Weekday.WEDNESDAY), date(2019, 9, 11)) + self.assertEqual(next_date(Weekday.THURSDAY), date(2019, 9, 5)) + self.assertEqual(next_date(Weekday.FRIDAY), date(2019, 9, 6)) + self.assertEqual(next_date(Weekday.SATURDAY), date(2019, 9, 7)) + self.assertEqual(next_date(Weekday.SUNDAY), date(2019, 9, 8)) + + +class DaysUntilFunctionTests(unittest.TestCase): + """Tests for days_until.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_days_until(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(days_until(Weekday.MONDAY), 6) + self.assertEqual(days_until(Weekday.TUESDAY), 0) + self.assertEqual(days_until(Weekday.WEDNESDAY), 1) + self.assertEqual(days_until(Weekday.THURSDAY), 2) + self.assertEqual(days_until(Weekday.FRIDAY), 3) + self.assertEqual(days_until(Weekday.SATURDAY), 4) + self.assertEqual(days_until(Weekday.SUNDAY), 5) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(days_until(Weekday.MONDAY), 4) + self.assertEqual(days_until(Weekday.TUESDAY), 5) + self.assertEqual(days_until(Weekday.WEDNESDAY), 6) + self.assertEqual(days_until(Weekday.THURSDAY), 0) + self.assertEqual(days_until(Weekday.FRIDAY), 1) + self.assertEqual(days_until(Weekday.SATURDAY), 2) + self.assertEqual(days_until(Weekday.SUNDAY), 3) + + +class DaysToTuesdayTests(unittest.TestCase): + """Tests for days_to_tuesday.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_days_to_tuesday(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(days_to_tuesday(), 0) + self.assertEqual(days_to_tuesday(after_today=True), 7) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(days_to_tuesday(), 5) + self.assertEqual(days_to_tuesday(after_today=True), 5) + + +class NextTuesdayTests(unittest.TestCase): + """Tests for next_tuesday.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_next_tuesday(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(next_tuesday(), date(2019, 9, 3)) + self.assertEqual(next_tuesday(after_today=True), date(2019, 9, 10)) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(next_tuesday(), date(2019, 9, 10)) + self.assertEqual(next_tuesday(after_today=True), date(2019, 9, 10)) + + +def NextDate(*args, **kwargs): + """Call a fresh import of the nextdate.NextDate class.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.NextDate(*args, **kwargs) + + +def next_date(*args, **kwargs): + """Call a fresh import of the nextdate.next_date function.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.next_date(*args, **kwargs) + + +def days_until(*args, **kwargs): + """Call a fresh import of the nextdate.days_until function.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.days_until(*args, **kwargs) + + +def days_to_tuesday(*args, **kwargs): + """Call a fresh import of the nextdate.days_to_tuesday function.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.days_to_tuesday(*args, **kwargs) + + +def next_tuesday(*args, **kwargs): + """Call a fresh import of the nextdate.next_tuesday function.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.next_tuesday(*args, **kwargs) + + +@contextmanager +def patch_date(year, month, day, hour=0, minute=0): + """Monkey patch the current time to be the given time.""" + import datetime + from unittest.mock import patch + + date_args = year, month, day + time_args = hour, minute + + class FakeDate(datetime.date): + """A datetime.date class with mocked today method.""" + + @classmethod + def today(cls): + return cls(*date_args) + + class FakeDateTime(datetime.datetime): + """A datetime.datetime class with mocked today, now methods.""" + + @classmethod + def today(cls): + return cls(*date_args, *time_args) + + @classmethod + def now(cls): + return cls.today() + + def set_date(year, month, day, *rest): + nonlocal date_args, time_args + date_args = year, month, day + time_args = rest + + with patch("datetime.datetime", FakeDateTime): + with patch("datetime.date", FakeDate): + yield set_date + + +class IMAPCheckerTests(unittest.TestCase): + """Tests for IMAPChecker.""" + + def test_initialization(self): + host = "example.com" + with patch("classes.IMAP4_SSL", autospec=True) as imap_mock: + IMAPChecker(host) + self.assertEqual(imap_mock.mock_calls, [call(host)]) + + def test_authentication(self): + host = "example.com" + username = "user@example.com" + password = "password" + with patch("classes.IMAP4_SSL", autospec=True) as imap_mock: + checker = IMAPChecker(host) + checker.authenticate(username, password) + self.assertEqual( + imap_mock.mock_calls, + [ + call(host), + call().login(username, password), + call().select("inbox"), + ], + ) + + def test_get_message_uids(self): + host = "example.com" + with patch("classes.IMAP4_SSL", autospec=True) as imap_mock: + checker = IMAPChecker(host) + uids = checker.get_message_uids() + imap_mock.assert_has_calls([call().uid("search", None, "ALL")]) + self.assertEqual( + uids, + ( + imap_mock.return_value.uid.return_value.__getitem__.return_value.__getitem__.return_value.split.return_value + ), + ) + + def test_get_message(self): + host = "example.com" + uid = "uid1" + with patch("classes.IMAP4_SSL", autospec=True) as imap_mock: + with patch("classes.Parser", autospec=True) as parser_mock: + imap_mock.return_value.uid.return_value = [ + "", + (("", sentinel.MessageText), ""), + ] + parser_mock.return_value.parsestr.return_value = sentinel.M + checker = IMAPChecker(host) + message = checker.get_message(uid) + self.assertEqual(imap_mock.mock_calls, [call(host), call().uid("fetch", uid, "(RFC822)")]) + self.assertEqual( + parser_mock.mock_calls, + [ + call(), + call().parsestr(sentinel.MessageText), + ], + ) + self.assertEqual(message, sentinel.M) + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/test.py b/scripts/intermediate_oop/test.py new file mode 100755 index 0000000..bf6ea7d --- /dev/null +++ b/scripts/intermediate_oop/test.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import print_function + +import sys +import unittest + +from test_data import MODULES, TESTS + + +def get_test(obj_name): + if obj_name not in TESTS: + raise SystemExit("Test for {} doesn't exist.".format(obj_name)) + return unittest.defaultTestLoader.loadTestsFromName(TESTS[obj_name]) + + +def run_tests(tests): + test_suite = unittest.TestSuite(tests) + unittest.TextTestRunner().run(test_suite) + + +def print_object_names(): + for module, objects in MODULES.items(): + print("\n{}:\n".format(module)) + for obj in objects: + print(obj) + print() + + +def main(*arguments): + if not arguments: + print("Please select a thing to test") + print_object_names() + elif len(arguments) > 1: + print( + """ +Can only call test.py with one argument: the name of the exercise being tested + +Examples: + +- python test.py get_hypotenuse +- python test.py hello.py +- python test.py BankAccount + +This test script runs Trey's tests against your code. +The tests are written in files that end in "_test.py". + +If you'd like to test your code manually, you can either: + +1. Open a Python REPL, import your code, and execute it with specific arguments +2. Write your own test code at the bottom of your file (e.g. functions.py) and +run that file (e.g. "python functions.py"). + +Consult the website for instructions for running the exercises and ask Trey +for help when you get stuck. + """.strip() + ) + elif " " in arguments[0] or "(" in arguments[0] or "," in arguments[0]: + print("Invalid characters found: {}\n".format(arguments[0])) + print("This test script doesn't accept code, just an exercise name.\n") + print("Example usage:") + print("python test.py \n") + else: + [argument] = arguments + if argument.startswith(("modules/", "modules\\", "./modules/")): + argument = argument.split("/", 1)[1] + if argument == "--all": + arguments = list(TESTS) + else: + arguments = [argument] + tests = [get_test(arg) for arg in arguments] + print("Testing {}\n".format(", ".join(arguments))) + test_classes = set( + tuple(test.id().split(".")[:-1]) for suite in tests for test in suite._tests + ) + for module, cls in test_classes: + print("Running {} test class in {}.py\n".format(cls, module)) + run_tests(tests) + + +if __name__ == "__main__": + # Version check before all else + major, minor, micro, releaselevel, serial = sys.version_info + if (major, minor) < (3, 5): + print("You are running Python {0}.{1}".format(major, minor)) + print("Must use Python version 3.5 or above") + sys.exit(1) + main(*sys.argv[1:]) diff --git a/scripts/intermediate_oop/test_data.py b/scripts/intermediate_oop/test_data.py new file mode 100755 index 0000000..2e031d6 --- /dev/null +++ b/scripts/intermediate_oop/test_data.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +TESTS = { + "days_to_tuesday": "refactoring_test.DaysToTuesdayTests", + "days_until": "refactoring_test.DaysUntilFunctionTests", + "IMAPChecker": "refactoring_test.IMAPCheckerTests", + "next_date": "refactoring_test.NextDateFunctionTests", + "NextDate": "refactoring_test.NextDateTests", + "next_tuesday": "refactoring_test.NextTuesdayTests", + "is_ok": "initial_test.InitialTests", + "Comparator": "dunder_test.ComparatorTests", + "FancyDict": "dunder_test.FancyDictTests", + "reloopable": "dunder_test.ReloopableTests", + "ReverseView": "dunder_test.ReverseViewTests", + "RomanNumeral": "dunder_test.RomanNumeralTests", + "Timer": "dunder_test.TimerTests", + "CyclicList": "inheritance_test.CyclicListTests", + "DoublyLinkedNode": "inheritance_test.DoublyLinkedNodeTests", + "EasyDict": "inheritance_test.EasyDictTests", + "FieldTrackerMixin": "inheritance_test.FieldTrackerMixinTests", + "LastUpdatedDictionary": "inheritance_test.LastUpdatedDictionaryTests", + "MaxCounter": "inheritance_test.MaxCounterTests", + "MinimumBalanceAccount": "inheritance_test.MinimumBalanceAccountTests", + "Node": "inheritance_test.NodeTests", + "OrderedCounter": "inheritance_test.OrderedCounterTests", + "Tree": "inheritance_test.TreeTests", + "Circle": "properties_test.CircleTests", + "Person": "properties_test.PersonTests", + "Vector": "properties_test.VectorTests", + "BankAccount": "classes_test.BankAccountTests", + "Flavor": "classes_test.FlavorTests", + "IceCream": "classes_test.IceCreamTests", + "MinHeap": "classes_test.MinHeapTests", + "MonthDelta": "classes_test.MonthDeltaTests", + "Month": "classes_test.MonthTests", + "Row": "classes_test.RowTests", + "Size": "classes_test.SizeTests", + "SuperMap": "classes_test.SuperMapTests", +} + +MODULES = { + "classes": [ + "BankAccount", + "Flavor", + "IceCream", + "MinHeap", + "MonthDelta", + "Month", + "Row", + "Size", + "SuperMap", + ], + "dunder": ["Comparator", "FancyDict", "reloopable", "ReverseView", "RomanNumeral", "Timer"], + "inheritance": [ + "CyclicList", + "DoublyLinkedNode", + "EasyDict", + "FieldTrackerMixin", + "LastUpdatedDictionary", + "MaxCounter", + "MinimumBalanceAccount", + "Node", + "OrderedCounter", + "Tree", + ], + "initial": ["is_ok"], + "properties": ["Circle", "Person", "Vector"], + "refactoring": [ + "days_to_tuesday", + "days_until", + "IMAPChecker", + "next_date", + "NextDate", + "next_tuesday", + ], +} diff --git a/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_3.py b/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_3.py new file mode 100644 index 0000000..d521fe8 --- /dev/null +++ b/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_3.py @@ -0,0 +1,33 @@ +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + if len(s) == 0: return 0 + # starting index + longest_so_far = 1 + chars_in_substring = [] + prev_char = None + hash_map = {} + exit_condition = False + start_index = 0 + current_index = 0 + while not exit_condition: + for char in s[start_index:]: + if (char != prev_char) and (char not in chars_in_substring): + chars_in_substring.append(char) + prev_char = char + hash_map[char] = current_index + else: + if len(chars_in_substring) > longest_so_far: + longest_so_far = len(chars_in_substring) + chars_in_substring = [] + start_index = hash_map[char] + 1 + # char = s[hash_map[char] + 1] + # chars_in_substring.append(char) + prev_char = None + hash_map = {} + # start_index = hash_map[char] + 1 + continue + current_index += 1 + if len(chars_in_substring) > longest_so_far: + longest_so_far = len(chars_in_substring) + exit_condition = True + return longest_so_far \ No newline at end of file diff --git a/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_chatgpt.py b/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_chatgpt.py new file mode 100644 index 0000000..e356f78 --- /dev/null +++ b/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_chatgpt.py @@ -0,0 +1,23 @@ +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + if len(s) == 0: + return 0 + + longest_so_far = 0 + start_index = 0 + hash_map = {} + + for current_index, char in enumerate(s): + if char in hash_map and hash_map[char] >= start_index: + # Move the start to one position after the last occurrence + start_index = hash_map[char] + 1 + hash_map[char] = current_index + longest_so_far = max(longest_so_far, current_index - start_index + 1) + + return longest_so_far + +if __name__ == "__main__": + s = "abcabcbb" + solution = Solution() + answer = solution.lengthOfLongestSubstring(s) + print(answer) \ No newline at end of file diff --git a/scripts/leetcode/3-longest-substring-without-repeating-characters/test_solution_3.py b/scripts/leetcode/3-longest-substring-without-repeating-characters/test_solution_3.py new file mode 100644 index 0000000..a9732fe --- /dev/null +++ b/scripts/leetcode/3-longest-substring-without-repeating-characters/test_solution_3.py @@ -0,0 +1,8 @@ +# test_solution.py +from solution_3 import Solution + +def test_longest_substring(): + s = Solution() + assert s.lengthOfLongestSubstring("abcabcbb") == 3 + assert s.lengthOfLongestSubstring("bbbbb") == 1 + assert s.lengthOfLongestSubstring("pwwkew") == 3 \ No newline at end of file diff --git a/scripts/leetcode/367-valid-perfect-square/solution_367.py b/scripts/leetcode/367-valid-perfect-square/solution_367.py new file mode 100644 index 0000000..72632a0 --- /dev/null +++ b/scripts/leetcode/367-valid-perfect-square/solution_367.py @@ -0,0 +1,30 @@ +import pytest + +class Solution: + def isPerfectSquare(self, num: int) -> bool: + if num == 1: + return True + found_square = False + lower_limit = 10 ** ((len(str(num)) // 2) - 1) + if lower_limit < 1: lower_limit = 1 + n = lower_limit + while (n*n <= num): + if num % n == 0: + if n*n == num: + return True + n += 1 + return False + + +def test_perfect_square_true(): + s = Solution() + assert s.isPerfectSquare(9) == True + assert s.isPerfectSquare(16) == True + assert s.isPerfectSquare(1) == True + assert s.isPerfectSquare(10000) == True + +def test_perfect_square_false(): + s = Solution() + assert s.isPerfectSquare(14) == False + assert s.isPerfectSquare(2) == False + assert s.isPerfectSquare(9999) == False diff --git a/scripts/leetcode/367-valid-perfect-square/solution_chatgpt.py b/scripts/leetcode/367-valid-perfect-square/solution_chatgpt.py new file mode 100644 index 0000000..beba32c --- /dev/null +++ b/scripts/leetcode/367-valid-perfect-square/solution_chatgpt.py @@ -0,0 +1,31 @@ +class Solution: + def isPerfectSquare(self, num: int) -> bool: + left, right = 1, num + while left <= right: + mid = (left + right) // 2 + square = mid * mid + if square == num: + return True + elif square < num: + left = mid + 1 + else: + right = mid - 1 + return False + + +if __name__ == "__main__": + num = 9 + solution = Solution() + answer = solution.isPerfectSquare(num) + print(answer) + + num = 14 + solution = Solution() + answer = solution.isPerfectSquare(num) + print(answer) + + num = 16 + solution = Solution() + answer = solution.isPerfectSquare(num) + print(answer) + \ No newline at end of file diff --git a/scripts/leetcode/367-valid-perfect-square/test_solution_367.py b/scripts/leetcode/367-valid-perfect-square/test_solution_367.py new file mode 100644 index 0000000..c1f2e97 --- /dev/null +++ b/scripts/leetcode/367-valid-perfect-square/test_solution_367.py @@ -0,0 +1,15 @@ +# test_solution.py +from solution_367 import Solution + +def test_perfect_square_true(): + s = Solution() + assert s.isPerfectSquare(9) == True + assert s.isPerfectSquare(16) == True + assert s.isPerfectSquare(1) == True + assert s.isPerfectSquare(10000) == True + +def test_perfect_square_false(): + s = Solution() + assert s.isPerfectSquare(14) == False + assert s.isPerfectSquare(2) == False + assert s.isPerfectSquare(9999) == False diff --git a/scripts/oop_course/__pycache__/classes.cpython-39.pyc b/scripts/oop_course/__pycache__/classes.cpython-39.pyc new file mode 100644 index 0000000..405a74e Binary files /dev/null and b/scripts/oop_course/__pycache__/classes.cpython-39.pyc differ diff --git a/scripts/oop_course/__pycache__/game_refactor.cpython-39.pyc b/scripts/oop_course/__pycache__/game_refactor.cpython-39.pyc new file mode 100644 index 0000000..2eb6c29 Binary files /dev/null and b/scripts/oop_course/__pycache__/game_refactor.cpython-39.pyc differ diff --git a/scripts/oop_course/classes.py b/scripts/oop_course/classes.py new file mode 100644 index 0000000..04792ce --- /dev/null +++ b/scripts/oop_course/classes.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +import random +import time +from enum import Enum + + +class Condition(Enum): + NEW = 0 + GOOD = 1 + OKAY = 2 + BAD = 3 + + +class MethodNowAllowed(Exception): + pass + + +class Bike: + num_wheels = 2 + counter = 0 + + def __init__( + self, + description: str, + condition: Condition = Condition.GOOD, + sale_price: float = 100, + cost: float = 0, + year: int = 2015, + ): + """_summary_ + + :param description: _description_, defaults to "Really Beautiful" + :type description: str, optional + :param condition: _description_, defaults to "Perfect Condition" + :type condition: str, optional + :param sale_price: _description_, defaults to 0 + :type sale_price: float, optional + :param cost: _description_, defaults to 0 + :type cost: float, optional + :param year: _description_, defaults to 2015 + :type year: int, optional + """ + self.description = description + self.condition = condition + self._sale_price = None # Private + self.cost = cost + self._slow_attribute = None + + self.age = 2023 - year + self.sold = False + self.premium = None + self.update_sale_price(sale_price) + Bike.counter += 1 + + def update_sale_price(self, sale_price: float): + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + if sale_price <= 0: + raise ValueError("Sale price must be greater than zero.") + self._sale_price = sale_price + + def is_premium(self): + self.premium = "Yes" + + def sell(self) -> float: + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + self.sold = True + profit = self.sale_price - self.cost + print(f"Sold for a profit: {profit}") + return profit + + def service(self, cost, new_sale_price=None, new_condition=None): + self.cost += cost + if new_sale_price is not None: + self.update_sale_price(new_sale_price) + if new_condition is not None: + self.condition = new_condition + + @property + def sale_price(self): + return self._sale_price + + @sale_price.setter + def sale_price(self, sale_price: float): + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + if sale_price <= 0: + raise ValueError("Sale price must be greater than zero.") + self._sale_price = sale_price + + @property + def profit(self): + return self.sale_price - self.cost + + @property # Used when an attribute is computationally intensive to calculate. Cache version is accessible + def slow_attribute(self): + if self._slow_attribute is not None: + print("Slow Attribute was already cached") + return self._slow_attribute + else: + print("Calculating Slow Attribute") + time.sleep(1) + self._slow_attribute = "Set" + return self._slow_attribute + + @staticmethod + def sing_the_bike_song(): + print("Singing the Bike Song") + + def __add__(self, other): + if isinstance(other, Bike): + self.cost += other.cost + + def __del__(self): + Bike.counter -= 1 + print("Bike deleted") + + def __str__(self): + """Called when str() or print()""" + return f"{self.description}: ${self.sale_price}" + + def __repr__(self): + return f"Bike({self.description}, {self.condition}, {self.sale_price}, {self.cost})" + + @staticmethod + def get_test_bike(): + return Bike(condition=Condition.GOOD, sale_price=1000, cost=0, description=Bike.__name__) + + @classmethod + def get_test_object(cls): + return cls( + condition=random.choice(list(Condition)), + sale_price=1000, + cost=0, + description=f"{cls.__name__}", + ) + + def child_method(self): + raise NotImplementedError("Method not implemented") + + +class Unicycle(Bike): + num_wheels = 1 + + def child_method(self): + print("Implemented") + + +if __name__ == "__main__": + my_bike = Bike( + description="Black and Beautiful", + condition=Condition.BAD, + sale_price=1000, + cost=100, + year=1965, + ) + test_unicycle = Unicycle.get_test_object() + test_bike = Bike.get_test_bike() + print(test_unicycle) + print(test_bike) + print([test_unicycle, test_bike]) + print(my_bike.slow_attribute) + print(my_bike.slow_attribute) + test_unicycle.child_method() diff --git a/scripts/oop_course/game_procedural.py b/scripts/oop_course/game_procedural.py new file mode 100644 index 0000000..5fd4eb2 --- /dev/null +++ b/scripts/oop_course/game_procedural.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import random + +if __name__ == "__main__": + is_over = False + player1 = 0 + player2 = 0 + while not is_over: + print(f"Player 1: {player1}") + print(f"Player 2: {player2}") + player1 += random.randint(1, 6) + player2 += random.randint(1, 6) + if player1 >= 100 or player2 >= 100: + is_over = True + print(f"Player 1: {player1}") + print(f"Player 2: {player2}") diff --git a/scripts/oop_course/game_refactor.py b/scripts/oop_course/game_refactor.py new file mode 100644 index 0000000..b8d37ea --- /dev/null +++ b/scripts/oop_course/game_refactor.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import random + + +class Game: + is_over = False + + def play(): + player1 = Player(id=1) + player2 = Player(id=2) + while not Game.is_over: + print(player1) + player1.play_turn() + + print(player2) + player2.play_turn() + + if player1.wins >= 100 or player2.wins >= 100: + Game.is_over = True + + print(player1) + print(player2) + + if player1.wins > player2.wins: + print("Player 1 Wins") + elif player1.wins < player2.wins: + print("Player 2 Wins") + else: + print("It's a draw.") + + +class Player: + def __init__(self, id: int): + self.id = id + self.wins = 0 + + def play_turn(self): + self.wins += random.randint(1, 6) + + def __str__(self): + return f"Player {self.id} has a total of {self.wins} Wins" + + +if __name__ == "__main__": + Game.play() diff --git a/scripts/oreilly_decorators/exercises/decorators.py b/scripts/oreilly_decorators/exercises/decorators.py new file mode 100644 index 0000000..a286512 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/decorators.py @@ -0,0 +1,58 @@ +"""Decorator exercises""" +from functools import wraps +from dataclasses import dataclass +import json + +NO_RETURN = object() + +def count_calls(func): + """Record calls to the given function.""" + def decorated_func(*args, **kwargs): + decorated_func.calls += 1 + return func(*args, **kwargs) + decorated_func.calls = 0 + return decorated_func + +def jsonify(func): + """Decorate function to JSON-encode return value.""" + @wraps(func) + def dec_func(*args, **kwargs): + result = func(*args, **kwargs) + return json.dumps(result) + return dec_func + + +def groot(func): + """Return function which prints 'Groot' (ignore decoratee).""" + @wraps(func) + def dec_func(*args, **kwargs): + print("Groot") + return dec_func + +def four(func): + """Return 4 (ignore decorated function).""" + return 4 + +@dataclass +class Call: + args: tuple + kwargs: dict + +def record_calls(func): + """Recording number of times a decorated function is called.""" + @wraps(func) + def dec_func(*args, **kwargs): + dec_func.call_count += 1 + call = Call(args=args, kwargs=kwargs) + dec_func.calls.append(call) + try: + call.exception = None + call.return_value = func(*args, **kwargs) + except Exception as e: + call.exception = e + call.return_value = NO_RETURN + raise + return call.return_value + dec_func.call_count = 0 + dec_func.calls = [] + return dec_func diff --git a/scripts/oreilly_decorators/exercises/decorators_test.py b/scripts/oreilly_decorators/exercises/decorators_test.py new file mode 100644 index 0000000..f8547a5 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/decorators_test.py @@ -0,0 +1,247 @@ +"""Tests for decorator exercises""" +from contextlib import redirect_stdout +from io import StringIO +import unittest + +from decorators import ( + count_calls, + jsonify, + groot, + four, + record_calls, +) + + +class CountCallsTests(unittest.TestCase): + + """Test for count_calls.""" + + def test_accepts_a_function(self): + # Function value is returned + def one(): return 1 + decorated = count_calls(one) + self.assertEqual(decorated(), 1) + self.assertEqual(decorated.calls, 1) + + def test_calls_a_function(self): + # Function is called each time + recordings = [] + def my_func(): + recordings.append('call') + return recordings + decorated = count_calls(my_func) + self.assertEqual(recordings, []) + self.assertEqual(decorated.calls, 0) + self.assertEqual(decorated(), ['call']) + self.assertEqual(decorated.calls, 1) + self.assertEqual(decorated(), ['call', 'call']) + self.assertEqual(decorated.calls, 2) + + def test_accepts_arguments(self): + # Function accepts positional arguments + @count_calls + def add(x, y): + return x + y + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(1, 3), 4) + + # Function accepts keyword arguments + recordings = [] + @count_calls + def my_func(*args, **kwargs): + recordings.append((args, kwargs)) + return recordings + self.assertEqual(my_func(), [((), {})]) + self.assertEqual(my_func(1, 2, a=3), [((), {}), ((1, 2), {'a': 3})]) + + # Exceptions are still counted as calls + @count_calls + def my_func(): + raise AssertionError("Function called too soon") + self.assertEqual(my_func.calls, 0) + with self.assertRaises(AssertionError): + my_func() + self.assertEqual(my_func.calls, 1) + self.assertEqual(my_func.calls, 1) + with self.assertRaises(AssertionError): + my_func() + self.assertEqual(my_func.calls, 2) + + +class JSONifyTests(unittest.TestCase): + + """Tests for jsonify.""" + + def test_serialize_none(self): + func = jsonify(lambda: None) + self.assertEqual(func(), 'null') + + def test_serialize_list(self): + def make_list(): return [4, 'hi', True, 5.5] + make_list = jsonify(make_list) + self.assertEqual(make_list(), '[4, "hi", true, 5.5]') + + def test_returned(self): + def return_hi(): return 'hi' + return_hi = jsonify(return_hi) + self.assertEqual(return_hi(), '"hi"') + + def test_takes_arguments(self): + def add(x, y): return x + y + add = jsonify(add) + self.assertEqual(add(1, 2), '3') + self.assertEqual(add(x=1, y=2), '3') + + +class GrootTests(unittest.TestCase): + + """Tests for groot.""" + + def test_print_groot(self): + def greet(name): print("Hello {}".format(name)) + greet = groot(greet) + with redirect_stdout(StringIO()) as stdout: + greet("Trey") + self.assertEqual(stdout.getvalue(), "Groot\n") + + def test_nothing_returned(self): + def return_hi(): return 'hi' + return_hi = groot(return_hi) + with redirect_stdout(StringIO()) as stdout: + self.assertEqual(return_hi(), None) + + def test_function_ignored(self): + def return_hi(): dictionary['key'] = 'value' + dictionary = {} + return_hi = groot(return_hi) + with redirect_stdout(StringIO()) as stdout: + self.assertEqual(return_hi(), None) + self.assertEqual(dictionary, {}) + + def test_takes_arguments(self): + def add(x, y): return x + y + add = groot(add) + with redirect_stdout(StringIO()) as stdout: + add(1, 2) + add(x=1, y=2) + + +class FourTests(unittest.TestCase): + + """Tests for four.""" + + def test_return_four(self): + def greet(name): print("Hello {}".format(name)) + with redirect_stdout(StringIO()) as stdout: + greet = four(greet) + self.assertEqual(stdout.getvalue(), '') + self.assertEqual(greet, 4) + + +class RecordCallsTests(unittest.TestCase): + + """Tests for record_calls.""" + + def test_call_count_starts_at_zero(self): + decorated = record_calls(lambda: None) + self.assertEqual(decorated.call_count, 0) + + def test_not_called_on_decoration_time(self): + def my_func(): + raise AssertionError("Function called too soon") + record_calls(my_func) + + def test_function_still_callable(self): + recordings = [] + def my_func(): + recordings.append('call') + decorated = record_calls(my_func) + self.assertEqual(recordings, []) + decorated() + self.assertEqual(recordings, ['call']) + decorated() + self.assertEqual(recordings, ['call', 'call']) + + def test_return_value(self): + def one(): return 1 + one = record_calls(one) + self.assertEqual(one(), 1) + + def test_takes_arguments(self): + def add(x, y): return x + y + add = record_calls(add) + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(1, 3), 4) + + def test_takes_keyword_arguments(self): + recordings = [] + @record_calls + def my_func(*args, **kwargs): + recordings.append((args, kwargs)) + return recordings + self.assertEqual(my_func(), [((), {})]) + self.assertEqual(my_func(1, 2, a=3), [((), {}), ((1, 2), {'a': 3})]) + + def test_call_count_increments(self): + decorated = record_calls(lambda: None) + self.assertEqual(decorated.call_count, 0) + decorated() + self.assertEqual(decorated.call_count, 1) + decorated() + self.assertEqual(decorated.call_count, 2) + + def test_different_functions(self): + my_func1 = record_calls(lambda: None) + my_func2 = record_calls(lambda: None) + my_func1() + self.assertEqual(my_func1.call_count, 1) + self.assertEqual(my_func2.call_count, 0) + my_func2() + self.assertEqual(my_func1.call_count, 1) + self.assertEqual(my_func2.call_count, 1) + + def test_docstring_and_name_preserved(self): + import pydoc + decorated = record_calls(example) + self.assertIn('function example', str(decorated)) + documentation = pydoc.render_doc(decorated) + self.assertIn('function example', documentation) + self.assertIn('Example function.', documentation) + self.assertIn('(a, b=True)', documentation) + + def test_record_arguments(self): + @record_calls + def my_func(*args, **kwargs): return args, kwargs + self.assertEqual(my_func.calls, []) + my_func() + self.assertEqual(len(my_func.calls), 1) + self.assertEqual(my_func.calls[0].args, ()) + self.assertEqual(my_func.calls[0].kwargs, {}) + my_func(1, 2, a=3) + self.assertEqual(len(my_func.calls), 2) + self.assertEqual(my_func.calls[1].args, (1, 2)) + self.assertEqual(my_func.calls[1].kwargs, {'a': 3}) + + def test_record_return_values(self): + from decorators import NO_RETURN + @record_calls + def my_func(*args, **kwargs): return sum(args), kwargs + my_func() + self.assertEqual(my_func.calls[0].return_value, (0, {})) + my_func(1, 2, a=3) + self.assertEqual(my_func.calls[1].return_value, (3, {'a': 3})) + self.assertIs(my_func.calls[1].exception, None) + with self.assertRaises(TypeError) as context: + my_func(1, 'hi', a=3) + self.assertIs(my_func.calls[2].return_value, NO_RETURN) + self.assertEqual(my_func.calls[2].exception, context.exception) + + +def example(a, b=True): + """Example function.""" + print('hello world') + + +if __name__ == "__main__": + from helpers import error_message + error_message() diff --git a/scripts/oreilly_decorators/exercises/functions.py b/scripts/oreilly_decorators/exercises/functions.py new file mode 100644 index 0000000..264fe30 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/functions.py @@ -0,0 +1,44 @@ +"""Function object exercises""" + + +def call(func, *args, **kwargs): + """Call the function provided with the given arguments.""" + return func(*args, **kwargs) + + +def call_later(func, *args, **kwargs): + """Return a function to call given function with provided arguments.""" + def new_func(): + return func(*args, **kwargs) + return new_func + +def exclude(func, iterable): + """Only keep items which fail a given predicate test""" + return [x for x in iterable if not func(x)] + +def call_logger(func): + """Return a new function that calls func and prints when it was called.""" + def new_func(*args, **kwargs): + print("Function started") + result = func(*args, **kwargs) + print("Function returned") + return result + return new_func + +def call_again(func, *args): + """Return function return value and a function to call again.""" + def partial_func(): + return func(*args) + return (func(*args), partial_func) + + +def only_once(func): + """Return new version of the function that can only be called once.""" + func.call_count = 0 + def new_func(*args, **kwargs): + func.call_count += 1 + if func.call_count <= 1: + return func(*args, **kwargs) + else: + raise ValueError("You can't call this function twice!") + return new_func diff --git a/scripts/oreilly_decorators/exercises/functions_test.py b/scripts/oreilly_decorators/exercises/functions_test.py new file mode 100644 index 0000000..132ae31 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/functions_test.py @@ -0,0 +1,130 @@ +"""Tests for function object exercises""" +from contextlib import redirect_stdout +from io import StringIO +from textwrap import dedent +import unittest + +from functions import ( + call, + call_later, + exclude, + call_logger, + call_again, + only_once, +) + + +class CallTests(unittest.TestCase): + + """Tests for call.""" + + def test_int_call(self): + self.assertEqual(call(int), 0) + + def test_five_call(self): + self.assertEqual(call(int, "5"), 5) + + def test_hello_call(self): + self.assertEqual(call(len, "hello"), 5) + + def test_zip_call(self): + self.assertEqual(list(call(zip, [1, 2], [3, 4])), [(1, 3), (2, 4)]) + + +class CallLaterTests(unittest.TestCase): + + """Tests for call_later.""" + + def test_append_to_list(self): + names = [] + append_name = call_later(names.append, "Trey") + self.assertIsNone(append_name()) + self.assertEqual(names, ['Trey']) + append_name() + self.assertEqual(names, ['Trey', 'Trey']) + + def test_zip_later(self): + call_zip = call_later(zip, [1, 2], [3, 4]) + self.assertEqual(list(call_zip()), [(1, 3), (2, 4)]) + + +class ExcludeTests(unittest.TestCase): + + """Tests for exclude.""" + + def test_bool_exclude(self): + self.assertEqual( + exclude(bool, [False, True, False]), + [False, False], + ) + + def test_lambda_exclude(self): + self.assertEqual( + exclude(lambda x: len(x) > 3, ["red", "blue", "green"]), + ['red'], + ) + + +class CallLoggerTests(unittest.TestCase): + + """Tests for call_logger.""" + + def test_prints_before_and_after(self): + def greet(): print("Hello") + greet = call_logger(greet) + with redirect_stdout(StringIO()) as stdout: + greet() + self.assertEqual(stdout.getvalue(), dedent(""" + Function started + Hello + Function returned + """).lstrip()) + + def test_returned(self): + def return_hi(): return 'hi' + return_hi = call_logger(return_hi) + with redirect_stdout(StringIO()) as stdout: + self.assertEqual(return_hi(), 'hi') + self.assertEqual(stdout.getvalue(), dedent(""" + Function started + Function returned + """).lstrip()) + + def test_takes_arguments(self): + def add(x, y): return x + y + add = call_logger(add) + with redirect_stdout(StringIO()) as stdout: + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(x=1, y=2), 3) + + +class CallAgainTests(unittest.TestCase): + + """Tests for call_again.""" + + def test_str_on_list(self): + names = [] + response, names_as_str = call_again(str, names) + self.assertEqual(response, '[]') + names.append("Diane") + self.assertEqual(names_as_str(), "['Diane']") + + +class OnlyOnceTests(unittest.TestCase): + + """Tests for only_once.""" + + def test_do_once(self): + def do_something(x, y): + return x * 2 + y ** 2 + + do_something_once = only_once(do_something) + do_something_once(1, 2) + with self.assertRaises(ValueError): + do_something_once(1, 2) + + +if __name__ == "__main__": + from helpers import error_message + error_message() + diff --git a/scripts/oreilly_decorators/exercises/helpers.py b/scripts/oreilly_decorators/exercises/helpers.py new file mode 100644 index 0000000..186a2a8 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/helpers.py @@ -0,0 +1,8 @@ +"""Test helpers""" +import sys + + +def error_message(): + print("Cannot run {} from the command-line.".format(sys.argv[0])) + print() + print("Run python test.py instead") diff --git a/scripts/oreilly_decorators/exercises/initial.py b/scripts/oreilly_decorators/exercises/initial.py new file mode 100644 index 0000000..992d23c --- /dev/null +++ b/scripts/oreilly_decorators/exercises/initial.py @@ -0,0 +1,12 @@ +"""Function for Initial Framework Test Check.""" + + +def is_ok(): + """Confirm Test Framework.""" + return ( + 'Congrats and welcome to the Test Framework! \n' + 'The message confirms the Test Framework is working! Yay! \n' + 'Pat yourself on the back for successful installation! \n' + 'Continue reading this section of the course instructions. \n' + 'We will get started in a moment.' + ) diff --git a/scripts/oreilly_decorators/exercises/initial_test.py b/scripts/oreilly_decorators/exercises/initial_test.py new file mode 100644 index 0000000..6e2caf0 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/initial_test.py @@ -0,0 +1,17 @@ +"""Test to confirm that the test framework is working.""" +import unittest +from initial import is_ok + + +class InitialTests(unittest.TestCase): + """Tests for is_ok.""" + + def test_confirm_test(self): + """Test passed.""" + confirm = is_ok() + print(confirm) + + +if __name__ == '__main__': + from helpers import error_message + error_message() diff --git a/scripts/oreilly_decorators/exercises/main_decorators.py b/scripts/oreilly_decorators/exercises/main_decorators.py new file mode 100644 index 0000000..e4ff92a --- /dev/null +++ b/scripts/oreilly_decorators/exercises/main_decorators.py @@ -0,0 +1,86 @@ +from math import sqrt +from decorators import count_calls +from decorators import jsonify +from decorators import groot +from decorators import four +from decorators import record_calls +import json + +def call_logger(func): + """Return a new function that calls func and prints when it was called.""" + def new_func(*args, **kwargs): + print("Function started") + result = func(*args, **kwargs) + print(result) + print("Function returned") + return result + return new_func + +def alvaro(func): + """leave your mark in the function""" + func.alvaro_was_here = "Definitely True" + func.__doc__ = """This is the new docstring. Made definitely by Alvaro""" + return func + +if __name__ == '__main__': + + # @call_logger + # def add(x,y): + # return x+y + + # add(4,5) + + # @alvaro + # def add(x, y): + # return x+y + + # print(add.__doc__) + # print(add.alvaro_was_here) + + @count_calls + def quadratic(a, b, c): + x1 = -b / (2*a) + x2 = sqrt(b**2 - 4*a*c) / (2*a) + return (x1 + x2), (x1 - x2) + print(quadratic.calls) + print(quadratic(2, 8, 6)) + # (-1.0, -3.0) + print(quadratic(a=4, b=9, c=2)) + # (-0.25, -2.0) + print(quadratic.calls) + # 2 + + + @jsonify + def get_thing(): + return {'trey': "red", 'diane': "purple"} + + print(get_thing()) + # '{"trey": "red", "diane": "purple"} + + @groot + def greet(name): + print("Hello {}".format(name)) + + a = greet("Trey") + print(a) + + @record_calls + def greet(name="world"): + """Greet someone by their name.""" + print(f"Hello {name}") + + greet("Trey") + # Hello Trey + + print(greet.call_count) + # 1 + + print(greet(name="Trey")) + # Hello Trey + + print(greet.call_count) + # 2 + + print(greet.calls) + # [Call(args=('Trey',), kwargs={}), Call(args=(), kwargs={'name': 'Trey'})] \ No newline at end of file diff --git a/scripts/oreilly_decorators/exercises/main_functions.py b/scripts/oreilly_decorators/exercises/main_functions.py new file mode 100644 index 0000000..cf6dc73 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/main_functions.py @@ -0,0 +1,73 @@ +from functions import call +from functions import call_later +from functions import exclude +from functions import call_logger +from functions import call_again +from functions import only_once + + +print(call(int)) +# 0 + +print(call(int, "5")) +# 5 + +print(call(len, "hello")) +# 5 + +print(list(call(zip, [1, 2], [3, 4]))) +# [(1, 3), (2, 4)] + +names = [] +append_name = call_later(names.append, "Trey") +append_name() +print(names) +# ['Trey'] + +append_name() +print(names) +# ['Trey', 'Trey'] + +call_zip = call_later(zip, [1, 2], [3, 4]) + +print(list(call_zip())) +# [(1, 3), (2, 4)] + + +print(exclude(bool, [False, True, False])) +# [False, False] + +print(exclude(lambda x: len(x) > 3, ["red", "blue", "green"])) +# ['red'] + + +def greet(): print("Hello") + +greet_now = call_logger(greet) + +greet_now() +# Function started +# Hello +# Function returned + +names = [] +response, names_as_str = call_again(str, names) +print(response) +# '[]' +names.append("Diane") +print(names_as_str()) +# "['Diane']" + + +def do_something(x, y): + print(f"doing something with {x} and {y}") + return x * 2 + y ** 2 +do_something_once = only_once(do_something) +print(do_something_once(1, 2)) +# doing something with 1 and 2 +# 6 + +try: + do_something_once(1, 2) +except ValueError as e: + print(e) \ No newline at end of file diff --git a/scripts/oreilly_decorators/exercises/more.py b/scripts/oreilly_decorators/exercises/more.py new file mode 100644 index 0000000..401c693 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/more.py @@ -0,0 +1,17 @@ +"""More advanced decorator exercises""" + + +def coalesce_all(): + """Decorator to coalesces arguments to default when sentinel is given.""" + + +def lazy_repr(): + """Class decorator that adds a __repr__ method automatically.""" + + +def positional_only(): + """Specify arguments to a function positionally only.""" + + +def at(): + """Chain all given decorators together.""" diff --git a/scripts/oreilly_decorators/exercises/more_test.py b/scripts/oreilly_decorators/exercises/more_test.py new file mode 100644 index 0000000..80ffd2b --- /dev/null +++ b/scripts/oreilly_decorators/exercises/more_test.py @@ -0,0 +1,214 @@ +"""Tests for more advanced decorator exercises""" +import math +import unittest + +from more import ( + coalesce_all, + lazy_repr, + positional_only, + at, +) + + +class CoalesceAllTests(unittest.TestCase): + + """Tests for coalesce_all.""" + + def test_coalesce_one_argument(self): + @coalesce_all("world") + def greet(greet="world"): + return "Hello {}".format(greet) + self.assertEqual(greet("Trey"), "Hello Trey") + self.assertEqual(greet("someone"), "Hello someone") + self.assertEqual(greet(""), "Hello ") + self.assertEqual(greet(None), "Hello world") + self.assertEqual(greet(), "Hello world") + + def test_coalesce_one_argument_from_empty_string(self): + @coalesce_all("world", sentinel="") + def greet(greet="world"): + return "Hello {}".format(greet) + self.assertEqual(greet("Trey"), "Hello Trey") + self.assertEqual(greet("someone"), "Hello someone") + self.assertEqual(greet(""), "Hello world") + self.assertEqual(greet(None), "Hello None") + self.assertEqual(greet(), "Hello world") + + def test_coalesce_multiple_arguments(self): + @coalesce_all(1) + def multiply(x, y): + return x * y + self.assertEqual(multiply(2, 3), 6) + self.assertEqual(multiply(None, None), 1) + self.assertEqual(multiply(2, None), 2) + self.assertEqual(multiply(None, 3), 3) + with self.assertRaises(TypeError): + multiply(4) + with self.assertRaises(TypeError): + multiply() + + def test_coalesce_keyword_arguments(self): + @coalesce_all(1) + def multiply(x, y): + return x * y + self.assertEqual(multiply(x=2, y=3), 6) + self.assertEqual(multiply(2, y=3), 6) + self.assertEqual(multiply(x=None, y=None), 1) + self.assertEqual(multiply(x=2, y=None), 2) + self.assertEqual(multiply(2, y=None), 2) + self.assertEqual(multiply(x=None, y=3), 3) + self.assertEqual(multiply(None, y=3), 3) + with self.assertRaises(TypeError): + multiply(x=4) + with self.assertRaises(TypeError): + multiply(y=4) + + +class LazyReprTests(unittest.TestCase): + + """Tests for lazy_repr.""" + + def test_with_concrete_attributes(self): + @lazy_repr + class Point: + def __init__(self, x, y, z): + self.x, self.y, self.z = x, y, z + self.assertEqual(str(Point(1, 2, 3)), "Point(x=1, y=2, z=3)") + self.assertEqual(repr(Point(x=3, y=4, z=5)), "Point(x=3, y=4, z=5)") + + def test_argument_without_an_attribute(self): + @lazy_repr + class BankAccount: + def __init__(self, opening_balance): + self.balance = opening_balance + + self.assertEqual(str(BankAccount(10)), "BankAccount(balance=10)") + self.assertEqual(repr(BankAccount(10)), "BankAccount(balance=10)") + + +class PositionalOnlyTests(unittest.TestCase): + + """Tests for positional_only.""" + + def test_restrict_all_arguments_to_positional(self): + @positional_only + def divide(x, y): return x / y + self.assertEqual(divide(21, 3), 7) + self.assertEqual(divide(5, 2), 2.5) + with self.assertRaises(TypeError): + divide(x=5, y=2) + with self.assertRaises(TypeError): + divide(5, y=2) + with self.assertRaises(TypeError): + divide(5, x=2) + with self.assertRaises(TypeError): + divide(1, 2, 3) + with self.assertRaises(TypeError): + divide(1) + + def test_no_keyword_arguments_allowed(self): + @positional_only + def my_func(a, b=2, **kwargs): return a + self.assertEqual(my_func(3), 3) + with self.assertRaises(TypeError): + my_func(3, b=3) + with self.assertRaises(TypeError): + my_func(3, a=3) + + def test_any_number_of_positional_arguments(self): + @positional_only + def add(a, b, *args): return a + b + sum(args) + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(1, 2, 3, 4), 10) + self.assertEqual(add(1, 2, 3), 6) + @positional_only + def product(*numbers): + total = 1 + for n in numbers: + total *= n + return total + self.assertEqual(product(1, 2, 3, 4), 24) + self.assertEqual(product(1, 2), 2) + self.assertEqual(product(10), 10) + self.assertEqual(product(), 1) + with self.assertRaises(TypeError): + add(1, 2, b=3) + with self.assertRaises(TypeError): + product(numbers=3) + + def test_with_positional_argument_count(self): + @positional_only(3) + def add(a, b, c, d): return a + b + c + d + self.assertEqual(add(1, 2, 3, 4), 10) + self.assertEqual(add(1, 2, 3, d=4), 10) + with self.assertRaises(TypeError): + add(1, 2, c=3, d=4) + with self.assertRaises(TypeError): + add(1, b=2, c=3, d=4) + with self.assertRaises(TypeError): + add(a=1, b=2, c=3, d=4) + @positional_only(2) + def divide(x=1, y=1): return x / y + self.assertEqual(divide(3, 2), 1.5) + with self.assertRaises(TypeError): + divide(x=3, y=2) + with self.assertRaises(TypeError): + divide(3, y=2) + + +class AtTests(unittest.TestCase): + + """Tests for at.""" + + def test_one_decorator(self): + @at(reprify) + def add(x, y): + return x + y + self.assertEqual(add(1, 2), '3') + self.assertEqual(add(1, 3), '4') + self.assertEqual(add(x=2, y=3), '5') + + def test_two_decorators(self): + @at(sqrtd, reprify) + def add(x, y): + return x + y + self.assertEqual(add(1, 3), '2.0') + self.assertEqual(add(x=20, y=5), '5.0') + + @at(sqrtd, of_squares) + def add(x, y): + return x + y + self.assertEqual(add(3, 4), 5) + self.assertEqual(add(8, 15), 17) + + def test_three_decorators(self): + @at(sqrtd, of_squares, reprify) + def add(x, y): + return x + y + self.assertEqual(add(3, 4), '5.0') + self.assertEqual(add(8, 15), '17.0') + + +def reprify(func): + def wrapper(*args, **kwargs): + return repr(func(*args, **kwargs)) + return wrapper + + +def sqrtd(func): + def wrapper(*args, **kwargs): + return math.sqrt(func(*args, **kwargs)) + return wrapper + + +def of_squares(func): + def wrapper(*args, **kwargs): + args = (n**2 for n in args) + kwargs = {k: v**2 for k, v in kwargs.items()} + return func(*args, **kwargs) + return wrapper + + +if __name__ == "__main__": + from helpers import error_message + error_message() diff --git a/scripts/oreilly_decorators/exercises/test.py b/scripts/oreilly_decorators/exercises/test.py new file mode 100644 index 0000000..c1ca3a6 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/test.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +from __future__ import print_function +import ast +from functools import partial +from inspect import getsource +import os +import re +import sys +from textwrap import dedent +import unittest + +from test_data import MODULES, TESTS + + +class Color: + BOLD = "\033[1m" + UNDER = "\033[4m" + END = "\033[0m" + RED = "\033[91m" + + +def _assert_op_sub(op, match): + node = ast.parse(match[0]) + expression1, expression2, *_ = node.body[0].value.args + if hasattr(ast, "unparse"): + expression1 = ast.unparse(expression1) + expression2 = ast.unparse(expression2) + else: + expression1 = ast.get_source_segment(match[0], expression1) + expression2 = ast.get_source_segment(match[0], expression2) + return f"assert {expression1} {op} {expression2}" + + +def reformat_source(method_source): + # Remove first line and dedent + source = dedent("".join(method_source.splitlines(keepends=True)[1:])) + + # Convert common assertions + source = re.sub( + r"self.assertEqual\((.*), (.*)\)", + partial(_assert_op_sub, "=="), + source + ) + source = re.sub( + r"self.assertIs\((.*), (.*)\)", + partial(_assert_op_sub, "is"), + source + ) + source = re.sub( + r"self.assertIn\((.*), (.*)\)", + partial(_assert_op_sub, "in"), + source + ) + + source = re.sub( + r"self.assertIsNone\((.*)\)", + r"assert \1 is None", + source + ) + source = re.sub( + r"self.assert(True|False)\((.*)\)", + r"assert \2 == \1", + source + ) + + # characters + source = re.sub( + r"self.assertEqual\(\n *(.*),\n *(.*?),?\n[ ]*\)\n", + r"assert \1 == \2\n", + source + ) + + # tags_equal + source = re.sub( + r"self.assert(True|False)\((.*\()\n(.*)\n(.*)\n([ ]*\))\)\n", + r"assert \2\n\3\n\4\n\5 == \1\n", + source + ) + + # count.py + source = re.sub( + r"self.assertEqual\((.*), (\[\n[^]]+\])\)\n", + r"assert \1 == \2\n", + source + ) + return source + + +def reformat_error(traceback_message): + *lines, last_line = str(traceback_message).splitlines(keepends=True) + extra = "" + if last_line.startswith("AssertionError: None !="): + extra += "Maybe your function didn't return anything?" + extra += "\nMore on return values at: https://trey.io/ret" + if "takes 0 positional arguments but" in last_line: + extra += "Your function doesn't accept any arguments yet." + extra += "\nMore on arguments at: https://trey.io/args" + if extra: + extra = f"\n{Color.BOLD}HINT:{Color.END} {extra}\n" + return "".join([*lines, Color.RED + last_line + Color.END, extra]) + + +class VerboseTestResult(unittest.TextTestResult): + def printErrorList(self, flavor, errors): + for test, err in errors: + self.stream.writeln(self.separator1) + description = self.getDescription(test) + self.stream.writeln(f"{flavor}: {description}") + self.stream.writeln(self.separator2) + if hasattr(test, "_testMethodName"): + full_source = getsource( + getattr(type(test), test._testMethodName) + ) + self.stream.writeln( + Color.BOLD + + reformat_source(full_source) + + Color.END + ) + self.stream.writeln(reformat_error(err)) + self.stream.flush() + + +class VerboseTestRunner(unittest.TextTestRunner): + resultclass = VerboseTestResult + + +def get_test(obj_name): + if obj_name not in TESTS: + raise SystemExit("Test for {} doesn't exist.".format(obj_name)) + return unittest.defaultTestLoader.loadTestsFromName(TESTS[obj_name]) + + +def run_tests(tests): + test_suite = unittest.TestSuite(tests) + return VerboseTestRunner(verbosity=2).run(test_suite).wasSuccessful() + + +def print_object_names(): + for module, objects in MODULES.items(): + print("\n{}:\n".format(module)) + for obj in objects: + print(obj) + print() + + +def main(*arguments): + os.system("") # Enables ANSI escape characters in terminal + if not arguments: + print("Please select a thing to test") + print_object_names() + elif len(arguments) > 1: + print(""" +Can only call test.py with one argument: the name of the exercise being tested + +Examples: + +- python test.py get_hypotenuse +- python test.py hello.py +- python test.py BankAccount + +This test script runs Trey's tests against your code. +The tests are written in files that end in "_test.py". + +If you'd like to test your code manually, you can either: + +1. Open a Python REPL, import your code, and execute it with specific arguments +2. Write your own test code at the bottom of your file (e.g. functions.py) and +run that file (e.g. "python functions.py"). + +Consult the website for instructions for running the exercises and ask Trey +for help when you get stuck. + """.strip()) + elif ' ' in arguments[0] or '(' in arguments[0] or ',' in arguments[0]: + print("Invalid characters found: {}\n".format(arguments[0])) + print("This test script doesn't accept code, just an exercise name.\n") + print("Example usage:") + print("python test.py \n") + else: + [argument] = arguments + if argument.startswith(('modules/', 'modules\\', './modules/')): + argument = argument.split('/', 1)[1] + if argument == '--all': + arguments = list(TESTS) + else: + arguments = [argument] + tests = [ + get_test(arg) + for arg in arguments + ] + print("Testing {}\n".format(', '.join(arguments))) + test_classes = set( + tuple(test.id().split('.')[:-1]) + for suite in tests + for test in suite._tests + ) + for module, cls in test_classes: + print("Running {} test class in {}.py\n".format(cls, module)) + success = run_tests(tests) + sys.exit(not success) + + +if __name__ == "__main__": + # Version check before all else + major, minor, micro, releaselevel, serial = sys.version_info + if (major, minor) < (3, 5): + print("You are running Python {0}.{1}".format(major, minor)) + print("Must use Python version 3.5 or above") + sys.exit(1) + main(*sys.argv[1:]) diff --git a/scripts/oreilly_decorators/exercises/test_data.py b/scripts/oreilly_decorators/exercises/test_data.py new file mode 100644 index 0000000..f5bdef0 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/test_data.py @@ -0,0 +1,45 @@ +TESTS = { + "count_calls": "decorators_test.CountCallsTests", + "four": "decorators_test.FourTests", + "groot": "decorators_test.GrootTests", + "jsonify": "decorators_test.JSONifyTests", + "record_calls": "decorators_test.RecordCallsTests", + "call_again": "functions_test.CallAgainTests", + "call_later": "functions_test.CallLaterTests", + "call_logger": "functions_test.CallLoggerTests", + "call": "functions_test.CallTests", + "exclude": "functions_test.ExcludeTests", + "only_once": "functions_test.OnlyOnceTests", + "is_ok": "initial_test.InitialTests", + "at": "more_test.AtTests", + "coalesce_all": "more_test.CoalesceAllTests", + "lazy_repr": "more_test.LazyReprTests", + "positional_only": "more_test.PositionalOnlyTests" +} + +MODULES = { + "decorators": [ + "count_calls", + "jsonify", + "groot", + "four", + "record_calls" + ], + "functions": [ + "call", + "call_later", + "exclude", + "call_logger", + "call_again", + "only_once" + ], + "more": [ + "coalesce_all", + "lazy_repr", + "positional_only", + "at" + ], + "initial": [ + "is_ok" + ] +} diff --git a/scripts/oreilly_decorators/main.py b/scripts/oreilly_decorators/main.py new file mode 100644 index 0000000..3ca2e47 --- /dev/null +++ b/scripts/oreilly_decorators/main.py @@ -0,0 +1,29 @@ +from functools import partial + +def custom_map(func, iterable): # same as map built-in function + return (func(x) for x in iterable) + +def custom_filter(func, iterable): # same as built-in filter function + return [x for x in iterable if func(x)] + +def custom_partial(func, *first_args): # same as functools.partial + """"Let's us preload a function with some arguments""" + def new_func(*args): + new_args = first_args + args + return func(*new_args) + return new_func + +def add_two(x, y): + return x+y + +if __name__ == '__main__': + l = [-4, -5, 1, 3, 4, 5] + print(list(custom_filter(lambda x: x > 0, l))) + partial_sum_four = partial(add_two, 4) + print(partial_sum_four(5)) + + count_lengths = partial(map, len) + word_lengths = count_lengths(["some", "words", "banana"]) + print(list(word_lengths)) + + \ No newline at end of file diff --git a/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Classes+Documentation.pdf b/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Classes+Documentation.pdf new file mode 100755 index 0000000..f08f51d Binary files /dev/null and b/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Classes+Documentation.pdf differ diff --git a/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Program+Requirements.pdf b/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Program+Requirements.pdf new file mode 100755 index 0000000..a13f190 Binary files /dev/null and b/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Program+Requirements.pdf differ diff --git a/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/coffee_maker.py b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/coffee_maker.py new file mode 100755 index 0000000..464d15f --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/coffee_maker.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +class CoffeeMaker: + """Models the machine that makes the coffee.""" + + def __init__(self): + self.resources = { + "water": 300, + "milk": 200, + "coffee": 100, + } + + def report(self): + """Prints a report of all resources.""" + print(f"Water: {self.resources['water']}ml") + print(f"Milk: {self.resources['milk']}ml") + print(f"Coffee: {self.resources['coffee']}g") + + def is_resource_sufficient(self, drink): + """Returns True when order can be made, False if ingredients are insufficient.""" + can_make = True + for item in drink.ingredients: + if drink.ingredients[item] > self.resources[item]: + print(f"Sorry there is not enough {item}.") + can_make = False + return can_make + + def make_coffee(self, order): + """Deducts the required ingredients from the resources.""" + for item in order.ingredients: + self.resources[item] -= order.ingredients[item] + print(f"Here is your {order.name} ☕️. Enjoy!") diff --git a/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/main.py b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/main.py new file mode 100755 index 0000000..ea81928 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/main.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from coffee_maker import CoffeeMaker +from menu import Menu, MenuItem +from money_machine import MoneyMachine diff --git a/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/menu.py b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/menu.py new file mode 100755 index 0000000..d177877 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/menu.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +class MenuItem: + """Models each Menu Item.""" + + def __init__(self, name, water, milk, coffee, cost): + self.name = name + self.cost = cost + self.ingredients = {"water": water, "milk": milk, "coffee": coffee} + + +class Menu: + """Models the Menu with drinks.""" + + def __init__(self): + self.menu = [ + MenuItem(name="latte", water=200, milk=150, coffee=24, cost=2.5), + MenuItem(name="espresso", water=50, milk=0, coffee=18, cost=1.5), + MenuItem(name="cappuccino", water=250, milk=50, coffee=24, cost=3), + ] + + def get_items(self): + """Returns all the names of the available menu items.""" + options = "" + for item in self.menu: + options += f"{item.name}/" + return options + + def find_drink(self, order_name): + """Searches the menu for a particular drink by name. + + Returns that item if it exists, otherwise returns None + """ + for item in self.menu: + if item.name == order_name: + return item + print("Sorry that item is not available.") diff --git a/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/money_machine.py b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/money_machine.py new file mode 100755 index 0000000..507c892 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/money_machine.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +class MoneyMachine: + CURRENCY = "$" + + COIN_VALUES = {"quarters": 0.25, "dimes": 0.10, "nickles": 0.05, "pennies": 0.01} + + def __init__(self): + self.profit = 0 + self.money_received = 0 + + def report(self): + """Prints the current profit.""" + print(f"Money: {self.CURRENCY}{self.profit}") + + def process_coins(self): + """Returns the total calculated from coins inserted.""" + print("Please insert coins.") + for coin in self.COIN_VALUES: + self.money_received += int(input(f"How many {coin}?: ")) * self.COIN_VALUES[coin] + return self.money_received + + def make_payment(self, cost): + """Returns True when payment is accepted, or False if insufficient.""" + self.process_coins() + if self.money_received >= cost: + change = round(self.money_received - cost, 2) + print(f"Here is {self.CURRENCY}{change} in change.") + self.profit += cost + self.money_received = 0 + return True + else: + print("Sorry that's not enough money. Money refunded.") + self.money_received = 0 + return False diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/api_client.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/api_client.py new file mode 100644 index 0000000..53f575c --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/api_client.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +import requests + + +class ApiClient: + URL = "https://opentdb.com/api.php?amount=10&type=boolean" + + def fetch_questions(self): + response = requests.get(self.URL) + if response.status_code == 200: + data = response.json() + return data["results"] + return [] diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py new file mode 100644 index 0000000..373c2ef --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from quiz_manager import QuizManager + +if __name__ == "__main__": + print("\n\nLET's Start the GAME!\n\n") + quiz = QuizManager() + quiz.start_quiz() diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/question.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/question.py new file mode 100644 index 0000000..ce39bb5 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/question.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +class Question: + def __init__(self, content): + self.question = content["question"] + self.correct_answer = content["correct_answer"] + + def check_answer(self, answer): + return answer.lower() == self.correct_answer.lower() diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/quiz_manager.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/quiz_manager.py new file mode 100644 index 0000000..4d56ad2 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/quiz_manager.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from api_client import ApiClient +from question import Question + + +class QuizManager: + self.score = 0 + self.api_client = ApiClient() + + def start_quiz(self): + question_list = self.api_client.fetch_questions() + for idx, q in enumerate(question_list): + question = Question(q) + print(f"\nQuestion number {idx+1}: ") + print(question.question) + answer = [] + while answer not in ["True", "False"]: + answer = input("Provide a True/False answer: ") + is_correct = question.check_answer(answer) + if is_correct: + print("Correct!") + self.score += 1 + else: + print("Incorrect!") + + print(f"\n\nFinal Score is: {self.score}/{len(question_list)} correct questions.") diff --git a/scripts/python_bootcamp_udemy/oop/day20_21-snake/main.py b/scripts/python_bootcamp_udemy/oop/day20_21-snake/main.py new file mode 100644 index 0000000..1c15ecd --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day20_21-snake/main.py @@ -0,0 +1,27 @@ +import time +from turtle import Turtle, Screen +from snake import Snake + +screen = Screen() +screen.setup(width=600, height=600) +screen.bgcolor('black') +screen.title("My Snake Game") +screen.tracer(0) + +snake = Snake() + +screen.listen() +screen.onkey(snake.up, "Up") +screen.onkey(snake.down, "Down") +screen.onkey(snake.left, "Left") +screen.onkey(snake.right, "Right") + +game_is_on = True +while game_is_on: + screen.update() + time.sleep(0.1) + + # snake._random_direction_change(p=0.5) + snake.move() + +screen.exitonclick() \ No newline at end of file diff --git a/scripts/python_bootcamp_udemy/oop/day20_21-snake/snake.py b/scripts/python_bootcamp_udemy/oop/day20_21-snake/snake.py new file mode 100644 index 0000000..5d63ae1 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day20_21-snake/snake.py @@ -0,0 +1,51 @@ +from turtle import Turtle +import numpy as np +MOVE_DISTANCE = 20 +STARTING_POSITION = [(0,0), (-20,0), (-40,0)] + +class Snake(): + "Class that represents the snake in the Snake Game." + def __init__(self, shape="square", color="white"): + self.shape = shape + self.color = color + + self.segments = None + self.initialize_segments() + + def initialize_segments(self): + """Initialize the three first segments of the snake at a fixed position.""" + snake_segment = [] + + for start_pos in STARTING_POSITION: + new_segment = Turtle() + new_segment.penup() + new_segment.shape(self.shape) + new_segment.color(self.color) + new_segment.goto(start_pos) + snake_segment.append(new_segment) + + self.segments = snake_segment + + def move(self): + """Move the snake in the direction is already heading""" + for idx in reversed(range(1, len(self.segments))): + newx, newy = self.segments[idx-1].pos() + self.segments[idx].goto(newx, newy) + self.segments[0].forward(MOVE_DISTANCE) + + def _random_direction_change(self, p): + """Randomly changes the direction the snake is heading to""" + if np.random.rand() < p: + self.segments[0].setheading(np.random.choice([0,90,180,270])) + + def up(self): + self.segments[0].setheading(90) + + def down(self): + self.segments[0].setheading(270) + + def right(self): + self.segments[0].setheading(0) + + def left(self): + self.segments[0].setheading(180) diff --git a/scripts/python_bootcamp_udemy/oop/dev.py b/scripts/python_bootcamp_udemy/oop/dev.py new file mode 100644 index 0000000..1a5d9bd --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/dev.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +from prettytable import PrettyTable + + +class Requirement: + water = 20 + coffee = 5 + milk = 20 + money = 2 + + +class MachineException(Exception): + pass + + +class Machine: + operating = True + + def __init__(self) -> None: + self.water = 100 + self.milk = 50 + self.coffee = 76 + self.money = 2.5 + + @staticmethod + def prompt(): + return input("What would you like? (espresso/latte/cappuccino/): ") + + def turn_off(self): + print("Deactivating machine...") + + def print_report(self): + table = PrettyTable() + table.add_column("Object", ["Water", "Milk", "Coffee", "Money"]) + table.add_column("Amount", [self.water, self.milk, self.coffee, self.money]) + table.add_column("Unit", ["ml", "ml", "g", "$"]) + print(table) + + def make_coffee(self): + print("Making coffee...") + self.water -= Requirement.water + self.milk -= Requirement.milk + self.coffee -= Requirement.coffee + self.money -= Requirement.money + input("Please take your coffee.") + + def validate_resources(self): + return ( + (self.water > Requirement.water) + & (self.coffee > Requirement.coffee) + & (self.milk > Requirement.milk) + ) + + def validate_money(self): + return self.money > Requirement.money + + def insert_money(self, coin): + if coin == "q": + new_money = 0.25 + elif coin == "d": + new_money = 0.10 + elif coin == "n": + new_money = 0.05 + elif coin == "p": + new_money = 0.01 + elif coin == "dollar": + new_money = 1 + self.money += new_money + + +if __name__ == "__main__": + machine = Machine() + + while machine.operating: + action = machine.prompt() + if action == "espresso" or action == "latte" or action == "cappuccino": + if not machine.validate_resources(): + print("No resources") + continue + while not machine.validate_money(): + print("Not enough money. Insert more money: ") + coin = input("dollar = $1,vq = $0.25, d = $0.10, n = $0.05, p = $0.01: ") + machine.insert_money(coin) + machine.make_coffee() + elif action == "report": + machine.print_report() + elif action == "off": + machine.turn_off() + machine.operating = False + else: + print("Select a valid input") diff --git a/scripts/python_bootcamp_udemy/oop/main.py b/scripts/python_bootcamp_udemy/oop/main.py new file mode 100644 index 0000000..bce9e7e --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/main.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +Note: you may need to ``apt install python3-tk`` + +How to implement OOP in python. + +First we need to know how to model a class. Think about what it ``has`` and what it ``does``. These will be its attributes and its methods. +An object is just a way of combining some piece of data and some functionality (this is the *encapsulation* property). The properties are +Abstraction, Encapsulation, Inheritance and Polymorphism. + +Many real objects and agents can be modelled using this framework. For example, a waiter in a restaurant can be modelled having as attributes whether +is carrying a plate or not, and also which tables is waiting. As methods, we could have something like take_order() and carry_order(). + +``Class`` is the blueprint. The different copies of this blueprint are called ``instances``. +""" + +from prettytable import PrettyTable +from prettytable.colortable import ColorTable, Themes + +table = ColorTable(theme=Themes.OCEAN) + +print(table) +# table = PrettyTable() + +table.header = False +table.add_column("Pokemon", ["Pikachu", "Squirtle", "Charizard"]) +table.add_column("Id", [25, 35, 100]) +table.add_column("Type", ["Electric", "Water", "Fire, Flying"]) + +print(table) +print(table.border) +print(table.header) diff --git a/scripts/python_bootcamp_udemy/trivia/main.py b/scripts/python_bootcamp_udemy/trivia/main.py new file mode 100644 index 0000000..02d05e7 --- /dev/null +++ b/scripts/python_bootcamp_udemy/trivia/main.py @@ -0,0 +1,4 @@ + + + +URL = "https://opentdb.com/api.php?amount=10&type=boolean" \ No newline at end of file diff --git a/scripts/python_training/__init__.py b/scripts/python_training/__init__.py new file mode 100755 index 0000000..993a15b --- /dev/null +++ b/scripts/python_training/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""Top level package for Novartis Python Training.""" +import os +from importlib import metadata +from pathlib import Path + +__version__ = metadata.version("python_training") + + +# Base path of python_training module +# (to be used when accessing non .py files in Novartis Python Training/) +WORKDIR = Path(os.getenv("WORKDIR", Path.cwd())) +BASEPATH = Path(__file__).parent +ASSET_DIR = BASEPATH / "assets" +M3_PATH = BASEPATH / "m3_internal_libraries" diff --git a/scripts/python_training/__main__.py b/scripts/python_training/__main__.py new file mode 100755 index 0000000..d94a23d --- /dev/null +++ b/scripts/python_training/__main__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# ruff: noqa: D401 +"""Entry point.""" +import click + +from . import __version__ +from .scripts import dummy + + +def _main() -> None: + """Run main function for entrypoint.""" + + @click.group(chain=True) + @click.version_option(__version__) + def entry_point() -> None: + """Package entry point.""" + + entry_point.add_command(dummy.main) + + entry_point() + + +if __name__ == "__main__": + _main() diff --git a/scripts/python_training/m1_python_basics/__init__.py b/scripts/python_training/m1_python_basics/__init__.py new file mode 100755 index 0000000..b362491 --- /dev/null +++ b/scripts/python_training/m1_python_basics/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 1: Python basics.""" diff --git a/scripts/python_training/m1_python_basics/task_1.py b/scripts/python_training/m1_python_basics/task_1.py new file mode 100755 index 0000000..c02f083 --- /dev/null +++ b/scripts/python_training/m1_python_basics/task_1.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""MODULE 1: Python Basics. + +TASK 1: This task is to practice the basic concepts learnt in the first module, found here: + + +Create a python class called "Vehicle". The class will have the following attributes: + * Brand + * Color + * Max_Capacity + * Speed + * Power_Source + * Current_Position + +The class will also have the following functions: + * Accelerate (acceleration, time) + * Brake (acceleration, time) + * Repaint (new_color) + * Maintain_Speed (time) +""" diff --git a/scripts/python_training/m1_python_basics/task_2.py b/scripts/python_training/m1_python_basics/task_2.py new file mode 100755 index 0000000..85a7df8 --- /dev/null +++ b/scripts/python_training/m1_python_basics/task_2.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +"""MODULE 1: Python Basics. + +TASK 2: This task is to practice the more advanced concepts learnt in the first module, found here: + + +Create several python classes that inherit from Vehicle. The classes are: + * CAR + - Attributes: + * Wheels + * License_Plate + * Max_Fuel + * Fuel_Left + * Max_Trunk_Space + * Trunk_Space_Left + * Consumption_Per_Km + - Functions: + * Register_License (new_license) + * Refuel (liters) + * Fill_Trunk (volume) + * Empty_Trunk (volume) + + * BOAT + - Attributes: + * Engines + * Food_Supply_Kg + * Passengers + * Destination + * Crew [formed by several employees: A captain, a chief engineer, a deckhand and a mate] + - Functions: + * Board (num_passengers) + * Ressupply (kg_food) + * Dock () + * Update_destination (new_dest) + + * BIKE + - Attributes: + * Wheels + * Owner + * Size + * Bike_Type + * Gear_Transmission + - Functions: + * Change_Owner (new_owner) + * Change_Transmission (new_trans) +""" diff --git a/scripts/python_training/m2_external_libraries/__init__.py b/scripts/python_training/m2_external_libraries/__init__.py new file mode 100755 index 0000000..491874a --- /dev/null +++ b/scripts/python_training/m2_external_libraries/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +"""Subpackage to implement the methods for Python external libraries training.""" + +from ._preprocess import preprocess +from ._utils import db_connector + +__all__ = ["db_connector", "preprocess"] diff --git a/scripts/python_training/m2_external_libraries/_preprocess.py b/scripts/python_training/m2_external_libraries/_preprocess.py new file mode 100755 index 0000000..d31bf15 --- /dev/null +++ b/scripts/python_training/m2_external_libraries/_preprocess.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +"""Functions for testing exercises. + +This module is designed to perform various manipulations and analyses on stock price data. +The data is expected to be in a pandas DataFrame with the following columns: + +- Date: The date of the stock data, in 'YYYY-MM-DD' format. +- Open: The opening price of the stock for the day. +- High: The highest price of the stock for the day. +- Low: The lowest price of the stock for the day. +- Close: The closing price of the stock for the day. +- Adj Close: The adjusted closing price of the stock for the day (adjusted for splits and dividends). +- Volume: The number of shares traded during the day. + +DataFrame example: + + Date Open High Low Close Adj Close Volume + 2020-01-01 100 110 95 105 105 10000 + 2020-01-02 105 115 100 110 110 15000 + ... + +These functions are designed to prepare stock price data for analysis and modeling. +They enable users to clean and transform the data, extract meaningful features, and ensure the data is +in a suitable format for various analytical tasks. +""" + +from typing import Union + +import pandas as pd + +from ._utils import db_connector + + +def preprocess( + start_date: Union[str, pd.Timestamp], + end_date: Union[str, pd.Timestamp], + window: int = 20, + lag_days: int = 5, +): + """Applies a series of preprocessing steps to stock price data. + + This function integrates several preprocessing steps: initial data preparation and normalization, + technical indicator enhancement, and the addition of lag features. It's designed to prepare stock + price data for further analysis or modeling. + + :param df: The input DataFrame containing stock price data. + :param start_date: The start date for filtering the data, in 'YYYY-MM-DD' format. + :param end_date: The end date for filtering the data, in 'YYYY-MM-DD' format. + :param window: The window size for calculating moving averages and volatility. Defaults to 20. + :param lag_days: The number of days to create lag features for. Defaults to 5. + + :returns: The processed DataFrame, ready for analysis or modeling, with normalized dates, + filtered by the specified date range, enhanced with technical indicators, and augmented with lag features. + """ + df = db_connector().read_from_db() + + if df is None: + return None + + return ( + df.pipe(_prepare_and_normalize_data, start_date=start_date, end_date=end_date) + .pipe(_enhance_with_technical_indicators, window=window) + .pipe(_add_lags, lag_days=lag_days) + ) + + +def _prepare_and_normalize_data( + df: pd.DataFrame, start_date: Union[str, pd.Timestamp], end_date: Union[str, pd.Timestamp] +): + """Prepares and normalizes stock price data for analysis. + + This function standardizes the date format, filters the dataset based on a specified date range, + generates additional date-related features, calculates daily returns, and ensures that the dataset + has no missing dates within the specified range by forward filling missing values. This comprehensive + preprocessing step is crucial for subsequent financial analysis and modeling tasks. + + :param df: DataFrame with stock price data, expecting columns 'Date' and 'Adj Close'. + :param start_date: String representing the start date in 'YYYY-MM-DD' format. + :param end_date: String representing the end date in 'YYYY-MM-DD' format. + + :return: A DataFrame that has been processed to include only the specified date range, + with added 'Month' and 'DayOfWeek' features, daily returns calculated, and missing dates + forward-filled. The DataFrame is indexed by the 'Date' column. + + :raises: ValueError: If the input DataFrame does not contain a 'Date' column or if the 'Date' column + cannot be converted to datetime format. Also raised if the specified start_date or end_date + are not in the correct format. + """ + if "Date" not in df.columns: + raise ValueError("DataFrame must contain a 'Date' column.") + try: + df["Date"] = pd.to_datetime(df["Date"]) + except ValueError as e: + raise ValueError( + "Error converting 'Date' column to datetime. Ensure the dates are in a correct format." + ) from e + + # Filter Date Range + start_date = pd.to_datetime(start_date) + end_date = pd.to_datetime(end_date) + df = df[(df["Date"] >= start_date) & (df["Date"] <= end_date)] + + # Generate Date Features + df["Month"] = df["Date"].dt.month + df["DayOfWeek"] = df["Date"].dt.dayofweek + + # Calculate Daily Returns + df["Daily Returns"] = df["Adj Close"].pct_change() + + # Fill Missing Dates (forward fill for simplicity) + df = df.set_index("Date").resample("D").ffill() + + return df + + +def _enhance_with_technical_indicators(df: pd.DataFrame, window: int = 20): + """Enriches a DataFrame with key technical indicators to aid in financial analysis. + + This function calculates and appends several technical indicators to the input DataFrame, + specifically a moving average and volatility measure. These indicators are widely used in + stock market analysis to understand price trends and market volatility over a specified + window of time. Additionally, the function ensures that the necessary prerequisites for + calculation are met, such as the presence of required columns. + + :param df: DataFrame after initial preprocessing, with 'Date' and 'Close' columns. + :param window: Integer representing the window size for the moving average. + + :return: The original DataFrame augmented with two new columns: + - 'MA_{window}': The moving average of the 'Close' prices over the specified window. + - 'Volatility': The rolling standard deviation of 'Daily Returns', representing price + volatility over the same window. + + :raises: + - ValueError: If the input DataFrame lacks the required 'Close' or 'Daily Returns' columns, + or if the 'window' parameter is not a positive integer. + - RuntimeError: If an unexpected error occurs during the calculation of technical indicators. + """ + # Validate required columns + required_columns = ["Close", "Daily Returns"] + missing_columns = [col for col in required_columns if col not in df.columns] + if missing_columns: + raise ValueError( + f"DataFrame is missing required columns for technical indicator calculations: {', '.join(missing_columns)}" + ) + + # Validate window size + if not isinstance(window, int) or window <= 0: + raise ValueError("'window' must be a positive integer.") + + try: + # Add Moving Average + df[f"MA_{window}"] = df["Close"].rolling(window=window).mean() + # Calculate Volatility (rolling standard deviation of daily returns) + df["Volatility"] = df["Daily Returns"].rolling(window=window).std() + except Exception as e: + raise RuntimeError(f"An error occurred while calculating technical indicators: {e}") + + return df + + +def _add_lags(df: pd.DataFrame, lag_days: int = 5): + """Enhances a DataFrame by adding lagged features for the 'Close' price column. + + This function creates new columns in the DataFrame, each representing the 'Close' price + shifted by a number of days specified by the 'lag_days' parameter. These lagged features + are useful for time series analysis and forecasting models, as they allow the model to + consider historical price movements. + + :param df: DataFrame with stock price data. + :param lag_days: Number of days to lag features by. + + :return: A DataFrame identical to the input but with additional columns for each + lagged feature. The names of these new columns follow the pattern 'Close_lag_X', where + X is the number of days the 'Close' price is lagged by. + + :raises: ValueError: If the input DataFrame does not contain a 'Close' column or if the 'lag_days' + parameter is not a positive integer. + """ + # Validate 'Close' column presence + if "Close" not in df.columns: + raise ValueError("DataFrame must contain a 'Close' column for lag feature calculations.") + + # Validate lag_days parameter + if not isinstance(lag_days, int) or lag_days <= 0: + raise ValueError("'lag_days' must be a positive integer.") + + try: + # Create Lag Features for 'Close' Price + for i in range(1, lag_days + 1): + df[f"Close_lag_{i}"] = df["Close"].shift(i) + except Exception as e: + # Catching a broad exception to handle unexpected errors during lag feature creation + raise RuntimeError(f"An error occurred while adding lag features: {e}") + + return df diff --git a/scripts/python_training/m2_external_libraries/_utils.py b/scripts/python_training/m2_external_libraries/_utils.py new file mode 100755 index 0000000..cc58c32 --- /dev/null +++ b/scripts/python_training/m2_external_libraries/_utils.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from loguru import logger + + +class db_connector: + def __init__(self) -> None: + pass + + def read_from_db(self): + """This function is a placeholder for database reading functionality. It's not implemented + yet and is intended to be mocked during testing to return a DataFrame with stock price data. + + :returns: None. Placeholder return value. When mocked, this should return a pd.DataFrame. + """ + logger.warning( + "This function is not implemented yet. For testing purposes, try mocking the read from db with data from the assets folder." + ) diff --git a/scripts/python_training/m3_internal_libraries/__init__.py b/scripts/python_training/m3_internal_libraries/__init__.py new file mode 100755 index 0000000..86d208c --- /dev/null +++ b/scripts/python_training/m3_internal_libraries/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 3: Internal libraries.""" diff --git a/scripts/python_training/m3_internal_libraries/query.sql b/scripts/python_training/m3_internal_libraries/query.sql new file mode 100755 index 0000000..41d1842 --- /dev/null +++ b/scripts/python_training/m3_internal_libraries/query.sql @@ -0,0 +1,12 @@ +WITH all_data AS ( + SELECT DISTINCT drug_id AS drug_id, + ims_id AS ims_id, + country AS country, + molecule AS molecule + FROM SNOWFLAKE_TABLE_WITH_INFO +) +SELECT drug_id, + ims_id, + country, + molecule +FROM all_data diff --git a/scripts/python_training/m3_internal_libraries/task_1.py b/scripts/python_training/m3_internal_libraries/task_1.py new file mode 100755 index 0000000..1630b18 --- /dev/null +++ b/scripts/python_training/m3_internal_libraries/task_1.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +"""MODULE 3: Internal Libraries. + +TASK 1: This task is to practice the basic concepts learnt in the third module, found here: + + +Create a python class that inherits from Extractor (can be found in the nvs-tk) that: + * Gets information from a database. + * Validates that the data is not empty. + * Validates that the data has the correct columns (Found in the sample QUERY). + * Validates that there are no duplicates. + * Doesn't manipulate the data, just returns it as is. + +You can find the query in this folder. +Assume that the SQL Repository has already been created and +you receive it in the Extractor as a parameter. +""" +import pandas as pd +from nvs_sql import SqlRepository +from nvs_tk import Extractor + + +class TaskExtractor(Extractor): + """Extractor designed for the task of module 3.""" + + def __init__(self, repo: SqlRepository) -> None: + """Your code here.""" + + def get(self) -> pd.DataFrame: + """Your code here.""" + + def validate(self, data: pd.DataFrame) -> None: + """Your code here.""" + + def clean(self, data: pd.DataFrame) -> pd.DataFrame: + """Your code here.""" diff --git a/scripts/python_training/m4_useful_tools/__init__.py b/scripts/python_training/m4_useful_tools/__init__.py new file mode 100755 index 0000000..8bdadea --- /dev/null +++ b/scripts/python_training/m4_useful_tools/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 4.""" diff --git a/scripts/python_training/m4_useful_tools/task_1.py b/scripts/python_training/m4_useful_tools/task_1.py new file mode 100755 index 0000000..1ee7732 --- /dev/null +++ b/scripts/python_training/m4_useful_tools/task_1.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""Task 1 of Module 4: Useful tools.""" +from datetime import datetime + +import pandas as pd +from loguru import logger + +from python_training.m4_useful_tools.utils.book_tools import insert_book_info_to_frame + + +def main() -> None: + """Get info from book.""" + programming_book = Book("Python Programming", "Jane Doe", 300, 2015) + logger.info(programming_book.book_info()) + + conelly_book = Book("Black Ice", "Michael Conelly", 400, 209) + library = pd.DataFrame(columns=["title", "author", "years_since_publication"]) + library = insert_book_info_to_frame(library, conelly_book.book_info()) + logger.info(library) + + +class Book: + """Class Book.""" + + def __init__(self, title: str, author: str, pages: int, year_of_publication: int) -> None: + self.title = title + self.author = author + self.pages = pages + self.year_of_publication = year_of_publication + + def book_info(self) -> dict: + """Extract the year_since_publication into a method using refactoring.""" + current_year = datetime.now().year + years_since_publication = current_year - self.year_of_publication + return { + "title": self.title, + "author": self.author, + "years_since_publication": years_since_publication, + } + + +if __name__ == "__main__": + main() diff --git a/scripts/python_training/m4_useful_tools/task_2.py b/scripts/python_training/m4_useful_tools/task_2.py new file mode 100755 index 0000000..3499936 --- /dev/null +++ b/scripts/python_training/m4_useful_tools/task_2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +"""Code to test breakpoints. + +Test the following kinds of breakpoints within the code: +- Conditional breakpoint. +- Hit Breakpoint. +- Log point. +""" +from loguru import logger + + +def main() -> None: + """Execute main function.""" + numbers = list(range(1, 21)) + squared_numbers = calculate_squares(numbers) + logger.info("Squared Numbers:", squared_numbers) + + +def calculate_squares(numbers: list) -> list: + """Find the squares of a list of numbers. + + :param numbers: List of numbers we want to square. + """ + squared_numbers = [] + for number in numbers: + squared = "I'm a Bug! 🐞" if number % 4 == 0 else number**2 + squared_numbers.append(squared) + return squared_numbers + + +if __name__ == "__main__": + main() diff --git a/scripts/python_training/m4_useful_tools/utils/__init__.py b/scripts/python_training/m4_useful_tools/utils/__init__.py new file mode 100755 index 0000000..bbedbce --- /dev/null +++ b/scripts/python_training/m4_useful_tools/utils/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Utils for module 4: Useful tools.""" diff --git a/scripts/python_training/m4_useful_tools/utils/book_tools.py b/scripts/python_training/m4_useful_tools/utils/book_tools.py new file mode 100755 index 0000000..ff31a5c --- /dev/null +++ b/scripts/python_training/m4_useful_tools/utils/book_tools.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +"""Utils for Module 4: Useful tools.""" +import pandas as pd + + +def insert_book_info_to_frame(frame: pd.DataFrame, book_info: dict) -> pd.DataFrame: + """Insert book information into a dataframe. + + :param frame: Data frame to insert book info. + :param book_info: Dictionary with book information. + """ + return frame.append(book_info, ignore_index=True) diff --git a/scripts/python_training/m5_best_practices/__init__.py b/scripts/python_training/m5_best_practices/__init__.py new file mode 100755 index 0000000..d274e41 --- /dev/null +++ b/scripts/python_training/m5_best_practices/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 5: Best practices.""" diff --git a/scripts/python_training/m5_best_practices/task_1_a.py b/scripts/python_training/m5_best_practices/task_1_a.py new file mode 100755 index 0000000..14d4630 --- /dev/null +++ b/scripts/python_training/m5_best_practices/task_1_a.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +"""MODULE 5: Best practices. + +TASK 1: This task is to practice the basic concepts learnt in the fifth module, found here: + + +In this script you will find several snippets of code. Some of them are properly coded, +but others are not. This doesn't mean that they wouldn't work, but they can be +improved in several ways. Some code snippets are in other scripts, because they use specific imports. +""" + +def example_1(num_1, num_2): + """Given two numbers, return true if the first one is bigger than the second one. + + :param num_1: First number. + :param num_2: Second number. + """ + return num_1 > num_2 + + +def example_2(num): + """Convert a number from 1 to 5 into a vowel. + + :param num: Number to convert. + """ + match num: + case 1: + return "a" + case 2: + return "e" + case 3: + return "i" + case 4: + return "o" + case 5: + return "u" + case _: + print("Sorry, invalid number.") + return 0 + + +def example_3(): + f = open("file.txt") + try: + f.read() + finally: + f.close() diff --git a/scripts/python_training/m5_best_practices/task_1_b.py b/scripts/python_training/m5_best_practices/task_1_b.py new file mode 100755 index 0000000..6766e25 --- /dev/null +++ b/scripts/python_training/m5_best_practices/task_1_b.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +from typing import * + + +class NumberContainer: + """Class NumberContainer.""" + + def __init__(self, value: Union[int, float]) -> None: + self.value = value + + def display_value(self): + """Display the value of the number.""" + print(f"Current value: {self.value}") diff --git a/scripts/python_training/m5_best_practices/task_1_c.py b/scripts/python_training/m5_best_practices/task_1_c.py new file mode 100755 index 0000000..824e525 --- /dev/null +++ b/scripts/python_training/m5_best_practices/task_1_c.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +from nvs_tk import Extractor + + +class TestExtractor(Extractor): + """Extractor used to retrieve data. + + :param repo: Repository used to retrieve data. + :param param1: Parameter 1. + :param param2: Parameter 2. + """ + + _QUERY_FILE = QUERIES_DIR / "file.sql" + + COLUMNS = frozenset( + { + "col_a", + "col_b", + "col_c", + "col_d", + } + ) + + def __init__(self, repo: SqlRepository, param1: None | int, param2: int) -> None: + self._repo = repo + self._param1 = param1 + self._param2 = param2 + + def get(self) -> pd.DataFrame: + try: + return self._repo.get( + self._QUERY_FILE, + params=utils.filter_version_id( + ("param1", self._param1), + ("param2", self._param2), + ), + ) + + except SqlRepository as exc: + raise ExtractorError("Extractor was unable to retrieve data from DB.") from exc + + def validate(self, data: pd.DataFrame) -> None: + val.validate_not_empty(data) + val.validate_correct_columns(data, self.COLUMNS) + val.validate_no_duplicates(data) + + def clean(self, data: pd.DataFrame) -> pd.DataFrame: + return data \ No newline at end of file diff --git a/scripts/python_training/m5_best_practices/task_1_d.py b/scripts/python_training/m5_best_practices/task_1_d.py new file mode 100755 index 0000000..5e8216a --- /dev/null +++ b/scripts/python_training/m5_best_practices/task_1_d.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +import sys + +import pandas as pd +from db_tools import start_connection, upload_to_db +from project import BASE_DIR + + +def upload_to_db(): + """Upload system information to database.""" + with open(BASE_DIR / "config.yaml") as file: + config = yaml.safe_load(file) + username = config["credentials"]["username"] + pwd = config["credentials"]["password"] + db = config["credentials"]["database"] + + version_info_dict = { + "major": sys.version_info.major, + "minor": sys.version_info.minor, + "micro": sys.version_info.micro, + "releaselevel": sys.version_info.releaselevel, + "serial": sys.version_info.serial, + } + + version_info_df = pd.DataFrame([version_info_dict]) + + conn = start_connection(db, username, pwd) + upload_to_db(conn, version_info_df) diff --git a/scripts/python_training/m6_python_for_pros/__init__.py b/scripts/python_training/m6_python_for_pros/__init__.py new file mode 100755 index 0000000..4f84c93 --- /dev/null +++ b/scripts/python_training/m6_python_for_pros/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 6: Python for pros.""" diff --git a/scripts/python_training/m7_resource_exploitation/__init__.py b/scripts/python_training/m7_resource_exploitation/__init__.py new file mode 100755 index 0000000..e03515a --- /dev/null +++ b/scripts/python_training/m7_resource_exploitation/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 7: Resource exploitation basics.""" diff --git a/scripts/python_training/m8_the_world_of_docker/__init__.py b/scripts/python_training/m8_the_world_of_docker/__init__.py new file mode 100755 index 0000000..99031bf --- /dev/null +++ b/scripts/python_training/m8_the_world_of_docker/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 8: The world of Docker.""" diff --git a/scripts/python_training/m9_more_libraries_and_tools/__init__.py b/scripts/python_training/m9_more_libraries_and_tools/__init__.py new file mode 100755 index 0000000..07f67be --- /dev/null +++ b/scripts/python_training/m9_more_libraries_and_tools/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 9: More libraries & tools.""" diff --git a/scripts/python_training/solutions/__init__.py b/scripts/python_training/solutions/__init__.py new file mode 100755 index 0000000..ef529ee --- /dev/null +++ b/scripts/python_training/solutions/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Solutions to the exercises.""" diff --git a/scripts/python_training/solutions/m1_python_basics.py b/scripts/python_training/solutions/m1_python_basics.py new file mode 100755 index 0000000..3928feb --- /dev/null +++ b/scripts/python_training/solutions/m1_python_basics.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +"""Solutions for the tasks 1 and 2 of module 1: Python Basics.""" +from loguru import logger + +"""TASK 1""" + + +class Vehicle: + """Class Vehicle.""" + + def __init__( + self, + brand: str, + color: str, + max_capacity: int, + speed: float, + power_source: str, + current_position: float, + ) -> None: + self.brand = brand + self.color = color + self.max_capacity = max_capacity + self.speed = speed + self.power_source = power_source + self.current_position = current_position + + def accelerate(self, acc: float, time: int) -> None: + """Calculate the new speed after accelerating. + + :param acc: Rate at which the vehicle is accelerating, in m/s**2. + :param time: Time that the acceleration lasts, in seconds. + """ + self.current_position = self.current_position + self.speed * time + 0.5 * acc * time**2 + self.speed = self.speed + acc * time + logger.info(f"Accelerated to {self.speed} m/s.") + + def brake(self, dec: float, time: int) -> None: + """Calculate the new speed after braking. The speed can not be less than 0. + + :param dec: Rate at which the vehicle is accelerating, in m/s**2. + :param time: Time that the acceleration lasts, in seconds. + """ + self.current_position = self.current_position + self.speed * time + 0.5 * dec * time**2 + self.speed = max(0, self.speed - dec * time) + logger.info(f"Applied brakes. Current speed: {self.speed} m/s.") + + def repaint(self, new_color: str) -> None: + """Change the color of the vehicle. + + :param new_color: New color of the vehicle. + """ + self.color = new_color + logger.info(f"{self.brand} has been repainted to {new_color}.") + + def maintain_speed(self, time: int) -> None: + """Calculate the position after maintaining the same speed for n seconds. + + :param time: Time that the speed is maintained, in seconds. + """ + self.current_position = self.current_position + self.speed * time + logger.info(f"{self.brand} is maintaining speed of {self.speed} km/h for {time} hours.") + + +"""This is an example of how we could use the class +trambesos = Vehicle(brand = "Alstom", color = "Blue", max_capacity = 150, +speed = 0, power_source = "Electric", current_position = 0) + +trambesos.accelerate(15, 4) +trambesos.maintain_speed(54) +trambesos.brake(30, 2) + +distance_run = trambesos.current_position +""" + + +"""TASK 2""" + + +class Car(Vehicle): + """Class Car inheriting from Vehicle.""" + + def __init__( + self, + brand: str, + color: str, + max_capacity: int, + speed: float, + power_source: str, + current_position: float, + wheels: int, + license_plate: str, + max_fuel: float, + fuel_left: float, + max_trunk_space: float, + trunk_space_left: float, + consumption_per_km: float, + ) -> None: + super().__init__(brand, color, max_capacity, speed, power_source, current_position) + self.wheels = wheels + self.license_plate = license_plate + self.max_fuel = max_fuel + self.fuel_left = fuel_left + self.max_trunk_space = max_trunk_space + self.trunk_space_left = trunk_space_left + self.consumption_per_km = consumption_per_km + + def register_license(self, new_license: str) -> None: + """Change the current license to a new one. + + :param new_license: New license of the car. + """ + self.license_plate = new_license + logger.info(f"License plate registered: {new_license}") + + def refuel(self, liters: float) -> None: + """Refill the amount of fuel left. + + :param liters: Liters of fuel added. + """ + self.fuel_left = max(self.max_fuel, self.fuel_left + liters) + logger.info(f"Refueled {liters} liters. Total fuel now: {self.fuel_left} liters.") + + def fill_trunk(self, volume: float) -> None: + """Load objects into the trunk. + + :param volume: Volume of the objects added. + """ + self.trunk_space_left = max(0, self.trunk_space_left - volume) + logger.info(f"Filled trunk with {volume} m3. Space remaining: {self.trunk_space_left} m3.") + + def empty_trunk(self, volume: float) -> None: + """Remove objects from the trunk. + + :param volume: Volume of the objects removed. + """ + self.trunk_space_left = max(self.max_trunk_space, self.trunk_space_left + volume) + logger.info( + f"Emptied {volume} m3 from the trunk. Space remaining: {self.trunk_space_left} m3." + ) + + +"""This is an example of how we could use the class. +car_instance = Car(brand="Toyota", color="Blue", max_capacity=5, speed=60, + power_source="Gasoline", current_position=(0, 0), + wheels=4, license_plate="ABC123", fuel_left=20, + trunk_space_left=3, consumption_per_km=0.1) + +car_instance.register_license("XYZ789") +car_instance.refuel(10) +car_instance.accelerate(10, 2) +car_instance.fill_trunk(2) +car_instance.empty_trunk(1) +""" + + +class Boat(Vehicle): + """Class Boat inheriting from Vehicle.""" + + def __init__( + self, + brand: str, + color: str, + max_capacity: int, + speed: float, + power_source: str, + current_position: float, + engines: int, + food_supply_kg: float, + passengers: int, + destination: str, + crew: list, + ) -> None: + super().__init__(brand, color, max_capacity, speed, power_source, current_position) + self.engines = engines + self.food_supply_kg = food_supply_kg + self.passengers = passengers + self.destination = destination + self.crew = crew + + def board(self, num_passengers: int) -> None: + """Increase the number of passengers. + + :param num_passengers: The amount of passengers that board. + """ + self.passengers += num_passengers + logger.info(f"Boarded {num_passengers} passengers. Total passengers now: {self.passengers}") + + def resupply(self, kg_food: float) -> None: + """Resupply the food left on the boat. + + :param kg_food: Kilograms of food added to the supply. + """ + self.food_supply_kg += kg_food + logger.info( + f"Resupplied {kg_food} kg of food. Total food supply now: {self.food_supply_kg} kg." + ) + + def dock(self) -> None: + """Dock the boat.""" + logger.info("The boat has docked.") + + def update_destination(self, new_dest: str) -> None: + """Update the destination of the boat after docking. + + :param new_dest: The new destination of the boat. + """ + self.destination = new_dest + logger.info(f"Destination updated to {new_dest}.") + + +"""This is an example of how we could use the class. +boat_instance = Boat(brand="SeaMaster", color="White", max_capacity=50, speed=30, + power_source="Diesel", current_position=(0, 0), + engines=2, food_supply_kg=100, passengers=10, destination="Island A", + crew=["Captain John", "Chief Engineer Sarah", "Deckhand Mike", "Mate Emily"]) + +boat_instance.board(5) +boat_instance.accelerate(15, 1) +boat_instance.resupply(50) +boat_instance.update_destination("Island B") +boat_instance.dock() +""" + + +class Bike(Vehicle): + """Class Bike inheriting from Vehicle.""" + + def __init__( + self, + brand: str, + color: str, + max_capacity: int, + speed: float, + power_source: str, + current_position: float, + wheels: int, + owner: str, + size: int, + bike_type: str, + gear_transmission: str, + ) -> None: + super().__init__(brand, color, max_capacity, speed, power_source, current_position) + self.wheels = wheels + self.owner = owner + self.size = size + self.bike_type = bike_type + self.gear_transmission = gear_transmission + + def change_owner(self, new_owner: str) -> None: + """Change the owner of the bike. + + :param new_owner: The new owner of the bike. + """ + self.owner = new_owner + logger.info(f"Changed owner to {new_owner}.") + + def change_transmission(self, new_trans: str) -> None: + """Change the gear transmission of the bike. + + :param new_trans: The new transmission. + """ + self.gear_transmission = new_trans + logger.info(f"Changed transmission to {new_trans}.") + + +"""This is an example of how we could use the class. +bike_instance = Bike(brand="MountainBike", color="Red", max_capacity=1, speed=20, + power_source="Manual", current_position=(0, 0), + wheels=2, owner="Alice", size="Medium", + bike_type="Mountain", gear_transmission="7-speed") + +bike_instance.accelerate(10, 2) +bike_instance.change_owner("Bob") +bike_instance.change_transmission("5-speed") +""" diff --git a/scripts/python_training/solutions/m3_internal_libraries.py b/scripts/python_training/solutions/m3_internal_libraries.py new file mode 100755 index 0000000..53dee68 --- /dev/null +++ b/scripts/python_training/solutions/m3_internal_libraries.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +"""Solutions for the task of module 3: Internal libraries.""" +import pandas as pd +from nvs_sql import SqlRepository, SqlRepositoryError +from nvs_tk import Extractor +from nvs_tk import validation as val +from nvs_tk.errors import ExtractorError + +from python_training import M3_PATH + + +class TaskExtractor(Extractor): + """Extractor designed for the task of module 3.""" + + _QUERY_FILE = M3_PATH / "query.sql" + + COLUMNS = frozenset( + { + "drug_id", + "ims_id", + "country", + "molecule", + } + ) + + def __init__(self, repo: SqlRepository) -> None: + self._repo = repo + + def get(self) -> pd.DataFrame: + """Get the data.""" + try: + return self._repo.get( + self._QUERY_FILE, + ) + + except SqlRepositoryError as exc: + raise ExtractorError("Extractor was unable to retrieve data from DB.") from exc + + def validate(self, data: pd.DataFrame) -> None: + """Validate the data.""" + val.validate_not_empty(data) + val.validate_correct_columns(data, self.COLUMNS) + val.validate_no_duplicates(data) + + def clean(self, data: pd.DataFrame) -> pd.DataFrame: + """Clean the data.""" + return data diff --git a/scripts/python_training/solutions/m5_best_practices_a.py b/scripts/python_training/solutions/m5_best_practices_a.py new file mode 100755 index 0000000..08a08eb --- /dev/null +++ b/scripts/python_training/solutions/m5_best_practices_a.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +"""Solutions for the task of module 5: Best practices.""" + +def example_1_corrected(num_1, num_2): + """Given two numbers, return true if the first one is bigger than the second one. + + :param num_1: First number. + :param num_2: Second number. + """ + return num_1 > num_2 + + +def example_2_corrected(num): + """Convert a number from 1 to 5 into a vowel. + + :param num: Number to convert. + """ + mapper = { + 1: "a", + 2: "e", + 3: "i", + 4: "o", + 5: "u" + } + + try: + return mapper[num] + except KeyError: + print("Sorry, invalid number") + return 0 + + +def example_3_corrected(): + with open("file.txt") as f: + f.read() diff --git a/scripts/python_training/solutions/m5_best_practices_b.py b/scripts/python_training/solutions/m5_best_practices_b.py new file mode 100755 index 0000000..fc66c66 --- /dev/null +++ b/scripts/python_training/solutions/m5_best_practices_b.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +from typing import Union + + +class NumberContainer: + """Class NumberContainer.""" + + def __init__(self, value: Union[int, float]) -> None: + self.value = value + + def display_value(self): + """Display the value of the number.""" + print(f"Current value: {self.value}") diff --git a/scripts/python_training/solutions/m5_best_practices_c.py b/scripts/python_training/solutions/m5_best_practices_c.py new file mode 100755 index 0000000..824e525 --- /dev/null +++ b/scripts/python_training/solutions/m5_best_practices_c.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +from nvs_tk import Extractor + + +class TestExtractor(Extractor): + """Extractor used to retrieve data. + + :param repo: Repository used to retrieve data. + :param param1: Parameter 1. + :param param2: Parameter 2. + """ + + _QUERY_FILE = QUERIES_DIR / "file.sql" + + COLUMNS = frozenset( + { + "col_a", + "col_b", + "col_c", + "col_d", + } + ) + + def __init__(self, repo: SqlRepository, param1: None | int, param2: int) -> None: + self._repo = repo + self._param1 = param1 + self._param2 = param2 + + def get(self) -> pd.DataFrame: + try: + return self._repo.get( + self._QUERY_FILE, + params=utils.filter_version_id( + ("param1", self._param1), + ("param2", self._param2), + ), + ) + + except SqlRepository as exc: + raise ExtractorError("Extractor was unable to retrieve data from DB.") from exc + + def validate(self, data: pd.DataFrame) -> None: + val.validate_not_empty(data) + val.validate_correct_columns(data, self.COLUMNS) + val.validate_no_duplicates(data) + + def clean(self, data: pd.DataFrame) -> pd.DataFrame: + return data \ No newline at end of file diff --git a/scripts/python_training/solutions/m5_best_practices_d.py b/scripts/python_training/solutions/m5_best_practices_d.py new file mode 100755 index 0000000..5e8216a --- /dev/null +++ b/scripts/python_training/solutions/m5_best_practices_d.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +import sys + +import pandas as pd +from db_tools import start_connection, upload_to_db +from project import BASE_DIR + + +def upload_to_db(): + """Upload system information to database.""" + with open(BASE_DIR / "config.yaml") as file: + config = yaml.safe_load(file) + username = config["credentials"]["username"] + pwd = config["credentials"]["password"] + db = config["credentials"]["database"] + + version_info_dict = { + "major": sys.version_info.major, + "minor": sys.version_info.minor, + "micro": sys.version_info.micro, + "releaselevel": sys.version_info.releaselevel, + "serial": sys.version_info.serial, + } + + version_info_df = pd.DataFrame([version_info_dict]) + + conn = start_connection(db, username, pwd) + upload_to_db(conn, version_info_df) diff --git a/scripts/python_training/solutions/tests/conftest.py b/scripts/python_training/solutions/tests/conftest.py new file mode 100755 index 0000000..5625e56 --- /dev/null +++ b/scripts/python_training/solutions/tests/conftest.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +"""Configuration file for pytest.""" +import numpy as np +import pandas as pd +import pytest + + +@pytest.fixture +def df(): + return pd.DataFrame( + { + "Date": ["2020-01-01", "2020-01-02", "2020-01-03", "2020-01-04", "2020-01-05"], + "Open": [100, 101, 100, 103, 104], + "High": [101, 102, 101, 104, 105], + "Low": [99, 100, 99, 102, 103], + "Close": [100.5, 101.5, 100.5, 103.5, 104.5], + "Adj Close": [100, 101, 100, 103, 104], + "Volume": [1000, 1100, 1200, 1300, 1400], + } + ) + + +@pytest.fixture +def df_with_daily_returns(df): + df["Daily Returns"] = [np.nan, 0.01, -0.01, 0.03, 0.01] + return df + + +@pytest.fixture +def start_date(): + return "2020-01-02" + + +@pytest.fixture +def end_date(): + return "2020-01-04" + + +@pytest.fixture +def window(): + return 3 + + +@pytest.fixture +def lag_days(): + return 3 diff --git a/scripts/python_training/solutions/tests/integration/test_preprocess.py b/scripts/python_training/solutions/tests/integration/test_preprocess.py new file mode 100755 index 0000000..c45c751 --- /dev/null +++ b/scripts/python_training/solutions/tests/integration/test_preprocess.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from unittest.mock import patch + +import pandas as pd +import pytest + +from python_training.m2_external_libraries import preprocess + + +@pytest.fixture +def stocks_parquet(): + return pd.read_parquet("tests/assets/stocks_data.parquet") + + +@patch("python_training.m2_external_libraries._utils.db_connector.read_from_db") +def test_preprocess(mock_read_from_db, start_date, end_date, window, lag_days): + mock_read_from_db.return_value = pd.read_parquet("tests/assets/stocks_data.parquet") + processed_df = preprocess(start_date, end_date, window, lag_days) + + assert not processed_df.empty + assert f"MA_{window}" in processed_df.columns + assert "Volatility" in processed_df.columns + for i in range(1, lag_days + 1): + assert f"Close_lag_{i}" in processed_df.columns + + assert processed_df.index.min() >= pd.to_datetime(start_date) + assert processed_df.index.max() <= pd.to_datetime(end_date) diff --git a/scripts/python_training/solutions/tests/unit/test_add_lags.py b/scripts/python_training/solutions/tests/unit/test_add_lags.py new file mode 100755 index 0000000..c308a35 --- /dev/null +++ b/scripts/python_training/solutions/tests/unit/test_add_lags.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +"""Test for ``_add_lags`` function.""" + +import pandas as pd +import pytest + +from python_training.m2_external_libraries._preprocess import _add_lags + + +def test_add_lags(df_with_daily_returns, lag_days): + lagged_df = _add_lags(df_with_daily_returns, lag_days) + + for i in range(1, lag_days + 1): + assert f"Close_lag_{i}" in lagged_df.columns + + for i in range(1, lag_days + 1): + expected_lag = df_with_daily_returns["Close"].shift(i) + pd.testing.assert_series_equal(lagged_df[f"Close_lag_{i}"], expected_lag, check_names=False) + + +def test_add_lags_missing_close_column(df_with_daily_returns): + df_modified = df_with_daily_returns.drop(columns=["Close"]) + + with pytest.raises( + ValueError, match="DataFrame must contain a 'Close' column for lag feature calculations." + ): + _add_lags(df_modified, lag_days=5) + + +def test_add_lags_invalid_lag_days(df_with_daily_returns): + with pytest.raises(ValueError, match="'lag_days' must be a positive integer."): + _add_lags(df_with_daily_returns, lag_days=-1) diff --git a/scripts/python_training/solutions/tests/unit/test_enhance_with_technical_indicators.py b/scripts/python_training/solutions/tests/unit/test_enhance_with_technical_indicators.py new file mode 100755 index 0000000..77ab224 --- /dev/null +++ b/scripts/python_training/solutions/tests/unit/test_enhance_with_technical_indicators.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +"""Test for ``_enhance_with_technical_indicators`` function.""" +import pandas as pd +import pytest + +from python_training.m2_external_libraries._preprocess import _enhance_with_technical_indicators + + +def test_enhance_with_technical_indicators(df_with_daily_returns, window): + enhanced_df = _enhance_with_technical_indicators(df_with_daily_returns, window) + expected_ma = df_with_daily_returns["Close"].rolling(window=window).mean().round(3) + expected_volatility = ( + df_with_daily_returns["Daily Returns"].rolling(window=window).std().round(3) + ) + + assert f"MA_{window}" in enhanced_df.columns + assert "Volatility" in enhanced_df.columns + pd.testing.assert_series_equal( + enhanced_df[f"MA_{window}"].round(3), expected_ma, check_names=False + ) + pd.testing.assert_series_equal( + enhanced_df["Volatility"].round(3), expected_volatility, check_names=False + ) + + +def test_enhance_with_technical_indicators_missing_columns(df_with_daily_returns, window): + df_modified = df_with_daily_returns.drop(columns=["Close"]) + + with pytest.raises( + ValueError, + match="DataFrame is missing required columns for technical indicator calculations", + ): + _enhance_with_technical_indicators(df_modified, window) + + +def test_enhance_with_technical_indicators_invalid_window(df_with_daily_returns): + with pytest.raises(ValueError, match="'window' must be a positive integer."): + _enhance_with_technical_indicators(df_with_daily_returns, window=-1) diff --git a/scripts/python_training/solutions/tests/unit/test_prepare_and_normalize_data.py b/scripts/python_training/solutions/tests/unit/test_prepare_and_normalize_data.py new file mode 100755 index 0000000..1fb7689 --- /dev/null +++ b/scripts/python_training/solutions/tests/unit/test_prepare_and_normalize_data.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +"""Test for ``_prepare_and_normalize_data`` function.""" +import numpy as np +import pandas as pd +import pytest + +from python_training.m2_external_libraries._preprocess import _prepare_and_normalize_data + + +def test_prepare_and_normalize_data(df, start_date, end_date): + processed_df = _prepare_and_normalize_data(df, start_date, end_date) + returns = processed_df["Daily Returns"].round(3).to_list() + + assert processed_df.index[0] == pd.to_datetime(start_date) + assert processed_df.index[-1] == pd.to_datetime(end_date) + + assert "Month" in processed_df.columns + assert "DayOfWeek" in processed_df.columns + assert "Daily Returns" in processed_df.columns + assert np.isnan(returns[0]) + assert returns[1:] == [-0.01, 0.03] + + +def test_prepare_and_normalize_data_missing_date_column(df, start_date, end_date): + df = df.drop(columns=["Date"]) + + with pytest.raises(ValueError, match="DataFrame must contain a 'Date' column."): + _prepare_and_normalize_data(df, start_date, end_date) + + +def test_prepare_and_normalize_data_invalid_date_format(df, start_date, end_date): + df["Date"] = ["not a date", "neither this", "2020-01-03", "2020-01-04", "2020-01-05"] + + with pytest.raises(ValueError, match="Error converting 'Date' column to datetime."): + _prepare_and_normalize_data(df, start_date, end_date)