mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
Доавление векторного поиска и репликации
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
# Postgres
|
||||
POSTGRES_HOST="db"
|
||||
POSTGRES_HOST="localhost"
|
||||
POSTGRES_PORT="5432"
|
||||
POSTGRES_USER="postgres"
|
||||
POSTGRES_PASSWORD="postgres"
|
||||
POSTGRES_DB="lib"
|
||||
|
||||
# Ollama
|
||||
OLLAMA_URL="http://localhost:11434"
|
||||
OLLAMA_MAX_LOADED_MODELS=1
|
||||
OLLAMA_NUM_THREADS=4
|
||||
OLLAMA_KEEP_ALIVE=5m
|
||||
|
||||
# Default admin account
|
||||
# DEFAULT_ADMIN_USERNAME="admin"
|
||||
# DEFAULT_ADMIN_EMAIL="admin@example.com"
|
||||
# DEFAULT_ADMIN_PASSWORD="password-is-generated-randomly-on-first-launch"
|
||||
# SECRET_KEY="your-secret-key-change-in-production"
|
||||
SECRET_KEY="your-secret-key-change-in-production"
|
||||
|
||||
# JWT
|
||||
ALGORITHM="HS256"
|
||||
|
||||
@@ -19,7 +19,6 @@ RUN uv sync --group dev --no-install-project
|
||||
|
||||
COPY ./library_service /code/library_service
|
||||
COPY ./alembic.ini /code/
|
||||
COPY ./data.py /code/
|
||||
|
||||
RUN useradd app && \
|
||||
chown -R app:app /code && \
|
||||
|
||||
+50
-3
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:17
|
||||
image: pgvector/pgvector:pg17
|
||||
container_name: db
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
@@ -11,6 +11,8 @@ services:
|
||||
- ./data/db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- proxy
|
||||
ports:
|
||||
- 5432:5432
|
||||
env_file:
|
||||
- ./.env
|
||||
healthcheck:
|
||||
@@ -19,6 +21,49 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
replication-setup:
|
||||
image: postgres:17-alpine
|
||||
container_name: replication-setup
|
||||
restart: "no"
|
||||
networks:
|
||||
- proxy
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ./setup-replication.sh:/setup-replication.sh
|
||||
entrypoint: ["/bin/sh", "/setup-replication.sh"]
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
llm:
|
||||
image: ollama/ollama:latest
|
||||
container_name: llm
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
volumes:
|
||||
- ./data/llm:/root/.ollama
|
||||
networks:
|
||||
- proxy
|
||||
# ports:
|
||||
# - 11434:11434
|
||||
env_file:
|
||||
- ./.env
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl http://localhost:11434"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 5g
|
||||
|
||||
api:
|
||||
build: .
|
||||
container_name: api
|
||||
@@ -30,8 +75,8 @@ services:
|
||||
max-file: "3"
|
||||
networks:
|
||||
- proxy
|
||||
ports:
|
||||
- 8000:8000
|
||||
# ports:
|
||||
# - 8000:8000
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
@@ -39,6 +84,8 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
llm:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
proxy: # Рекомендуется использовать через реверс-прокси
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Postgres
|
||||
POSTGRES_HOST="db"
|
||||
POSTGRES_PORT="5432"
|
||||
POSTGRES_USER="postgres"
|
||||
POSTGRES_PASSWORD="postgres"
|
||||
POSTGRES_DB="lib"
|
||||
REMOTE_HOST=
|
||||
REMOTE_PORT=
|
||||
NODE_ID=
|
||||
|
||||
# Ollama
|
||||
OLLAMA_URL="http://llm:11434"
|
||||
OLLAMA_MAX_LOADED_MODELS=1
|
||||
OLLAMA_NUM_THREADS=4
|
||||
OLLAMA_KEEP_ALIVE=5m
|
||||
|
||||
# Default admin account
|
||||
DEFAULT_ADMIN_USERNAME="admin"
|
||||
DEFAULT_ADMIN_EMAIL="admin@example.com"
|
||||
DEFAULT_ADMIN_PASSWORD="Password12345"
|
||||
SECRET_KEY="your-secret-key-change-in-production"
|
||||
|
||||
# JWT
|
||||
ALGORITHM="HS256"
|
||||
REFRESH_TOKEN_EXPIRE_DAYS="7"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES="15"
|
||||
PARTIAL_TOKEN_EXPIRE_MINUTES="5"
|
||||
|
||||
# Hash
|
||||
ARGON2_TYPE="id"
|
||||
ARGON2_TIME_COST="3"
|
||||
ARGON2_MEMORY_COST="65536"
|
||||
ARGON2_PARALLELISM="4"
|
||||
ARGON2_SALT_LENGTH="16"
|
||||
ARGON2_HASH_LENGTH="48"
|
||||
|
||||
# Recovery codes
|
||||
RECOVERY_CODES_COUNT="10"
|
||||
RECOVERY_CODE_SEGMENTS="4"
|
||||
RECOVERY_CODE_SEGMENT_BYTES="2"
|
||||
RECOVERY_MIN_REMAINING_WARNING="3"
|
||||
RECOVERY_MAX_AGE_DAYS="365"
|
||||
|
||||
# TOTP_2FA
|
||||
TOTP_ISSUER="LiB"
|
||||
TOTP_VALID_WINDOW="1"
|
||||
@@ -0,0 +1,43 @@
|
||||
# Postgres
|
||||
POSTGRES_HOST="localhost"
|
||||
POSTGRES_PORT="5432"
|
||||
POSTGRES_USER="postgres"
|
||||
POSTGRES_PASSWORD="postgres"
|
||||
POSTGRES_DB="lib"
|
||||
|
||||
# Ollama
|
||||
OLLAMA_URL="http://localhost:11434"
|
||||
OLLAMA_MAX_LOADED_MODELS=1
|
||||
OLLAMA_NUM_THREADS=4
|
||||
OLLAMA_KEEP_ALIVE=5m
|
||||
|
||||
# Default admin account
|
||||
DEFAULT_ADMIN_USERNAME="admin"
|
||||
DEFAULT_ADMIN_EMAIL="admin@example.com"
|
||||
DEFAULT_ADMIN_PASSWORD="Password12345"
|
||||
SECRET_KEY="your-secret-key-change-in-production"
|
||||
|
||||
# JWT
|
||||
ALGORITHM="HS256"
|
||||
REFRESH_TOKEN_EXPIRE_DAYS="7"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES="15"
|
||||
PARTIAL_TOKEN_EXPIRE_MINUTES="5"
|
||||
|
||||
# Hash
|
||||
ARGON2_TYPE="id"
|
||||
ARGON2_TIME_COST="3"
|
||||
ARGON2_MEMORY_COST="65536"
|
||||
ARGON2_PARALLELISM="4"
|
||||
ARGON2_SALT_LENGTH="16"
|
||||
ARGON2_HASH_LENGTH="48"
|
||||
|
||||
# Recovery codes
|
||||
RECOVERY_CODES_COUNT="10"
|
||||
RECOVERY_CODE_SEGMENTS="4"
|
||||
RECOVERY_CODE_SEGMENT_BYTES="2"
|
||||
RECOVERY_MIN_REMAINING_WARNING="3"
|
||||
RECOVERY_MAX_AGE_DAYS="365"
|
||||
|
||||
# TOTP_2FA
|
||||
TOTP_ISSUER="LiB"
|
||||
TOTP_VALID_WINDOW="1"
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE PUBLICATION all_tables_pub FOR ALL TABLES;
|
||||
|
||||
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
|
||||
SELECT pg_reload_conf();
|
||||
@@ -11,6 +11,7 @@ from alembic import command
|
||||
from alembic.config import Config
|
||||
from fastapi import FastAPI, Depends, Request, Response, status
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from ollama import Client, ResponseError
|
||||
from sqlmodel import Session
|
||||
|
||||
from library_service.auth import run_seeds
|
||||
@@ -21,6 +22,7 @@ from library_service.settings import (
|
||||
engine,
|
||||
get_app,
|
||||
get_logger,
|
||||
OLLAMA_URL,
|
||||
)
|
||||
|
||||
|
||||
@@ -51,6 +53,13 @@ async def lifespan(_):
|
||||
except Exception as e:
|
||||
logger.error(f"[-] Seeding failed: {e}")
|
||||
|
||||
logger.info("[+] Loading ollama models...")
|
||||
ollama_client = Client(host=OLLAMA_URL)
|
||||
try:
|
||||
ollama_client.pull("mxbai-embed-large")
|
||||
ollama_client.pull("llama3.2")
|
||||
except ResponseError as e:
|
||||
logger.error(f"[-] Failed to pull models {e}")
|
||||
asyncio.create_task(cleanup_task())
|
||||
logger.info("[+] Starting application...")
|
||||
yield # Обработка запросов
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy import Column, String
|
||||
from sqlmodel import Field, Relationship
|
||||
|
||||
@@ -25,6 +26,7 @@ class Book(BookBase, table=True):
|
||||
sa_column=Column(String, nullable=False, default="active"),
|
||||
description="Статус",
|
||||
)
|
||||
embedding: list[float] | None = Field(sa_column=Column(Vector(1024)))
|
||||
authors: List["Author"] = Relationship(
|
||||
back_populates="books", link_model=AuthorBookLink
|
||||
)
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
"""Модуль работы с книгами"""
|
||||
|
||||
from pydantic import Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from sqlalchemy.orm import selectinload, defer
|
||||
|
||||
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 ollama import Client
|
||||
from sqlmodel import Session, select, col, func
|
||||
|
||||
from library_service.auth import RequireStaff
|
||||
from library_service.settings import get_session
|
||||
from library_service.settings import get_session, OLLAMA_URL
|
||||
from library_service.models.enums import BookStatus
|
||||
from library_service.models.db import (
|
||||
Author,
|
||||
@@ -32,6 +40,7 @@ from library_service.models.dto.misc import (
|
||||
|
||||
|
||||
router = APIRouter(prefix="/books", tags=["books"])
|
||||
ollama_client = Client(host=OLLAMA_URL)
|
||||
|
||||
|
||||
def close_active_loan(session: Session, book_id: int) -> None:
|
||||
@@ -47,72 +56,64 @@ def close_active_loan(session: Session, book_id: int) -> None:
|
||||
session.add(active_loan)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/filter",
|
||||
response_model=BookFilteredList,
|
||||
summary="Фильтрация книг",
|
||||
description="Фильтрация списка книг по названию, авторам и жанрам с пагинацией",
|
||||
)
|
||||
from sqlalchemy import select, func, distinct, case, exists
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
|
||||
@router.get("/filter", response_model=BookFilteredList)
|
||||
def filter_books(
|
||||
session: Session = Depends(get_session),
|
||||
q: str | None = Query(None, max_length=50, description="Поиск"),
|
||||
min_page_count: int | None = Query(
|
||||
None, ge=0, description="Минимальное количество страниц"
|
||||
),
|
||||
max_page_count: int | None = Query(
|
||||
None, ge=0, description="Максимальное количество страниц"
|
||||
),
|
||||
author_ids: List[int] | None = Query(None, gt=0, description="Список ID авторов"),
|
||||
genre_ids: List[int] | None = Query(None, gt=0, description="Список ID жанров"),
|
||||
page: int = Query(1, gt=0, description="Номер страницы"),
|
||||
size: int = Query(20, gt=0, le=100, description="Количество элементов на странице"),
|
||||
min_page_count: int | None = Query(None, ge=0),
|
||||
max_page_count: int | None = Query(None, ge=0),
|
||||
author_ids: List[Annotated[int, Field(gt=0)]] | None = Query(None),
|
||||
genre_ids: List[Annotated[int, Field(gt=0)]] | None = Query(None),
|
||||
page: int = Query(1, gt=0),
|
||||
size: int = Query(20, gt=0, le=100),
|
||||
):
|
||||
"""Возвращает отфильтрованный список книг с пагинацией"""
|
||||
statement = select(Book).distinct()
|
||||
|
||||
if q:
|
||||
statement = statement.where(
|
||||
(col(Book.title).ilike(f"%{q}%")) | (col(Book.description).ilike(f"%{q}%"))
|
||||
statement = select(Book).options(
|
||||
selectinload(Book.authors), selectinload(Book.genres), defer(Book.embedding)
|
||||
)
|
||||
|
||||
if min_page_count:
|
||||
statement = statement.where(Book.page_count >= min_page_count)
|
||||
|
||||
if max_page_count:
|
||||
statement = statement.where(Book.page_count <= max_page_count)
|
||||
|
||||
if author_ids:
|
||||
statement = statement.join(AuthorBookLink).where(
|
||||
AuthorBookLink.author_id.in_( # ty: ignore[unresolved-attribute, unresolved-reference]
|
||||
author_ids
|
||||
statement = statement.where(
|
||||
exists().where(
|
||||
AuthorBookLink.book_id == Book.id,
|
||||
AuthorBookLink.author_id.in_(author_ids),
|
||||
)
|
||||
)
|
||||
|
||||
if genre_ids:
|
||||
statement = statement.join(GenreBookLink).where(
|
||||
GenreBookLink.genre_id.in_( # ty: ignore[unresolved-attribute, unresolved-reference]
|
||||
genre_ids
|
||||
for genre_id in genre_ids:
|
||||
statement = statement.where(
|
||||
exists().where(
|
||||
GenreBookLink.book_id == Book.id, GenreBookLink.genre_id == genre_id
|
||||
)
|
||||
)
|
||||
|
||||
total_statement = select(func.count()).select_from(statement.subquery())
|
||||
total = session.exec(total_statement).one()
|
||||
count_statement = select(func.count()).select_from(statement.subquery())
|
||||
total = session.scalar(count_statement)
|
||||
|
||||
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))
|
||||
|
||||
keyword_match = case((Book.title.ilike(f"%{q}%"), 0), else_=1)
|
||||
statement = statement.order_by(keyword_match, distance_col)
|
||||
else:
|
||||
statement = statement.order_by(Book.id)
|
||||
|
||||
offset = (page - 1) * size
|
||||
statement = statement.offset(offset).limit(size)
|
||||
results = session.exec(statement).all()
|
||||
results = session.scalars(statement).unique().all()
|
||||
|
||||
books_with_data = []
|
||||
for db_book in results:
|
||||
books_with_data.append(
|
||||
BookWithAuthorsAndGenres(
|
||||
**db_book.model_dump(),
|
||||
authors=[AuthorRead(**a.model_dump()) for a in db_book.authors],
|
||||
genres=[GenreRead(**g.model_dump()) for g in db_book.genres],
|
||||
)
|
||||
)
|
||||
|
||||
return BookFilteredList(books=books_with_data, total=total)
|
||||
return BookFilteredList(books=results, total=total)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -127,11 +128,13 @@ def create_book(
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Создает новую книгу в системе"""
|
||||
db_book = Book(**book.model_dump())
|
||||
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())
|
||||
return BookRead(**db_book.model_dump(exclude={"embedding"}))
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -144,7 +147,8 @@ def read_books(session: Session = Depends(get_session)):
|
||||
"""Возвращает список всех книг"""
|
||||
books = session.exec(select(Book)).all()
|
||||
return BookList(
|
||||
books=[BookRead(**book.model_dump()) for book in books], total=len(books)
|
||||
books=[BookRead(**book.model_dump(exclude={"embedding"})) for book in books],
|
||||
total=len(books),
|
||||
)
|
||||
|
||||
|
||||
@@ -165,19 +169,19 @@ def get_book(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
||||
)
|
||||
|
||||
authors = session.exec(
|
||||
authors = session.scalars(
|
||||
select(Author).join(AuthorBookLink).where(AuthorBookLink.book_id == book_id)
|
||||
).all()
|
||||
|
||||
author_reads = [AuthorRead(**author.model_dump()) for author in authors]
|
||||
|
||||
genres = session.exec(
|
||||
genres = session.scalars(
|
||||
select(Genre).join(GenreBookLink).where(GenreBookLink.book_id == book_id)
|
||||
).all()
|
||||
|
||||
genre_reads = [GenreRead(**genre.model_dump()) for genre in genres]
|
||||
|
||||
book_data = book.model_dump()
|
||||
book_data = book.model_dump(exclude={"embedding"})
|
||||
book_data["authors"] = author_reads
|
||||
book_data["genres"] = genre_reads
|
||||
|
||||
@@ -186,7 +190,7 @@ def get_book(
|
||||
|
||||
@router.put(
|
||||
"/{book_id}",
|
||||
response_model=Book,
|
||||
response_model=BookRead,
|
||||
summary="Обновить информацию о книге",
|
||||
description="Обновляет информацию о книге в системе",
|
||||
)
|
||||
@@ -221,11 +225,19 @@ def update_book(
|
||||
if book_update.description is not None:
|
||||
db_book.description = book_update.description
|
||||
|
||||
full_text = (
|
||||
(book_update.title or db_book.title)
|
||||
+ " "
|
||||
+ (book_update.description or db_book.description)
|
||||
)
|
||||
emb = ollama_client.embeddings(model="mxbai-embed-large", prompt=full_text)
|
||||
db_book.embedding = emb["embedding"]
|
||||
|
||||
session.add(db_book)
|
||||
session.commit()
|
||||
session.refresh(db_book)
|
||||
|
||||
return BookRead(**db_book.model_dump())
|
||||
return BookRead(**db_book.model_dump(exclude={"embedding"}))
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -249,6 +261,7 @@ def delete_book(
|
||||
id=(book.id or 0),
|
||||
title=book.title,
|
||||
description=book.description,
|
||||
page_count=book.page_count,
|
||||
status=book.status,
|
||||
)
|
||||
session.delete(book)
|
||||
|
||||
@@ -95,7 +95,9 @@ USER = os.getenv("POSTGRES_USER")
|
||||
PASSWORD = os.getenv("POSTGRES_PASSWORD")
|
||||
DATABASE = os.getenv("POSTGRES_DB")
|
||||
|
||||
if not all([HOST, PORT, USER, PASSWORD, DATABASE]):
|
||||
OLLAMA_URL = os.getenv("OLLAMA_URL")
|
||||
|
||||
if not all([HOST, PORT, USER, PASSWORD, DATABASE, OLLAMA_URL]):
|
||||
raise ValueError("Missing required POSTGRES environment variables")
|
||||
|
||||
POSTGRES_DATABASE_URL = f"postgresql://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}"
|
||||
|
||||
@@ -242,7 +242,7 @@ $(document).ready(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parseInt(pageCount)) {
|
||||
if (!pageCount) {
|
||||
Utils.showToast("Введите количество страниц", "error");
|
||||
return;
|
||||
}
|
||||
@@ -253,7 +253,7 @@ $(document).ready(() => {
|
||||
const bookPayload = {
|
||||
title: title,
|
||||
description: description || null,
|
||||
page_count: pageCount ? parseInt(pageCount) : null,
|
||||
page_count: pageCount,
|
||||
};
|
||||
|
||||
const createdBook = await Api.post("/api/books/", bookPayload);
|
||||
|
||||
@@ -331,7 +331,7 @@ $(document).ready(() => {
|
||||
|
||||
const title = $titleInput.val().trim();
|
||||
const description = $descInput.val().trim();
|
||||
const pages = $pagesInput.val();
|
||||
const pages = parseInt($("#book-page-count").val()) || null;
|
||||
const status = $statusSelect.val();
|
||||
|
||||
if (!title) {
|
||||
@@ -343,7 +343,7 @@ $(document).ready(() => {
|
||||
if (title !== originalBook.title) payload.title = title;
|
||||
if (description !== (originalBook.description || ""))
|
||||
payload.description = description || null;
|
||||
if (pageCount !== originalBook.page_count) payload.page_count = pages;
|
||||
if (pages !== originalBook.page_count) payload.page_count = pages;
|
||||
if (status !== originalBook.status) payload.status = status;
|
||||
|
||||
if (Object.keys(payload).length === 0) {
|
||||
|
||||
@@ -168,8 +168,7 @@
|
||||
jsPlumb.ready(function () {
|
||||
const instance = jsPlumb.getInstance({
|
||||
Container: "erDiagram",
|
||||
Connector: ["Flowchart", { stub: 30, gap: 10, cornerRadius: 5, alwaysRespectStubs: true }],
|
||||
ConnectionOverlays: [["Arrow", { location: 1, width: 10, length: 10, foldback: 0.8 }]]
|
||||
Connector: ["Flowchart", { stub: 30, gap: 10, cornerRadius: 5, alwaysRespectStubs: true }]
|
||||
});
|
||||
|
||||
const container = document.getElementById("erDiagram");
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
</button>
|
||||
<a
|
||||
id="cancel-btn"
|
||||
href="/"
|
||||
href="/books"
|
||||
class="flex-1 flex justify-center items-center px-6 py-3 bg-white border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition text-center"
|
||||
>
|
||||
Отмена
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
import sqlmodel, pgvector
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Book vector search
|
||||
|
||||
Revision ID: 6c616cc9d1f0
|
||||
Revises: c5dfc16bdc66
|
||||
Create Date: 2026-01-27 22:37:48.077761
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel, pgvector
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "6c616cc9d1f0"
|
||||
down_revision: Union[str, None] = "c5dfc16bdc66"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"book",
|
||||
sa.Column(
|
||||
"embedding", pgvector.sqlalchemy.vector.VECTOR(dim=1024), nullable=True
|
||||
),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("book", "embedding")
|
||||
# ### end Alembic commands ###
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "LiB"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
description = "Это простое API для управления авторами, книгами и их жанрами."
|
||||
authors = [{ name = "wowlikon" }]
|
||||
readme = "README.md"
|
||||
@@ -23,6 +23,8 @@ dependencies = [
|
||||
"pyotp>=2.9.0",
|
||||
"slowapi>=0.1.9",
|
||||
"limits>=5.6.0",
|
||||
"ollama>=0.6.1",
|
||||
"pgvector>=0.4.2",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== Настройка репликации ==="
|
||||
echo "Этот узел: NODE_ID=${NODE_ID}"
|
||||
echo "Удаленный хост: ${REMOTE_HOST}"
|
||||
|
||||
echo "Ждем локальную базу..."
|
||||
sleep 10
|
||||
|
||||
until PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c '\q' 2>/dev/null; do
|
||||
echo "Локальная база не готова, ждем..."
|
||||
sleep 2
|
||||
done
|
||||
echo "Локальная база готова"
|
||||
|
||||
echo "Настройка генераторов ID (NODE_ID=${NODE_ID})..."
|
||||
|
||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
|
||||
DO \$\$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_schema, table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE is_identity = 'YES'
|
||||
AND table_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE format(
|
||||
'ALTER TABLE %I.%I ALTER COLUMN %I SET GENERATED BY DEFAULT AS IDENTITY (START WITH %s INCREMENT BY 2)',
|
||||
r.table_schema, r.table_name, r.column_name, ${NODE_ID}
|
||||
);
|
||||
RAISE NOTICE 'Настроен ID для %.%', r.table_name, r.column_name;
|
||||
END LOOP;
|
||||
END \$\$;
|
||||
EOF
|
||||
|
||||
echo "Ждем удаленный хост ${REMOTE_HOST}:${REMOTE_PORT}..."
|
||||
TIMEOUT=300
|
||||
ELAPSED=0
|
||||
|
||||
while ! PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${REMOTE_HOST}" -p ${REMOTE_PORT} -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c '\q' 2>/dev/null; do
|
||||
sleep 5
|
||||
ELAPSED=$((ELAPSED + 5))
|
||||
if [ $ELAPSED -ge $TIMEOUT ]; then
|
||||
echo "Таймаут ожидания удаленного хоста. Репликация НЕ настроена."
|
||||
echo "Вы можете запустить этот скрипт вручную позже:"
|
||||
echo "docker compose restart replication-setup"
|
||||
exit 0
|
||||
fi
|
||||
echo "Удаленный хост недоступен, ждем... (${ELAPSED}s/${TIMEOUT}s)"
|
||||
done
|
||||
echo "Удаленный хост доступен"
|
||||
|
||||
EXISTING=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -tAc "SELECT COUNT(*) FROM pg_subscription WHERE subname = 'sub_from_remote';")
|
||||
|
||||
if [ "$EXISTING" -gt 0 ]; then
|
||||
echo "Подписка уже существует, пропускаем создание"
|
||||
else
|
||||
echo "Создаем подписку на удаленный хост..."
|
||||
|
||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
|
||||
CREATE SUBSCRIPTION sub_from_remote
|
||||
CONNECTION 'host=${REMOTE_HOST} port=${REMOTE_PORT} user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB}'
|
||||
PUBLICATION all_tables_pub
|
||||
WITH (
|
||||
origin = none
|
||||
);
|
||||
EOF
|
||||
|
||||
echo "Подписка создана!"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Репликация настроена!"
|
||||
echo "Этот узел (${NODE_ID}) теперь синхронизирован с ${REMOTE_HOST}"
|
||||
@@ -631,7 +631,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/e8/ef/324f4a28ed0152a32
|
||||
|
||||
[[package]]
|
||||
name = "lib"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -640,7 +640,9 @@ dependencies = [
|
||||
{ name = "jinja2" },
|
||||
{ name = "json-log-formatter" },
|
||||
{ name = "limits" },
|
||||
{ name = "ollama" },
|
||||
{ name = "passlib", extra = ["argon2"] },
|
||||
{ name = "pgvector" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pydantic", extra = ["email"] },
|
||||
{ name = "pyotp" },
|
||||
@@ -670,7 +672,9 @@ requires-dist = [
|
||||
{ name = "jinja2", specifier = ">=3.1.6" },
|
||||
{ name = "json-log-formatter", specifier = ">=1.1.1" },
|
||||
{ name = "limits", specifier = ">=5.6.0" },
|
||||
{ name = "ollama", specifier = ">=0.6.1" },
|
||||
{ name = "passlib", extras = ["argon2"], specifier = ">=1.7.4" },
|
||||
{ name = "pgvector", specifier = ">=0.4.2" },
|
||||
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.12.5" },
|
||||
{ name = "pyotp", specifier = ">=2.9.0" },
|
||||
@@ -820,6 +824,80 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ollama"
|
||||
version = "0.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.11.5"
|
||||
@@ -905,6 +983,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pgvector"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.1.0"
|
||||
|
||||
Reference in New Issue
Block a user