diff --git a/.pyflyby b/.pyflyby index db34f23..41d35d2 100644 --- a/.pyflyby +++ b/.pyflyby @@ -1,3 +1,5 @@ +from learn_sql_model.optional import _optional_import_ + from learn_sql_model.api.websocket_connection_manager import manager from learn_sql_model.config import Config diff --git a/Dockerfile b/Dockerfile index 16592c5..6379386 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,9 @@ WORKDIR /app Copy pyproject.toml /app COPY learn_sql_model/__about__.py /app/learn_sql_model/__about__.py COPY README.md /app -RUN pip3 install . +RUN pip3 install '.[api]' COPY . /app -RUN pip3 install . +RUN pip3 install '.[api]' EXPOSE 5000 diff --git a/learn_sql_model/cli/api.py b/learn_sql_model/cli/api.py index 24ccb13..2dadd71 100644 --- a/learn_sql_model/cli/api.py +++ b/learn_sql_model/cli/api.py @@ -1,11 +1,12 @@ import httpx from rich.console import Console import typer -import uvicorn from learn_sql_model.cli.common import verbose_callback from learn_sql_model.config import get_config +from learn_sql_model.optional import _optional_import_ +uvicorn = _optional_import_("uvicorn", group="api") api_app = typer.Typer() diff --git a/learn_sql_model/cli/hero.py b/learn_sql_model/cli/hero.py index 1815a2f..a3a186e 100644 --- a/learn_sql_model/cli/hero.py +++ b/learn_sql_model/cli/hero.py @@ -6,7 +6,6 @@ from rich.console import Console import typer from learn_sql_model.config import get_config -from learn_sql_model.factories.hero import HeroFactory from learn_sql_model.models.hero import ( Hero, HeroCreate, @@ -15,6 +14,13 @@ from learn_sql_model.models.hero import ( HeroUpdate, Heros, ) +from learn_sql_model.optional import _optional_import_ + +HeroFactory = _optional_import_( + "learn_sql_model.factories.hero", + "HeroFactory", + group="api", +) hero_app = typer.Typer() @@ -46,7 +52,7 @@ def list( ) -> Union[Hero, List[Hero]]: "list many heros" heros = Heros.list(where=where, offset=offset, limit=limit) - Console().print(heros) + Console().print(hero) return hero diff --git a/learn_sql_model/game/game.py b/learn_sql_model/game/game.py index 8497d8b..f409c22 100644 --- a/learn_sql_model/game/game.py +++ b/learn_sql_model/game/game.py @@ -1,24 +1,24 @@ import atexit -import pygame from typer import Typer from websocket import create_connection -from learn_sql_model.game.menu import Menu from learn_sql_model.config import get_config from learn_sql_model.console import console -from learn_sql_model.factories.hero import HeroFactory -from learn_sql_model.models.hero import HeroCreate, HeroDelete, HeroUpdate, Heros +from learn_sql_model.game.map import Map +from learn_sql_model.game.menu import Menu +from learn_sql_model.game.player import Player +from learn_sql_model.optional import _optional_import_ + +pygame = _optional_import_("pygame", group="game") speed = 10 config = get_config() + class Client: def __init__(self): - hero = HeroFactory().build(size=50, x=100, y=100) - self.hero = HeroCreate(**hero.dict()).post() - self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) pygame.display.set_caption("Learn SQL Model") self.clock = pygame.time.Clock() @@ -30,13 +30,15 @@ class Client: self.moving_left = False self.moving_right = False self.ticks = 0 - self.others = [] + self.player = Player(self) self.menu = Menu(self) + self.map = Map(self) self.font = pygame.font.SysFont("", 50) + self.joysticks = {} atexit.register(self.quit) - @ property + @property def ws(self): def connect(): self._ws = create_connection( @@ -59,67 +61,23 @@ class Client: console.print("render") self.render() time = self.clock.tick(60) + self.elapsed = time / 100 self.ticks += 1 console.print(f"time: {time}") console.print(f"ticks: {self.ticks}") self.quit() def quit(self): - try: - HeroDelete(id=self.hero.id).delete() - except: - pass + self.running = False + self.player.quit() def update(self): - if self.moving_up: - self.hero.y -= speed - if self.moving_down: - self.hero.y += speed - if self.moving_left: - self.hero.x -= speed - if self.moving_right: - self.hero.x += speed - - if self.hero.x < 0 + self.hero.size: - self.hero.x = 0 + self.hero.size - if self.hero.x > self.screen.get_width() - self.hero.size: - self.hero.x = self.screen.get_width() - self.hero.size - if self.hero.y < 0 + self.hero.size: - self.hero.y = 0 + self.hero.size - if self.hero.y > self.screen.get_height() - self.hero.size: - self.hero.y = self.screen.get_height() - self.hero.size - - if self.ticks % 5 == 0 or self.ticks == 0: - console.print("updating") - update = HeroUpdate(**self.hero.dict(exclude_unset=True)) - console.print(update) - self.ws.send(update.json()) - console.print("sent") - - raw_heros = self.ws.recv() - console.print(raw_heros) - self.others = Heros.parse_raw(raw_heros) + ... def render(self): self.screen.fill((0, 0, 0)) - - for other in self.others.heros: - if other.id != self.hero.id: - pygame.draw.circle( - self.screen, (255, 0, 0), (other.x, other.y), other.size - ) - self.screen.blit( - self.font.render(other.name, False, (255, 255, 255), 1), - (other.x, other.y), - ) - - pygame.draw.circle( - self.screen, (0, 0, 255), (self.hero.x, self.hero.y), self.hero.size - ) - self.screen.blit( - self.font.render(self.hero.name, False, (255, 255, 255)), - (self.hero.x, self.hero.y), - ) + self.map.render() + self.player.render() # update the screen self.menu.render() @@ -127,52 +85,18 @@ class Client: def handle_events(self): self.events = pygame.event.get() + self.menu.handle_events(self.events) + self.player.handle_events() for event in self.events: if event.type == pygame.QUIT: self.running = False - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - self.running = False - if event.key == pygame.K_LEFT: - self.moving_left = True - if event.key == pygame.K_RIGHT: - self.moving_right = True - if event.key == pygame.K_UP: - self.moving_up = True - if event.key == pygame.K_DOWN: - self.moving_down = True - # wasd - if event.key == pygame.K_w: - self.moving_up = True - if event.key == pygame.K_s: - self.moving_down = True - if event.key == pygame.K_a: - self.moving_left = True - if event.key == pygame.K_d: - self.moving_right = True - # controller left joystick - - if event.type == pygame.KEYUP: - if event.key == pygame.K_LEFT: - self.moving_left = False - if event.key == pygame.K_RIGHT: - self.moving_right = False - if event.key == pygame.K_UP: - self.moving_up = False - if event.key == pygame.K_DOWN: - self.moving_down = False - # wasd - if event.key == pygame.K_w: - self.moving_up = False - if event.key == pygame.K_s: - self.moving_down = False - if event.key == pygame.K_a: - self.moving_left = False - if event.key == pygame.K_d: - self.moving_right = False - if event.type == pygame.MOUSEBUTTONDOWN: - if event.button == 1: # Left mouse button - self.menu.handle_click() + if event.type == pygame.JOYDEVICEADDED: + # This event will be generated when the program starts for every + # joystick, filling up the list without needing to create them manually. + joy = pygame.joystick.Joystick(event.device_index) + self.joysticks[joy.get_instance_id()] = joy + if event.type == pygame.JOYDEVICEREMOVED: + del self.joysticks[event.instance_id] def check_events(self): pass @@ -184,7 +108,7 @@ class Client: game_app = Typer() -@ game_app.command() +@game_app.command() def run(): client = Client() client.run() diff --git a/learn_sql_model/game/map.py b/learn_sql_model/game/map.py new file mode 100644 index 0000000..616c75b --- /dev/null +++ b/learn_sql_model/game/map.py @@ -0,0 +1,99 @@ +from learn_sql_model.optional import _optional_import_ +import pydantic +from rich.console import Console + +snoise2 = _optional_import_("noise", "snoise2", group="game") +pygame = _optional_import_("pygame", group="game") + + +console = Console() + + +class Point(pydantic.BaseModel): + x: int + y: int + + +class Map: + def __init__(self, game): + self.game = game + # self.grass = pygame.image.load("grass.webp").convert_alpha() + # self.rock = pygame.image.load("rock.jpg").convert_alpha() + # self.dirt = pygame.image.load("dirt.jpg").convert_alpha() + self.brown = (204, 153, 102) + self.grey = (128, 128, 128) + self.green = (0, 255, 0) + self.white = (255, 255, 255) + self.resolution = 16 + self.scale = 0.14 # Determines the "smoothness" of the terrain + self.scale = 0.05 # Determines the "smoothness" of the terrain + self.offset = Point(x=0, y=0) + self.last_offset = self.offset + self.screen_width = self.game.screen.get_width() + self.screen_height = self.game.screen.get_height() + self.octaves = 2 # Number of layers of noise to combine + self.persistence = 0.05 # Amplitude of each octave + self.lacunarity = 1.0 # Frequency of each octave + self.thresh = 125 + self.pre_draw() + + def refresh_surf(self): + self.surf = pygame.Surface((self.screen_width, self.screen_height)) + + def get_noise(self, x, y): + value = snoise2( + (x + self.offset.x) * self.scale, + (y + self.offset.y) * self.scale, + self.octaves, + self.persistence, + self.lacunarity, + ) + value = (value + 1) / 2 * 255 + return value + + def render(self): + self.game.screen.blit( + pygame.transform.scale(self.surf, (self.screen_width, self.screen_height)), + (0, 0), + ) + + def point_check_collision(self, x, y, thresh=None): + return self.get_noise(x / self.resolution, y / self.resolution) < ( + thresh or self.thresh + ) + + def pre_draw(self): + self.refresh_surf() + for x in range(int(self.screen_width)): + for y in range(int(self.screen_height)): + if not self.point_check_collision(x, y): + pygame.draw.rect( + self.surf, + self.white, + ( + x, + y, + 1, + 1, + ), + ) + pygame.image.save(self.surf, "map.png") + # av1 = ( + # Image.open("rock.jpg") + # .convert("RGB") + # .resize((self.screen_width, self.screen_height)) + # ) + # av2 = ( + # Image.open("dirt.jpg") + # .convert("RGB") + # .resize((self.screen_width, self.screen_height)) + # ) + # mask = ( + # Image.open("map.png") + # .convert("L") + # .resize((self.screen_width, self.screen_height)) + # .filter(ImageFilter.GaussianBlur(3)) + # ) + # Image.composite(av2, av1, mask).save("result.png") + # result = pygame.image.load("result.png") + # self.surf.blit(result, (0, 0)) diff --git a/learn_sql_model/game/menu.py b/learn_sql_model/game/menu.py index 3864cf5..c7ddc9c 100644 --- a/learn_sql_model/game/menu.py +++ b/learn_sql_model/game/menu.py @@ -1,29 +1,26 @@ -from pydantic import BaseModel -from typing import Tuple, Callable +from typing import Callable, Tuple + +from pydantic import BaseModel -import pygame screen_sizes = [ - (480, 360), # 360p - (640, 480), # VGA - (800, 600), # SVGA - (1024, 768), # XGA - (1280, 720), # HD 720p - (1366, 768), # HD 1366x768 - (1600, 900), # HD+ 1600x900 + (480, 360), # 360p + (640, 480), # VGA + (800, 600), # SVGA + (1024, 768), # XGA + (1280, 720), # HD 720p + (1366, 768), # HD 1366x768 + (1600, 900), # HD+ 1600x900 (1920, 1080), # Full HD 1080p (2560, 1440), # 2K / QHD 1440p - (3840, 2160) # 4K / UHD 2160p + (3840, 2160), # 4K / UHD 2160p ] - class MenuItem(BaseModel): display_text: str on_click: Callable = None - text_color: Tuple[str, str, str] = (0, 0, 0) - - + text_color: Tuple[str, str, str] = (0, 0, 0) class Menu: @@ -32,24 +29,53 @@ class Menu: self.game = game self.hamburger = Hamburger(game) - self.menu_width = min(max(200, self.game.screen.get_width() - * 0.8), self.game.screen.get_width()) - self.menu_height = min(max(200, self.game.screen.get_height() - * 0.8), self.game.screen.get_height()) + + self.padding = 10 + self.font_size = 50 + self.line_height = 55 + + self.menu_width = min( + max(200, self.game.screen.get_width() * 0.8), self.game.screen.get_width() + ) + self.menu_height = min( + max(200, self.game.screen.get_height() * 0.8), self.game.screen.get_height() + ) self.x = (self.game.screen.get_width() - self.menu_width) / 2 self.y = (self.game.screen.get_height() - self.menu_height) / 2 self.color = (100, 100, 100) self.is_menu_open = False self.surface = pygame.Surface((self.menu_width, self.menu_height)) - self.font = pygame.font.SysFont("", 50) - self.screen_size_index = False - self.padding = 10 + self.font = pygame.font.SysFont("", self.font_size) - self.items = [ - MenuItem(display_text='Menu'), - MenuItem(display_text='Screen Size'), - MenuItem(display_text=f'{self.game.screen.get_width()}x{self.game.screen.get_height()}', color=(50, 0, 0)) + self.screen_size_index = False + + @property + def items(self) -> list[MenuItem]: + return [ + MenuItem( + display_text="Menu", + on_click=lambda: print("clicked on me, the menu"), + ), + MenuItem( + display_text="Screen Size", + on_click=self.next_screen_size, + ), + MenuItem( + display_text=f"{self.game.screen.get_width()}x{self.game.screen.get_height()}", + color=(50, 0, 0), + on_click=self.next_screen_size, + ), + MenuItem( + display_text=f"{self.game.player.hero.name}", + color=(50, 0, 0), + on_click=self.game.player.rename_hero, + ), + MenuItem( + display_text="quit", + color=(50, 0, 0), + on_click=lambda: self.game.quit(), + ), ] def render(self): @@ -60,39 +86,42 @@ class Menu: for item in self.items: text = self.font.render(item.display_text, True, item.text_color) self.surface.blit(text, pos) - pos = (pos[0], pos[1] + 50) + pos = (pos[0], pos[1] + self.line_height) - # put text in the menu surface - # text = self.font.render("Menu", True, (0, 0, 0)) - # self.surface.blit(text, (0, 0)) - # text = self.font.render("Screen Size", True, (0, 0, 0)) - # self.surface.blit(text, (0, 60)) - # text = self.font.render( - # f'{self.game.screen.get_width()}x{self.game.screen.get_height()}', True, (25, 25, 25)) - # self.surface.blit(text, (10, 120)) - # self.game.screen.blit(self.surface, (self.x, self.y)) - + self.hamburger.render() + def next_screen_size(self): + if self.screen_size_index is False: + self.screen = pygame.display.set_mode(screen_sizes[0]) + self.screen_size_index = 0 + if self.screen_size_index == len(screen_sizes) - 1: + self.screen_size_index = 0 + else: + self.screen_size_index += 1 + self.screen = pygame.display.set_mode(screen_sizes[self.screen_size_index]) + def get_mouse_pos(self): - 'get mouse position relative to self.surface' + "get mouse position relative to self.surface" x, y = pygame.mouse.get_pos() return x - self.x, y - self.y + def handle_events(self, events): + self.hamburger.handle_events(self, events) + for event in events: + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # Left mouse button + self.handle_click() + def handle_click(self): - self.hamburger.handle_click(self) pos = self.get_mouse_pos() - print(pos) - if pos[1] > 120 and pos[1] < 180 and pos[0] > 0 and pos[0] < self.menu_width: - if self.screen_size_index is False: - self.screen = pygame.display.set_mode(screen_sizes[0]) - self.screen_size_index = 0 - if self.screen_size_index == len(screen_sizes) - 1: - self.screen_size_index = 0 - else: - self.screen_size_index += 1 - self.screen = pygame.display.set_mode(screen_sizes[self.screen_size_index]) + pos_idx = int(pos[1] // self.line_height) + if pos_idx > len(self.items): + return + if pos_idx < 0: + return + self.items[pos_idx].on_click() class Hamburger: @@ -106,21 +135,45 @@ class Hamburger: self.x = self.game.screen.get_width() - self.hamburger_width - 20 self.y = 20 self.color = (100, 100, 100) - self.rect = pygame.Rect(self.x, self.y, self.hamburger_width, - self.hamburger_height) - self.surface = pygame.Surface( - (self.hamburger_width, self.hamburger_height)) + self.rect = pygame.Rect( + self.x, self.y, self.hamburger_width, self.hamburger_height + ) + self.surface = pygame.Surface((self.hamburger_width, self.hamburger_height)) def render(self): - pygame.draw.rect(self.surface, self.color, - (0, 0, self.hamburger_width, self.bar_height),) - pygame.draw.rect(self.surface, self.color, - (0, self.bar_height + self.bar_spacing, self.hamburger_width, self.bar_height),) - pygame.draw.rect(self.surface, self.color, - (0, 2 * (self.bar_height + self.bar_spacing), self.hamburger_width, self.bar_height),) + pygame.draw.rect( + self.surface, + self.color, + (0, 0, self.hamburger_width, self.bar_height), + ) + pygame.draw.rect( + self.surface, + self.color, + ( + 0, + self.bar_height + self.bar_spacing, + self.hamburger_width, + self.bar_height, + ), + ) + pygame.draw.rect( + self.surface, + self.color, + ( + 0, + 2 * (self.bar_height + self.bar_spacing), + self.hamburger_width, + self.bar_height, + ), + ) self.game.screen.blit(self.surface, (self.x, self.y)) + def handle_events(self, menu: Menu, events): + for event in events: + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # Left mouse button + self.handle_click(menu) def handle_click(self, menu): pos = pygame.mouse.get_pos() diff --git a/learn_sql_model/game/player.py b/learn_sql_model/game/player.py new file mode 100644 index 0000000..ce9a8b9 --- /dev/null +++ b/learn_sql_model/game/player.py @@ -0,0 +1,219 @@ +from learn_sql_model.console import console +from learn_sql_model.models.hero import HeroCreate, HeroDelete, HeroUpdate, Heros +from learn_sql_model.optional import _optional_import_ + +pygame = _optional_import_("pygame", group="game") +HeroFactory = _optional_import_( + "learn_sql_model.factories.hero", + "HeroFactory", + group="game", +) + + +class Player: + def __init__(self, game): + hero = HeroFactory().build(size=25, x=100, y=100) + self.hero = HeroCreate(**hero.dict()).post() + + self.game = game + self.others = Heros(heros=[]) + self.width = 16 + self.height = 16 + self.white = (255, 255, 255) + self.x = self.game.screen.get_width() / 2 + self.y = self.game.screen.get_height() / 2 + self.speed = 5 + self.max_speed = 5 + self.image = pygame.image.load("player.png").convert_alpha() + self.x_last = self.x + self.y_last = self.y + self.hitbox_surface = pygame.Surface((self.width, self.height)) + self.hitbox_surface.fill(self.white) + pygame.draw.rect( + self.hitbox_surface, (255, 0, 0), (0, 0, self.width, self.height), 1 + ) + self.hitbox_surface.set_alpha(0) + self.moving_up = False + self.moving_down = False + self.moving_left = False + self.moving_right = False + + def rename_hero(self): + old_hero = self.hero + hero = HeroFactory().build( + size=self.hero.size, x=self.hero.x, y=self.hero.y, id=old_hero.id + ) + self.hero = HeroCreate(**hero.dict()).post() + + def quit(self): + try: + HeroDelete(id=self.hero.id).delete() + except: + pass + + def handle_events(self): + # Update the self + for event in self.game.events: + if event.type == pygame.QUIT: + self.running = False + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + self.running = False + if event.key == pygame.K_LEFT: + self.speed = self.max_speed + self.moving_left = True + if event.key == pygame.K_RIGHT: + self.speed = self.max_speed + self.moving_right = True + if event.key == pygame.K_UP: + self.speed = self.max_speed + self.moving_up = True + if event.key == pygame.K_DOWN: + self.speed = self.max_speed + self.moving_down = True + # wasd + if event.key == pygame.K_w: + self.speed = self.max_speed + self.moving_up = True + if event.key == pygame.K_s: + self.speed = self.max_speed + self.moving_down = True + if event.key == pygame.K_a: + self.speed = self.max_speed + self.moving_left = True + if event.key == pygame.K_d: + self.speed = self.max_speed + self.moving_right = True + + if event.type == pygame.KEYUP: + if event.key == pygame.K_LEFT: + self.moving_left = False + if event.key == pygame.K_RIGHT: + self.moving_right = False + if event.key == pygame.K_UP: + self.moving_up = False + if event.key == pygame.K_DOWN: + self.moving_down = False + # wasd + if event.key == pygame.K_w: + self.moving_up = False + if event.key == pygame.K_s: + self.moving_down = False + if event.key == pygame.K_a: + self.moving_left = False + if event.key == pygame.K_d: + self.moving_right = False + + for joystick in self.joysticks.values(): + if abs(joystick.get_axis(0)) > 0.2: + self.x += joystick.get_axis(0) * 10 * self.speed * self.elapsed + if abs(joystick.get_axis(1)) > 0.2: + self.y += joystick.get_axis(1) * 10 * self.speed * self.elapsed + + if abs(joystick.get_axis(3)) > 0.2 and abs(joystick.get_axis(4)) > 0.2: + pygame.mouse.set_pos( + ( + pygame.mouse.get_pos()[0] + joystick.get_axis(3) * 32, + pygame.mouse.get_pos()[1] + joystick.get_axis(4) * 32, + ) + ) + elif abs(joystick.get_axis(3)) > 0.2: + pygame.mouse.set_pos( + ( + pygame.mouse.get_pos()[0] + joystick.get_axis(3) * 32, + pygame.mouse.get_pos()[1], + ) + ) + elif abs(joystick.get_axis(4)) > 0.2: + pygame.mouse.set_pos( + ( + pygame.mouse.get_pos()[0], + pygame.mouse.get_pos()[1] + joystick.get_axis(4) * 32, + ) + ) + if self.moving_left: + self.hero.x -= self.speed + if self.moving_right: + self.hero.x += self.speed + if self.moving_up: + self.hero.y -= self.speed + if self.moving_down: + self.hero.y += self.speed + # Check for self collisions with the walls and the black tiles on the map + if self.hero.x < 0: + self.hero.x = 0 + if self.hero.x > self.game.screen.get_width() - self.width: + self.hero.x = self.game.screen.get_width() - self.width + if self.hero.y < 0: + self.hero.y = 0 + if self.hero.y > self.game.screen.get_height() - self.height: + self.hero.y = self.game.screen.get_height() - self.height + + self.pos = pygame.math.Vector2(self.hero.x, self.hero.y) + + if self.game.map.point_check_collision(self.pos.x, self.pos.y): + + start_pos = pygame.math.Vector2(self.x_last, self.y_last) + end_pos = pygame.math.Vector2(self.hero.x, self.hero.y) + movement_vector = end_pos - start_pos + try: + movement_direction = movement_vector.normalize() + except: + end_pos = pygame.math.Vector2(self.hero.x + 128, self.hero.y + 128) + movement_vector = end_pos - start_pos + movement_direction = movement_vector.normalize() + movement_speed = 0.05 + + self.hero.x = self.x_last + self.hero.y = self.y_last + + self.pos = pygame.math.Vector2(start_pos) + + while self.game.map.point_check_collision(self.pos.x, self.pos.y): + self.pos += movement_speed * movement_direction + self.hero.x = self.pos.x + self.hero.y = self.pos.y + + self.pos -= movement_speed * movement_direction + self.hero.x = self.pos.x + self.hero.y = self.pos.y + + self.x_last = self.hero.x + self.y_last = self.hero.y + + if self.game.ticks % 5 == 0 or self.game.ticks == 0: + console.print("updating") + update = HeroUpdate(**self.hero.dict(exclude_unset=True)) + console.print(update) + self.game.ws.send(update.json()) + console.print("sent") + + raw_heros = self.game.ws.recv() + console.print(raw_heros) + self.others = Heros.parse_raw(raw_heros) + + def draw(self): + self.move() + self.game.screen.blit( + pygame.transform.scale(self.image, (16, 16)), + (self.x - 8 - self.game.map.offset.x, self.y - 8 - self.game.map.offset.y), + ) + + def render(self): + for other in self.others.heros: + if other.id != self.hero.id: + pygame.draw.circle( + self.game.screen, (255, 0, 0), (other.x, other.y), other.size + ) + self.game.screen.blit( + self.game.font.render(other.name, False, (255, 255, 255), 1), + (other.x, other.y), + ) + + pygame.draw.circle( + self.game.screen, (0, 0, 255), (self.hero.x, self.hero.y), self.hero.size + ) + self.game.screen.blit( + self.game.font.render(self.hero.name, False, (255, 255, 255), 1), + (self.hero.x, self.hero.y), + ) diff --git a/learn_sql_model/optional.py b/learn_sql_model/optional.py new file mode 100644 index 0000000..b43f240 --- /dev/null +++ b/learn_sql_model/optional.py @@ -0,0 +1,63 @@ +import textwrap + + +def _optional_import_( + module: str, + name: str = None, + group: str = None, + package="learn_sql_model", +): + """ + lazily throws import errors only then the optional import is used, and + includes a group install command for the user to install all dependencies + for the requested feature. + """ + import importlib + + try: + module = importlib.import_module(module) + return module if name is None else getattr(module, name) + except ImportError as e: + msg = textwrap.dedent( + f""" + "pip install '{package}[{group}]'" package to make use of this feature + Alternatively "pip install '{package}[all]'" package to install all optional dependencies + """ + ) + import_error = e + + class _failed_import: + """ + Lazily throw an import error. Errors should be thrown whether the + user tries to call the module, get an attubute from the module, or + getitem from the module. + + """ + + def _failed_import(self, *args): + raise ImportError(msg) from import_error + + def __call__(self, *args): + """ + Throw error if the user tries to call the module i.e + _optional_import_('dummy')() + """ + self._failed_import(*args) + + def __getattr__(self, name): + """ + Throw error if the user tries to get an attribute from the + module i.e _optional_import_('dummy').dummy. + """ + if name == "_failed_import": + return object.__getattribute__(self, name) + self._failed_import() + + def __getitem__(self, name): + """ + Throw error if the user tries to get an item from the module + i.e _optional_import_('dummy')['dummy'] + """ + self._failed_import() + + return _failed_import() diff --git a/pyproject.toml b/pyproject.toml index 549f520..9263d99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,36 +24,51 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ -"python-socketio[client]", -"fastapi-socketio", -"psycopg2-binary", -'pygame', -'black', -'alembic', -'pygame', -'pyinstaller', - "pyflyby", + "python-socketio[client]", "anyconfig", "copier", "engorgio", "fastapi", "httpx", - "passlib[bcrypt]", - "polyfactory", - "psycopg2", - "python-jose[cryptography]", - "python-multipart", + "pydantic[dotenv]", + "pyflyby", + "pyinstaller", "rich", "sqlmodel", "textual", "toml", "trogon", "typer", - "uvicorn[standard]", ] dynamic = ["version"] +[project.optional-dependencies] +game = [ + "noise", + "pygame", + "polyfactory", + "faker", +] +api = [ + "fastapi-socketio", + "passlib[bcrypt]", + "psycopg2", + "psycopg2-binary", + "python-jose[cryptography]", + "python-multipart", + "uvicorn[standard]", +] +manage = [ + "alembic", + "polyfactory", + "faker", +] +all = [ + "learn_sql_model[game, api, manage]", +] + + [project.urls] Documentation = "https://github.com/waylonwalker/learn-sql-model#readme" Issues = "https://github.com/waylonwalker/learn-sql-model/issues"