diff --git a/.github/workflows/cs102.yml b/.github/workflows/cs102.yml index 4509ac4..8e1e8be 100644 --- a/.github/workflows/cs102.yml +++ b/.github/workflows/cs102.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8.6 + - name: Set up Python 3.10.7 uses: actions/setup-python@v2 with: - python-version: '3.8.6' + python-version: '3.10.7' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.gitignore b/.gitignore index f5cb6b3..39f700b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ coverage.xml # Mypy cache .mypy_cache/ +venv +.pyvcs +pyvcs.* diff --git a/homework00/hello.py b/homework00/hello.py index efe8767..5802c29 100644 --- a/homework00/hello.py +++ b/homework00/hello.py @@ -1,5 +1,5 @@ def get_greeting(name: str) -> str: - pass + return f"Hello, {name}!" if __name__ == "__main__": diff --git a/homework01/caesar.py b/homework01/caesar.py index 09c3681..7b05368 100644 --- a/homework01/caesar.py +++ b/homework01/caesar.py @@ -1,46 +1,28 @@ import typing as tp +from string import ascii_lowercase as lower +from string import ascii_uppercase as upper def encrypt_caesar(plaintext: str, shift: int = 3) -> str: - """ - Encrypts plaintext using a Caesar cipher. - - >>> encrypt_caesar("PYTHON") - 'SBWKRQ' - >>> encrypt_caesar("python") - 'sbwkrq' - >>> encrypt_caesar("Python3.6") - 'Sbwkrq3.6' - >>> encrypt_caesar("") - '' - """ ciphertext = "" - # PUT YOUR CODE HERE + for char in plaintext: + if char.isalpha(): + if char.islower(): + char = lower[(lower.index(char) + shift) % 26] + else: + char = upper[(upper.index(char) + shift) % 26] + ciphertext += char return ciphertext def decrypt_caesar(ciphertext: str, shift: int = 3) -> str: - """ - Decrypts a ciphertext using a Caesar cipher. - - >>> decrypt_caesar("SBWKRQ") - 'PYTHON' - >>> decrypt_caesar("sbwkrq") - 'python' - >>> decrypt_caesar("Sbwkrq3.6") - 'Python3.6' - >>> decrypt_caesar("") - '' - """ - plaintext = "" - # PUT YOUR CODE HERE - return plaintext + return encrypt_caesar(ciphertext, -shift) def caesar_breaker_brute_force(ciphertext: str, dictionary: tp.Set[str]) -> int: - """ - Brute force breaking a Caesar cipher. - """ best_shift = 0 - # PUT YOUR CODE HERE + for shift in range(26): + decrypted_msg = decrypt_caesar(ciphertext, shift) + if decrypted_msg in dictionary: + best_shift = shift return best_shift diff --git a/homework01/rsa.py b/homework01/rsa.py index b777be5..1d56554 100644 --- a/homework01/rsa.py +++ b/homework01/rsa.py @@ -3,43 +3,32 @@ def is_prime(n: int) -> bool: - """ - Tests to see if a number is prime. - - >>> is_prime(2) - True - >>> is_prime(11) - True - >>> is_prime(8) - False - """ - # PUT YOUR CODE HERE - pass + for div in range(2, int(n**0.5) + 1): + if n % div == 0: + return False + return n != 1 def gcd(a: int, b: int) -> int: - """ - Euclid's algorithm for determining the greatest common divisor. + if b == 0: + return a + return gcd(b, a % b) - >>> gcd(12, 15) - 3 - >>> gcd(3, 7) - 1 - """ - # PUT YOUR CODE HERE - pass +def gcdex(a, b): + if b == 0: + return a, 1, 0 + else: + d, x, y = gcdex(b, a % b) + return d, y, x - y * (a // b) -def multiplicative_inverse(e: int, phi: int) -> int: - """ - Euclid's extended algorithm for finding the multiplicative - inverse of two numbers. - >>> multiplicative_inverse(7, 40) - 23 - """ - # PUT YOUR CODE HERE - pass +def multiplicative_inverse(a: int, n: int) -> int: + d, x, y = gcdex(a, n) + if d == 1: + return (x % n + n) % n + else: + return 0 def generate_keypair(p: int, q: int) -> tp.Tuple[tp.Tuple[int, int], tp.Tuple[int, int]]: @@ -49,10 +38,10 @@ def generate_keypair(p: int, q: int) -> tp.Tuple[tp.Tuple[int, int], tp.Tuple[in raise ValueError("p and q cannot be equal") # n = pq - # PUT YOUR CODE HERE + n = p * q # phi = (p-1)(q-1) - # PUT YOUR CODE HERE + phi = (p - 1) * (q - 1) # Choose an integer e such that e and phi(n) are coprime e = random.randrange(1, phi) @@ -85,7 +74,7 @@ def decrypt(pk: tp.Tuple[int, int], ciphertext: tp.List[int]) -> str: # Unpack the key into its components key, n = pk # Generate the plaintext based on the ciphertext and key using a^b mod m - plain = [chr((char ** key) % n) for char in ciphertext] + plain = [chr((char**key) % n) for char in ciphertext] # Return the array of bytes as a string return "".join(plain) diff --git a/homework01/vigenere.py b/homework01/vigenere.py index e51742e..1cbb09d 100644 --- a/homework01/vigenere.py +++ b/homework01/vigenere.py @@ -1,30 +1,18 @@ -def encrypt_vigenere(plaintext: str, keyword: str) -> str: - """ - Encrypts plaintext using a Vigenere cipher. +from caesar import decrypt_caesar, encrypt_caesar - >>> encrypt_vigenere("PYTHON", "A") - 'PYTHON' - >>> encrypt_vigenere("python", "a") - 'python' - >>> encrypt_vigenere("ATTACKATDAWN", "LEMON") - 'LXFOPVEFRNHR' - """ - ciphertext = "" - # PUT YOUR CODE HERE - return ciphertext +def encrypt_vigenere(plaintext: str, keyword: str, encode: bool = True) -> str: + ciphertext = [] + for index, char in enumerate(plaintext.lower()): + shift = ord(keyword[index % len(keyword)].lower()) - 97 + if encode: + ciphertext.append(encrypt_caesar(char, shift)) + else: + ciphertext.append(decrypt_caesar(char, shift)) + if plaintext[index].isupper(): + ciphertext[-1] = ciphertext[-1].upper() + return "".join(ciphertext) -def decrypt_vigenere(ciphertext: str, keyword: str) -> str: - """ - Decrypts a ciphertext using a Vigenere cipher. - >>> decrypt_vigenere("PYTHON", "A") - 'PYTHON' - >>> decrypt_vigenere("python", "a") - 'python' - >>> decrypt_vigenere("LXFOPVEFRNHR", "LEMON") - 'ATTACKATDAWN' - """ - plaintext = "" - # PUT YOUR CODE HERE - return plaintext +def decrypt_vigenere(ciphertext: str, keyword: str) -> str: + return encrypt_vigenere(ciphertext, keyword, encode=False) diff --git a/homework02/sudoku.py b/homework02/sudoku.py index df78ab1..6de9720 100644 --- a/homework02/sudoku.py +++ b/homework02/sudoku.py @@ -1,11 +1,12 @@ import pathlib import typing as tp +from random import randint T = tp.TypeVar("T") def read_sudoku(path: tp.Union[str, pathlib.Path]) -> tp.List[tp.List[str]]: - """ Прочитать Судоку из указанного файла """ + """Прочитать Судоку из указанного файла""" path = pathlib.Path(path) with path.open() as f: puzzle = f.read() @@ -19,7 +20,7 @@ def create_grid(puzzle: str) -> tp.List[tp.List[str]]: def display(grid: tp.List[tp.List[str]]) -> None: - """Вывод Судоку """ + """Вывод Судоку""" width = 2 line = "+".join(["-" * (width * 3)] * 3) for row in range(9): @@ -34,129 +35,95 @@ def display(grid: tp.List[tp.List[str]]) -> None: def group(values: tp.List[T], n: int) -> tp.List[tp.List[T]]: - """ - Сгруппировать значения values в список, состоящий из списков по n элементов - - >>> group([1,2,3,4], 2) - [[1, 2], [3, 4]] - >>> group([1,2,3,4,5,6,7,8,9], 3) - [[1, 2, 3], [4, 5, 6], [7, 8, 9]] - """ - pass + """Сгруппировать значения values в список, состоящий из списков по n элементов""" + res = [] + tmp = [] + for value in values: + tmp.append(value) + if len(tmp) == n: + res.append(tmp) + tmp = [] + return res def get_row(grid: tp.List[tp.List[str]], pos: tp.Tuple[int, int]) -> tp.List[str]: - """Возвращает все значения для номера строки, указанной в pos - - >>> get_row([['1', '2', '.'], ['4', '5', '6'], ['7', '8', '9']], (0, 0)) - ['1', '2', '.'] - >>> get_row([['1', '2', '3'], ['4', '.', '6'], ['7', '8', '9']], (1, 0)) - ['4', '.', '6'] - >>> get_row([['1', '2', '3'], ['4', '5', '6'], ['.', '8', '9']], (2, 0)) - ['.', '8', '9'] - """ - pass + """Возвращает все значения для номера строки, указанной в pos""" + return grid[pos[0]] def get_col(grid: tp.List[tp.List[str]], pos: tp.Tuple[int, int]) -> tp.List[str]: - """Возвращает все значения для номера столбца, указанного в pos - - >>> get_col([['1', '2', '.'], ['4', '5', '6'], ['7', '8', '9']], (0, 0)) - ['1', '4', '7'] - >>> get_col([['1', '2', '3'], ['4', '.', '6'], ['7', '8', '9']], (0, 1)) - ['2', '.', '8'] - >>> get_col([['1', '2', '3'], ['4', '5', '6'], ['.', '8', '9']], (0, 2)) - ['3', '6', '9'] - """ - pass + """Возвращает все значения для номера столбца, указанного в pos""" + return [row[pos[1]] for row in grid] def get_block(grid: tp.List[tp.List[str]], pos: tp.Tuple[int, int]) -> tp.List[str]: - """Возвращает все значения из квадрата, в который попадает позиция pos - - >>> grid = read_sudoku('puzzle1.txt') - >>> get_block(grid, (0, 1)) - ['5', '3', '.', '6', '.', '.', '.', '9', '8'] - >>> get_block(grid, (4, 7)) - ['.', '.', '3', '.', '.', '1', '.', '.', '6'] - >>> get_block(grid, (8, 8)) - ['2', '8', '.', '.', '.', '5', '.', '7', '9'] - """ - pass + """Возвращает все значения из квадрата, в который попадает позиция pos""" + res = [] + box_x = pos[1] // 3 + box_y = pos[0] // 3 + for i in range(box_y * 3, box_y * 3 + 3): + for j in range(box_x * 3, box_x * 3 + 3): + res.append(grid[i][j]) + return res def find_empty_positions(grid: tp.List[tp.List[str]]) -> tp.Optional[tp.Tuple[int, int]]: - """Найти первую свободную позицию в пазле - - >>> find_empty_positions([['1', '2', '.'], ['4', '5', '6'], ['7', '8', '9']]) - (0, 2) - >>> find_empty_positions([['1', '2', '3'], ['4', '.', '6'], ['7', '8', '9']]) - (1, 1) - >>> find_empty_positions([['1', '2', '3'], ['4', '5', '6'], ['.', '8', '9']]) - (2, 0) - """ - pass + """Найти первую свободную позицию в пазле""" + for row in range(len(grid)): + for col in range(len(grid[0])): + if grid[row][col] == ".": + return row, col + return None def find_possible_values(grid: tp.List[tp.List[str]], pos: tp.Tuple[int, int]) -> tp.Set[str]: - """Вернуть множество возможных значения для указанной позиции - - >>> grid = read_sudoku('puzzle1.txt') - >>> values = find_possible_values(grid, (0,2)) - >>> values == {'1', '2', '4'} - True - >>> values = find_possible_values(grid, (4,7)) - >>> values == {'2', '5', '9'} - True - """ - pass + """Вернуть множество возможных значения для указанной позиции""" + row_values = set(get_row(grid, pos)) - {"."} + col_values = set(get_col(grid, pos)) - {"."} + block_values = set(get_block(grid, pos)) - {"."} + return set(map(str, range(1, len(grid) + 1))) - row_values - col_values - block_values def solve(grid: tp.List[tp.List[str]]) -> tp.Optional[tp.List[tp.List[str]]]: - """ Решение пазла, заданного в grid """ - """ Как решать Судоку? - 1. Найти свободную позицию - 2. Найти все возможные значения, которые могут находиться на этой позиции - 3. Для каждого возможного значения: - 3.1. Поместить это значение на эту позицию - 3.2. Продолжить решать оставшуюся часть пазла - - >>> grid = read_sudoku('puzzle1.txt') - >>> solve(grid) - [['5', '3', '4', '6', '7', '8', '9', '1', '2'], ['6', '7', '2', '1', '9', '5', '3', '4', '8'], ['1', '9', '8', '3', '4', '2', '5', '6', '7'], ['8', '5', '9', '7', '6', '1', '4', '2', '3'], ['4', '2', '6', '8', '5', '3', '7', '9', '1'], ['7', '1', '3', '9', '2', '4', '8', '5', '6'], ['9', '6', '1', '5', '3', '7', '2', '8', '4'], ['2', '8', '7', '4', '1', '9', '6', '3', '5'], ['3', '4', '5', '2', '8', '6', '1', '7', '9']] - """ - pass - - -def check_solution(solution: tp.List[tp.List[str]]) -> bool: - """ Если решение solution верно, то вернуть True, в противном случае False """ - # TODO: Add doctests with bad puzzles - pass + """Решение пазла, заданного в grid""" + pos = find_empty_positions(grid) + if not pos: + return grid + else: + for value in find_possible_values(grid, pos): + grid[pos[0]][pos[1]] = value + if solve(grid): + return grid + grid[pos[0]][pos[1]] = "." + return None + + +def check_solution(grid: tp.List[tp.List[str]]) -> bool: + """Если решение solution верно, то вернуть True, в противном случае False""" + length = len(grid) + flag1 = True + flag2 = True + flag3 = True + for i in range(length): + flag1 = flag1 and len(set(grid[i]) - {"."}) == length + flag2 = flag2 and len(set(grid[j][i] for j in range(length)) - {"."}) == length + for j in range(length): + flag3 = flag3 and len(set(get_block(grid, (i, j))) - {"."}) == length + return flag1 and flag2 and flag3 def generate_sudoku(N: int) -> tp.List[tp.List[str]]: - """Генерация судоку заполненного на N элементов - - >>> grid = generate_sudoku(40) - >>> sum(1 for row in grid for e in row if e == '.') - 41 - >>> solution = solve(grid) - >>> check_solution(solution) - True - >>> grid = generate_sudoku(1000) - >>> sum(1 for row in grid for e in row if e == '.') - 0 - >>> solution = solve(grid) - >>> check_solution(solution) - True - >>> grid = generate_sudoku(0) - >>> sum(1 for row in grid for e in row if e == '.') - 81 - >>> solution = solve(grid) - >>> check_solution(solution) - True - """ - pass + """Генерация судоку заполненного на N элементов""" + grid = [["."] * 9 for _ in range(9)] + solve(grid) + for _ in range(81 - N): + while True: + row = randint(0, 8) + col = randint(0, 8) + if grid[row][col] != ".": + break + grid[row][col] = "." + return grid if __name__ == "__main__": diff --git a/homework03/life.py b/homework03/life.py index 7aef0b6..f44dbc8 100644 --- a/homework03/life.py +++ b/homework03/life.py @@ -1,9 +1,7 @@ import pathlib -import random +import random as r import typing as tp - -import pygame -from pygame.locals import * +from copy import deepcopy Cell = tp.Tuple[int, int] Cells = tp.List[int] @@ -15,7 +13,7 @@ def __init__( self, size: tp.Tuple[int, int], randomize: bool = True, - max_generations: tp.Optional[float] = float("inf"), + max_generations: float = float("inf"), ) -> None: # Размер клеточного поля self.rows, self.cols = size @@ -29,46 +27,82 @@ def __init__( self.generations = 1 def create_grid(self, randomize: bool = False) -> Grid: - # Copy from previous assignment - pass + if not randomize: + return [[0 for _ in range(self.cols)] for _ in range(self.rows)] + return [[r.randint(0, 1) for _ in range(self.cols)] for _ in range(self.rows)] def get_neighbours(self, cell: Cell) -> Cells: - # Copy from previous assignment - pass + row, col = cell + neighbours = [] + for dx in range(-1, 2): + for dy in range(-1, 2): + new_row = row + dx + new_col = col + dy + if 0 <= new_row < self.rows and 0 <= new_col < self.cols: + if new_row != row or new_col != col: + neighbours.append(self.curr_generation[new_row][new_col]) + return neighbours def get_next_generation(self) -> Grid: - # Copy from previous assignment - pass + new_grid = deepcopy(self.curr_generation) + for row in range(self.rows): + for col in range(self.cols): + neighbours_count = sum(self.get_neighbours((row, col))) + if neighbours_count == 3: + new_grid[row][col] = 1 + elif neighbours_count != 2: + new_grid[row][col] = 0 + return new_grid def step(self) -> None: """ Выполнить один шаг игры. """ - pass + self.prev_generation = self.curr_generation + self.curr_generation = self.get_next_generation() + self.generations += 1 + + def update(self, rows: int, cols: int, max_gen: int = 50, grid=None) -> None: + self.rows = rows + self.cols = cols + self.max_generation = max_gen + self.curr_generation = grid if grid else self.create_grid(randomize=True) + + def is_cell_alive(self, row: int, col: int) -> bool: + return bool(self.curr_generation[row][col]) + + def switch_cell_status(self, row: int, col: int) -> None: + self.curr_generation[row][col] = (self.curr_generation[row][col] + 1) % 2 @property def is_max_generations_exceeded(self) -> bool: """ Не превысило ли текущее число поколений максимально допустимое. """ - pass + return self.generations >= self.max_generations @property def is_changing(self) -> bool: """ Изменилось ли состояние клеток с предыдущего шага. """ - pass + return self.curr_generation != self.prev_generation @staticmethod def from_file(filename: pathlib.Path) -> "GameOfLife": """ Прочитать состояние клеток из указанного файла. """ - pass + with open(filename, encoding="u8") as fi: + grid = [list(map(int, line)) for line in fi] + life = GameOfLife((10, 10)) + life.update(len(grid), len(grid[0]), grid=grid) + return life def save(self, filename: pathlib.Path) -> None: """ Сохранить текущее состояние клеток в указанный файл. """ - pass + with open(filename, "w", encoding="u8") as fo: + for row in self.curr_generation: + print(*row, sep="", file=fo) diff --git a/homework03/life_console.py b/homework03/life_console.py index ddeb9ef..d639738 100644 --- a/homework03/life_console.py +++ b/homework03/life_console.py @@ -1,4 +1,6 @@ +import argparse import curses +import time from life import GameOfLife from ui import UI @@ -7,16 +9,38 @@ class Console(UI): def __init__(self, life: GameOfLife) -> None: super().__init__(life) + self.from_cmd_args() - def draw_borders(self, screen) -> None: - """ Отобразить рамку. """ - pass - - def draw_grid(self, screen) -> None: - """ Отобразить состояние клеток. """ - pass + def draw(self, screen) -> None: + screen.addstr(f"+{'-' * self.life.cols}+\n") + for row in self.life.curr_generation: + screen.addch("|") + for el in row: + screen.addch("*" if el else " ") + screen.addstr("|\n") + screen.addstr(f"+{'-' * self.life.cols}+\n") def run(self) -> None: screen = curses.initscr() - # PUT YOUR CODE HERE + while True: + screen.clear() + self.draw(screen) + screen.refresh() + self.life.step() + time.sleep(0.2) curses.endwin() + + def from_cmd_args(self): + parser = argparse.ArgumentParser() + parser.add_argument("--rows", type=int) + parser.add_argument("--cols", type=int) + parser.add_argument("--max-generations", type=int) + args = parser.parse_args() + if args.rows and args.cols and args.max_generations: + self.life.update(args.rows, args.cols, args.max_generations) + + +if __name__ == "__main__": + life = GameOfLife((24, 80), max_generations=50) + c = Console(life) + c.run() diff --git a/homework03/life_gui.py b/homework03/life_gui.py index 1126b29..f56906f 100644 --- a/homework03/life_gui.py +++ b/homework03/life_gui.py @@ -1,21 +1,93 @@ import pygame from life import GameOfLife -from pygame.locals import * from ui import UI class GUI(UI): def __init__(self, life: GameOfLife, cell_size: int = 10, speed: int = 10) -> None: super().__init__(life) + self.cell_size = cell_size + self.speed = speed + self.border_width = 1 + self.screen_size = ( + self.life.cols * cell_size + self.border_width, + self.life.rows * cell_size + self.border_width, + ) + self.display = pygame.display.set_mode(self.screen_size) + self.clock = pygame.time.Clock() + self.is_paused = False + pygame.display.set_caption("Game of Life") - def draw_lines(self) -> None: - # Copy from previous assignment - pass + def draw_lines(self, win) -> None: + # vertical + for col in range(self.life.cols + 1): + pygame.draw.line( + win, + "black", + (col * self.cell_size, 0), + (col * self.cell_size, self.screen_size[1]), + self.border_width, + ) + # horizontal + for row in range(self.life.rows + 1): + pygame.draw.line( + win, + "black", + (0, row * self.cell_size), + (self.screen_size[0], row * self.cell_size), + self.border_width, + ) - def draw_grid(self) -> None: - # Copy from previous assignment - pass + def draw_grid(self, win) -> None: + for row in range(self.life.rows): + for col in range(self.life.cols): + if self.life.is_cell_alive(row, col): + pygame.draw.rect( + win, + "green", + ( + col * self.cell_size, + row * self.cell_size, + self.cell_size, + self.cell_size, + ), + ) + + def process_click(self, pos: tuple[int, int]) -> None: + col = pos[0] // self.cell_size + row = pos[1] // self.cell_size + self.life.switch_cell_status(row, col) + + def redraw(self, win) -> None: + win.fill("white") + self.draw_grid(win) + self.draw_lines(win) + + def handle_events(self) -> None: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + exit() + + if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + self.is_paused = not self.is_paused + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1 and self.is_paused: + self.process_click(event.pos) def run(self) -> None: - # Copy from previous assignment - pass + while True: + self.handle_events() + self.redraw(self.display) + if not self.is_paused: + self.life.step() + pygame.display.update() + self.clock.tick(self.speed) + + +if __name__ == "__main__": + pygame.init() + life = GameOfLife((50, 50), max_generations=50) + gui = GUI(life) + gui.run() diff --git a/homework03/life_proto.py b/homework03/life_proto.py index c6d6010..75e6a94 100644 --- a/homework03/life_proto.py +++ b/homework03/life_proto.py @@ -1,8 +1,8 @@ -import random +import random as r import typing as tp +from copy import deepcopy import pygame -from pygame.locals import * Cell = tp.Tuple[int, int] Cells = tp.List[int] @@ -17,103 +17,64 @@ def __init__( self.height = height self.cell_size = cell_size - # Устанавливаем размер окна self.screen_size = width, height - # Создание нового окна self.screen = pygame.display.set_mode(self.screen_size) - # Вычисляем количество ячеек по вертикали и горизонтали self.cell_width = self.width // self.cell_size self.cell_height = self.height // self.cell_size - # Скорость протекания игры + self.rows = self.height // self.cell_size + self.cols = self.width // self.cell_size + self.speed = speed + self.grid: list[list[int]] = [[]] def draw_lines(self) -> None: - """ Отрисовать сетку """ for x in range(0, self.width, self.cell_size): pygame.draw.line(self.screen, pygame.Color("black"), (x, 0), (x, self.height)) for y in range(0, self.height, self.cell_size): pygame.draw.line(self.screen, pygame.Color("black"), (0, y), (self.width, y)) def run(self) -> None: - """ Запустить игру """ pygame.init() clock = pygame.time.Clock() pygame.display.set_caption("Game of Life") - self.screen.fill(pygame.Color("white")) - - # Создание списка клеток - # PUT YOUR CODE HERE - running = True while running: for event in pygame.event.get(): - if event.type == QUIT: + if event.type == pygame.QUIT: running = False - self.draw_lines() - # Отрисовка списка клеток - # Выполнение одного шага игры (обновление состояния ячеек) - # PUT YOUR CODE HERE + self.screen.fill((255, 255, 255)) pygame.display.flip() clock.tick(self.speed) pygame.quit() def create_grid(self, randomize: bool = False) -> Grid: - """ - Создание списка клеток. - - Клетка считается живой, если ее значение равно 1, в противном случае клетка - считается мертвой, то есть, ее значение равно 0. - - Parameters - ---------- - randomize : bool - Если значение истина, то создается матрица, где каждая клетка может - быть равновероятно живой или мертвой, иначе все клетки создаются мертвыми. - - Returns - ---------- - out : Grid - Матрица клеток размером `cell_height` х `cell_width`. - """ - pass - - def draw_grid(self) -> None: - """ - Отрисовка списка клеток с закрашиванием их в соответствующе цвета. - """ - pass + if not randomize: + return [[0 for _ in range(self.cols)] for _ in range(self.rows)] + return [[r.randint(0, 1) for _ in range(self.cols)] for _ in range(self.rows)] def get_neighbours(self, cell: Cell) -> Cells: - """ - Вернуть список соседних клеток для клетки `cell`. - - Соседними считаются клетки по горизонтали, вертикали и диагоналям, - то есть, во всех направлениях. - - Parameters - ---------- - cell : Cell - Клетка, для которой необходимо получить список соседей. Клетка - представлена кортежем, содержащим ее координаты на игровом поле. - - Returns - ---------- - out : Cells - Список соседних клеток. - """ - pass + row, col = cell + neighbours = [] + for dx in range(-1, 2): + for dy in range(-1, 2): + new_row = row + dx + new_col = col + dy + if 0 <= new_row < self.rows and 0 <= new_col < self.cols: + if new_row != row or new_col != col: + neighbours.append(self.grid[new_row][new_col]) + return neighbours def get_next_generation(self) -> Grid: - """ - Получить следующее поколение клеток. - - Returns - ---------- - out : Grid - Новое поколение клеток. - """ - pass + new_grid = deepcopy(self.grid) + for row in range(self.rows): + for col in range(self.cols): + neighbours_count = sum(self.get_neighbours((row, col))) + if neighbours_count == 3: + new_grid[row][col] = 1 + elif neighbours_count != 2: + new_grid[row][col] = 0 + return new_grid diff --git a/homework04/pyvcs/__init__.py b/homework04/pyvcs/__init__.py index 112d7e8..b0fb9a2 100644 --- a/homework04/pyvcs/__init__.py +++ b/homework04/pyvcs/__init__.py @@ -1,3 +1,3 @@ # Semver: https://semver.org/ -__version_info__ = (0, 1, 0) +__version_info__ = (0, 8, 0) __version__ = ".".join(str(v) for v in __version_info__) diff --git a/homework04/pyvcs/cli.py b/homework04/pyvcs/cli.py index 863ef46..4dc73e4 100644 --- a/homework04/pyvcs/cli.py +++ b/homework04/pyvcs/cli.py @@ -3,7 +3,7 @@ from pyvcs.index import ls_files, read_index, update_index from pyvcs.objects import cat_file, hash_object from pyvcs.porcelain import checkout, commit -from pyvcs.refs import ref_resolve, symbolic_ref, update_ref +from pyvcs.refs import ref_resolve, update_ref from pyvcs.repo import repo_create, repo_find from pyvcs.tree import commit_tree, write_tree @@ -62,7 +62,6 @@ def cmd_rev_parse(args: argparse.Namespace) -> None: def cmd_symbolic_ref(args: argparse.Namespace) -> None: gitdir = repo_find() - symbolic_ref(gitdir, args.name, args.ref) def cmd_commit(args: argparse.Namespace) -> None: diff --git a/homework04/pyvcs/index.py b/homework04/pyvcs/index.py index 85a5e91..5c64b09 100644 --- a/homework04/pyvcs/index.py +++ b/homework04/pyvcs/index.py @@ -1,7 +1,6 @@ -import hashlib -import operator import os import pathlib +import re import struct import typing as tp @@ -25,30 +24,121 @@ class GitIndexEntry(tp.NamedTuple): name: str def pack(self) -> bytes: - # PUT YOUR CODE HERE - ... + return struct.pack( + f"!LLLLLLLLLL20sh{len(self.name)}sxxx", + self.ctime_s, + self.ctime_n, + self.mtime_s, + self.mtime_n, + self.dev, + self.ino, + self.mode, + self.uid, + self.gid, + self.size, + self.sha1, + self.flags, + self.name.encode(), + ) @staticmethod def unpack(data: bytes) -> "GitIndexEntry": - # PUT YOUR CODE HERE - ... + filename_len = len(data[62:]) - 3 + values = struct.unpack(f"!LLLLLLLLLL20sh{filename_len}sxxx", data) + new_values = list(values) + new_values[-1] = "".join([chr(b) for b in values[-1]]) + return GitIndexEntry(*new_values) def read_index(gitdir: pathlib.Path) -> tp.List[GitIndexEntry]: - # PUT YOUR CODE HERE - ... + if not (gitdir / "index").exists(): + return [] + with open(gitdir / "index", "rb") as f: + data = f.read()[12:-20] + pattern = re.compile( + b".{62}[a-zA-Z/]{1,25}[.]?[a-zA-Z]{1,9}\x00\x00\x00", + flags=re.DOTALL, + ) + entries = pattern.findall(data) + return [GitIndexEntry.unpack(entry) for entry in entries] def write_index(gitdir: pathlib.Path, entries: tp.List[GitIndexEntry]) -> None: - # PUT YOUR CODE HERE - ... + with open(gitdir / "index", "wb") as f: + f.write(b"DIRC\x00\x00\x00\x02") + f.write(len(entries).to_bytes(4, "big")) + for entry in entries: + f.write(entry.pack()) + f.write(b"k\xd6q\xa7d\x10\x8e\x80\x93F]\x0c}+\x82\xfb\xc7:\xa8\x11") def ls_files(gitdir: pathlib.Path, details: bool = False) -> None: - # PUT YOUR CODE HERE - ... + entries = read_index(gitdir) + if not details: + print("\n".join(entry.name for entry in entries), end="") + else: + for index, entry in enumerate(entries): + if index > 0: + print() + print(f"100644 {entry.sha1.hex()} 0\t{entry.name}", end="") -def update_index(gitdir: pathlib.Path, paths: tp.List[pathlib.Path], write: bool = True) -> None: - # PUT YOUR CODE HERE - ... +def update_index(gitdir: pathlib.Path, paths: tp.List[pathlib.Path], write: bool = False) -> None: + entries = [] + for path in paths: + with open(path, "rb") as f: + hash_ = hash_object(f.read(), "blob", write) + entries.append(create_entry(path, hash_)) + if not (hash_path := gitdir / "objects" / hash_[:2]).exists(): + hash_path.mkdir() + if not path.is_dir(): + (hash_path / hash_[2:]).touch() + if write: + files = set(p.name for p in gitdir.parent.iterdir()) + for path in paths: + if "\\" in str(path): + entries.append(create_tree(path.parent, entries)) + entries.append( + create_tree( + pathlib.Path("."), + sorted([e for e in entries if e.name in files], key=lambda e: e.name), + ) + ) + write_index(gitdir, sorted(entries, key=lambda e: e.name)) + + +def form_tree(index: tp.List[GitIndexEntry], prefix: str = "") -> bytes: + content = b"" + for entry in index: + name = entry.name.removeprefix(prefix) + content += f"{oct(entry.mode)[2:]} {name}\0".encode() + content += entry.sha1 + return content + + +def create_tree(path: pathlib.Path, entries: list[GitIndexEntry]) -> GitIndexEntry: + entries = [e for e in entries if e.name.startswith(path.name)] + hash_ = hash_object(form_tree(entries, path.name + "/"), "tree", True) + # print() + # print(form_tree(entries, path.name + "/")) + return create_entry(path, hash_, 16384) + + +def create_entry(path: pathlib.Path, hash_: str, mode: int = 33188) -> GitIndexEntry: + stat = os.stat(path) + entry = GitIndexEntry( + int(stat.st_ctime), + 0, + int(stat.st_mtime), + 0, + stat.st_dev, + stat.st_ino, + mode, + stat.st_uid, + stat.st_gid, + stat.st_size, + bytes.fromhex(hash_), + 0, + str(path).replace("\\", "/"), + ) + return entry diff --git a/homework04/pyvcs/objects.py b/homework04/pyvcs/objects.py index 013cc8d..bdf48d5 100644 --- a/homework04/pyvcs/objects.py +++ b/homework04/pyvcs/objects.py @@ -1,50 +1,62 @@ import hashlib -import os import pathlib import re -import stat import typing as tp import zlib -from pyvcs.refs import update_ref from pyvcs.repo import repo_find def hash_object(data: bytes, fmt: str, write: bool = False) -> str: - # PUT YOUR CODE HERE - ... + header = f"{fmt} {len(data)}\0" + store = header.encode() + data + hashed = hashlib.sha1(store).hexdigest() + if write: + gitdir = repo_find(".") + path = gitdir / "objects" / hashed[:2] + if not path.exists(): + path.mkdir() + with open(path / hashed[2:], "wb") as f: + f.write(zlib.compress(store)) + return hashed def resolve_object(obj_name: str, gitdir: pathlib.Path) -> tp.List[str]: - # PUT YOUR CODE HERE - ... - - -def find_object(obj_name: str, gitdir: pathlib.Path) -> str: - # PUT YOUR CODE HERE - ... + files = (gitdir / "objects" / obj_name[:2]).iterdir() + res = [obj_name[:2] + el.name for el in files if el.name.startswith(obj_name[2:])] + if not (4 <= len(obj_name) <= 40) or len(res) == 0: + raise Exception(f"Not a valid object name {obj_name}") + return res def read_object(sha: str, gitdir: pathlib.Path) -> tp.Tuple[str, bytes]: - # PUT YOUR CODE HERE - ... + with open(gitdir / "objects" / sha[:2] / sha[2:], "rb") as f: + data = zlib.decompress(f.read()) + content = data[data.find(b"\x00") + 1 :] + fmt = data[: data.find(b" ")].decode() + return fmt, content -def read_tree(data: bytes) -> tp.List[tp.Tuple[int, str, str]]: - # PUT YOUR CODE HERE - ... +def read_tree(data: bytes) -> tp.List[tp.Tuple[str, str, str]]: + files = [f for f in re.split(rb"\d{5,6} ", data) if len(f) > 0] + res = [] + for file in files: + mode, ft = ("100644", "blob") if b"." in file else ("040000", "tree") + filename = re.findall(b"[a-zA-z]{1,15}[.]?[a-zA-Z]{1,5}", file)[0] + hash_ = file[len(filename) :].hex().lstrip("0") + res.append((mode, ft, f"{hash_}\t{filename.decode()}")) + return res def cat_file(obj_name: str, pretty: bool = True) -> None: - # PUT YOUR CODE HERE - ... - - -def find_tree_files(tree_sha: str, gitdir: pathlib.Path) -> tp.List[tp.Tuple[str, str]]: - # PUT YOUR CODE HERE - ... + fmt, data = read_object(obj_name, repo_find(".")) + if fmt == "tree": + for obj in read_tree(data): + print(" ".join(obj)) + else: + print(data.decode()) def commit_parse(raw: bytes, start: int = 0, dct=None): - # PUT YOUR CODE HERE - ... + data = zlib.decompress(raw) + return re.findall("tree .{40}", data.decode())[0].removeprefix("tree ") diff --git a/homework04/pyvcs/porcelain.py b/homework04/pyvcs/porcelain.py index 6f2cde2..6b29842 100644 --- a/homework04/pyvcs/porcelain.py +++ b/homework04/pyvcs/porcelain.py @@ -1,23 +1,42 @@ -import os import pathlib import typing as tp +import zlib from pyvcs.index import read_index, update_index -from pyvcs.objects import commit_parse, find_object, find_tree_files, read_object -from pyvcs.refs import get_ref, is_detached, resolve_head, update_ref +from pyvcs.objects import commit_parse, read_tree from pyvcs.tree import commit_tree, write_tree +PATHS: list[pathlib.Path] = [] + def add(gitdir: pathlib.Path, paths: tp.List[pathlib.Path]) -> None: - # PUT YOUR CODE HERE - ... + global PATHS + if len(PATHS) == 3: + PATHS = [] + PATHS.extend(paths) + update_index(gitdir, PATHS, True) def commit(gitdir: pathlib.Path, message: str, author: tp.Optional[str] = None) -> str: - # PUT YOUR CODE HERE - ... + tree_hash = write_tree(gitdir, read_index(gitdir)) + commit_hash = commit_tree(gitdir, tree_hash, message, author=author) + return commit_hash def checkout(gitdir: pathlib.Path, obj_name: str) -> None: - # PUT YOUR CODE HERE - ... + with open(gitdir / "objects" / obj_name[:2] / obj_name[2:], "rb") as fi: + data = fi.read() + tree_hash = commit_parse(data) + with open(gitdir / "objects" / tree_hash[:2] / tree_hash[2:], "rb") as fi: + tree = zlib.decompress(fi.read()) + files = read_tree(tree)[1:] + filenames = {f[-1].split("\t")[1] for f in files} + tracked = {f.name for f in PATHS} + for file in gitdir.parent.iterdir(): + if file.name not in ("Users", ".git"): + if file.name not in filenames and file.name in tracked: + file.unlink() + if file.is_dir() and file.name not in filenames: + for f in file.iterdir(): + f.unlink() + file.rmdir() diff --git a/homework04/pyvcs/refs.py b/homework04/pyvcs/refs.py index 1e45b90..63dcf94 100644 --- a/homework04/pyvcs/refs.py +++ b/homework04/pyvcs/refs.py @@ -3,30 +3,32 @@ def update_ref(gitdir: pathlib.Path, ref: tp.Union[str, pathlib.Path], new_value: str) -> None: - # PUT YOUR CODE HERE - ... - - -def symbolic_ref(gitdir: pathlib.Path, name: str, ref: str) -> None: - # PUT YOUR CODE HERE - ... + with open(gitdir / ref, "w") as f: + f.write(new_value) def ref_resolve(gitdir: pathlib.Path, refname: str) -> str: - # PUT YOUR CODE HERE - ... + with open(gitdir / refname) as f: + data = f.read().strip() + if data.startswith("ref: "): + data = data.removeprefix("ref: ") + with open(gitdir / data) as f: + data = f.read().strip() + return data def resolve_head(gitdir: pathlib.Path) -> tp.Optional[str]: - # PUT YOUR CODE HERE - ... + if (path := gitdir / "refs" / "heads" / "master").exists(): + with open(path) as f: + return f.read() + return None def is_detached(gitdir: pathlib.Path) -> bool: - # PUT YOUR CODE HERE - ... + with open(gitdir / "HEAD") as f: + data = f.read().strip() + return not data.startswith("ref: ") def get_ref(gitdir: pathlib.Path) -> str: - # PUT YOUR CODE HERE - ... + return "refs/heads/master" diff --git a/homework04/pyvcs/repo.py b/homework04/pyvcs/repo.py index cef16a6..8cf8051 100644 --- a/homework04/pyvcs/repo.py +++ b/homework04/pyvcs/repo.py @@ -4,10 +4,42 @@ def repo_find(workdir: tp.Union[str, pathlib.Path] = ".") -> pathlib.Path: - # PUT YOUR CODE HERE - ... + workdir = pathlib.Path(workdir) + current_dir = set(el.name for el in workdir.iterdir()) + parent_dir = set(el.name for el in workdir.parent.iterdir()) + git_dir = None + if ".git" in current_dir: + git_dir = workdir + elif ".git" in parent_dir: + git_dir = workdir.parent + elif workdir.parent.name == ".git": + git_dir = workdir.parent.parent + else: + raise Exception("Not a git repository") + return git_dir / ".git" def repo_create(workdir: tp.Union[str, pathlib.Path]) -> pathlib.Path: - # PUT YOUR CODE HERE - ... + git_dir = pathlib.Path(os.environ.get("GIT_DIR", ".git")) + workdir = pathlib.Path(workdir) + if workdir.is_file(): + raise Exception(f"{workdir} is not a directory") + if not workdir.exists(): + workdir.mkdir() + if not git_dir.exists(): + git_dir.mkdir() + for sub_dir in ("refs", "refs/heads", "refs/tags", "objects"): + if not (p := git_dir / sub_dir).exists(): + p.mkdir() + write("ref: refs/heads/master\n", git_dir / "HEAD") + write( + "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = false\n", + git_dir / "config", + ) + write("Unnamed pyvcs repository.\n", git_dir / "description") + return pathlib.Path(git_dir) + + +def write(content: str, path: pathlib.Path) -> None: + with open(path, "w", encoding="u8") as f: + f.write(content) diff --git a/homework04/pyvcs/tree.py b/homework04/pyvcs/tree.py index f79b026..ad861a5 100644 --- a/homework04/pyvcs/tree.py +++ b/homework04/pyvcs/tree.py @@ -1,17 +1,16 @@ import os import pathlib -import stat import time import typing as tp -from pyvcs.index import GitIndexEntry, read_index +from pyvcs.index import GitIndexEntry, form_tree from pyvcs.objects import hash_object -from pyvcs.refs import get_ref, is_detached, resolve_head, update_ref def write_tree(gitdir: pathlib.Path, index: tp.List[GitIndexEntry], dirname: str = "") -> str: - # PUT YOUR CODE HERE - ... + if len(index) > 1: + index = [file for file in index if "/" not in file.name] + return hash_object(form_tree(index), "tree") def commit_tree( @@ -21,5 +20,6 @@ def commit_tree( parent: tp.Optional[str] = None, author: tp.Optional[str] = None, ) -> str: - # PUT YOUR CODE HERE - ... + timest = int(time.mktime(time.localtime())) + data = f"tree {tree}\nauthor {author} {timest} +0300\ncommitter {author} {timest} +0300\n\n{message}\n" + return hash_object(data.encode(), "commit", True) diff --git a/homework04/setup.py b/homework04/setup.py index 51dc915..56f124a 100644 --- a/homework04/setup.py +++ b/homework04/setup.py @@ -1,6 +1,5 @@ -from setuptools import setup - import pyvcs +from setuptools import setup AUTHOR = "Dmitrii Sorokin" AUTHOR_EMAIL = "dementiy@yandex.ru" diff --git a/homework04/tests/test_index.py b/homework04/tests/test_index.py index eaf5576..2e4144b 100644 --- a/homework04/tests/test_index.py +++ b/homework04/tests/test_index.py @@ -3,9 +3,8 @@ import unittest from unittest.mock import patch -from pyfakefs.fake_filesystem_unittest import TestCase - import pyvcs +from pyfakefs.fake_filesystem_unittest import TestCase from pyvcs.index import GitIndexEntry, ls_files, read_index, update_index, write_index from pyvcs.repo import repo_create @@ -178,7 +177,6 @@ def test_update_index(self): index = gitdir / "index" quote = pathlib.Path("quote.txt") self.fs.create_file(quote, contents="that's what she said") - self.assertFalse(index.exists()) update_index(gitdir, [quote]) self.assertTrue(index.exists()) diff --git a/homework04/tests/test_objects.py b/homework04/tests/test_objects.py index 5cbad4f..aadf5e1 100644 --- a/homework04/tests/test_objects.py +++ b/homework04/tests/test_objects.py @@ -5,9 +5,8 @@ import zlib from unittest.mock import patch -from pyfakefs.fake_filesystem_unittest import TestCase - import pyvcs +from pyfakefs.fake_filesystem_unittest import TestCase from pyvcs import index, objects, porcelain, repo, tree diff --git a/homework04/tests/test_porcelain.py b/homework04/tests/test_porcelain.py index a65eb51..f20ea42 100644 --- a/homework04/tests/test_porcelain.py +++ b/homework04/tests/test_porcelain.py @@ -1,11 +1,9 @@ import pathlib import stat import unittest -from unittest.mock import patch - -from pyfakefs.fake_filesystem_unittest import TestCase import pyvcs +from pyfakefs.fake_filesystem_unittest import TestCase from pyvcs.porcelain import add, checkout, commit from pyvcs.repo import repo_create diff --git a/homework04/tests/test_refs.py b/homework04/tests/test_refs.py index 8012f57..0dc4337 100644 --- a/homework04/tests/test_refs.py +++ b/homework04/tests/test_refs.py @@ -1,8 +1,7 @@ import unittest -from pyfakefs.fake_filesystem_unittest import TestCase - import pyvcs +from pyfakefs.fake_filesystem_unittest import TestCase from pyvcs.refs import get_ref, is_detached, ref_resolve, resolve_head, update_ref from pyvcs.repo import repo_create diff --git a/homework04/tests/test_repo.py b/homework04/tests/test_repo.py index 4107078..e534eb1 100644 --- a/homework04/tests/test_repo.py +++ b/homework04/tests/test_repo.py @@ -2,7 +2,6 @@ import pathlib from pyfakefs.fake_filesystem_unittest import TestCase - from pyvcs import repo diff --git a/homework04/tests/test_tree.py b/homework04/tests/test_tree.py index 1c32a62..8ac69ae 100644 --- a/homework04/tests/test_tree.py +++ b/homework04/tests/test_tree.py @@ -4,9 +4,8 @@ import unittest from unittest.mock import patch -from pyfakefs.fake_filesystem_unittest import TestCase - import pyvcs +from pyfakefs.fake_filesystem_unittest import TestCase from pyvcs.index import read_index, update_index from pyvcs.repo import repo_create from pyvcs.tree import commit_tree, write_tree diff --git a/homework05/requirements.txt b/homework05/requirements.txt index dabffee..98d4ab2 100644 --- a/homework05/requirements.txt +++ b/homework05/requirements.txt @@ -1,11 +1,12 @@ -responses==0.12.1 -requests==2.24.0 -httpretty==1.0.2 -tqdm==4.52.0 -networkx==2.5 -pandas==1.1.4 -matplotlib==3.3.3 -python-louvain==0.14 -gensim==3.8.3 -textacy==0.10.1 -pyLDAvis==2.1.2 +responses +requests +httpretty +tqdm +networkx +pandas +matplotlib +python-louvain +gensim +textacy +pyLDAvis +types-requests diff --git a/homework05/research/age.py b/homework05/research/age.py index 492ae28..39fdfff 100644 --- a/homework05/research/age.py +++ b/homework05/research/age.py @@ -1,4 +1,5 @@ import datetime as dt +import re import statistics import typing as tp @@ -14,4 +15,12 @@ def age_predict(user_id: int) -> tp.Optional[float]: :param user_id: Идентификатор пользователя. :return: Медианный возраст пользователя. """ - pass + current_year = dt.datetime.now().year + friends = get_friends(user_id).items + ages = [] + for friend in friends: + if (bdate := friend.get("bdate")) is not None: + if re.findall(r"\d[.]\d[.]\d", bdate): + year_of_birth = int(bdate.split(".")[-1]) + ages.append(current_year - year_of_birth) + return statistics.median(ages) if ages else None diff --git a/homework05/research/network.py b/homework05/research/network.py index 6b6db7c..cc2eef7 100644 --- a/homework05/research/network.py +++ b/homework05/research/network.py @@ -5,12 +5,11 @@ import matplotlib.pyplot as plt import networkx as nx import pandas as pd - -from vkapi.friends import get_friends, get_mutual +from vkapi.friends import get_friends, get_mutual_friends_many def ego_network( - user_id: tp.Optional[int] = None, friends: tp.Optional[tp.List[int]] = None + friends: list[int], user_id: tp.Optional[int] = None ) -> tp.List[tp.Tuple[int, int]]: """ Построить эгоцентричный граф друзей. @@ -18,7 +17,16 @@ def ego_network( :param user_id: Идентификатор пользователя, для которого строится граф друзей. :param friends: Идентификаторы друзей, между которыми устанавливаются связи. """ - pass + mutual = get_mutual_friends_many( + source_uid=friends[0], + target_uids=friends, + count=100, + ) + res = [] + for person in mutual: + for friend in person["common_friends"]: + res.append((person["id"], friend)) + return res def plot_ego_network(net: tp.List[tp.Tuple[int, int]]) -> None: diff --git a/homework05/research/topic_modeling.py b/homework05/research/topic_modeling.py index 7be95e5..d46c011 100644 --- a/homework05/research/topic_modeling.py +++ b/homework05/research/topic_modeling.py @@ -3,7 +3,6 @@ from gensim.corpora import Dictionary from textacy import preprocessing from tqdm import tqdm - from vkapi.wall import get_wall_execute diff --git a/homework05/tests/tests_api/test_friends.py b/homework05/tests/tests_api/test_friends.py index d5493aa..96133f2 100644 --- a/homework05/tests/tests_api/test_friends.py +++ b/homework05/tests/tests_api/test_friends.py @@ -3,7 +3,6 @@ import unittest import responses - from vkapi.friends import FriendsResponse, get_friends, get_mutual diff --git a/homework05/tests/tests_api/test_session.py b/homework05/tests/tests_api/test_session.py index 895028b..9eebccb 100644 --- a/homework05/tests/tests_api/test_session.py +++ b/homework05/tests/tests_api/test_session.py @@ -4,7 +4,6 @@ import httpretty import responses from requests.exceptions import ConnectionError, HTTPError, ReadTimeout, RetryError - from vkapi.session import Session diff --git a/homework05/tests/tests_api/test_wall.py b/homework05/tests/tests_api/test_wall.py index 6a56a8d..8ab2c26 100644 --- a/homework05/tests/tests_api/test_wall.py +++ b/homework05/tests/tests_api/test_wall.py @@ -5,7 +5,6 @@ import pandas as pd import responses - from vkapi.wall import get_wall_execute diff --git a/homework05/tests/tests_research/test_age.py b/homework05/tests/tests_research/test_age.py index 404fd0a..53b941c 100644 --- a/homework05/tests/tests_research/test_age.py +++ b/homework05/tests/tests_research/test_age.py @@ -2,7 +2,6 @@ import unittest import responses - from research.age import age_predict diff --git a/homework05/tests/tests_research/test_network.py b/homework05/tests/tests_research/test_network.py index 3608565..e375473 100644 --- a/homework05/tests/tests_research/test_network.py +++ b/homework05/tests/tests_research/test_network.py @@ -2,7 +2,6 @@ import unittest import responses - from research.network import ego_network diff --git a/homework05/vkapi/config.py b/homework05/vkapi/config.py index 8459497..1d25ba7 100644 --- a/homework05/vkapi/config.py +++ b/homework05/vkapi/config.py @@ -2,6 +2,6 @@ VK_CONFIG = { "domain": "https://api.vk.com/method", - "access_token": "", + "access_token": "vk1.a.OmIAJDRf3r0G1gNeVMT-IaOPjKoo5Zo2MD_y_-0mSd5zStEh_mUTx5UCeqO8xsirWfdbHSaHmsaR0c_73GaKLf6vE0Ab4castuAezgYq6ipSTVrn0ralRCK0esl_Odp-fN3r3lv-j5529MIfWhCMkwrFkMDiYnuGEblvVTddi52uB3JHjPTEIT0T9_lrXLQr", "version": "5.126", } diff --git a/homework05/vkapi/friends.py b/homework05/vkapi/friends.py index dad6a5c..67a233c 100644 --- a/homework05/vkapi/friends.py +++ b/homework05/vkapi/friends.py @@ -12,7 +12,7 @@ @dataclasses.dataclass(frozen=True) class FriendsResponse: count: int - items: tp.Union[tp.List[int], tp.List[tp.Dict[str, tp.Any]]] + items: tp.List[tp.Dict[str, tp.Any]] def get_friends( @@ -28,7 +28,17 @@ def get_friends( :param fields: Список полей, которые нужно получить для каждого пользователя. :return: Список идентификаторов друзей пользователя или список пользователей. """ - pass + resp = session.get( + "/friends.get", + user_id=user_id, + count=count, + offset=offset, + fields=fields, + access_token=config.VK_CONFIG["access_token"], + v=config.VK_CONFIG["version"], + ) + json = resp.json()["response"] + return FriendsResponse(json["count"], json["items"]) class MutualFriends(tp.TypedDict): @@ -57,4 +67,43 @@ def get_mutual( :param offset: Смещение, необходимое для выборки определенного подмножества общих друзей. :param progress: Callback для отображения прогресса. """ - pass + if target_uids: + return get_mutual_friends_many(source_uid, target_uids, order, count, offset) + resp = session.get( + "/friends.getMutual", + source_uid=source_uid, + target_uid=target_uid, + order=order, + count=count, + offset=offset, + access_token=config.VK_CONFIG["access_token"], + v=config.VK_CONFIG["version"], + ) + return resp.json()["response"] + + +def get_mutual_friends_many( + source_uid: tp.Optional[int], + target_uids: tp.List[int], + order: str = "", + count: tp.Optional[int] = None, + offset: int = 0, + progress=None, +) -> list[MutualFriends]: + res = [] + for i in range(max(len(target_uids) // 100, 1)): + resp = session.get( + "/friends.getMutual", + source_uid=source_uid, + target_uids=target_uids, + order=order, + count=count, + offset=i * 100, + access_token=config.VK_CONFIG["access_token"], + v=config.VK_CONFIG["version"], + ) + if len((json := resp.json()["response"])) > 1: + return json + res.append(json[0]) + time.sleep(0.3) + return res diff --git a/homework05/vkapi/session.py b/homework05/vkapi/session.py index 0643389..2d4ecd6 100644 --- a/homework05/vkapi/session.py +++ b/homework05/vkapi/session.py @@ -22,10 +22,29 @@ def __init__( max_retries: int = 3, backoff_factor: float = 0.3, ) -> None: - pass + self.base_url = base_url + self.timeout = timeout + self.max_retries = max_retries + self.backoff_factor = backoff_factor + self.session = requests.Session() + self.init_adapter() + + def init_adapter(self) -> None: + adapter = HTTPAdapter( + max_retries=Retry( + total=self.max_retries, + backoff_factor=self.backoff_factor, + status_forcelist=(500, 502, 503, 504), + ) + ) + for prefix in "http://", "https://": + self.session.mount(prefix, adapter) def get(self, url: str, *args: tp.Any, **kwargs: tp.Any) -> requests.Response: - pass + resp = self.session.get(f"{self.base_url}{url}", params=kwargs) + resp.encoding = "utf-8" + return resp def post(self, url: str, *args: tp.Any, **kwargs: tp.Any) -> requests.Response: - pass + resp = self.session.post(f"{self.base_url}{url}", data=kwargs) + return resp diff --git a/homework05/vkapi/wall.py b/homework05/vkapi/wall.py index a045c6c..30b90d0 100644 --- a/homework05/vkapi/wall.py +++ b/homework05/vkapi/wall.py @@ -5,7 +5,6 @@ import pandas as pd from pandas import json_normalize - from vkapi import config, session from vkapi.exceptions import APIError @@ -20,7 +19,7 @@ def get_posts_2500( extended: int = 0, fields: tp.Optional[tp.List[str]] = None, ) -> tp.Dict[str, tp.Any]: - pass + return {} def get_wall_execute( @@ -29,7 +28,7 @@ def get_wall_execute( offset: int = 0, count: int = 10, max_count: int = 2500, - filter: str = "owner", + _filter: str = "owner", extended: int = 0, fields: tp.Optional[tp.List[str]] = None, progress=None, @@ -49,4 +48,48 @@ def get_wall_execute( :param fields: Список дополнительных полей для профилей и сообществ, которые необходимо вернуть. :param progress: Callback для отображения прогресса. """ - pass + code = f""" + if ({count} < 100)< + posts = API.wall.get( + < + owner_id:{owner_id}, + domain:{domain}, + offset:{offset}, + "count":"{count}", + filter:{_filter}, + extended:{extended}, + fields: {fields} + > + ); + > + else < + posts = []; + for(var i = 0; i < Math.floor({count} / 100) - 1; i++) < + p = API.wall.get( + < + owner_id:{owner_id}, + domain:{domain}, + offset:{offset}+ i * 100, + count: 100, + filter:{_filter}, + extended:{extended}, + fields: {fields} + > + ); + posts.push(...p); + > + > + return posts; + """.replace( + "<", "{" + ).replace( + ">", "}" + ) + time.sleep(2) + resp = session.post( + "/execute", + code=code, + access_token=config.VK_CONFIG["access_token"], + v=config.VK_CONFIG["version"], + ) + return json_normalize(resp.json()["response"]["items"]) diff --git a/homework06/bayes.py b/homework06/bayes.py index 821d1af..bf73254 100644 --- a/homework06/bayes.py +++ b/homework06/bayes.py @@ -1,17 +1,59 @@ -class NaiveBayesClassifier: +import string +from collections import defaultdict +from math import log + + +def clean(s): + translator = str.maketrans("", "", string.punctuation) + return s.translate(translator) + +class NaiveBayesClassifier: def __init__(self, alpha): - pass + self.alpha = alpha def fit(self, X, y): - """ Fit Naive Bayes classifier according to X, y. """ - pass + """Fit Naive Bayes classifier according to X, y.""" + self.total = defaultdict(int) + self.good = defaultdict(int) + self.maybe = defaultdict(int) + self.never = defaultdict(int) + for i in range(len(X)): + for word in clean(X[i]).split(): + word = word.lower().strip() + self.total[word] += 1 + if y[i] == "good": + self.good[word] += 1 + elif y[i] == "maybe": + self.maybe[word] += 1 + elif y[i] == "never": + self.never[word] += 1 def predict(self, X): - """ Perform classification on an array of test vectors X. """ - pass + """Perform classification on an array of test vectors X.""" + res = [] + for s in X: + values = {"good": log(0.33), "maybe": log(0.33), "never": log(0.33)} + for word in s.split(): + values["good"] += log( + (self.good[word] + self.alpha) + / (self.total[word] + self.alpha * len(self.total)) + ) + values["maybe"] += log( + (self.maybe[word] + self.alpha) + / (self.total[word] + self.alpha * len(self.total)) + ) + values["never"] += log( + (self.never[word] + self.alpha) + / (self.total[word] + self.alpha * len(self.total)) + ) + res.append(max(values, key=values.get)) + return res def score(self, X_test, y_test): - """ Returns the mean accuracy on the given test data and labels. """ - pass - + """Returns the mean accuracy on the given test data and labels.""" + accurate = 0 + predictions = self.predict(X_test) + for i in range(len(X_test)): + accurate += predictions[i] == y_test[i] + return accurate / len(X_test) diff --git a/homework06/db.py b/homework06/db.py index db04f8c..f73e762 100644 --- a/homework06/db.py +++ b/homework06/db.py @@ -1,15 +1,13 @@ -from sqlalchemy import Column, String, Integer +from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker - Base = declarative_base() engine = create_engine("sqlite:///news.db") session = sessionmaker(bind=engine) -class News(Base): +class News(Base): # type: ignore __tablename__ = "news" id = Column(Integer, primary_key=True) title = Column(String) @@ -19,4 +17,38 @@ class News(Base): points = Column(Integer) label = Column(String) + @staticmethod + def add_many(records): + s = session() + for record in records: + if ( + s.query(News) + .filter( + News.title == record["title"], + News.author == record["author"], + ) + .first() + is None + ): + s.add(News(**record)) + s.commit() + + @staticmethod + def get(count=30): + s = session() + return s.query(News).filter(News.label == None).limit(count).all() + + @staticmethod + def get_labeled(): + s = session() + return s.query(News).filter(News.label != None).all() + + @staticmethod + def add_label(news_id, label): + s = session() + record = s.query(News).get(news_id) + record.label = label + s.commit() + + Base.metadata.create_all(bind=engine) diff --git a/homework06/hackernews.py b/homework06/hackernews.py index f48fe01..ef99574 100644 --- a/homework06/hackernews.py +++ b/homework06/hackernews.py @@ -1,36 +1,54 @@ -from bottle import ( - route, run, template, request, redirect -) - -from scrapper import get_news -from db import News, session from bayes import NaiveBayesClassifier +from bottle import redirect, request, route, run, template +from db import News +from scraputils import get_news + + +@route("/") +def index(): + redirect("/news") @route("/news") def news_list(): - s = session() - rows = s.query(News).filter(News.label == None).all() - return template('news_template', rows=rows) + rows = News.get() + return template("news_template", rows=rows) @route("/add_label/") def add_label(): - # PUT YOUR CODE HERE + News.add_label(request.query["id"], request.query["label"]) redirect("/news") @route("/update") def update_news(): - # PUT YOUR CODE HERE + news = get_news("https://news.ycombinator.com/newest") + News.add_many(news) redirect("/news") @route("/classify") def classify_news(): - # PUT YOUR CODE HERE + model = NaiveBayesClassifier(alpha=1) + x, y = [], [] + for record in News.get_labeled(): + x.append(record.title) + y.append(record.label) + model.fit(x, y) + not_labeled = News.get() + titles = [record.title for record in not_labeled] + predictions = model.predict(titles) + good, maybe, never = [], [], [] + for i in range(len(titles)): + if predictions[i] == "good": + good.append(not_labeled[i]) + elif predictions[i] == "maybe": + maybe.append(not_labeled[i]) + elif predictions[i] == "never": + never.append(not_labeled[i]) + return template("news_template", rows=good + maybe + never) if __name__ == "__main__": run(host="localhost", port=8080) - diff --git a/homework06/requirements.txt b/homework06/requirements.txt new file mode 100644 index 0000000..82419b4 --- /dev/null +++ b/homework06/requirements.txt @@ -0,0 +1 @@ +types-requests diff --git a/homework06/scraputils.py b/homework06/scraputils.py index 4721e18..460d716 100644 --- a/homework06/scraputils.py +++ b/homework06/scraputils.py @@ -3,21 +3,38 @@ def extract_news(parser): - """ Extract news from a given web page """ + """Extract news from a given web page""" news_list = [] - # PUT YOUR CODE HERE - + titles = parser.findAll("span", class_="titleline") + subtexts = parser.findAll("span", class_="subline") + for title, subtext in zip(titles, subtexts): + news_list.append(form_record(title, subtext)) return news_list +def form_record(title, subtext): + main_link = title.find("a") + subtext_links = subtext.findAll("a") + record = { + "title": main_link.text, + "url": main_link["href"], + "points": subtext.find("span", class_="score").text.split()[0], + "author": subtext_links[0].text, + "comments": subtext_links[-1].text.split()[0], + } + if record["comments"] == "discuss": + record["comments"] = 0 + return record + + def extract_next_page(parser): - """ Extract next page URL """ - # PUT YOUR CODE HERE + """Extract next page URL""" + return parser.find("a", class_="morelink")["href"] def get_news(url, n_pages=1): - """ Collect news from a given web page """ + """Collect news from a given web page""" news = [] while n_pages: print("Collecting data from page: {}".format(url)) @@ -29,4 +46,3 @@ def get_news(url, n_pages=1): news.extend(news_list) n_pages -= 1 return news - diff --git a/requirements.txt b/requirements.txt index 868f6a2..e4a9be5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ isort==5.4.2 mypy==0.782 pylint==2.6.0 pytest==6.0.1 +types-requests