diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b9295b0..9ecd3fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,15 +21,15 @@ repos: - id: check-added-large-files args: ["--maxkb=1000"] - # - repo: https://github.com/astral-sh/uv-pre-commit - # # uv version. - # rev: 0.4.20 - # hooks: - # # Update the uv lockfile - # - id: uv-lock - # # Run the pip compile - # - id: pip-compile - # args: [requirements.in, -o, requirements.txt] + - repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.5.10 + hooks: + # Update the uv lockfile + - id: uv-lock + # Export to requirements.txt + - id: uv-export + args: [--quiet, --frozen, --no-hashes, --no-group, dev, --no-group, doc, --no-group, test, --format, requirements-txt, --output-file, requirements.txt] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.0 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8de7162 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,43 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Simple App", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": [ + "api.gui.simple_app:app", + "--host", + "0.0.0.0", + "--port", + "5001", + // "--reload" + ], + "justMyCode": true, + // "jinja": true, + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + }, + { + "name": "Debug Game App", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": [ + "api.gui.game_app:app", + "--host", + "0.0.0.0", + "--port", + "5001", + // "--reload" + ], + "justMyCode": true, + // "jinja": true, + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + } + ] +} diff --git a/Makefile b/Makefile index e9691f3..f5673c3 100644 --- a/Makefile +++ b/Makefile @@ -196,8 +196,9 @@ profile-builtin: ## Profile the file with cProfile and shows the report in the t docker-build: ## Build docker image docker build --tag ${DOCKER_IMAGE} --file docker/Dockerfile --target ${DOCKER_TARGET} . -export-requirements: ## Export requirements to requirements.txt - @uv export --frozen --no-hashes --no-group dev --no-group doc --no-group test --format requirements-txt --output-file requirements.txt - run-fasthtml: ## Run fasthtml app - uv run uvicorn api.gui.game_app:app --host 0.0.0.0 --port 5001 + # uv run --module api/gui/game_app + uv run uvicorn api.gui.game_app:app --host 0.0.0.0 --port 5002 + +run-fasthtml-simple: ## Run simple fasthtml app + uv run uvicorn api.gui.simple_app:app --host 0.0.0.0 --port 5002 diff --git a/README.md b/README.md index 4d8da0d..138520f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ make run-fasthtml 3. Open your browser and navigate to: ``` -http://localhost:5001 +http://localhost:5002 ``` ## Game Rules diff --git a/api/gui/fasthtml_app.py b/api/gui/fasthtml_app.py index 7f12ec7..2fbc47e 100644 --- a/api/gui/fasthtml_app.py +++ b/api/gui/fasthtml_app.py @@ -65,7 +65,7 @@ def post(query: str = ""): if not query: return Div("Start typing to search movies...", id="search-results") - results = fuzzy_search_movies(query) + results = fuzzy_search_movies(query=query, include_backdrops=True) if not results: return Div("No movies found", id="search-results") diff --git a/api/gui/game_app.py b/api/gui/game_app.py index 0989d79..7d8ba54 100644 --- a/api/gui/game_app.py +++ b/api/gui/game_app.py @@ -7,7 +7,9 @@ Form, Img, Input, + Option, P, + Select, Style, Titled, fast_app, @@ -15,16 +17,45 @@ from api.utils.movie import fuzzy_search_movies, get_random_movie_with_details -app, rt = fast_app() - -# Store current game state -current_game = {} +app, rt = fast_app(secret_key="your-secret-key-here") # Add secret key for session @rt("/") -def get(): - # Add top navigation with new game button +def get(session): + # Get current category from session, default to 'popular' + current_category = session.get("game", {}).get("category", "popular") + + # Add top navigation with category selector and new game button top_nav = Div( + Form( + Select( + Option( + "Popular Movies", + value="popular", + selected=current_category == "popular", + ), + Option( + "Top Rated Movies", + value="top_rated", + selected=current_category == "top_rated", + ), + Option( + "Now Playing", + value="now_playing", + selected=current_category == "now_playing", + ), + Option( + "Upcoming Movies", + value="upcoming", + selected=current_category == "upcoming", + ), + name="category", + hx_post="/new-game", + hx_target="body", + hx_trigger="change", + ), + style="display: inline-block; margin-right: 1rem;", + ), Button("New Game", hx_post="/new-game", hx_target="body"), style="text-align: right; margin-bottom: 1rem;", ) @@ -42,6 +73,12 @@ def get(): object-fit: cover; border-radius: 8px; } + /* Add title styling */ + h1 { + text-align: center; + margin: 2rem 0; + } + /* Rest of existing styles */ .search-results { margin-top: 1rem; } @@ -77,15 +114,28 @@ def get(): .guess-used { background-color: #666; } + .search-form { + display: flex; + gap: 1rem; + align-items: center; + } + .search-form input[type="search"] { + flex: 1; + margin: 0; + } + .search-form button { + margin: 0; + } """), Div( Input( type="search", name="query", - placeholder="Search for a movie...", + placeholder="Guess the movie...", hx_post="/search", - hx_trigger="keyup changed delay:500ms", + hx_trigger="input changed delay:200ms", hx_target="#search-results", + autocomplete="off", # To prevent browser autocomplete from interfering ), Button( "Submit Guess", @@ -93,23 +143,23 @@ def get(): hx_include="#search-form", hx_target="#search-results", ), + cls="search-form", ), id="search-form", ) - # Initialize new game if not exists - if not current_game: + # Initialize new game if not exists in session + if "game" not in session: movie = get_random_movie_with_details() - # Start with just the first backdrop current_backdrop = movie["backdrops"][0] if movie["backdrops"] else None - current_game.update( - { - "movie": movie, - "current_backdrop_index": 0, - "shown_backdrops": [current_backdrop] if current_backdrop else [], - "guesses_remaining": 5, # Add guess counter - } - ) + session["game"] = { + "movie": movie, + "current_backdrop_index": 0, + "shown_backdrops": [current_backdrop] if current_backdrop else [], + "guesses_remaining": 5, + } + + current_game = session["game"] # Get game state from session backdrop = None if current_game["shown_backdrops"]: @@ -143,7 +193,8 @@ def get(): results_div = Div(id="search-results") - # Add top_nav to the Container + # Add top_nav to the Container with reordered elements: + # Move search_box and results_div together, before the backdrop return Titled( "Movie Guess Game", Container(top_nav, backdrop, guess_indicators, search_box, results_div), @@ -152,45 +203,48 @@ def get(): @rt("/search") def post(query: str = ""): - if not query: - return Div("Start typing to search for movies...", id="search-results") + MIN_CHARS = 2 - results = fuzzy_search_movies(query) + if not query or len(query) < MIN_CHARS: + return Div("Start typing to search for movies...", id="search-results") + results = fuzzy_search_movies(query=query, limit=3, include_backdrops=False) if not results: return Div("No movies found", id="search-results") - # Create clickable list items that populate the search input + # Create clickable list items that populate the search input and automatically submit movie_items = [ Div( f"{movie['title']} ({movie['release_date'][:4]})", - # Fixed string escaping by using format() instead of f-string - onclick='document.querySelector(\'[name="query"]\').value = "{}";'.format( - movie["title"].replace('"', '\\"') - ), + onclick=""" + document.querySelector('[name="query"]').value = "{}"; + document.querySelector('#search-form button').click(); + """.format(movie["title"].replace('"', '\\"')), cls="search-item", ) - for movie in results[:5] # Limit to 5 results + for movie in results[:3] ] return Div(*movie_items, id="search-results", cls="search-results") @rt("/guess") -def post(query: str = ""): +def post(query: str = "", session=None): # Add session parameter if not query: return Div("Please select a movie to guess", id="search-results") - results = fuzzy_search_movies(query) + results = fuzzy_search_movies(query=query, limit=3, include_backdrops=False) if not results: return Div("No movies found", id="search-results") + current_game = session["game"] # Get game state from session current_movie = current_game["movie"] movie = results[0] # Use the best match is_correct = movie["id"] == current_movie["id"] # Decrease remaining guesses current_game["guesses_remaining"] = current_game.get("guesses_remaining", 5) - 1 + session["game"] = current_game # Save updated game state back to session # Update guess counter display updated_counter = Div( @@ -248,6 +302,7 @@ def post(query: str = ""): ] # Replace the current backdrop with the new one current_game["shown_backdrops"] = [next_backdrop] + session["game"] = current_game # Save updated game state back to session backdrop_url = f"https://image.tmdb.org/t/p/w1280{next_backdrop}" return ( Div( @@ -277,10 +332,29 @@ def post(query: str = ""): @rt("/new-game") -def post(): - current_game.clear() - return get() +def post(category: str = "popular", session=None): + if "game" in session: + del session["game"] + # Get new movie from selected category + movie = get_random_movie_with_details(category=category) + current_backdrop = movie["backdrops"][0] if movie["backdrops"] else None + session["game"] = { + "movie": movie, + "current_backdrop_index": 0, + "shown_backdrops": [current_backdrop] if current_backdrop else [], + "guesses_remaining": 5, + "category": category, # Store selected category + } + + return get(session) + + +# FIXME: Doesn't work since it can't find the `api` module # if __name__ == "__main__": -# serve() +# import uvicorn + +# # serve() + +# uvicorn.run("__main__:app", host="0.0.0.0", port=5002, reload=True, log_config=None) diff --git a/api/gui/simple_app.py b/api/gui/simple_app.py index 5b0486c..4c4211c 100644 --- a/api/gui/simple_app.py +++ b/api/gui/simple_app.py @@ -1,126 +1,17 @@ -import os -from dataclasses import dataclass - -import redis -from fasthtml.common import ( - AX, - Button, - Card, - CheckboxX, - Div, - Form, - Group, - Hidden, - Input, - Li, - Titled, - Ul, - clear, - fast_app, - fill_form, - patch, -) -from tinyredis import TinyRedis +from fasthtml.common import Div, P, fast_app app, rt = fast_app() -@dataclass -class Todo: - id: str = None - title: str = "" - done: bool = False - priority: int = 0 - - -todos = TinyRedis(redis.from_url(os.environ["VERCEL_KV_REDIS_URL"]), Todo) - - -def tid(id): - return f"todo-{id}" - - -@patch -def __ft__(self: Todo): - show = AX(self.title, f"/todos/{self.id}", "current-todo") - edit = AX("edit", f"/edit/{self.id}", "current-todo") - dt = " ✅" if self.done else "" - cts = ( - dt, - show, - " | ", - edit, - Hidden(id="id", value=self.id), - Hidden(id="priority", value="0"), - ) - return Li(*cts, id=f"todo-{self.id}") - - -def mk_input(**kw): - return Input(id="new-title", name="title", placeholder="New Todo", **kw) - - @rt("/") -async def get(): - add = Form( - Group(mk_input(), Button("Add")), - hx_post="/", - target_id="todo-list", - hx_swap="beforeend", - ) - items = sorted(todos(), key=lambda o: o.priority) - frm = Form( - *items, id="todo-list", cls="sortable", hx_post="/reorder", hx_trigger="end" - ) - return Titled("Todo list", Card(Ul(frm), header=add, footer=Div(id="current-todo"))) +def get(session): + session.setdefault("counter", 0) + session["counter"] = session.get("counter") + 1 + counter = session["counter"] - -@rt("/reorder") -def post(id: list[str]): - items = todos() - pos = {u: i for i, u in enumerate(id)} - for o in items: - o.priority = pos[o.id] - todos.insert_all(items) - return tuple(sorted(items, key=lambda o: o.priority)) - - -@rt("/todos/{id}") -async def delete(id: str): - todos.delete(id) - return clear("current-todo") - - -@rt("/") -async def post(todo: Todo): - return todos.insert(todo), mk_input(hx_swap_oob="true") - - -@rt("/edit/{id}") -async def get(id: str): - res = Form( - Group(Input(id="title"), Button("Save")), - Hidden(id="id"), - CheckboxX(id="done", label="Done"), - hx_put="/", - target_id=tid(id), - id="edit", - ) - return fill_form(res, todos[id]) - - -@rt("/") -async def put(todo: Todo): - return todos.update(todo), clear("current-todo") + return Div(P(f"Hello World -- {counter}"), hx_get="/change") -@rt("/todos/{id}") -async def get(id: str): - todo = todos[id] - btn = Button( - "delete", - hx_delete=f"/todos/{todo.id}", - target_id=tid(todo.id), - hx_swap="outerHTML", - ) - return Div(Div(todo.title), btn) +@rt("/change") +def get(): + return P("Nice to be here!") diff --git a/api/gui/simple_todo_app.py b/api/gui/simple_todo_app.py new file mode 100644 index 0000000..5b0486c --- /dev/null +++ b/api/gui/simple_todo_app.py @@ -0,0 +1,126 @@ +import os +from dataclasses import dataclass + +import redis +from fasthtml.common import ( + AX, + Button, + Card, + CheckboxX, + Div, + Form, + Group, + Hidden, + Input, + Li, + Titled, + Ul, + clear, + fast_app, + fill_form, + patch, +) +from tinyredis import TinyRedis + +app, rt = fast_app() + + +@dataclass +class Todo: + id: str = None + title: str = "" + done: bool = False + priority: int = 0 + + +todos = TinyRedis(redis.from_url(os.environ["VERCEL_KV_REDIS_URL"]), Todo) + + +def tid(id): + return f"todo-{id}" + + +@patch +def __ft__(self: Todo): + show = AX(self.title, f"/todos/{self.id}", "current-todo") + edit = AX("edit", f"/edit/{self.id}", "current-todo") + dt = " ✅" if self.done else "" + cts = ( + dt, + show, + " | ", + edit, + Hidden(id="id", value=self.id), + Hidden(id="priority", value="0"), + ) + return Li(*cts, id=f"todo-{self.id}") + + +def mk_input(**kw): + return Input(id="new-title", name="title", placeholder="New Todo", **kw) + + +@rt("/") +async def get(): + add = Form( + Group(mk_input(), Button("Add")), + hx_post="/", + target_id="todo-list", + hx_swap="beforeend", + ) + items = sorted(todos(), key=lambda o: o.priority) + frm = Form( + *items, id="todo-list", cls="sortable", hx_post="/reorder", hx_trigger="end" + ) + return Titled("Todo list", Card(Ul(frm), header=add, footer=Div(id="current-todo"))) + + +@rt("/reorder") +def post(id: list[str]): + items = todos() + pos = {u: i for i, u in enumerate(id)} + for o in items: + o.priority = pos[o.id] + todos.insert_all(items) + return tuple(sorted(items, key=lambda o: o.priority)) + + +@rt("/todos/{id}") +async def delete(id: str): + todos.delete(id) + return clear("current-todo") + + +@rt("/") +async def post(todo: Todo): + return todos.insert(todo), mk_input(hx_swap_oob="true") + + +@rt("/edit/{id}") +async def get(id: str): + res = Form( + Group(Input(id="title"), Button("Save")), + Hidden(id="id"), + CheckboxX(id="done", label="Done"), + hx_put="/", + target_id=tid(id), + id="edit", + ) + return fill_form(res, todos[id]) + + +@rt("/") +async def put(todo: Todo): + return todos.update(todo), clear("current-todo") + + +@rt("/todos/{id}") +async def get(id: str): + todo = todos[id] + btn = Button( + "delete", + hx_delete=f"/todos/{todo.id}", + target_id=tid(todo.id), + hx_swap="outerHTML", + ) + return Div(Div(todo.title), btn) diff --git a/api/utils/general.py b/api/utils/general.py index 82e8604..397db61 100644 --- a/api/utils/general.py +++ b/api/utils/general.py @@ -1,10 +1,11 @@ """General utility functions.""" import importlib -import logging import os +import time +from functools import wraps -logger = logging.getLogger(__name__) +from loguru import logger def check_env_vars(env_vars: list[str] | None = None) -> None: @@ -54,3 +55,17 @@ def is_module_installed(module_name: str, throw_error: bool = False) -> bool: message = f"Module {module_name} is not installed." raise ImportError(message) from e return False + + +def timing_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + start_time = time.time() + result = func(*args, **kwargs) + end_time = time.time() + logger.info( + f"{func.__name__} took {end_time - start_time:.2f} seconds to execute" + ) + return result + + return wrapper diff --git a/api/utils/movie.py b/api/utils/movie.py index 1221e0e..f8fb9f5 100644 --- a/api/utils/movie.py +++ b/api/utils/movie.py @@ -2,9 +2,12 @@ import random +from loguru import logger from thefuzz import fuzz from tmdbv3api import Movie, Search, TMDb +from api.utils.general import timing_decorator + tmdb = TMDb() movie_api = Movie() @@ -15,22 +18,33 @@ # Fallback image URL when no backdrop is found FALLBACK_IMAGE_URL = "https://placehold.co/500x281/808080/FFFFFF/png?text=No+Image" +# Available movie categories and their methods +MOVIE_CATEGORIES = { + "popular": movie_api.popular, + "top_rated": movie_api.top_rated, + "now_playing": movie_api.now_playing, + "upcoming": movie_api.upcoming, +} -def get_random_movie() -> Movie: - """Get a random movie from TMDB popular movies. - Examples: - >>> movie = get_random_movie() - >>> isinstance(movie, Movie) - True +def get_random_movie(category: str = "popular") -> Movie: + """Get a random movie from specified TMDB category. + + Args: + category: The category to select from (default: "popular") + Options: "popular", "top_rated", "now_playing", "upcoming" Returns: - A Movie object representing a randomly selected popular movie from TMDB. + A Movie object representing a randomly selected movie from the specified category. """ - popular = movie_api.popular() - return random.choice(popular) + # Get the category method or default to popular if invalid + category_method = MOVIE_CATEGORIES.get(category, MOVIE_CATEGORIES["popular"]) + # Get movies from the category + movies = category_method() + return random.choice(movies) +@timing_decorator def get_movie_posters(movie_id: int) -> list[str]: """Get all available posters for a movie. @@ -51,6 +65,7 @@ def get_movie_posters(movie_id: int) -> list[str]: return [img.file_path for img in images.posters] +@timing_decorator def get_movie_backdrops(movie_id: int) -> list[str]: """Get all available backdrops for a movie. @@ -71,8 +86,9 @@ def get_movie_backdrops(movie_id: int) -> list[str]: return [img.file_path for img in images.backdrops] +@timing_decorator def fuzzy_search_movies( - query: str, threshold: int = 60, limit: int = 5 + query: str, threshold: int = 60, limit: int = 5, include_backdrops: bool = True ) -> list[dict] | None: """Search movies with fuzzy matching. @@ -87,6 +103,7 @@ def fuzzy_search_movies( query: The search term to look for. threshold: Minimum similarity score (0-100) for fuzzy matching. limit: Maximum number of results to return. + include_backdrops: Whether to include backdrop images in results (default: True). Returns: A list of movie dictionaries that match the search criteria, or None if no matches found. @@ -97,19 +114,21 @@ def fuzzy_search_movies( # Apply fuzzy matching fuzzy_matches = [] for result in results: - title = result.title + # Safely get title, skip if not a string + title = getattr(result, "title", None) + if not isinstance(title, str): + logger.warning(f"Invalid title type for movie: {type(title)}") + continue + # Calculate similarity ratio ratio = fuzz.ratio(query.lower(), title.lower()) if ratio >= threshold: - # Get first backdrop or None - backdrops = get_movie_backdrops(result.id) - - backdrop_image_url = ( - f"{TMDB_IMG_BASE_PATH}{backdrops[0]}" - if backdrops - else FALLBACK_IMAGE_URL - ) + backdrop_image_url = FALLBACK_IMAGE_URL + if include_backdrops is True: + backdrops = get_movie_backdrops(result.id) + if backdrops: + backdrop_image_url = f"{TMDB_IMG_BASE_PATH}{backdrops[0]}" fuzzy_matches.append( { @@ -131,15 +150,38 @@ def fuzzy_search_movies( return sorted_matches[:limit] -def get_random_movie_with_details() -> dict: - """Get a random movie with all its details including backdrops. +@timing_decorator +def get_random_movie_with_details( + min_backdrops: int = 5, + category: str = "popular", + depth: int = 0, + max_depth: int = 5, +) -> dict: + """Get a random movie with at least specified number of backdrops. + + Args: + min_backdrops: Minimum number of backdrops required (default: 5) + category: The category to select from (default: "popular") + Options: "popular", "top_rated", "now_playing", "upcoming" + depth: Current recursion depth (default: 0) Returns: A dictionary containing movie details including title, backdrops, etc. """ - movie = get_random_movie() + movie = get_random_movie(category) backdrops = get_movie_backdrops(movie.id) + # Recursively try another movie if this one doesn't have enough backdrops + if len(backdrops) < min_backdrops: + logger.debug( + f"Movie {movie.title} has {len(backdrops)} backdrops, trying another..." + ) + return ( + get_random_movie_with_details(min_backdrops, category, depth + 1) + if depth < max_depth + else {"error": "Max recursion depth reached"} + ) + return { "id": movie.id, "title": movie.title, diff --git a/configs/logger/dummy_log_config.json b/configs/logger/dummy_log_config.json new file mode 100644 index 0000000..9cef848 --- /dev/null +++ b/configs/logger/dummy_log_config.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "disable_existing_loggers": false +} diff --git a/pyproject.toml b/pyproject.toml index ec991ba..895e248 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,8 @@ classifiers = [ ] dependencies = [ - "python-fasthtml>=0.10.1", + "loguru>=0.7.3", + "python-fasthtml>=0.12.0", "thefuzz[speedup]>=0.22.1", "tinyredis>=0.0.2", "tmdbv3api>=1.9.0", diff --git a/requirements.txt b/requirements.txt index 3751450..2474487 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,27 @@ # This file was autogenerated by uv via the following command: # uv export --frozen --no-hashes --no-group dev --no-group doc --no-group test --format requirements-txt --output-file requirements.txt anyio==4.7.0 +apsw==3.48.0.0 +apswutils==0.0.2 beautifulsoup4==4.12.3 certifi==2024.8.30 charset-normalizer==3.4.0 click==8.1.7 -colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32' +colorama==0.4.6 ; sys_platform == 'win32' fastcore==1.7.27 -fastlite==0.0.13 +fastlite==0.1.1 h11==0.14.0 httpcore==1.0.7 httptools==0.6.4 httpx==0.28.1 idna==3.10 itsdangerous==2.2.0 +loguru==0.7.3 oauthlib==3.2.2 packaging==24.2 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -python-fasthtml==0.10.1 +python-fasthtml==0.12.0 python-multipart==0.0.19 pyyaml==6.0.2 rapidfuzz==3.10.1 @@ -27,7 +30,6 @@ requests==2.32.3 six==1.17.0 sniffio==1.3.1 soupsieve==2.6 -sqlite-minutils==4.0.3 starlette==0.42.0 thefuzz==0.22.1 tinyredis==0.0.2 @@ -38,3 +40,4 @@ uvicorn==0.33.0 uvloop==0.21.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' watchfiles==1.0.3 websockets==14.1 +win32-setctime==1.2.0 ; sys_platform == 'win32' diff --git a/uv.lock b/uv.lock index f9b3fe4..1a1619b 100644 --- a/uv.lock +++ b/uv.lock @@ -24,6 +24,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, ] +[[package]] +name = "apsw" +version = "3.48.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/db/06485deedcac623addcfd13dceee54904d5477e66b406b0bf3f0418701c2/apsw-3.48.0.0.tar.gz", hash = "sha256:7c4492a55bd5c9f63821edd0162d6177f383b4733cfe421bd3bde5151e80c49b", size = 1037214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/e4/3925f809d560b1bd2d0eb3d86b2444322077a0ef2a2c5574f71862e4c853/apsw-3.48.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f9e0010654a5c777105528394078d77128126657ca250af29cc6418fe4fbdccd", size = 1835648 }, + { url = "https://files.pythonhosted.org/packages/f0/64/b9c88b89936d4fdfcc0266981d4af92080f43f5809a06b39fa205b3c9295/apsw-3.48.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1045520cb9c113cc3c1328896c58522b45f622dd66bce81eb8eaf8d671eb16d7", size = 1773007 }, + { url = "https://files.pythonhosted.org/packages/c6/6d/3a6cd9c2167ce99c3a64285802007a15140437d34e4f848b5839d43484cc/apsw-3.48.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0bb77cb8a62089cba2101d84fe6052f6ece6e5f28cc8c724d9b332d4f2e1993", size = 6518416 }, + { url = "https://files.pythonhosted.org/packages/8e/1c/02caa68f1213ff765b5bfb3a8af365236720436939f564a49b04336ebdc8/apsw-3.48.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b1590b66a419984898796e1ba0251f81fbdf97ab4f29d7a427648dd0bb89a7", size = 6435935 }, + { url = "https://files.pythonhosted.org/packages/d2/b4/d86e1ac821540fe2cd4010a610e121e11bd04373466cd4c854c260729dcb/apsw-3.48.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5389511068e4977179a4978d92df0dd943a28635ba5e289b895387adc436184", size = 6387775 }, + { url = "https://files.pythonhosted.org/packages/ab/91/c45d24e413428b45d089bb8736e7e8c941d64ffd32f5d7b7d2d493fc9970/apsw-3.48.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:45670822d98d3545f63203b1db2efde5eb08380989d8c61506e43207badf1be6", size = 6533327 }, + { url = "https://files.pythonhosted.org/packages/4d/fe/f6a5e890b99c42e040ffa54e7d8f4934c621f534a7cd67763c7df1728ab1/apsw-3.48.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b3b402ea4dd4e8943129ca98f7a4167341fd748525c74f6cd6310d247e3e3ca", size = 6436618 }, + { url = "https://files.pythonhosted.org/packages/1f/ed/53eee29c9c702419ca104ed67f4f7d8439e262d5340a081f5102597844fa/apsw-3.48.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bdcf0e772adbd69eeafa7f6dab5635623cf171282608b6e9354eb076cfb4afe", size = 6417649 }, + { url = "https://files.pythonhosted.org/packages/20/b4/ba348c577e335326fadd2752e21eb5f71e007f428cfcfbd98ae431d5623f/apsw-3.48.0.0-cp312-cp312-win32.whl", hash = "sha256:3bfc2ee0f930d994848993c10bb5973b50b8542d07d0e479ab6665ea609a06fb", size = 1494385 }, + { url = "https://files.pythonhosted.org/packages/3c/01/99deccb56796a31cfcc2bf480e764d3a165faa992d5da4880bec2a896d82/apsw-3.48.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0fa43d5624bfcec017ee87d2d296d0bcead9396afcfff2314dfa516b82d8274", size = 1653735 }, +] + +[[package]] +name = "apswutils" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apsw" }, + { name = "fastcore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/55/b18acc76ecb52b9703a89a17a8646ea6aa28355e1358420b07edace93884/apswutils-0.0.2.tar.gz", hash = "sha256:146b3d1f18d08551d2a0eb8f0b7325c2904978e42105284434c667428f98356c", size = 50854 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/e5/2c36e0e7b2c79fdc6ae40dcad3790b420a3bd2a88ae5bccbba3e67cb904d/apswutils-0.0.2-py3-none-any.whl", hash = "sha256:8f98661f7110868fe509ebc5241ec01a9ea33dbce22c284717cded5402c4b864", size = 80452 }, +] + [[package]] name = "asttokens" version = "3.0.0" @@ -123,7 +154,7 @@ name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ @@ -232,15 +263,15 @@ wheels = [ [[package]] name = "fastlite" -version = "0.0.13" +version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "apswutils" }, { name = "fastcore" }, - { name = "sqlite-minutils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/86/c6392b7d6fbfa3f5eb22054645ec7be550a21bfb00ce9586ba2bc18e12d2/fastlite-0.0.13.tar.gz", hash = "sha256:e3039dd3ef144953691ea93ed902e46885a7d904feae92a11c835196693ad370", size = 20596 } +sdist = { url = "https://files.pythonhosted.org/packages/15/d6/0a6dc989095fc973e9d97f61c2d637042a59b53e6ae4bab23c68dfbfbcd8/fastlite-0.1.1.tar.gz", hash = "sha256:cbbbc70b3a58189416627a5eaa8f3f88c8c93fa2262e151c1be5705d657177c8", size = 20888 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/b4/0d9e28d1de34378033ce6305f531c2e9dd4adc00915d57f83d1acfa805d7/fastlite-0.0.13-py3-none-any.whl", hash = "sha256:8a8fac42ff71cdebd03272a8a2d8564c266dda1510b8d3e2dfaeae8a69c3e834", size = 16464 }, + { url = "https://files.pythonhosted.org/packages/1d/cc/e0997edb370cadd4bb2e211da368ed6c4fa5cfe9d983cf38becc3e637eb9/fastlite-0.1.1-py3-none-any.whl", hash = "sha256:4e1988d9dc720a97f9717999b67a6ff45c0e3e323cf53af48e45cb9ac91b7e5a", size = 16644 }, ] [[package]] @@ -360,7 +391,7 @@ name = "ipykernel" version = "6.29.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appnope", marker = "platform_system == 'Darwin'" }, + { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython" }, @@ -462,6 +493,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 }, ] +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, +] + [[package]] name = "markdown" version = "3.7" @@ -516,7 +560,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, @@ -636,6 +680,7 @@ name = "movie-guess" version = "0.0.1" source = { virtual = "." } dependencies = [ + { name = "loguru" }, { name = "python-fasthtml" }, { name = "thefuzz" }, { name = "tinyredis" }, @@ -663,7 +708,8 @@ test = [ [package.metadata] requires-dist = [ - { name = "python-fasthtml", specifier = ">=0.10.1" }, + { name = "loguru", specifier = ">=0.7.3" }, + { name = "python-fasthtml", specifier = ">=0.12.0" }, { name = "thefuzz", extras = ["speedup"], specifier = ">=0.22.1" }, { name = "tinyredis", specifier = ">=0.0.2" }, { name = "tmdbv3api", specifier = ">=1.9.0" }, @@ -977,7 +1023,7 @@ wheels = [ [[package]] name = "python-fasthtml" -version = "0.10.1" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -991,9 +1037,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/04/01b730f51d657cfc0ed252e4378b1fda3762662a5cabc1fdb392d2c134a6/python-fasthtml-0.10.1.tar.gz", hash = "sha256:1e635394f93a192d7c1d360cb24ce815f5d10b93a3567ebc8c4076bed21a162d", size = 55508 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/56/333adb8b47f269e4cb65e5c0c1a5cf2d6c80ca8a8681aaef179682492cb3/python_fasthtml-0.12.0.tar.gz", hash = "sha256:b3caa830dc91796c356fbe282eaca858adba67d63f08543ec1a03a3506bd3505", size = 56424 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/d4/c416632e7189b85a2cb0a81a845c8de1c4178bc64151a0d7c78698167229/python_fasthtml-0.10.1-py3-none-any.whl", hash = "sha256:93e75bc540c226ffec559ba69c92fb78b6d62ee45842787fdb71e455c4d88c3f", size = 58846 }, + { url = "https://files.pythonhosted.org/packages/5c/69/b4015f83d1609d9fd08e2b39417f06bb359ceaa72b839bbf82f56668adf1/python_fasthtml-0.12.0-py3-none-any.whl", hash = "sha256:0522e76c2f30b9da409c3bea69e753110e1255a9c45263e483175fd08b4a2aae", size = 59593 }, ] [[package]] @@ -1189,18 +1235,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, ] -[[package]] -name = "sqlite-minutils" -version = "4.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastcore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/de/25926f1caafee46ee4a7f617ab624652e6c44ba8b2351399c626b10553d4/sqlite-minutils-4.0.3.tar.gz", hash = "sha256:c3fc6d20f86b2cd0071eaa9e356024ef83aba3ce9d9606e50c4ab255c3279a71", size = 73036 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/12/78e39e4734bb8770955a6ec7f5dc1a918d56c8f94a1eaa2c7fb91f2fec2a/sqlite_minutils-4.0.3-py3-none-any.whl", hash = "sha256:33254c37d88b8dfd6218ba0d7c4d64232a2bf234e15b841459c5676dea2b2db0", size = 79288 }, -] - [[package]] name = "stack-data" version = "0.6.3" @@ -1434,3 +1468,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, ] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, +]