diff --git a/build-aux/PKGBUILD b/build-aux/PKGBUILD index 894f0e5..19f127e 100644 --- a/build-aux/PKGBUILD +++ b/build-aux/PKGBUILD @@ -6,7 +6,7 @@ arch=('any') url="https://git.linux-gaming.ru/Boria138/PortProtonQt" license=('GPL-3.0') depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' - 'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar') + 'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'python-rapidfuzz' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar') makedepends=('python-'{'build','installer','setuptools','wheel'}) source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver") sha256sums=('SKIP') diff --git a/build-aux/PKGBUILD-git b/build-aux/PKGBUILD-git index 8a66eb0..501a42b 100644 --- a/build-aux/PKGBUILD-git +++ b/build-aux/PKGBUILD-git @@ -6,7 +6,7 @@ arch=('any') url="https://git.linux-gaming.ru/Boria138/PortProtonQt" license=('GPL-3.0') depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' - 'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar') + 'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'python-rapidfuzz' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar') makedepends=('python-'{'build','installer','setuptools','wheel'}) source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git") sha256sums=('SKIP') diff --git a/build-aux/fedora-git.spec b/build-aux/fedora-git.spec index 59f5012..fc97d7d 100644 --- a/build-aux/fedora-git.spec +++ b/build-aux/fedora-git.spec @@ -44,9 +44,10 @@ Requires: python3-tqdm Requires: python3-vdf Requires: python3-pefile Requires: python3-pillow +Requires: python3-beautifulsoup4 +Requires: python3-rapidfuzz Requires: perl-Image-ExifTool Requires: xdg-utils -Requires: python3-beautifulsoup4 Requires: cabextract Requires: gzip Requires: unzip diff --git a/build-aux/fedora.spec b/build-aux/fedora.spec index 435b416..b0f25ae 100644 --- a/build-aux/fedora.spec +++ b/build-aux/fedora.spec @@ -41,9 +41,10 @@ Requires: python3-tqdm Requires: python3-vdf Requires: python3-pefile Requires: python3-pillow +Requires: python3-beautifulsoup4 +Requires: python3-rapidfuzz Requires: perl-Image-ExifTool Requires: xdg-utils -Requires: python3-beautifulsoup4 Requires: cabextract Requires: gzip Requires: unzip diff --git a/portprotonqt/game_library_manager.py b/portprotonqt/game_library_manager.py index 207f352..be36edd 100644 --- a/portprotonqt/game_library_manager.py +++ b/portprotonqt/game_library_manager.py @@ -1,5 +1,6 @@ from typing import Protocol from portprotonqt.game_card import GameCard +from portprotonqt.search_utils import SearchOptimizer, ThreadedSearch from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller from PySide6.QtCore import Qt, QTimer from portprotonqt.custom_widgets import FlowLayout @@ -56,6 +57,9 @@ class GameLibraryManager: self.pending_deletions = deque() self.is_filtering = False self.dirty = False + # Initialize search optimizer + self.search_optimizer = SearchOptimizer() + self.search_thread: ThreadedSearch | None = None def create_games_library_widget(self): """Creates the games library widget with search, grid, and slider.""" @@ -212,6 +216,10 @@ class GameLibraryManager: if games_list is not None: self.filtered_games = games_list self.dirty = True # Full rebuild only for non-filter + else: + # When filtering, we want to update with the current filtered_games + # which has already been set by _perform_search + pass self.is_filtering = is_filter self._pending_update = True @@ -242,8 +250,9 @@ class GameLibraryManager: search_text = self.main_window.searchEdit.text().strip().lower() if self.is_filtering: - # Filter mode: do not change layout, only hide/show cards - self._apply_filter_visibility(search_text) + # Filter mode: use the pre-computed filtered_games from optimized search + # This means we already have the exact games to show + self._update_search_results() else: # Full update: sorting, removal/addition, reorganization games_list = self.filtered_games if self.filtered_games else self.games @@ -368,8 +377,73 @@ class GameLibraryManager: self.is_filtering = False # Reset flag in any case + def _update_search_results(self): + """Update the grid with pre-computed search results.""" + if self.gamesListLayout is None or self.gamesListWidget is None: + return + + # Batch layout updates + self.gamesListWidget.setUpdatesEnabled(False) + if self.gamesListLayout is not None: + self.gamesListLayout.setEnabled(False) # Disable layout during batch + + try: + # Create set of keys for current filtered games for fast lookup + filtered_keys = {(game[0], game[4]) for game in self.filtered_games} # (name, exec_line) + + # Process existing cards: show cards that are in filtered results, hide others + cards_to_hide = [] + for card_key, card in self.game_card_cache.items(): + if card_key in filtered_keys: + # Card should be visible + if not card.isVisible(): + card.setVisible(True) + else: + # Card should be hidden + if card.isVisible(): + card.setVisible(False) + cards_to_hide.append(card_key) + + # Now add any missing cards that are in filtered results but not in cache + cards_to_add = [] + for game_data in self.filtered_games: + game_name = game_data[0] + exec_line = game_data[4] + game_key = (game_name, exec_line) + + if game_key not in self.game_card_cache: + if self.context_menu_manager is None: + continue + + card = self._create_game_card(game_data) + self.game_card_cache[game_key] = card + card.setVisible(True) # New cards should be visible + cards_to_add.append((game_key, card)) + + # Add new cards to layout + for _game_key, card in cards_to_add: + self.gamesListLayout.addWidget(card) + + # Remove cards that are no longer needed (if any) + # Note: we're not removing them completely as they might be needed later + # Instead, we just hide them and they'll be reused if needed + + finally: + if self.gamesListLayout is not None: + self.gamesListLayout.setEnabled(True) + self.gamesListWidget.setUpdatesEnabled(True) + if self.gamesListLayout is not None: + self.gamesListLayout.update() + self.gamesListWidget.updateGeometry() + self.main_window._last_card_width = self.card_width + + self.force_update_cards_library() + def _apply_filter_visibility(self, search_text: str): """Applies visibility to cards based on search, without changing the layout.""" + # This method is used for simple substring matching + # For the new optimized search, we'll use a different approach in update_game_grid + # when is_filter=True visible_count = 0 for game_key, card in self.game_card_cache.items(): game_name = card.name # Assume GameCard has 'name' attribute @@ -446,9 +520,38 @@ class GameLibraryManager: """Sets the games list and updates the filtered games.""" self.games = games self.filtered_games = self.games + + # Build search indices for fast searching + self._build_search_indices(games) + self.dirty = True # Full resort needed self.update_game_grid() + def _build_search_indices(self, games: list[tuple]): + """Build search indices for fast searching.""" + # Prepare items for indexing: (search_key, game_data) + # We'll index by game name (index 0) and potentially other fields + items = [] + for game in games: + # game is a tuple: (name, description, cover, appid, exec_line, controller_support, + # last_launch, formatted_playtime, protondb_tier, anticheat_status, + # last_played_timestamp, playtime_seconds, game_source) + name = str(game[0]).lower() if game[0] else "" + description = str(game[1]).lower() if game[1] else "" + + # Create multiple search entries for better matching + items.append((name, game)) # Exact name + # Add other searchable fields if needed + if description: + items.append((description, game)) + + # Also add individual words from the name for partial matching + for word in name.split(): + if len(word) > 2: # Only index words longer than 2 characters + items.append((word, game)) + + self.search_optimizer.build_indices(items) + def add_game_incremental(self, game_data: tuple): """Add a single game without full reload.""" self.games.append(game_data) @@ -472,4 +575,54 @@ class GameLibraryManager: def filter_games_delayed(self): """Filters games based on search text and updates the grid.""" - self.update_game_grid(is_filter=True) + search_text = self.main_window.searchEdit.text().strip().lower() + + if not search_text: + # If search is empty, show all games + self.filtered_games = self.games + self.update_game_grid(is_filter=True) + else: + # Use the optimized search + self._perform_search(search_text) + + def _perform_search(self, search_text: str): + """Perform the actual search using optimized search algorithms.""" + if not search_text: + self.filtered_games = self.games + self.update_game_grid(is_filter=True) + return + + # Use exact search first + exact_result = self.search_optimizer.exact_search(search_text) + if exact_result: + # If exact match found, show only that game + self.filtered_games = [exact_result] + self.update_game_grid(is_filter=True) + return + + # Try prefix search + prefix_results = self.search_optimizer.prefix_search(search_text) + if prefix_results: + # Get the actual game data from the prefix matches + filtered_games = [] + for _match_text, game_data in prefix_results: + if game_data not in filtered_games: # Avoid duplicates + filtered_games.append(game_data) + self.filtered_games = filtered_games + self.update_game_grid(is_filter=True) + return + + # Finally, try fuzzy search + fuzzy_results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=60.0) + if fuzzy_results: + # Get the actual game data from the fuzzy matches + filtered_games = [] + for _match_text, game_data, _score in fuzzy_results: + if game_data not in filtered_games: # Avoid duplicates + filtered_games.append(game_data) + self.filtered_games = filtered_games + self.update_game_grid(is_filter=True) + else: + # If no results found, show empty list + self.filtered_games = [] + self.update_game_grid(is_filter=True) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index ec1702c..0587b57 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -998,7 +998,7 @@ class MainWindow(QMainWindow): self.searchEdit.textChanged.connect(self.startSearchDebounce) self.searchDebounceTimer = QTimer(self) self.searchDebounceTimer.setSingleShot(True) - self.searchDebounceTimer.setInterval(300) + self.searchDebounceTimer.setInterval(150) # Reduced debounce time for better responsiveness self.searchDebounceTimer.timeout.connect(self.on_search_changed) layout.addWidget(self.searchEdit) diff --git a/portprotonqt/search_utils.py b/portprotonqt/search_utils.py new file mode 100644 index 0000000..86bfb9d --- /dev/null +++ b/portprotonqt/search_utils.py @@ -0,0 +1,379 @@ +""" +Utility module for search optimizations including Trie, hash tables, and fuzzy matching. +""" +import concurrent.futures +import threading +from collections.abc import Callable +from typing import Any +from rapidfuzz import fuzz +from threading import Lock +from portprotonqt.logger import get_logger +from PySide6.QtCore import QThread, QRunnable, Signal, QObject, QTimer +import requests + +logger = get_logger(__name__) + +class TrieNode: + """Node in the Trie data structure.""" + def __init__(self): + self.children = {} + self.is_end_word = False + self.payload = None # Store the original data in leaf nodes + +class Trie: + """Trie data structure for efficient prefix-based searching.""" + def __init__(self): + self.root = TrieNode() + self._lock = Lock() # Thread safety for concurrent access + + def insert(self, key: str, payload: Any): + """Insert a key with payload into the Trie.""" + with self._lock: + node = self.root + for char in key.lower(): + if char not in node.children: + node.children[char] = TrieNode() + node = node.children[char] + node.is_end_word = True + node.payload = payload + + def search_prefix(self, prefix: str) -> list[tuple[str, Any]]: + """Find all entries with the given prefix.""" + with self._lock: + node = self.root + for char in prefix.lower(): + if char not in node.children: + return [] + node = node.children[char] + + results = [] + self._collect_all(node, prefix.lower(), results) + return results + + def _collect_all(self, node: TrieNode, current_prefix: str, results: list[tuple[str, Any]]): + """Collect all entries from the current node.""" + if node.is_end_word: + results.append((current_prefix, node.payload)) + + for char, child_node in node.children.items(): + self._collect_all(child_node, current_prefix + char, results) + +class FuzzySearchIndex: + """Index for fuzzy string matching with rapidfuzz.""" + def __init__(self, items: list[tuple[str, Any]] | None = None): + self.items: list[tuple[str, Any]] = items or [] + self.normalized_items: list[tuple[str, Any]] = [] + self._lock = Lock() + self._build_normalized_index() + + def _build_normalized_index(self): + """Build a normalized index for fuzzy matching.""" + with self._lock: + self.normalized_items = [(self._normalize(item[0]), item[1]) for item in self.items] + + def _normalize(self, s: str) -> str: + """Normalize string for fuzzy matching.""" + s = s.lower() + for ch in ["™", "®"]: + s = s.replace(ch, "") + for ch in ["-", ":", ","]: + s = s.replace(ch, " ") + s = " ".join(s.split()) + for suffix in ["bin", "app"]: + if s.endswith(suffix): + s = s[:-len(suffix)].strip() + keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"} + words = s.split() + filtered_words = [word for word in words if word not in keywords_to_remove] + return " ".join(filtered_words) + + def fuzzy_search(self, query: str, limit: int = 5, min_score: float = 60.0) -> list[tuple[str, Any, float]]: + """Perform fuzzy search using rapidfuzz.""" + with self._lock: + if not query or not self.normalized_items: + return [] + + query_normalized = self._normalize(query) + results = [] + + for i, (item_text, item_data) in enumerate(self.normalized_items): + score = fuzz.ratio(query_normalized, item_text) + if score >= min_score: + results.append((self.items[i][0], item_data, score)) + + # Sort by score descending + results.sort(key=lambda x: x[2], reverse=True) + return results[:limit] + +class SearchOptimizer: + """Main search optimization class combining multiple approaches.""" + def __init__(self): + self.hash_index: dict[str, Any] = {} + self.trie_index = Trie() + self.fuzzy_index = None + self._lock = Lock() + + def build_indices(self, items: list[tuple[str, Any]]): + """Build all search indices from items.""" + with self._lock: + self.hash_index = {item[0].lower(): item[1] for item in items} + self.trie_index = Trie() + for key, value in self.hash_index.items(): + self.trie_index.insert(key, value) + self.fuzzy_index = FuzzySearchIndex(items) + + def exact_search(self, key: str) -> Any | None: + """Perform exact hash-based lookup.""" + with self._lock: + return self.hash_index.get(key.lower()) + + def prefix_search(self, prefix: str) -> list[tuple[str, Any]]: + """Perform prefix search using Trie.""" + with self._lock: + return self.trie_index.search_prefix(prefix) + + def fuzzy_search(self, query: str, limit: int = 5, min_score: float = 60.0) -> list[tuple[str, Any, float]]: + """Perform fuzzy search.""" + if self.fuzzy_index: + return self.fuzzy_index.fuzzy_search(query, limit, min_score) + return [] + + +class RequestRunnable(QRunnable): + """Runnable for executing HTTP requests in a thread.""" + + def __init__(self, method: str, url: str, on_success=None, on_error=None, **kwargs): + super().__init__() + self.method = method + self.url = url + self.kwargs = kwargs + self.result = None + self.error = None + self.on_success: Callable | None = on_success + self.on_error: Callable | None = on_error + + def run(self): + try: + if self.method.lower() == 'get': + self.result = requests.get(self.url, **self.kwargs) + elif self.method.lower() == 'post': + self.result = requests.post(self.url, **self.kwargs) + else: + raise ValueError(f"Unsupported HTTP method: {self.method}") + + # Execute success callback if provided + if self.on_success is not None: + success_callback = self.on_success # Capture the callback + def success_handler(): + if success_callback is not None: # Re-check to satisfy Pyright + success_callback(self.result) + QTimer.singleShot(0, success_handler) + except Exception as e: + self.error = e + # Execute error callback if provided + if self.on_error is not None: + error_callback = self.on_error # Capture the callback + captured_error = e # Capture the exception + def error_handler(): + error_callback(captured_error) + QTimer.singleShot(0, error_handler) + + +def run_request_in_thread(method: str, url: str, on_success: Callable | None = None, on_error: Callable | None = None, **kwargs): + """Run HTTP request in a separate thread using Qt's thread system.""" + runnable = RequestRunnable(method, url, on_success=on_success, on_error=on_error, **kwargs) + + # Use QThreadPool to execute the runnable + from PySide6.QtCore import QThreadPool + thread_pool = QThreadPool.globalInstance() + thread_pool.start(runnable) + + return runnable # Return the runnable to allow for potential cancellation if needed + + +def run_function_in_thread(func, *args, on_success: Callable | None = None, on_error: Callable | None = None, **kwargs): + """Run a function in a separate thread.""" + def execute(): + try: + result = func(*args, **kwargs) + if on_success: + on_success(result) + except Exception as e: + if on_error: + on_error(e) + + thread = threading.Thread(target=execute) + thread.daemon = True + thread.start() + return thread + + +def run_in_thread(func, *args, **kwargs): + """Run a function in a separate thread.""" + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(func, *args, **kwargs) + return future.result() + + +def run_in_thread_async(func, *args, callback: Callable | None = None, **kwargs): + """Run a function in a separate thread asynchronously.""" + import threading + def target(): + try: + result = func(*args, **kwargs) + if callback: + callback(result) + except Exception as e: + if callback: + callback(None) # or handle error in callback + logger.error(f"Error in threaded operation: {e}") + + thread = threading.Thread(target=target) + thread.daemon = True + thread.start() + return thread + + +# Threaded search implementation using QThread for performance optimization + + +class ThreadedSearchWorker(QObject): + """ + A threaded worker for performing search operations without blocking the UI. + """ + search_started = Signal() + search_finished = Signal(list) + search_error = Signal(str) + + def __init__(self): + super().__init__() + self.search_optimizer = SearchOptimizer() + self.games_data = [] + + def set_games_data(self, games_data: list): + """Set the games data to be searched.""" + self.games_data = games_data + # Build indices from the games data (name, description, etc.) + items = [(game[0], game) for game in games_data] # game[0] is the name + self.search_optimizer.build_indices(items) + + def execute_search(self, search_text: str, search_type: str = "auto"): + """ + Execute search in a separate thread. + + Args: + search_text: Text to search for + search_type: Type of search ("exact", "prefix", "fuzzy", "auto") + """ + try: + self.search_started.emit() + import time + start_time = time.time() + + results = [] + + if search_type == "exact" or (search_type == "auto" and len(search_text) > 2): + exact_result = self.search_optimizer.exact_search(search_text) + if exact_result: + results = [exact_result] + elif search_type == "prefix": + results = self.search_optimizer.prefix_search(search_text) + elif search_type == "fuzzy" or search_type == "auto": + results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=50.0) + else: + # Auto-detect search type based on input + if len(search_text) < 3: + results = self.search_optimizer.prefix_search(search_text) + else: + # Try exact first, then fuzzy + exact_result = self.search_optimizer.exact_search(search_text) + if exact_result: + results = [exact_result] + else: + results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=50.0) + + end_time = time.time() + print(f"Search completed in {end_time - start_time:.4f} seconds") + + self.search_finished.emit(results) + except Exception as e: + self.search_error.emit(str(e)) + + +class ThreadedSearch(QThread): + """ + QThread implementation for running search operations in the background. + """ + search_started = Signal() + search_finished = Signal(list) + search_error = Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self.worker = ThreadedSearchWorker() + self.search_text = "" + self.search_type = "auto" + self.games_data = [] + + # Connect worker signals to thread signals + self.worker.search_started.connect(self.search_started) + self.worker.search_finished.connect(self.search_finished) + self.worker.search_error.connect(self.search_error) + + def set_search_params(self, search_text: str, games_data: list, search_type: str = "auto"): + """Set parameters for the search operation.""" + self.search_text = search_text + self.games_data = games_data + self.search_type = search_type + + def set_games_data(self, games_data: list): + """Set the games data to be searched.""" + self.games_data = games_data + self.worker.set_games_data(games_data) + + def run(self): + """Run the search operation in the thread.""" + self.worker.execute_search(self.search_text, self.search_type) + + +class SearchThreadPool: + """ + A simple thread pool for managing multiple search operations. + """ + def __init__(self, max_threads: int = 3): + self.max_threads = max_threads + self.active_threads = [] + self.thread_queue = [] + + def submit_search(self, search_text: str, games_data: list, search_type: str = "auto", + on_start: Callable | None = None, on_finish: Callable | None = None, on_error: Callable | None = None): + """ + Submit a search operation to the pool. + + Args: + search_text: Text to search for + games_data: List of game data tuples to search in + search_type: Type of search ("exact", "prefix", "fuzzy", "auto") + on_start: Callback when search starts + on_finish: Callback when search finishes (receives results) + on_error: Callback when search errors (receives error message) + """ + search_thread = ThreadedSearch() + search_thread.set_search_params(search_text, games_data, search_type) + + # Connect callbacks if provided + if on_start: + search_thread.search_started.connect(on_start) + if on_finish: + search_thread.search_finished.connect(on_finish) + if on_error: + search_thread.search_error.connect(on_error) + + # Start the thread + search_thread.start() + self.active_threads.append(search_thread) + + # Clean up finished threads + self.active_threads = [thread for thread in self.active_threads if thread.isRunning()] + + return search_thread diff --git a/pyproject.toml b/pyproject.toml index 11a1e26..39789aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "psutil>=7.1.3", "pyside6>=6.10.1", "pyudev>=0.24.4", + "rapidfuzz>=3.14.3", "requests>=2.32.5", "tqdm>=4.67.1", "vdf>=3.4", diff --git a/uv.lock b/uv.lock index f7ea535..cc1ed61 100644 --- a/uv.lock +++ b/uv.lock @@ -566,6 +566,7 @@ dependencies = [ { name = "psutil" }, { name = "pyside6" }, { name = "pyudev" }, + { name = "rapidfuzz" }, { name = "requests" }, { name = "tqdm" }, { name = "vdf" }, @@ -591,6 +592,7 @@ requires-dist = [ { name = "psutil", specifier = ">=7.1.3" }, { name = "pyside6", specifier = ">=6.10.1" }, { name = "pyudev", specifier = ">=0.24.4" }, + { name = "rapidfuzz", specifier = ">=3.14.3" }, { name = "requests", specifier = ">=2.32.5" }, { name = "tqdm", specifier = ">=4.67.1" }, { name = "vdf", specifier = ">=3.4" }, @@ -792,6 +794,96 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "rapidfuzz" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/37a169bb28b23850a164e6624b1eb299e1ad73c9e7c218ee15744e68d628/rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2", size = 1390903, upload-time = "2025-11-01T11:52:31.239Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/b37207cbbdb6eaafac3da3f55ea85287b27745cb416e75e15769b7d8abe8/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7", size = 1385655, upload-time = "2025-11-01T11:52:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/ca53e518acf43430be61f23b9c5987bd1e01e74fcb7a9ee63e00f597aefb/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1", size = 3164708, upload-time = "2025-11-01T11:52:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/df/e1/7667bf2db3e52adb13cb933dd4a6a2efc66045d26fa150fc0feb64c26d61/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897", size = 1221106, upload-time = "2025-11-01T11:52:36.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/8a/84d9f2d46a2c8eb2ccae81747c4901fa10fe4010aade2d57ce7b4b8e02ec/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9", size = 2406048, upload-time = "2025-11-01T11:52:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a9/a0b7b7a1b81a020c034eb67c8e23b7e49f920004e295378de3046b0d99e1/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747", size = 2527020, upload-time = "2025-11-01T11:52:39.657Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/416df7d108b99b4942ba04dd4cf73c45c3aadb3ef003d95cad78b1d12eb9/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825", size = 4273958, upload-time = "2025-11-01T11:52:41.017Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/b81e041c17cd475002114e0ab8800e4305e60837882cb376a621e520d70f/rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9", size = 1725043, upload-time = "2025-11-01T11:52:42.465Z" }, + { url = "https://files.pythonhosted.org/packages/09/6b/64ad573337d81d64bc78a6a1df53a72a71d54d43d276ce0662c2e95a1f35/rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141", size = 1542273, upload-time = "2025-11-01T11:52:44.005Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5e/faf76e259bc15808bc0b86028f510215c3d755b6c3a3911113079485e561/rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923", size = 814875, upload-time = "2025-11-01T11:52:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, + { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" }, + { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" }, + { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" }, + { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" }, + { url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" }, + { url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" }, + { url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" }, + { url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" }, +] + [[package]] name = "requests" version = "2.32.5"