From ce35c3aceb652734e0df2f7e93d5bb56435c6eab Mon Sep 17 00:00:00 2001 From: Dan Fields Date: Mon, 12 Feb 2024 21:50:14 -0500 Subject: [PATCH 1/4] exercise 1 work --- Dockerfile | 2 ++ __init__.py | 0 dan_solutions/__init__.py | 0 dan_solutions/ex_1/__init__.py | 0 dan_solutions/ex_1/art.py | 15 +++++++++++++++ dan_solutions/ex_1/pcost.py | 29 +++++++++++++++++++++++++++++ dan_solutions/ex_1/stock.py | 12 ++++++++++++ 7 files changed, 58 insertions(+) create mode 100644 __init__.py create mode 100644 dan_solutions/__init__.py create mode 100644 dan_solutions/ex_1/__init__.py create mode 100644 dan_solutions/ex_1/art.py create mode 100644 dan_solutions/ex_1/pcost.py create mode 100644 dan_solutions/ex_1/stock.py diff --git a/Dockerfile b/Dockerfile index 2a5c1702..9b40e92f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,8 @@ ARG env RUN echo $env +ENV PYTHONPATH "${PYTHONPATH}:/usr/src/app" + WORKDIR /usr/src/app COPY requirements.txt . diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dan_solutions/__init__.py b/dan_solutions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dan_solutions/ex_1/__init__.py b/dan_solutions/ex_1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dan_solutions/ex_1/art.py b/dan_solutions/ex_1/art.py new file mode 100644 index 00000000..fcad39d8 --- /dev/null +++ b/dan_solutions/ex_1/art.py @@ -0,0 +1,15 @@ +import random +import sys + +chars = "\\|/" + + +def draw(rows: int, columns: int): + for _ in range(rows): + print("".join(random.choice(chars) for _ in range(columns))) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + raise SystemExit("Usage: art.py rows columns") + draw(int(sys.argv[1]), int(sys.argv[2])) diff --git a/dan_solutions/ex_1/pcost.py b/dan_solutions/ex_1/pcost.py new file mode 100644 index 00000000..17b862f4 --- /dev/null +++ b/dan_solutions/ex_1/pcost.py @@ -0,0 +1,29 @@ +from dan_solutions.ex_1.stock import Stock + + +def read_file(path: str) -> list[Stock]: + stocks: list[Stock] = [] + with open(path, "r") as f: + for line in f.readlines(): + data = line.split() + name, number_of_shares, share_price = tuple(data) + try: + stock = Stock( + name=name, + number_of_shares=int(number_of_shares), + share_price=float(share_price), + ) + stocks.append(stock) + except ValueError as ve: + print(f"WARNING: cannot parse {data}: {ve}") + + return stocks + + +def portofolio_cost(path: str) -> float: + stocks = read_file(path) + return sum([stock.cost for stock in stocks]) + + +if __name__ == "__main__": + print(f"portfolio total: ${portofolio_cost('Data/portfolio.dat'):.2f}") diff --git a/dan_solutions/ex_1/stock.py b/dan_solutions/ex_1/stock.py new file mode 100644 index 00000000..11c7a63a --- /dev/null +++ b/dan_solutions/ex_1/stock.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + + +@dataclass +class Stock: + name: str + number_of_shares: int + share_price: float + + @property + def cost(self) -> float: + return self.number_of_shares * self.share_price From d7815e2f0af5e82d8344672473e96bfa83b056c4 Mon Sep 17 00:00:00 2001 From: Dan Fields Date: Thu, 29 Feb 2024 19:58:52 -0500 Subject: [PATCH 2/4] exercise twos --- dan_solutions/ex_2/ex2_2.py | 71 ++++++++++++++++++++++ dan_solutions/ex_2/ex2_3.txt | 2 + dan_solutions/ex_2/mutint.py | 66 +++++++++++++++++++++ dan_solutions/ex_2/readrides.py | 101 ++++++++++++++++++++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 dan_solutions/ex_2/ex2_2.py create mode 100644 dan_solutions/ex_2/ex2_3.txt create mode 100644 dan_solutions/ex_2/mutint.py create mode 100644 dan_solutions/ex_2/readrides.py diff --git a/dan_solutions/ex_2/ex2_2.py b/dan_solutions/ex_2/ex2_2.py new file mode 100644 index 00000000..a1455df1 --- /dev/null +++ b/dan_solutions/ex_2/ex2_2.py @@ -0,0 +1,71 @@ +import csv +from collections import Counter, defaultdict + + +class Route: + __slots__ = ["number", "date", "daytype", "rides"] + + def __init__(self, number: str, date: str, daytype: str, rides: int): + self.number = number + self.date = date + self.daytype = daytype + self.rides = rides + + +def read_data(file_name: str) -> list[Route]: + records: list[Route] = [] + with open(file_name) as f: + rows = csv.reader(f) + _ = next(rows) # Skip headers + for row in rows: + route = Route(row[0], row[1], row[2], int(row[3])) + records.append(route) + + return records + + +if __name__ == "__main__": + routes = read_data("Data/ctabus.csv") + + unique_routes = {route.number for route in routes} + number_of_unique_routes = len(unique_routes) + print(f"1: {number_of_unique_routes=}") + + target_day = "02/02/2011" + target_route = "22" + number_of_target_day_routes = sum( + [ + route.rides + for route in routes + if route.number == target_route and route.date == target_day + ] + ) + print(f"2: {number_of_target_day_routes=} for {target_route} on {target_day}") + + ride_counter = Counter[str]() + for route in routes: + ride_counter[route.number] = +route.rides + print(f"3: {ride_counter=}") + + rides_by_year: dict[tuple[str, str], int] = defaultdict() + for route in routes: + rides_by_year[route.number, route.date[-4:]] = ( + rides_by_year.get((route.number, route.date[-4:]), 0) + route.rides + ) + + growth_counter = Counter[str]() + for route in unique_routes: + for year in range(2011, 2001, -1): + if (route, str(year)) in rides_by_year: + current = rides_by_year[route, str(year)] + + prev = 0 + for prev_year in range(year - 1, 2001, -1): + if (route, str(prev_year)) in rides_by_year: + prev = rides_by_year[route, str(prev_year)] + break + + change = current - prev + growth_counter[route] = change + + print(f"4. {growth_counter.most_common(5)=}") diff --git a/dan_solutions/ex_2/ex2_3.txt b/dan_solutions/ex_2/ex2_3.txt new file mode 100644 index 00000000..a6de81d4 --- /dev/null +++ b/dan_solutions/ex_2/ex2_3.txt @@ -0,0 +1,2 @@ +>>> tracemalloc.get_traced_memory() +(102976, 132591) \ No newline at end of file diff --git a/dan_solutions/ex_2/mutint.py b/dan_solutions/ex_2/mutint.py new file mode 100644 index 00000000..ce72d039 --- /dev/null +++ b/dan_solutions/ex_2/mutint.py @@ -0,0 +1,66 @@ +# from dan_solutions.ex_2.mutint import MutInt + + +from functools import total_ordering +from typing import Any + + +@total_ordering +class MutInt: + __slots__ = ["value"] + + def __init__(self, value: int): + self.value = value + + def __str__(self): + return str(self.value) + + def __repr__(self): + return f"MutInt({self.value!r})" + + def __format__(self, fmt: str): + return format(self.value, fmt) + + def __add__(self, other: Any): + if isinstance(other, MutInt): + return MutInt(self.value + other.value) + elif isinstance(other, int): + return MutInt(self.value + other) + else: + return NotImplemented + + __radd__ = __add__ # Reversed operands + + def __iadd__(self, other: Any): + if isinstance(other, MutInt): + self.value += other.value + return self + elif isinstance(other, int): + self.value += other + return self + else: + return NotImplemented + + def __eq__(self, other: Any): + if isinstance(other, MutInt): + return self.value == other.value + elif isinstance(other, int): + return self.value == other + else: + return NotImplemented + + def __lt__(self, other: Any): + if isinstance(other, MutInt): + return self.value < other.value + elif isinstance(other, int): + return self.value < other + else: + return NotImplemented + + def __int__(self): + return self.value + + def __float__(self): + return float(self.value) + + __index__ = __int__ # Make indexing work diff --git a/dan_solutions/ex_2/readrides.py b/dan_solutions/ex_2/readrides.py new file mode 100644 index 00000000..54f655a1 --- /dev/null +++ b/dan_solutions/ex_2/readrides.py @@ -0,0 +1,101 @@ +import csv +import typing +from dataclasses import dataclass + + +def read_rides_as_tuples(row: list[str]) -> tuple[str, str, str, int]: + route = row[0] + date = row[1] + daytype = row[2] + rides = int(row[3]) + return (route, date, daytype, rides) + + +def read_rides_as_dict(row: list[str]) -> dict[str, str | int]: + return { + "route": row[0], + "date": row[1], + "daytype": row[2], + "rides": int(row[3]), + } + + +class Row: + def __init__(self, route: str, date: str, daytype: str, rides: int): + self.route = route + self.date = date + self.daytype = daytype + self.rides = rides + + +def read_rides_as_class(row: list[str]) -> Row: + return Row(row[0], row[1], row[2], int(row[3])) + + +class RowNamedTuple(typing.NamedTuple): + route: str + date: str + daytype: str + rides: int + + +def read_rides_as_named_tuple(row: list[str]) -> RowNamedTuple: + return RowNamedTuple(row[0], row[1], row[2], int(row[3])) + + +class RowWithSlots: + __slots__ = ["route", "date", "daytype", "rides"] + + def __init__(self, route: str, date: str, daytype: str, rides: int): + self.route = route + self.date = date + self.daytype = daytype + self.rides = rides + + +def read_rides_as_class_with_slots(row: list[str]) -> RowWithSlots: + return RowWithSlots(row[0], row[1], row[2], int(row[3])) + + +def read_data(file_name: str, collector: typing.Callable[[list[str]], typing.Any]): + records: list[typing.Any] = [] + with open(file_name) as f: + rows = csv.reader(f) + _ = next(rows) # Skip headers + for row in rows: + records.append(collector(row)) + + return records + + +@dataclass +class Result: + method: str + current: int + peak: int + + +if __name__ == "__main__": + import tracemalloc + + test_methods = [ + read_rides_as_tuples, + read_rides_as_dict, + read_rides_as_class, + read_rides_as_named_tuple, + read_rides_as_class_with_slots, + ] + + results: list[Result] = [] + for test_method in test_methods: + tracemalloc.start() + rows = read_data("Data/ctabus.csv", test_method) + current, peak = tracemalloc.get_traced_memory() + results.append(Result(test_method.__name__, current, peak)) + tracemalloc.reset_peak() + + for result in results: + print(f"{result.method=}: {result.current=:,} / {result.peak=:,}") + + min_result = min(results, key=lambda r: r.current) + print(f"most efficient: {min_result.method}") From 1d2fad8b5392c322103a1d502e235d59adf582d5 Mon Sep 17 00:00:00 2001 From: Dan Fields Date: Thu, 21 Mar 2024 22:00:55 -0400 Subject: [PATCH 3/4] ex 3 --- dan_solutions/ex_3/lenny.py | 31 ++++++++ dan_solutions/ex_3/reader.py | 14 ++++ dan_solutions/ex_3/stock.py | 122 +++++++++++++++++++++++++++++ dan_solutions/ex_3/table_format.py | 122 +++++++++++++++++++++++++++++ 4 files changed, 289 insertions(+) create mode 100644 dan_solutions/ex_3/lenny.py create mode 100644 dan_solutions/ex_3/reader.py create mode 100644 dan_solutions/ex_3/stock.py create mode 100644 dan_solutions/ex_3/table_format.py diff --git a/dan_solutions/ex_3/lenny.py b/dan_solutions/ex_3/lenny.py new file mode 100644 index 00000000..67938d4d --- /dev/null +++ b/dan_solutions/ex_3/lenny.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass + + +@dataclass +class Gecko: + name: str + color: str + +done = False +geckos: list[Gecko] = [] +while done: + name = input("what is the gecko's name ? ") + color = input("what is the gecko's color ? ") + if name == "done": + done = True + else: + geckos.append(Gecko(name=name, color=color)) + +done = False +while done: + first_gecko_name = input("first gecko to marry") + if first_gecko_name == "done": + done = True + continue + second_gecko_name = input("second gecko to marry") + + first_gecko_color = [] + print(f"Their baby gecko's color is {} and {}") + + + diff --git a/dan_solutions/ex_3/reader.py b/dan_solutions/ex_3/reader.py new file mode 100644 index 00000000..dbc8a980 --- /dev/null +++ b/dan_solutions/ex_3/reader.py @@ -0,0 +1,14 @@ +import csv +from typing import TypeVar + +TClass = TypeVar("TClass") + + +def read_csv_as_instances(filename: str, cls: TClass) -> list[TClass]: + records: list[TClass] = [] + with open(filename) as f: + rows = csv.reader(f) + _ = next(rows) + for row in rows: + records.append(cls.from_row(row)) # type: ignore + return records diff --git a/dan_solutions/ex_3/stock.py b/dan_solutions/ex_3/stock.py new file mode 100644 index 00000000..bbcd4321 --- /dev/null +++ b/dan_solutions/ex_3/stock.py @@ -0,0 +1,122 @@ +from dataclasses import dataclass +from decimal import Decimal +from typing import Any, Callable, Self + + +@dataclass +class Stock: + __slots__ = ("_name", "_shares", "_price") + + _types = (str, int, float) + + def __init__(self, name: str, shares: int, price: float): + self._name = name + self._shares = shares + self._price = price + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + if not isinstance(value, self._types[0]): # type: ignore + raise TypeError(f"{value} is not str") + + @property + def shares(self) -> int: + return self._shares + + @shares.setter + def shares(self, value: int): + if not isinstance(value, self._types[1]): # type: ignore + raise TypeError(f"{value} is not int") + self._shares = value + + @property + def price(self) -> float: + return self._price + + @price.setter + def price(self, value: float): + if not isinstance(value, self._types[2]): + raise TypeError(f"{value} is not float") + self._price = value + + @property + def cost(self) -> float: + return self.shares * self.price + + def sell(self, number_of_shares_to_sell: int): + self.shares -= number_of_shares_to_sell + + @classmethod + def from_row(cls, row: list[str]) -> Self | None: + try: + values = [func(val) for func, val in zip(cls._types, row)] + return cls(*values) # type: ignore + except ValueError as ve: + print(f"WARNING: cannot parse {row}: {ve}") + return None + + def __repr__(self) -> str: + return f"Stock('{self.name}', {self.price}, {self.shares})" + + def __eq__(self, other: object) -> bool: + return isinstance(other, Stock) and ( + (self.name, self.shares, self.price) + == (other.name, other.shares, other.price) + ) + + +class DStock(Stock): + _types = (str, int, Decimal) + + +def print_portfolio(portfolio: list[Stock]): + def get_max_length(get_attr: Callable[[Stock], Any]) -> int: + return max([len(str(get_attr(stock))) for stock in portfolio]) + + padding = 6 + name_column_width = padding + get_max_length(lambda s: s.name) + shares_column_width = padding + get_max_length(lambda s: s.shares) + prices_column_width = padding + get_max_length(lambda s: s.price) + + def build_column(width: int, value: str) -> str: + return f"{' ' * (width - len(value))}{value}" + + def build_name_column(value: str) -> str: + return build_column(name_column_width, value) + + def build_shares_column(value: str) -> str: + return build_column(shares_column_width, value) + + def build_prices_column(value: str) -> str: + return build_column(prices_column_width, value) + + header = [ + build_name_column("name"), + build_shares_column("shares"), + build_prices_column("price"), + ] + + sep = [ + "-" * name_column_width, + "-" * shares_column_width, + "-" * prices_column_width, + ] + + stock_rows: list[list[str]] = [] + for stock in portfolio: + stock_rows.append( + [ + build_name_column(stock.name), + build_shares_column(str(stock.shares)), + build_prices_column(f"{stock.price:.2f}"), + ] + ) + + print(" ".join(header)) + print(" ".join(sep)) + for row in stock_rows: + print(" ".join(row)) diff --git a/dan_solutions/ex_3/table_format.py b/dan_solutions/ex_3/table_format.py new file mode 100644 index 00000000..9c628d26 --- /dev/null +++ b/dan_solutions/ex_3/table_format.py @@ -0,0 +1,122 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any, Literal, Type + + +def print_table_old(data: list[object], columns: list[str]): + @dataclass + class Output: + column: str + value: str + + max_width_map: dict[str, int] = {column: len(column) for column in columns} + outputs: list[list[Output]] = [] + for item in data: + line: list[Output] = [] + for column in columns: + value = str(getattr(item, column, "")) + line.append(Output(column, value)) + value_width = len(value) + if max_width_map.get(column, 0) < value_width: + max_width_map[column] = value_width + outputs.append(line) + + padding = 5 + + def print_row(data: list[str]): + print(" ".join(data)) + + def build_column(column: str, value: str) -> str: + return f"{' ' * abs(max_width_map[column] - len(value)+ padding)}{value}" + + print_row([build_column(column, column) for column in columns]) + print_row(["-" * (max_width_map[column] + padding) for column in columns]) + + for output in outputs: + print_row([build_column(o.column, o.value) for o in output]) + + +class TableFormatter(ABC): + @abstractmethod + def headings(self, headers: list[str]) -> None: + raise NotImplementedError() + + @abstractmethod + def row(self, row_data: list[Any]) -> None: + raise NotImplementedError() + + +class ColumnFormatMixin: + formats: list[str] = [] + + def row(self, row_data: list[Any]): + row_data = [(fmt % d) for fmt, d in zip(self.formats, row_data)] + super().row(row_data) # type: ignore + + +class UpperHeadersMixin: + def headings(self, headers: list[str]): + super().headings([h.upper() for h in headers]) # type: ignore + + +class TextTableFormatter(TableFormatter): + def headings(self, headers: list[str]): + print(" ".join("%10s" % h for h in headers)) + print(("-" * 10 + " ") * len(headers)) + + def row(self, row_data: list[Any]): + print(" ".join("%10s" % d for d in row_data)) + + +class CSVTableFormatter(TableFormatter): + def headings(self, headers: list[str]) -> None: + print(",".join(headers)) + + def row(self, row_data: list[Any]) -> None: + print(",".join(str(d) for d in row_data)) + + +class HTMLTableFormatter(TableFormatter): + def headings(self, headers: list[str]) -> None: + print(f" {' '.join(f"{h}" for h in headers) } ") + + def row(self, row_data: list[Any]) -> None: + print(f" {' '.join(f"{r}" for r in row_data) } ") + + +def print_table(records: list[object], fields: list[str], formatter: TableFormatter): + if not isinstance(formatter, TableFormatter): # type: ignore + raise TypeError(f"formatter {formatter} not compatable.") + + formatter.headings(fields) + for r in records: + rowdata = [getattr(r, fieldname) for fieldname in fields] + formatter.row(rowdata) + + +def create_formatter( + format: Literal["text", "csv", "html"], + column_formats: list[str] | None = None, + upper_headers: bool = False, +) -> TableFormatter: + formatter_map: dict[str, Type[TableFormatter]] = { + "text": TextTableFormatter, + "csv": CSVTableFormatter, + "html": HTMLTableFormatter, + } + + formatter = formatter_map[format] # type: ignore + if not formatter: + raise Exception(f"{format} not supported") + + if column_formats: + + class formatter(ColumnFormatMixin, formatter): # type: ignore + formats = column_formats + + if upper_headers: + + class formatter(UpperHeadersMixin, formatter): # type: ignore + pass + + return formatter() # type: ignore From 03ada86a798acb6334a8ccd326353ed67c4c3921 Mon Sep 17 00:00:00 2001 From: Dan Fields Date: Sat, 6 Apr 2024 21:36:27 -0400 Subject: [PATCH 4/4] 4 --- Exercises/ex4_1.md | 2 +- dan_solutions/ex_4/descrip.py | 15 ++++++ dan_solutions/ex_4/validate.py | 96 ++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 dan_solutions/ex_4/descrip.py create mode 100644 dan_solutions/ex_4/validate.py diff --git a/Exercises/ex4_1.md b/Exercises/ex4_1.md index 3201d9cb..58b1dc34 100644 --- a/Exercises/ex4_1.md +++ b/Exercises/ex4_1.md @@ -1,4 +1,4 @@ -\[ [Index](index.md) | [Exercise 3.8](ex3_8.md) | [Exercise 4.2](ex4_2.md) \] +py\[ [Index](index.md) | [Exercise 3.8](ex3_8.md) | [Exercise 4.2](ex4_2.md) \] # Exercise 4.1 diff --git a/dan_solutions/ex_4/descrip.py b/dan_solutions/ex_4/descrip.py new file mode 100644 index 00000000..2657886c --- /dev/null +++ b/dan_solutions/ex_4/descrip.py @@ -0,0 +1,15 @@ +from typing import Any + + +class Descriptor: + def __init__(self, name: str): + self.name = name + + def __get__(self, instance: object, cls: type): + print("%s:__get__" % self.name) + + def __set__(self, instance: object, value: Any): + print("%s:__set__ %s" % (self.name, value)) + + def __delete__(self, instance: object): + print("%s:__delete__" % self.name) diff --git a/dan_solutions/ex_4/validate.py b/dan_solutions/ex_4/validate.py new file mode 100644 index 00000000..0e743196 --- /dev/null +++ b/dan_solutions/ex_4/validate.py @@ -0,0 +1,96 @@ +from dataclasses import dataclass +from typing import Any + + +class Validator: + def __init__(self, name: str | None = None): + self.name = name + + def __set_name__(self, cls: type, name: str): + self.name = name + + @classmethod + def check(cls, value: Any) -> Any: + return value + + def __set__(self, instance: object, value: Any): + if self.name: + instance.__dict__[self.name] = self.check(value) + + +class Typed(Validator): + expected_type = object + + @classmethod + def check(cls, value: Any): + if not isinstance(value, cls.expected_type): + raise TypeError(f"Expected {cls.expected_type}") + return super().check(value) + + +class Integer(Typed): + expected_type = int + + +class Float(Typed): + expected_type = float + + +class String(Typed): + expected_type = str + + +class Positive(Validator): + @classmethod + def check(cls, value: int | float): + if value < 0: + raise ValueError("Expected >= 0") + return super().check(value) + + +class NonEmpty(Validator): + @classmethod + def check(cls, value: str | list[Any]): + if len(value) == 0: + raise ValueError("Must be non-empty") + return super().check(value) + + +class PositiveInteger(Integer, Positive): + pass + + +class PositiveFloat(Float, Positive): + pass + + +class NonEmptyString(String, NonEmpty): + pass + + +@dataclass +class Stock: + name = String() + shares = PositiveInteger() + price = PositiveFloat() + + def __init__(self, name: str, shares: int, price: float): + self.name = name + self.shares = shares + self.price = price + + @property + def cost(self) -> float: + return self.shares * self.price # type: ignore + + def sell(self, number_of_shares_to_sell: int): + self.shares -= number_of_shares_to_sell # type: ignore + + def __repr__(self) -> str: + return f"Stock('{self.name}', {self.price}, {self.shares})" + + def __eq__(self, other: object) -> bool: + return isinstance(other, Stock) and ( + (self.name, self.shares, self.price) + == (other.name, other.shares, other.price) + )