mirror of
https://github.com/th3r00t/pyShelf.git
synced 2026-04-28 01:59:35 -04:00
Updated ui, better balance.
This commit is contained in:
4
pyproject.toml
vendored
4
pyproject.toml
vendored
@@ -25,7 +25,7 @@ dependencies = [
|
||||
"mobi-python", "lxml", "sqlalchemy", "sqlalchemy.orm", "fastapi[all]",
|
||||
"jinja2", "libsass", "nodejs-bin", "npm", "brotlipy", "debugpy", "pudb",
|
||||
"ptipython", "chardet", "pre-commit", "coverage[toml]>=6.5","pytest",
|
||||
"black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243"
|
||||
"black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243", "rapidfuzz"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -90,4 +90,4 @@ include_trailing_comma = true
|
||||
line_length = 88
|
||||
multi_line_output = 3
|
||||
use_parentheses = true
|
||||
known_third_party = ["backend", "bs4", "django", "interface", "mobi", "prompt_toolkit", "psycopg2-binary", "pyfiglet", "requests"]
|
||||
known_third_party = ["backend", "bs4", "django", "interface", "mobi", "prompt_toolkit", "psycopg2-binary", "pyfiglet", "requests"]
|
||||
|
||||
145
src/backend/lib/storage.py
vendored
145
src/backend/lib/storage.py
vendored
@@ -1,5 +1,7 @@
|
||||
"""Pyshelf's Main Storage Class."""
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from rapidfuzz import process, fuzz
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import Session
|
||||
from pathlib import Path
|
||||
@@ -89,16 +91,16 @@ class Storage:
|
||||
# collections = self.parse_collections_from_path(book)
|
||||
# breakpoint()
|
||||
_book = Book(
|
||||
title=book[0],
|
||||
author=book[1],
|
||||
cover=cover_image,
|
||||
file_name=book[3],
|
||||
description=book[4],
|
||||
identifier=book[5],
|
||||
publisher=book[6],
|
||||
rights=book[8],
|
||||
tags=book[9],
|
||||
)
|
||||
title=book[0],
|
||||
author=book[1],
|
||||
cover=cover_image,
|
||||
file_name=book[3],
|
||||
description=book[4],
|
||||
identifier=book[5],
|
||||
publisher=book[6],
|
||||
rights=book[8],
|
||||
tags=book[9],
|
||||
)
|
||||
session.add(_book)
|
||||
session.commit()
|
||||
session.close()
|
||||
@@ -163,21 +165,15 @@ class Storage:
|
||||
|
||||
# get all books and paths
|
||||
books = session.execute(select(Book.id, Book.file_name)).all()
|
||||
|
||||
|
||||
for book_id, file_name in books:
|
||||
try:
|
||||
relative_parts = Path(file_name).relative_to(self.config.book_path).parts
|
||||
except ValueError:
|
||||
continue # skip books outside the configured path
|
||||
|
||||
# exclude the actual file name
|
||||
folder = relative_parts[1]
|
||||
# for folder in folders:
|
||||
# clean_name = re.sub(r"^[0-9][0-9]*|-|\ \B", "", folder).strip()
|
||||
# if not clean_name:
|
||||
# continue
|
||||
|
||||
# check if collection exists
|
||||
# check if collection exists
|
||||
collection = session.execute(
|
||||
select(Collection).where(Collection.name == folder)
|
||||
).scalar_one_or_none()
|
||||
@@ -201,36 +197,6 @@ class Storage:
|
||||
session.close()
|
||||
self.config.logger.info("Finished making collections.")
|
||||
|
||||
|
||||
|
||||
# def get_books(self, collection=None, skip=None, limit=None):
|
||||
# """Get books from database.
|
||||
#
|
||||
# Parameters
|
||||
# ----------
|
||||
# collection : str
|
||||
# Collection to filter by.
|
||||
#
|
||||
# Returns
|
||||
# -------
|
||||
# _result : ScalarResult Object
|
||||
# """
|
||||
# session = Session(self.engine)
|
||||
# if collection:
|
||||
# _result = session.execute(
|
||||
# select(Book)
|
||||
# .join(Collection)
|
||||
# # .where(Collection.id == collection)
|
||||
# .where(Collection.name == collection)
|
||||
# .offset(skip)
|
||||
# .limit(limit)
|
||||
# ).all()
|
||||
# else:
|
||||
# _result = session.execute(select(Book).offset(skip).limit(limit)).all()
|
||||
# session.close()
|
||||
# return _result
|
||||
|
||||
|
||||
def get_books(self, collection=None, skip=None, limit=None):
|
||||
"""Get books from database.
|
||||
|
||||
@@ -263,7 +229,6 @@ class Storage:
|
||||
).scalars().all()
|
||||
return result
|
||||
|
||||
|
||||
def get_book(self, id):
|
||||
"""Get book from database.
|
||||
|
||||
@@ -293,11 +258,7 @@ class Storage:
|
||||
select(Collection).join(BookCollection).distinct()
|
||||
).scalars().all()
|
||||
return result
|
||||
# session = Session(self.engine)
|
||||
# _result = session.execute(select(Collection).join(Book)).all()
|
||||
# session.close()
|
||||
# return _result
|
||||
|
||||
|
||||
def get_collection(self, name):
|
||||
"""Get collection from database.
|
||||
|
||||
@@ -309,3 +270,79 @@ class Storage:
|
||||
_result = session.execute(select(Collection).where(Collection.name == name).join(Book)).all()
|
||||
session.close()
|
||||
return _result
|
||||
|
||||
# def fuzzy_search_books(self, query: str, limit: int = 30):
|
||||
# """Fuzzy search for books by title, author, or tags."""
|
||||
# with Session(self.engine) as session:
|
||||
# books = session.execute(select(Book)).scalars().all()
|
||||
#
|
||||
# # Prepare a combined text field
|
||||
# book_choices = {book.id: f"{book.title or ''} {book.author or ''} {book.tags or ''}"
|
||||
# for book in books}
|
||||
#
|
||||
# # Use RapidFuzz to score
|
||||
# results = process.extract(query, book_choices, scorer=fuzz.WRatio, limit=limit)
|
||||
#
|
||||
# # results = [(matched_text, score, book_id), ...]
|
||||
# book_ids = [book_id for (_, score, book_id) in results if score > 50] # threshold
|
||||
# books = [b for b in books if b.id in book_ids]
|
||||
# return books
|
||||
|
||||
|
||||
def parse_advanced_query(self, query: str) -> dict:
|
||||
"""Parse a query like 'title:"dark tower" author:king tags:fantasy'"""
|
||||
fields = ["title", "author", "tags"]
|
||||
tokens = re.findall(r'(\w+:"[^"]+"|\w+:\S+|"[^"]+"|\S+)', query)
|
||||
parsed = defaultdict(list)
|
||||
|
||||
for token in tokens:
|
||||
field_match = re.match(r"(\w+):\"(.+?)\"", token) or re.match(r"(\w+):(\S+)", token)
|
||||
if field_match:
|
||||
field, value = field_match.groups()
|
||||
field = field.lower()
|
||||
if field in fields:
|
||||
parsed[field].append(value.strip('"'))
|
||||
elif token.startswith('"') and token.endswith('"'):
|
||||
parsed["keywords"].append(token.strip('"'))
|
||||
else:
|
||||
parsed["keywords"].append(token)
|
||||
|
||||
return parsed
|
||||
|
||||
def fuzzy_search_books(self, query: str, limit: int = 30):
|
||||
parsed = self.parse_advanced_query(query)
|
||||
|
||||
with Session(self.engine) as session:
|
||||
books = session.execute(select(Book)).scalars().all()
|
||||
|
||||
# Apply field filters
|
||||
def match_field(book, field, values):
|
||||
content = (getattr(book, field) or "").lower()
|
||||
return all(v.lower() in content for v in values)
|
||||
|
||||
filtered = []
|
||||
for book in books:
|
||||
if any(
|
||||
not match_field(book, field, values)
|
||||
for field, values in parsed.items()
|
||||
if field in ("title", "author", "tags")
|
||||
):
|
||||
continue
|
||||
filtered.append(book)
|
||||
|
||||
# Apply fuzzy keyword match if needed
|
||||
if "keywords" in parsed:
|
||||
book_choices = {
|
||||
b.id: f"{b.title or ''} {b.author or ''} {b.tags or ''}" for b in filtered
|
||||
}
|
||||
fuzzy_results = process.extract(
|
||||
" ".join(parsed["keywords"]),
|
||||
book_choices,
|
||||
scorer=fuzz.WRatio,
|
||||
limit=limit
|
||||
)
|
||||
matched_ids = [book_id for _, score, book_id in fuzzy_results if score > 50]
|
||||
return [b for b in filtered if b.id in matched_ids]
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
55
src/frontend/lib/FastAPIServer.py
vendored
55
src/frontend/lib/FastAPIServer.py
vendored
@@ -86,21 +86,21 @@ def books_tojson(obj) -> dumps:
|
||||
def book_tojson(book) -> dumps:
|
||||
"""Convert a book object to a json."""
|
||||
return dumps({
|
||||
"book_id": book.id,
|
||||
"title": book.title,
|
||||
"author": book.author,
|
||||
"categories": book.categories,
|
||||
"cover": base64decode(book.cover),
|
||||
"pages": book.pages,
|
||||
"progress": book.progress,
|
||||
"file_name": book.file_name,
|
||||
"description": book.description,
|
||||
"date": convertDateTime(book.date),
|
||||
"rights": book.rights,
|
||||
"tags": book.tags,
|
||||
"identifier": book.identifier,
|
||||
"publisher": book.publisher,
|
||||
})
|
||||
"book_id": book.id,
|
||||
"title": book.title,
|
||||
"author": book.author,
|
||||
"categories": book.categories,
|
||||
"cover": base64decode(book.cover),
|
||||
"pages": book.pages,
|
||||
"progress": book.progress,
|
||||
"file_name": book.file_name,
|
||||
"description": book.description,
|
||||
"date": convertDateTime(book.date),
|
||||
"rights": book.rights,
|
||||
"tags": book.tags,
|
||||
"identifier": book.identifier,
|
||||
"publisher": book.publisher,
|
||||
})
|
||||
|
||||
def tojson(obj) -> dumps:
|
||||
return dumps(obj)
|
||||
@@ -178,6 +178,11 @@ class FastAPIServer():
|
||||
collections = storage.get_collections()
|
||||
"""Home page responder."""
|
||||
total_books = len(storage.get_books())
|
||||
if skip <= 0:
|
||||
skip_num = 0
|
||||
skip = 0
|
||||
else:
|
||||
skip_num = skip * limit
|
||||
context = {
|
||||
"request": request,
|
||||
"total_pages": math.ceil(total_books / limit),
|
||||
@@ -203,7 +208,7 @@ class FastAPIServer():
|
||||
book = storage.get_book(book_id)
|
||||
"""Home page responder."""
|
||||
return JSONResponse(content=book_tojson(book))
|
||||
|
||||
|
||||
@app.get("/api/get_book/{book_id}", response_class=FileResponse)
|
||||
async def book(request: Request, book_id: int):
|
||||
storage = Storage(Config(os.path.abspath(os.getcwd())))
|
||||
@@ -221,7 +226,7 @@ class FastAPIServer():
|
||||
"""Home page responder."""
|
||||
return JSONResponse(content=collections_tojson(collections))
|
||||
|
||||
@app.get("/api/collection/{collection}", response_class=JSONResponse)
|
||||
@app.get("/api/collection/{collection}", response_class=HTMLResponse)
|
||||
async def collection(request: Request, collection: str, skip:int=0, limit:int=30):
|
||||
"""Collection file responder."""
|
||||
storage = Storage(Config(os.path.abspath(os.getcwd())))
|
||||
@@ -243,6 +248,22 @@ class FastAPIServer():
|
||||
"limit": limit
|
||||
}
|
||||
return templates.TemplateResponse("collection.html", context)
|
||||
|
||||
@app.get("/api/search", response_class=HTMLResponse)
|
||||
async def search_books_api(request: Request, search: str):
|
||||
"""Collection file responder."""
|
||||
storage = Storage(Config(os.path.abspath(os.getcwd())))
|
||||
books = storage.fuzzy_search_books(search)
|
||||
total_books = len(books)
|
||||
collections = storage.get_collections()
|
||||
context = {
|
||||
"request": request,
|
||||
"books": books,
|
||||
"collections": collections,
|
||||
"total_pages": 1,
|
||||
"total_books": total_books,
|
||||
}
|
||||
return templates.TemplateResponse("search.html", context)
|
||||
|
||||
async def run(self):
|
||||
"""Front end server entrypoint."""
|
||||
|
||||
12
src/frontend/static/styles/pyShelf.sass
vendored
12
src/frontend/static/styles/pyShelf.sass
vendored
@@ -55,6 +55,14 @@ $footer-background-color: $ps-color-primary-trans !important
|
||||
margin: 1rem;
|
||||
// background-color: $ps-color-background;
|
||||
|
||||
.is-rounded-left
|
||||
border-top-left-radius: 20px !important;
|
||||
border-bottom-left-radius: 20px !important;
|
||||
|
||||
.is-rounded-right
|
||||
border-top-right-radius: 20px !important;
|
||||
border-bottom-right-radius: 20px !important;
|
||||
|
||||
.book-thumbnail
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
@@ -112,6 +120,10 @@ $footer-background-color: $ps-color-primary-trans !important
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
.search_form
|
||||
margin-top: auto !important;
|
||||
margin-bottom: auto !important;
|
||||
|
||||
.footer
|
||||
display: flex;
|
||||
padding: 1rem !important;
|
||||
|
||||
@@ -1,81 +1,94 @@
|
||||
<script type="text/javascript">
|
||||
const collections = {{ collections|collections_tojson }};
|
||||
const collections = {{ collections|collections_tojson }};
|
||||
</script>
|
||||
<nav id="navbar-main" class="navbar is-fixed-top is-dark" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="https://github.com/th3r00t/pyShelf">
|
||||
<img src="{{ url_for('static', path='images/svg/logo-no-background-no-border.svg') }}" width="112" height="28" />
|
||||
</a>
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="navbarMain" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item">
|
||||
Home
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Ebooks
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Comics
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Documentation
|
||||
</a>
|
||||
<div class="navbar-item dropdown is-hoverable is-right" id="more-menu">
|
||||
<a class="navbar-link">
|
||||
More
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item">
|
||||
About
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Logs
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Contact
|
||||
</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item">
|
||||
Report an issue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select is-small is-rounded is-link" id="collection_dropdown">
|
||||
<!-- <select id="collection_select" onchange="window.location.href='/api/collection/' + this.value"> -->
|
||||
<select id="collection_select">
|
||||
<option value="" disabled selected>Select a collection</option>
|
||||
{% if collections is not defined or collections|length == 0 %}
|
||||
<option value="" disabled>No collections available</option>
|
||||
{% endif %}
|
||||
{% for collection in collections %}
|
||||
<option value="{{collection.id}}" class="collection_selection">{{collection.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<script>
|
||||
document.getElementById("collection_select").addEventListener("change", function() {
|
||||
const value = encodeURIComponent(this.value);
|
||||
window.location.href = `/api/collection/${value}`;
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<a class="button is-primary">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button is-light">
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="https://github.com/th3r00t/pyShelf">
|
||||
<img src="{{ url_for('static', path='images/svg/logo-no-background-no-border.svg') }}" width="112" height="28" />
|
||||
</a>
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="navbarMain" class="navbar-menu">
|
||||
<div class="navbar-start is-flex is-flex-grow-1">
|
||||
<a class="navbar-item" href="/">Home</a>
|
||||
<div class="navbar-item is-flex-grow-1 px-2">
|
||||
<div class="field is-grouped is-fullwidth" style="width: 100%;">
|
||||
<p class="control is-expanded">
|
||||
<span class="select is-small is-rounded is-link is-fullwidth">
|
||||
<select id="collection_select">
|
||||
<option value="" disabled selected>Select a collection</option>
|
||||
{% if collections is not defined or collections|length == 0 %}
|
||||
<option value="" disabled>No collections available</option>
|
||||
{% endif %}
|
||||
{% for collection in collections %}
|
||||
<option value="{{collection.id}}" class="collection_selection">
|
||||
<!-- {{collection.name[:80]}} -->
|
||||
{{collection.name}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item is-flex-grow-1 px-2">
|
||||
<form action="/api/search" method="get" class="field has-addons is-flex-grow-1">
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
class="input is-small is-dark is-link is-rounded-left"
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Search books..." />
|
||||
</div>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button is-small is-dark is-link is-rounded-right"
|
||||
type="submit">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item dropdown is-hoverable is-right" id="more-menu">
|
||||
<a class="navbar-link">
|
||||
More
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item">
|
||||
About
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Logs
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Documentation
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Contact
|
||||
</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item">
|
||||
Report an issue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<a class="button is-primary is-small is-rounded is-dark is-link">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button is-dark is-small is-rounded is-link">
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
45
src/frontend/templates/search.html
Normal file
45
src/frontend/templates/search.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
{% block javascript %}
|
||||
<script type="text/javascript" src={{ url_for('static', path='script/pako.min.js') }}>
|
||||
<script type="text/javascript">
|
||||
const books = {{ books|books_tojson }};
|
||||
let inflatedJSON = {};
|
||||
const pako = require('pako');
|
||||
inflatedJSON = JSON.parse(pako.inflate(books, { to: 'string'}));
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% include 'header.html' %}
|
||||
{% include 'navigation.html' %}
|
||||
<section id="master-container">
|
||||
<div id="book-shelf">
|
||||
{% for book in books %}
|
||||
{% set cover = book.cover|b64decode %}
|
||||
{% if cover != 'None' %}
|
||||
<div class="is-dark book" id="{{book.id}}" onclick="window.location.href='/api/get_book/{{ book.id }}'">
|
||||
<div class="image book-thumbnail">
|
||||
<figure class="image is-4by3">
|
||||
<img src="data:;base64,{{ book.cover|b64decode }}" alt="{{ book.title }}">
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="is-dark book" id="{{book.id}}" onclick="window.location.href='/api/get_book/{{ book.id }}'">
|
||||
<div class="image book-thumbnail"
|
||||
style="
|
||||
background-image: url({{ url_for('static', path='images/no-cover.jpg') }});
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
">
|
||||
<figure class="image is-4by3">
|
||||
<div class="no-image-title">{{ book.title }}</div>
|
||||
<!-- alt="{{ book.title }}" -->
|
||||
</figure>
|
||||
</div>
|
||||
<!-- <p class="content">{{ book.description|summarize }}</p> -->
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% include 'footer.html' %}
|
||||
40
uv.lock
generated
vendored
40
uv.lock
generated
vendored
@@ -1231,6 +1231,7 @@ dependencies = [
|
||||
{ name = "pudb" },
|
||||
{ name = "pypdf2" },
|
||||
{ name = "pytest" },
|
||||
{ name = "rapidfuzz" },
|
||||
{ name = "requests" },
|
||||
{ name = "ruff" },
|
||||
{ name = "sqlalchemy" },
|
||||
@@ -1261,6 +1262,7 @@ requires-dist = [
|
||||
{ name = "pudb" },
|
||||
{ name = "pypdf2" },
|
||||
{ name = "pytest" },
|
||||
{ name = "rapidfuzz" },
|
||||
{ name = "requests" },
|
||||
{ name = "ruff", specifier = ">=0.0.243" },
|
||||
{ name = "sqlalchemy" },
|
||||
@@ -1329,6 +1331,44 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rapidfuzz"
|
||||
version = "3.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501, upload-time = "2025-04-03T20:36:13.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379, upload-time = "2025-04-03T20:36:16.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986, upload-time = "2025-04-03T20:36:18.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809, upload-time = "2025-04-03T20:36:20.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394, upload-time = "2025-04-03T20:36:22.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544, upload-time = "2025-04-03T20:36:24.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796, upload-time = "2025-04-03T20:36:26.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016, upload-time = "2025-04-03T20:36:28.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725, upload-time = "2025-04-03T20:36:30.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052, upload-time = "2025-04-03T20:36:32.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219, upload-time = "2025-04-03T20:36:35.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924, upload-time = "2025-04-03T20:36:37.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915, upload-time = "2025-04-03T20:36:39.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985, upload-time = "2025-04-03T20:36:41.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116, upload-time = "2025-04-03T20:36:43.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/76/606e71e4227790750f1646f3c5c873e18d6cfeb6f9a77b2b8c4dec8f0f66/rapidfuzz-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09e908064d3684c541d312bd4c7b05acb99a2c764f6231bd507d4b4b65226c23", size = 1982282, upload-time = "2025-04-03T20:36:46.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/f5/d0b48c6b902607a59fd5932a54e3518dae8223814db8349b0176e6e9444b/rapidfuzz-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57c390336cb50d5d3bfb0cfe1467478a15733703af61f6dffb14b1cd312a6fae", size = 1439274, upload-time = "2025-04-03T20:36:48.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/cf/c3ac8c80d8ced6c1f99b5d9674d397ce5d0e9d0939d788d67c010e19c65f/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0da54aa8547b3c2c188db3d1c7eb4d1bb6dd80baa8cdaeaec3d1da3346ec9caa", size = 1399854, upload-time = "2025-04-03T20:36:50.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/5d/ca8698e452b349c8313faf07bfa84e7d1c2d2edf7ccc67bcfc49bee1259a/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df8e8c21e67afb9d7fbe18f42c6111fe155e801ab103c81109a61312927cc611", size = 5308962, upload-time = "2025-04-03T20:36:52.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/0a/bebada332854e78e68f3d6c05226b23faca79d71362509dbcf7b002e33b7/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:461fd13250a2adf8e90ca9a0e1e166515cbcaa5e9c3b1f37545cbbeff9e77f6b", size = 1625016, upload-time = "2025-04-03T20:36:54.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/0c/9e58d4887b86d7121d1c519f7050d1be5eb189d8a8075f5417df6492b4f5/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2b3dd5d206a12deca16870acc0d6e5036abeb70e3cad6549c294eff15591527", size = 1600414, upload-time = "2025-04-03T20:36:56.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/df/6096bc669c1311568840bdcbb5a893edc972d1c8d2b4b4325c21d54da5b1/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1343d745fbf4688e412d8f398c6e6d6f269db99a54456873f232ba2e7aeb4939", size = 3053179, upload-time = "2025-04-03T20:36:59.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/5179c583b75fce3e65a5cd79a3561bd19abd54518cb7c483a89b284bf2b9/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b1b065f370d54551dcc785c6f9eeb5bd517ae14c983d2784c064b3aa525896df", size = 2456856, upload-time = "2025-04-03T20:37:01.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/64/e9804212e3286d027ac35bbb66603c9456c2bce23f823b67d2f5cabc05c1/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:11b125d8edd67e767b2295eac6eb9afe0b1cdc82ea3d4b9257da4b8e06077798", size = 7567107, upload-time = "2025-04-03T20:37:04.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f2/7d69e7bf4daec62769b11757ffc31f69afb3ce248947aadbb109fefd9f65/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c33f9c841630b2bb7e69a3fb5c84a854075bb812c47620978bddc591f764da3d", size = 2854192, upload-time = "2025-04-03T20:37:06.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/21/ab4ad7d7d0f653e6fe2e4ccf11d0245092bef94cdff587a21e534e57bda8/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae4574cb66cf1e85d32bb7e9ec45af5409c5b3970b7ceb8dea90168024127566", size = 3398876, upload-time = "2025-04-03T20:37:09.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a8/45bba94c2489cb1ee0130dcb46e1df4fa2c2b25269e21ffd15240a80322b/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e05752418b24bbd411841b256344c26f57da1148c5509e34ea39c7eb5099ab72", size = 4377077, upload-time = "2025-04-03T20:37:11.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/f3/5e0c6ae452cbb74e5436d3445467447e8c32f3021f48f93f15934b8cffc2/rapidfuzz-3.13.0-cp313-cp313-win32.whl", hash = "sha256:0e1d08cb884805a543f2de1f6744069495ef527e279e05370dd7c83416af83f8", size = 1822066, upload-time = "2025-04-03T20:37:14.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e3/a98c25c4f74051df4dcf2f393176b8663bfd93c7afc6692c84e96de147a2/rapidfuzz-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a7c6232be5f809cd39da30ee5d24e6cadd919831e6020ec6c2391f4c3bc9264", size = 1615100, upload-time = "2025-04-03T20:37:16.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/b1/05cd5e697c00cd46d7791915f571b38c8531f714832eff2c5e34537c49ee/rapidfuzz-3.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:3f32f15bacd1838c929b35c84b43618481e1b3d7a61b5ed2db0291b70ae88b53", size = 858976, upload-time = "2025-04-03T20:37:19.336Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
|
||||
Reference in New Issue
Block a user