forked from Boria138/PortProtonQt
perf(search): implement full async + indexed search system with major performance gains
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@@ -6,7 +6,7 @@ arch=('any')
|
|||||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||||
license=('GPL-3.0')
|
license=('GPL-3.0')
|
||||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
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'})
|
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ arch=('any')
|
|||||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||||
license=('GPL-3.0')
|
license=('GPL-3.0')
|
||||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
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'})
|
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ Requires: python3-tqdm
|
|||||||
Requires: python3-vdf
|
Requires: python3-vdf
|
||||||
Requires: python3-pefile
|
Requires: python3-pefile
|
||||||
Requires: python3-pillow
|
Requires: python3-pillow
|
||||||
|
Requires: python3-beautifulsoup4
|
||||||
|
Requires: python3-rapidfuzz
|
||||||
Requires: perl-Image-ExifTool
|
Requires: perl-Image-ExifTool
|
||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
Requires: python3-beautifulsoup4
|
|
||||||
Requires: cabextract
|
Requires: cabextract
|
||||||
Requires: gzip
|
Requires: gzip
|
||||||
Requires: unzip
|
Requires: unzip
|
||||||
|
|||||||
@@ -41,9 +41,10 @@ Requires: python3-tqdm
|
|||||||
Requires: python3-vdf
|
Requires: python3-vdf
|
||||||
Requires: python3-pefile
|
Requires: python3-pefile
|
||||||
Requires: python3-pillow
|
Requires: python3-pillow
|
||||||
|
Requires: python3-beautifulsoup4
|
||||||
|
Requires: python3-rapidfuzz
|
||||||
Requires: perl-Image-ExifTool
|
Requires: perl-Image-ExifTool
|
||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
Requires: python3-beautifulsoup4
|
|
||||||
Requires: cabextract
|
Requires: cabextract
|
||||||
Requires: gzip
|
Requires: gzip
|
||||||
Requires: unzip
|
Requires: unzip
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
from portprotonqt.game_card import GameCard
|
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.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller
|
||||||
from PySide6.QtCore import Qt, QTimer
|
from PySide6.QtCore import Qt, QTimer
|
||||||
from portprotonqt.custom_widgets import FlowLayout
|
from portprotonqt.custom_widgets import FlowLayout
|
||||||
@@ -56,6 +57,9 @@ class GameLibraryManager:
|
|||||||
self.pending_deletions = deque()
|
self.pending_deletions = deque()
|
||||||
self.is_filtering = False
|
self.is_filtering = False
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
|
# Initialize search optimizer
|
||||||
|
self.search_optimizer = SearchOptimizer()
|
||||||
|
self.search_thread: ThreadedSearch | None = None
|
||||||
|
|
||||||
def create_games_library_widget(self):
|
def create_games_library_widget(self):
|
||||||
"""Creates the games library widget with search, grid, and slider."""
|
"""Creates the games library widget with search, grid, and slider."""
|
||||||
@@ -212,6 +216,10 @@ class GameLibraryManager:
|
|||||||
if games_list is not None:
|
if games_list is not None:
|
||||||
self.filtered_games = games_list
|
self.filtered_games = games_list
|
||||||
self.dirty = True # Full rebuild only for non-filter
|
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.is_filtering = is_filter
|
||||||
self._pending_update = True
|
self._pending_update = True
|
||||||
|
|
||||||
@@ -242,8 +250,9 @@ class GameLibraryManager:
|
|||||||
search_text = self.main_window.searchEdit.text().strip().lower()
|
search_text = self.main_window.searchEdit.text().strip().lower()
|
||||||
|
|
||||||
if self.is_filtering:
|
if self.is_filtering:
|
||||||
# Filter mode: do not change layout, only hide/show cards
|
# Filter mode: use the pre-computed filtered_games from optimized search
|
||||||
self._apply_filter_visibility(search_text)
|
# This means we already have the exact games to show
|
||||||
|
self._update_search_results()
|
||||||
else:
|
else:
|
||||||
# Full update: sorting, removal/addition, reorganization
|
# Full update: sorting, removal/addition, reorganization
|
||||||
games_list = self.filtered_games if self.filtered_games else self.games
|
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
|
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):
|
def _apply_filter_visibility(self, search_text: str):
|
||||||
"""Applies visibility to cards based on search, without changing the layout."""
|
"""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
|
visible_count = 0
|
||||||
for game_key, card in self.game_card_cache.items():
|
for game_key, card in self.game_card_cache.items():
|
||||||
game_name = card.name # Assume GameCard has 'name' attribute
|
game_name = card.name # Assume GameCard has 'name' attribute
|
||||||
@@ -446,9 +520,38 @@ class GameLibraryManager:
|
|||||||
"""Sets the games list and updates the filtered games."""
|
"""Sets the games list and updates the filtered games."""
|
||||||
self.games = games
|
self.games = games
|
||||||
self.filtered_games = self.games
|
self.filtered_games = self.games
|
||||||
|
|
||||||
|
# Build search indices for fast searching
|
||||||
|
self._build_search_indices(games)
|
||||||
|
|
||||||
self.dirty = True # Full resort needed
|
self.dirty = True # Full resort needed
|
||||||
self.update_game_grid()
|
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):
|
def add_game_incremental(self, game_data: tuple):
|
||||||
"""Add a single game without full reload."""
|
"""Add a single game without full reload."""
|
||||||
self.games.append(game_data)
|
self.games.append(game_data)
|
||||||
@@ -472,4 +575,54 @@ class GameLibraryManager:
|
|||||||
|
|
||||||
def filter_games_delayed(self):
|
def filter_games_delayed(self):
|
||||||
"""Filters games based on search text and updates the grid."""
|
"""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)
|
||||||
|
|||||||
@@ -998,7 +998,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.searchEdit.textChanged.connect(self.startSearchDebounce)
|
self.searchEdit.textChanged.connect(self.startSearchDebounce)
|
||||||
self.searchDebounceTimer = QTimer(self)
|
self.searchDebounceTimer = QTimer(self)
|
||||||
self.searchDebounceTimer.setSingleShot(True)
|
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)
|
self.searchDebounceTimer.timeout.connect(self.on_search_changed)
|
||||||
|
|
||||||
layout.addWidget(self.searchEdit)
|
layout.addWidget(self.searchEdit)
|
||||||
|
|||||||
379
portprotonqt/search_utils.py
Normal file
379
portprotonqt/search_utils.py
Normal file
@@ -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
|
||||||
@@ -36,6 +36,7 @@ dependencies = [
|
|||||||
"psutil>=7.1.3",
|
"psutil>=7.1.3",
|
||||||
"pyside6>=6.10.1",
|
"pyside6>=6.10.1",
|
||||||
"pyudev>=0.24.4",
|
"pyudev>=0.24.4",
|
||||||
|
"rapidfuzz>=3.14.3",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"tqdm>=4.67.1",
|
"tqdm>=4.67.1",
|
||||||
"vdf>=3.4",
|
"vdf>=3.4",
|
||||||
|
|||||||
92
uv.lock
generated
92
uv.lock
generated
@@ -566,6 +566,7 @@ dependencies = [
|
|||||||
{ name = "psutil" },
|
{ name = "psutil" },
|
||||||
{ name = "pyside6" },
|
{ name = "pyside6" },
|
||||||
{ name = "pyudev" },
|
{ name = "pyudev" },
|
||||||
|
{ name = "rapidfuzz" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "vdf" },
|
{ name = "vdf" },
|
||||||
@@ -591,6 +592,7 @@ requires-dist = [
|
|||||||
{ name = "psutil", specifier = ">=7.1.3" },
|
{ name = "psutil", specifier = ">=7.1.3" },
|
||||||
{ name = "pyside6", specifier = ">=6.10.1" },
|
{ name = "pyside6", specifier = ">=6.10.1" },
|
||||||
{ name = "pyudev", specifier = ">=0.24.4" },
|
{ name = "pyudev", specifier = ">=0.24.4" },
|
||||||
|
{ name = "rapidfuzz", specifier = ">=3.14.3" },
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
{ name = "tqdm", specifier = ">=4.67.1" },
|
{ name = "tqdm", specifier = ">=4.67.1" },
|
||||||
{ name = "vdf", specifier = ">=3.4" },
|
{ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user