mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
изображение книги
This commit is contained in:
Vendored
+1
@@ -1,4 +1,5 @@
|
||||
.env
|
||||
library_service/static/books/
|
||||
*.log
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Модуль DB-моделей книг"""
|
||||
|
||||
from typing import TYPE_CHECKING, List
|
||||
from uuid import UUID
|
||||
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy import Column, String
|
||||
@@ -26,7 +27,8 @@ class Book(BookBase, table=True):
|
||||
sa_column=Column(String, nullable=False, default="active"),
|
||||
description="Статус",
|
||||
)
|
||||
embedding: list[float] | None = Field(sa_column=Column(Vector(1024)))
|
||||
embedding: list[float] | None = Field(sa_column=Column(Vector(1024)), description="Эмбэдинг для векторного поиска")
|
||||
preview_id: UUID | None = Field(default=None, unique=True, index=True, description="UUID файла изображения")
|
||||
authors: List["Author"] = Relationship(
|
||||
back_populates="books", link_model=AuthorBookLink
|
||||
)
|
||||
|
||||
@@ -46,6 +46,7 @@ class BookRead(BookBase):
|
||||
|
||||
id: int = Field(description="Идентификатор")
|
||||
status: BookStatus = Field(description="Статус")
|
||||
preview_url: str | None = Field(None, description="URL изображения")
|
||||
|
||||
|
||||
class BookList(SQLModel):
|
||||
|
||||
@@ -39,6 +39,7 @@ class BookWithAuthors(SQLModel):
|
||||
description: str = Field(description="Описание")
|
||||
page_count: int = Field(description="Количество страниц")
|
||||
status: BookStatus | None = Field(None, description="Статус")
|
||||
preview_url: str | None = Field(default=None, description="URL изображения")
|
||||
authors: List[AuthorRead] = Field(
|
||||
default_factory=list, description="Список авторов"
|
||||
)
|
||||
@@ -52,6 +53,7 @@ class BookWithGenres(SQLModel):
|
||||
description: str = Field(description="Описание")
|
||||
page_count: int = Field(description="Количество страниц")
|
||||
status: BookStatus | None = Field(None, description="Статус")
|
||||
preview_url: str | None = Field(default=None, description="URL изображения")
|
||||
genres: List[GenreRead] = Field(default_factory=list, description="Список жанров")
|
||||
|
||||
|
||||
@@ -63,6 +65,7 @@ class BookWithAuthorsAndGenres(SQLModel):
|
||||
description: str = Field(description="Описание")
|
||||
page_count: int = Field(description="Количество страниц")
|
||||
status: BookStatus | None = Field(None, description="Статус")
|
||||
preview_url: str | None = Field(default=None, description="URL изображения")
|
||||
authors: List[AuthorRead] = Field(
|
||||
default_factory=list, description="Список авторов"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Модуль работы с книгами"""
|
||||
import shutil
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import Field
|
||||
from typing_extensions import Annotated
|
||||
@@ -10,12 +12,12 @@ from sqlalchemy import text, case, distinct
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status, UploadFile, File
|
||||
from ollama import Client
|
||||
from sqlmodel import Session, select, col, func
|
||||
|
||||
from library_service.auth import RequireStaff
|
||||
from library_service.settings import get_session, OLLAMA_URL
|
||||
from library_service.settings import get_session, OLLAMA_URL, BOOKS_PREVIEW_DIR
|
||||
from library_service.models.enums import BookStatus
|
||||
from library_service.models.db import (
|
||||
Author,
|
||||
@@ -47,9 +49,9 @@ def close_active_loan(session: Session, book_id: int) -> None:
|
||||
"""Закрывает активную выдачу книги при изменении статуса"""
|
||||
active_loan = session.exec(
|
||||
select(BookUserLink)
|
||||
.where(BookUserLink.book_id == book_id)
|
||||
.where(BookUserLink.returned_at == None) # noqa: E711
|
||||
).first()
|
||||
.where(BookUserLink.book_id == book_id) # ty: ignore
|
||||
.where(BookUserLink.returned_at == None) # ty: ignore
|
||||
).first() # ty: ignore
|
||||
|
||||
if active_loan:
|
||||
active_loan.returned_at = datetime.now(timezone.utc)
|
||||
@@ -72,19 +74,19 @@ def filter_books(
|
||||
size: int = Query(20, gt=0, le=100),
|
||||
):
|
||||
statement = select(Book).options(
|
||||
selectinload(Book.authors), selectinload(Book.genres), defer(Book.embedding)
|
||||
selectinload(Book.authors), selectinload(Book.genres), defer(Book.embedding) # ty: ignore
|
||||
)
|
||||
|
||||
if min_page_count:
|
||||
statement = statement.where(Book.page_count >= min_page_count)
|
||||
statement = statement.where(Book.page_count >= min_page_count) # ty: ignore
|
||||
if max_page_count:
|
||||
statement = statement.where(Book.page_count <= max_page_count)
|
||||
statement = statement.where(Book.page_count <= max_page_count) # ty: ignore
|
||||
|
||||
if author_ids:
|
||||
statement = statement.where(
|
||||
exists().where(
|
||||
AuthorBookLink.book_id == Book.id,
|
||||
AuthorBookLink.author_id.in_(author_ids),
|
||||
AuthorBookLink.book_id == Book.id, # ty: ignore
|
||||
AuthorBookLink.author_id.in_(author_ids), # ty: ignore
|
||||
)
|
||||
)
|
||||
|
||||
@@ -92,7 +94,7 @@ def filter_books(
|
||||
for genre_id in genre_ids:
|
||||
statement = statement.where(
|
||||
exists().where(
|
||||
GenreBookLink.book_id == Book.id, GenreBookLink.genre_id == genre_id
|
||||
GenreBookLink.book_id == Book.id, GenreBookLink.genre_id == genre_id # ty: ignore
|
||||
)
|
||||
)
|
||||
|
||||
@@ -101,13 +103,13 @@ def filter_books(
|
||||
|
||||
if q:
|
||||
emb = ollama_client.embeddings(model="mxbai-embed-large", prompt=q)["embedding"]
|
||||
distance_col = Book.embedding.cosine_distance(emb)
|
||||
statement = statement.where(Book.embedding.is_not(None))
|
||||
distance_col = Book.embedding.cosine_distance(emb) # ty: ignore
|
||||
statement = statement.where(Book.embedding.is_not(None)) # ty: ignore
|
||||
|
||||
keyword_match = case((Book.title.ilike(f"%{q}%"), 0), else_=1)
|
||||
keyword_match = case((Book.title.ilike(f"%{q}%"), 0), else_=1) # ty: ignore
|
||||
statement = statement.order_by(keyword_match, distance_col)
|
||||
else:
|
||||
statement = statement.order_by(Book.id)
|
||||
statement = statement.order_by(Book.id) # ty: ignore
|
||||
|
||||
offset = (page - 1) * size
|
||||
statement = statement.offset(offset).limit(size)
|
||||
@@ -131,10 +133,16 @@ def create_book(
|
||||
full_text = book.title + " " + book.description
|
||||
emb = ollama_client.embeddings(model="mxbai-embed-large", prompt=full_text)
|
||||
db_book = Book(**book.model_dump(), embedding=emb["embedding"])
|
||||
|
||||
session.add(db_book)
|
||||
session.commit()
|
||||
session.refresh(db_book)
|
||||
return BookRead(**db_book.model_dump(exclude={"embedding"}))
|
||||
|
||||
book_data = db_book.model_dump(exclude={"embedding", "preview_id"})
|
||||
if db_book.preview_id:
|
||||
book_data["preview_url"] = f"/static/books/{db_book.preview_id}.png"
|
||||
|
||||
return BookRead(**book_data)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -145,9 +153,17 @@ def create_book(
|
||||
)
|
||||
def read_books(session: Session = Depends(get_session)):
|
||||
"""Возвращает список всех книг"""
|
||||
books = session.exec(select(Book)).all()
|
||||
books = session.exec(select(Book)).all() # ty: ignore
|
||||
|
||||
books_data = []
|
||||
for book in books:
|
||||
book_data = book.model_dump(exclude={"embedding", "preview_id"})
|
||||
if book.preview_id:
|
||||
book_data["preview_url"] = f"/static/books/{book.preview_id}.png"
|
||||
books_data.append(book_data)
|
||||
|
||||
return BookList(
|
||||
books=[BookRead(**book.model_dump(exclude={"embedding"})) for book in books],
|
||||
books=[BookRead(**book_data) for book_data in books_data],
|
||||
total=len(books),
|
||||
)
|
||||
|
||||
@@ -170,18 +186,20 @@ def get_book(
|
||||
)
|
||||
|
||||
authors = session.scalars(
|
||||
select(Author).join(AuthorBookLink).where(AuthorBookLink.book_id == book_id)
|
||||
select(Author).join(AuthorBookLink).where(AuthorBookLink.book_id == book_id) # ty: ignore
|
||||
).all()
|
||||
|
||||
author_reads = [AuthorRead(**author.model_dump()) for author in authors]
|
||||
|
||||
genres = session.scalars(
|
||||
select(Genre).join(GenreBookLink).where(GenreBookLink.book_id == book_id)
|
||||
select(Genre).join(GenreBookLink).where(GenreBookLink.book_id == book_id) # ty: ignore
|
||||
).all()
|
||||
|
||||
genre_reads = [GenreRead(**genre.model_dump()) for genre in genres]
|
||||
|
||||
book_data = book.model_dump(exclude={"embedding"})
|
||||
book_data = book.model_dump(exclude={"embedding", "preview_id"})
|
||||
if book.preview_id:
|
||||
book_data["preview_url"] = f"/static/books/{book.preview_id}.png"
|
||||
book_data["authors"] = author_reads
|
||||
book_data["genres"] = genre_reads
|
||||
|
||||
@@ -233,11 +251,18 @@ def update_book(
|
||||
emb = ollama_client.embeddings(model="mxbai-embed-large", prompt=full_text)
|
||||
db_book.embedding = emb["embedding"]
|
||||
|
||||
if book_update.page_count is not None:
|
||||
db_book.page_count = book_update.page_count
|
||||
|
||||
session.add(db_book)
|
||||
session.commit()
|
||||
session.refresh(db_book)
|
||||
|
||||
return BookRead(**db_book.model_dump(exclude={"embedding"}))
|
||||
book_data = db_book.model_dump(exclude={"embedding", "preview_id"})
|
||||
if db_book.preview_id:
|
||||
book_data["preview_url"] = f"/static/books/{db_book.preview_id}.png"
|
||||
|
||||
return BookRead(**book_data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -267,3 +292,61 @@ def delete_book(
|
||||
session.delete(book)
|
||||
session.commit()
|
||||
return book_read
|
||||
|
||||
@router.post("/{book_id}/preview")
|
||||
async def upload_book_preview(
|
||||
current_user: RequireStaff,
|
||||
file: UploadFile = File(...),
|
||||
book_id: int = Path(..., gt=0),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
if not file.content_type == "image/png":
|
||||
raise HTTPException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, "PNG required")
|
||||
|
||||
if (file.size or 0) > 10 * 1024 * 1024:
|
||||
raise HTTPException(status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, "File larger than 10 MB")
|
||||
|
||||
file_uuid= uuid4()
|
||||
filename = f"{file_uuid}.png"
|
||||
file_path = BOOKS_PREVIEW_DIR / filename
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
shutil.copyfileobj(file.file, f)
|
||||
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
file_path.unlink()
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Book not found")
|
||||
|
||||
if book.preview_id:
|
||||
old_path = BOOKS_PREVIEW_DIR / f"{book.preview_id}.png"
|
||||
if old_path.exists():
|
||||
old_path.unlink()
|
||||
|
||||
book.preview_id = file_uuid
|
||||
session.add(book)
|
||||
session.commit()
|
||||
|
||||
return {"preview_url": f"/static/books/{filename}"}
|
||||
|
||||
|
||||
@router.delete("/{book_id}/preview")
|
||||
async def remove_book_preview(
|
||||
current_user: RequireStaff,
|
||||
book_id: int = Path(..., gt=0),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Book not found")
|
||||
|
||||
if book.preview_id:
|
||||
old_path = BOOKS_PREVIEW_DIR / f"{book.preview_id}.png"
|
||||
if old_path.exists():
|
||||
old_path.unlink()
|
||||
|
||||
book.preview_id = None
|
||||
session.add(book)
|
||||
session.commit()
|
||||
|
||||
return {"preview_url": None}
|
||||
|
||||
@@ -181,6 +181,7 @@ async def api_info(app=Depends(lambda: get_app())):
|
||||
description="Возвращает схему базы данных с описаниями полей",
|
||||
)
|
||||
async def api_schema():
|
||||
"""Возвращает информацию для создания er-диаграммы"""
|
||||
return generator.generate()
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ from toml import load
|
||||
|
||||
load_dotenv()
|
||||
|
||||
BOOKS_PREVIEW_DIR = Path(__file__).parent / "static" / "books"
|
||||
BOOKS_PREVIEW_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open("pyproject.toml", "r", encoding="utf-8") as f:
|
||||
_pyproject = load(f)
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ $(document).ready(() => {
|
||||
function renderBook(book) {
|
||||
$("#book-title").text(book.title);
|
||||
$("#book-id").text(`ID: ${book.id}`);
|
||||
const $coverContainer = $("#book-cover-container");
|
||||
if (book.page_count && book.page_count > 0) {
|
||||
$("#book-page-count-value").text(book.page_count);
|
||||
$("#book-page-count-text").removeClass("hidden");
|
||||
@@ -253,6 +254,30 @@ $(document).ready(() => {
|
||||
$("#book-actions-container").empty();
|
||||
}
|
||||
|
||||
if (book.preview_url) {
|
||||
$coverContainer.html(`
|
||||
<img
|
||||
src="${Utils.escapeHtml(book.preview_url)}"
|
||||
alt="Обложка книги ${Utils.escapeHtml(book.title)}"
|
||||
class="w-full h-full object-cover"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"
|
||||
/>
|
||||
<div class="hidden w-full h-full bg-gradient-to-br from-gray-400 to-gray-600 flex items-center justify-center">
|
||||
<svg class="w-20 h-20 text-white opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
$coverContainer.html(`
|
||||
<div class="w-full h-full bg-gradient-to-br from-gray-400 to-gray-600 flex items-center justify-center">
|
||||
<svg class="w-20 h-20 text-white opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (book.genres && book.genres.length > 0) {
|
||||
$("#genres-section").removeClass("hidden");
|
||||
const $genres = $("#genres-container");
|
||||
|
||||
@@ -66,8 +66,10 @@
|
||||
class="flex flex-col items-center mb-6 md:mb-0 md:mr-8 flex-shrink-0 w-full md:w-auto"
|
||||
>
|
||||
<div
|
||||
class="w-40 h-56 bg-gradient-to-br from-gray-400 to-gray-600 rounded-lg flex items-center justify-center shadow-md mb-4"
|
||||
id="book-cover-container"
|
||||
class="w-40 h-56 rounded-lg shadow-md mb-4 overflow-hidden flex items-center justify-center bg-gray-100"
|
||||
>
|
||||
<div class="w-full h-full bg-gradient-to-br from-gray-400 to-gray-600 flex items-center justify-center">
|
||||
<svg
|
||||
class="w-20 h-20 text-white opacity-80"
|
||||
fill="none"
|
||||
@@ -82,6 +84,7 @@
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="book-status-container"
|
||||
class="relative w-full flex justify-center z-10 mb-4"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Book preview
|
||||
|
||||
Revision ID: abbc38275032
|
||||
Revises: 6c616cc9d1f0
|
||||
Create Date: 2026-02-01 14:41:14.611420
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel, pgvector
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'abbc38275032'
|
||||
down_revision: Union[str, None] = '6c616cc9d1f0'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('book', sa.Column('preview_id', sa.Uuid(), nullable=True))
|
||||
op.create_index(op.f('ix_book_preview_id'), 'book', ['preview_id'], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_book_preview_id'), table_name='book')
|
||||
op.drop_column('book', 'preview_id')
|
||||
# ### end Alembic commands ###
|
||||
@@ -479,7 +479,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
||||
@@ -487,7 +486,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
|
||||
@@ -495,7 +493,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
|
||||
@@ -503,7 +500,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
|
||||
@@ -631,7 +627,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/e8/ef/324f4a28ed0152a32
|
||||
|
||||
[[package]]
|
||||
name = "lib"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
|
||||
Reference in New Issue
Block a user