diff --git a/backend/routers/books.py b/backend/__init__.py similarity index 100% rename from backend/routers/books.py rename to backend/__init__.py diff --git a/backend/database.py b/backend/database.py index 6c45712..d56fb04 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2,8 +2,12 @@ import psycopg from dotenv import load_dotenv import time +import logging -load_dotenv(dotenv_path="../.env") +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "../.env"), override=True) def get_db_connection(): return psycopg.connect( @@ -18,8 +22,10 @@ def init_db(): retries = 5 while retries > 0: try: + logger.info("Attempting to connect to the database...") with get_db_connection() as conn: with conn.cursor() as cur: + logger.info("Connected to the database. Creating tables...") cur.execute(""" DO $$ BEGIN @@ -33,61 +39,94 @@ def init_db(): $$; """) cur.execute(""" - CREATE TABLE IF NOT EXISTS books ( - id SERIAL PRIMARY KEY, - title VARCHAR(255) NOT NULL, - published_year INT NOT NULL, - isbn VARCHAR(20) UNIQUE NOT NULL - ); - CREATE TABLE IF NOT EXISTS categories ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE NOT NULL - ); - - CREATE TABLE IF NOT EXISTS book_categories ( - book_id INT REFERENCES books(id) ON DELETE CASCADE, - category_id INT REFERENCES categories(id) ON DELETE CASCADE, - PRIMARY KEY (book_id, category_id) - ); - CREATE TABLE IF NOT EXISTS authors ( - id SERIAL PRIMARY KEY, - first_name VARCHAR(255) NOT NULL, - last_name VARCHAR(255) NOT NULL - ); - CREATE TABLE IF NOT EXISTS book_authors ( - book_id INT REFERENCES books(id) ON DELETE CASCADE, - author_id INT REFERENCES authors(id) ON DELETE CASCADE, - PRIMARY KEY (book_id, author_id) - ); - CREATE TABLE IF NOT EXISTS book_copies ( - id SERIAL PRIMARY KEY, - book_id INT REFERENCES books(id) ON DELETE CASCADE, - barcode VARCHAR(50) UNIQUE NOT NULL, - status book_status NOT NULL DEFAULT 'active' - ); - CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - first_name VARCHAR(255) NOT NULL, - last_name VARCHAR(255) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - password VARCHAR(255) NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - fine DECIMAL(10, 2) NOT NULL DEFAULT 0.00, - access_level access_level NOT NULL DEFAULT 'member', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - CREATE TABLE IF NOT EXISTS loans ( - id SERIAL PRIMARY KEY, - copy_id INT REFERENCES book_copies(id) ON DELETE CASCADE, - user_id INT REFERENCES users(id) ON DELETE CASCADE, - loan_date DATE NOT NULL DEFAULT CURRENT_DATE, - due_date DATE NOT NULL, - return_date DATE - ); - """) - print("Database initialized successfully.") - break + CREATE TABLE IF NOT EXISTS books ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + published_year INT NOT NULL, + isbn VARCHAR(20) UNIQUE NOT NULL, + summary TEXT + ); + CREATE TABLE IF NOT EXISTS categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL + ); + + CREATE TABLE IF NOT EXISTS book_categories ( + book_id INT REFERENCES books(id) ON DELETE CASCADE, + category_id INT REFERENCES categories(id) ON DELETE CASCADE, + PRIMARY KEY (book_id, category_id) + ); + CREATE TABLE IF NOT EXISTS authors ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL + ); + CREATE TABLE IF NOT EXISTS book_authors ( + book_id INT REFERENCES books(id) ON DELETE CASCADE, + author_id INT REFERENCES authors(id) ON DELETE CASCADE, + PRIMARY KEY (book_id, author_id) + ); + CREATE TABLE IF NOT EXISTS book_copies ( + id SERIAL PRIMARY KEY, + book_id INT REFERENCES books(id) ON DELETE CASCADE, + barcode VARCHAR(50) UNIQUE NOT NULL, + status book_status NOT NULL DEFAULT 'active' + ); + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + fine DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + access_level access_level NOT NULL DEFAULT 'member', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS loans ( + id SERIAL PRIMARY KEY, + copy_id INT REFERENCES book_copies(id) ON DELETE CASCADE, + user_id INT REFERENCES users(id) ON DELETE CASCADE, + loan_date DATE NOT NULL DEFAULT CURRENT_DATE, + due_date DATE NOT NULL, + return_date DATE + ); + """) + logger.info("Tables created successfully. Checking environment variables...") + + test_user_vars = { + "TEST_USER_FIRST_NAME": os.getenv("TEST_USER_FIRST_NAME"), + "TEST_USER_LAST_NAME": os.getenv("TEST_USER_LAST_NAME"), + "TEST_USER_EMAIL": os.getenv("TEST_USER_EMAIL"), + "TEST_USER_PASSWORD": os.getenv("TEST_USER_PASSWORD"), + } + for var, value in test_user_vars.items(): + logger.info(f"{var}: {value}") + if not all(test_user_vars.values()): + raise ValueError("Missing required environment variables for the test user.") + logger.info("Adding test user...") + cur.execute(""" + INSERT INTO users (first_name, last_name, email, password, access_level) + VALUES (%s, %s, %s, %s, %s) + ON CONFLICT (email) DO NOTHING; + """, ( + test_user_vars["TEST_USER_FIRST_NAME"], + test_user_vars["TEST_USER_LAST_NAME"], + test_user_vars["TEST_USER_EMAIL"], + test_user_vars["TEST_USER_PASSWORD"], + "member" + )) + + if cur.rowcount > 0: + logger.info(f"Test user created successfully. Rows affected: {cur.rowcount}") + else: + logger.info("Test user already exists (skipped due to conflict).") + + conn.commit() + logger.info("Database initialization completed.") + break except Exception as e: retries -= 1 - print(f"Waiting for database... ({retries} retries left). Error: {e}") + logger.error(f"Database initialization failed: {e}") + logger.info(f"Retrying... ({retries} retries left)") time.sleep(2) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 677904c..6e9e92c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,11 @@ from fastapi import FastAPI from database import get_db_connection, init_db +import threading +from tools.book_scraper import scrape_books +from routers import router_loans as loans app = FastAPI(title="Library") +app.include_router(loans.router) @app.on_event("startup") def startup_event(): @@ -20,4 +24,11 @@ def health_check(): @app.get("/") def root(): - return {"message": "Library API is running"} \ No newline at end of file + return {"message": "Library API is running"} + +@app.post("/scrape-books") +def run_scraper(count: int = 10): + def scraper_thread(): + scrape_books(count) + threading.Thread(target=scraper_thread, daemon=True).start() + return {"status": "started", "target_count": count} \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index b5e9b27..9aaedc7 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,11 +5,14 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ + "deep-translator>=1.11.4", "dotenv>=0.9.9", "fastapi>=0.135.1", "psycopg2-binary>=2.9.11", "psycopg[binary]>=3.3.3", + "pytest>=9.0.3", "python-dotenv>=1.2.2", "qdrant-client>=1.17.1", + "requests>=2.33.1", "uvicorn>=0.42.0", ] diff --git a/backend/routers/router_loans.py b/backend/routers/router_loans.py new file mode 100644 index 0000000..467e81b --- /dev/null +++ b/backend/routers/router_loans.py @@ -0,0 +1,111 @@ +from fastapi import APIRouter, HTTPException +from datetime import date +from schemas.schema_loans import Loan, LoanCreate +from database import get_db_connection +router = APIRouter(prefix="/loans", tags=["loans"]) + +@router.post("/", response_model=Loan) +async def create_loan(loan: LoanCreate): + try: + with get_db_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT id FROM book_copies WHERE book_id = %s AND status = 'active' ORDER BY id LIMIT 1", + (loan.copy_id,) + ) + copy = cur.fetchone() + if not copy: + raise HTTPException(status_code=404, detail="Book copy not found") + + cur.execute("SELECT id FROM users WHERE id = %s", (loan.user_id,)) + if not cur.fetchone(): + raise HTTPException(status_code=404, detail="User not found") + + cur.execute( + """INSERT INTO loans (copy_id, user_id, due_date) + VALUES (%s, %s, %s) + RETURNING id, copy_id, user_id, loan_date, due_date, return_date""", + (loan.copy_id, loan.user_id, loan.due_date) + ) + result = cur.fetchone() + + cur.execute( + "UPDATE book_copies SET status = 'loaned' WHERE id = %s", + (loan.copy_id,) + ) + conn.commit() + + return { + "id": result[0], + "copy_id": result[1], + "user_id": result[2], + "loan_date": result[3], + "due_date": result[4], + "return_date": result[5] + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/{loan_id}/return") +async def return_loan(loan_id: int): + try: + with get_db_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT copy_id, user_id, due_date FROM loans WHERE id = %s", + (loan_id,) + ) + result = cur.fetchone() + if not result: + raise HTTPException(status_code=404, detail="Loan not found") + + copy_id, user_id, due_date = result + today = date.today() + + if today > due_date: + days_overdue = (today - due_date).days + fine = days_overdue + else: + fine = 0 + + cur.execute( + """UPDATE loans SET return_date = CURRENT_DATE + WHERE id = %s""", + (loan_id,) + ) + + cur.execute( + """UPDATE users SET fine = fine + %s WHERE id = %s""", + (fine, user_id) + ) + + cur.execute( + "SELECT fine FROM users WHERE id = %s", + (user_id,) + ) + total_fine = cur.fetchone()[0] + + if total_fine > 20: + cur.execute( + "UPDATE users SET is_active = FALSE WHERE id = %s", + (user_id,) + ) + + cur.execute( + "UPDATE book_copies SET status = 'active' WHERE id = %s", + (copy_id,) + ) + conn.commit() + + return { + "message": "Book returned successfully", + "fine_added": fine, + "total_fine": total_fine, + "user_blocked": total_fine > 20 + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/schemas/authors.py b/backend/schemas/schema_authors.py similarity index 100% rename from backend/schemas/authors.py rename to backend/schemas/schema_authors.py diff --git a/backend/schemas/books.py b/backend/schemas/schema_books.py similarity index 84% rename from backend/schemas/books.py rename to backend/schemas/schema_books.py index c4e723f..a27e57f 100644 --- a/backend/schemas/books.py +++ b/backend/schemas/schema_books.py @@ -1,8 +1,8 @@ from pydantic import BaseModel, field from typing import List -from authors import Author -from categories import Category -from enums import BookStatus +from schema_authors import Author +from schema_categories import Category +from schema_enums import BookStatus class BookCopyBase(BaseModel): barcode: str @@ -18,6 +18,7 @@ class BookBase(BaseModel): title: str published_year: int isbn: str + summary: str class BookCreate(BookBase): author_ids: List[int] diff --git a/backend/schemas/categories.py b/backend/schemas/schema_categories.py similarity index 100% rename from backend/schemas/categories.py rename to backend/schemas/schema_categories.py diff --git a/backend/schemas/enums.py b/backend/schemas/schema_enums.py similarity index 100% rename from backend/schemas/enums.py rename to backend/schemas/schema_enums.py diff --git a/backend/schemas/loans.py b/backend/schemas/schema_loans.py similarity index 100% rename from backend/schemas/loans.py rename to backend/schemas/schema_loans.py diff --git a/backend/schemas/users.py b/backend/schemas/schema_users.py similarity index 92% rename from backend/schemas/users.py rename to backend/schemas/schema_users.py index e7210db..dd1dff8 100644 --- a/backend/schemas/users.py +++ b/backend/schemas/schema_users.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, EmailStr from datetime import datetime from decimal import Decimal -from enums import AccessLevel +from schema_enums import AccessLevel class UserBase(BaseModel): first_name: str diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_loans.py b/backend/tests/test_loans.py new file mode 100644 index 0000000..317170f --- /dev/null +++ b/backend/tests/test_loans.py @@ -0,0 +1,224 @@ +import pytest +from datetime import date, timedelta +from unittest.mock import patch, MagicMock +from fastapi.testclient import TestClient +from fastapi import FastAPI +from routers.router_loans import router + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + + +@pytest.fixture +def mock_db_connection(): + """Mock połączenia z bazą danych""" + with patch('routers.router_loans.get_db_connection') as mock: + yield mock + + +class TestReturnLoan: + """Testy dla zwracania książek i naliczania kar""" + + def test_return_loan_on_time_no_fine(self, mock_db_connection): + """Zwrot przed deadline'em – bez kary""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + # Loan data: wypożyczono dzisiaj, termin za 14 dni + due_date = date.today() + timedelta(days=14) + mock_cursor.fetchone.side_effect = [ + (1, 1, due_date), # copy_id, user_id, due_date + (0,), # current fine + ] + + response = client.post("/loans/1/return") + + assert response.status_code == 200 + assert response.json()["fine_added"] == 0 + assert response.json()["total_fine"] == 0 + assert response.json()["user_blocked"] is False + + def test_return_loan_overdue_with_fine(self, mock_db_connection): + """Zwrot po deadline'em – naliczenie kary""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + # Loan data: deadline był 5 dni temu + due_date = date.today() - timedelta(days=5) + mock_cursor.fetchone.side_effect = [ + (1, 1, due_date), # copy_id, user_id, due_date + (5,), # current fine (5 dni * 1 = 5) + ] + + response = client.post("/loans/1/return") + + assert response.status_code == 200 + assert response.json()["fine_added"] == 5 + assert response.json()["total_fine"] == 5 + assert response.json()["user_blocked"] is False + + def test_return_loan_blocks_user_over_20_fine(self, mock_db_connection): + """Kara > 20 – zablokowanie użytkownika""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + # Loan data: 25 dni opóźnienia + due_date = date.today() - timedelta(days=25) + mock_cursor.fetchone.side_effect = [ + (1, 1, due_date), # copy_id, user_id, due_date + (25,), # current fine (25 dni) + ] + + response = client.post("/loans/1/return") + + assert response.status_code == 200 + assert response.json()["fine_added"] == 25 + assert response.json()["total_fine"] == 25 + assert response.json()["user_blocked"] is True + + # Sprawdź czy UPDATE users SET is_active = FALSE został wywołany + mock_cursor.execute.assert_any_call( + "UPDATE users SET is_active = FALSE WHERE id = %s", + (1,) + ) + + def test_return_loan_not_found(self, mock_db_connection): + """Pożyczka nie istnieje""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + mock_cursor.fetchone.return_value = None + + response = client.post("/loans/999/return") + + assert response.status_code == 404 + assert "not found" in response.json()["detail"].lower() + + def test_return_loan_accumulates_fines(self, mock_db_connection): + """Kara się kumuluje – poprzednia kara + nowa kara""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + # Loan data: 10 dni opóźnienia, użytkownik ma już 15 kar + due_date = date.today() - timedelta(days=10) + mock_cursor.fetchone.side_effect = [ + (1, 1, due_date), # copy_id, user_id, due_date + (25,), # total fine (15 poprzednich + 10 nowych) + ] + + response = client.post("/loans/1/return") + + assert response.status_code == 200 + assert response.json()["fine_added"] == 10 + assert response.json()["total_fine"] == 25 + assert response.json()["user_blocked"] is True + + def test_return_loan_updates_book_status(self, mock_db_connection): + """Status książki zmienia się z 'loaned' na 'active'""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + due_date = date.today() + timedelta(days=14) + mock_cursor.fetchone.side_effect = [ + (1, 1, due_date), + (0,), + ] + + response = client.post("/loans/1/return") + + assert response.status_code == 200 + # Sprawdź czy UPDATE na book_copies został wykonany + mock_cursor.execute.assert_any_call( + "UPDATE book_copies SET status = 'active' WHERE id = %s", + (1,) + ) + +class TestCreateLoan: + """Testy dla tworzenia pożyczek""" + + def test_create_loan_success(self, mock_db_connection): + """Pomyślne utworzenie pożyczki""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + due_date = date.today() + timedelta(days=14) + loan_date = date.today() + + mock_cursor.fetchone.side_effect = [ + (1,), # copy exists + (1,), # user exists + (1, 1, 1, loan_date, due_date, None), # loan created + ] + + response = client.post( + "/loans/", + json={ + "copy_id": 1, + "user_id": 1, + "due_date": due_date.isoformat() + } + ) + + assert response.status_code == 200 + assert response.json()["id"] == 1 + assert response.json()["user_id"] == 1 + + def test_create_loan_book_copy_not_found(self, mock_db_connection): + """Egzemplarz książki nie istnieje""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + mock_cursor.fetchone.return_value = None + + response = client.post( + "/loans/", + json={ + "copy_id": 999, + "user_id": 1, + "due_date": date.today().isoformat() + } + ) + + assert response.status_code == 404 + assert "not found" in response.json()["detail"].lower() + + def test_create_loan_user_not_found(self, mock_db_connection): + """Użytkownik nie istnieje""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_db_connection.return_value.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + + mock_cursor.fetchone.side_effect = [ + (1,), # copy exists + None, # user doesn't exist + ] + + response = client.post( + "/loans/", + json={ + "copy_id": 1, + "user_id": 999, + "due_date": date.today().isoformat() + } + ) + + assert response.status_code == 404 + assert "user not found" in response.json()["detail"].lower() \ No newline at end of file diff --git a/backend/tools/__init__.py b/backend/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tools/book_scraper.py b/backend/tools/book_scraper.py new file mode 100644 index 0000000..7e5c15a --- /dev/null +++ b/backend/tools/book_scraper.py @@ -0,0 +1,254 @@ +import requests +import time +import logging +import hashlib +from deep_translator import GoogleTranslator +from database import get_db_connection + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" +) + + +def generate_deterministic_isbn(title, author): + combined = f"{title}:{author}".encode('utf-8') + hash_hex = hashlib.md5(combined).hexdigest()[:13] + return f"{hash_hex}" + + +def scrape_books(target_count=20000): + logging.info("Starting book scraping from Open Library...") + try: + conn = get_db_connection() + logging.info("Connected to the database.") + except Exception as e: + logging.error(f"Database connection error: {e}") + return + + queries = [ + #"fiction", "computers", "science", "history", "biography", + #"business", "art", "cooking", "psychology", "philosophy", + #"crime", "fantasy", "romance", "horror", "travel", + #"drama", "poetry", "self-help", "health", "thriller", + #"mystery", "young adult", "adventure", "classic", "children", + #"education", + + #"technology", + #"math", "astronomy", "nature", + #"medicine", "law", "politics", "economics", "sports", + #"music", "literature", "comics", "graphic novel", "war", + #"religion", "spirituality", "engineering", "programming", + + #"architecture", "design", "photography", "animation", "film", + #"dance", "theater", "humor", "satire", "paranormal", + #"superhero", "spy", "detective", "historical fiction", "dystopian", + #"utopian", "cyberpunk", "steampunk", "apocalyptic", "post-apocalyptic", + #"science fiction", "alternate history", "magic", + #"mythology", "legend", "folklore", "western", "noir", "essay", "journalism", "sociology", + #"anthropology", "linguistics", "ethics" + + + "fiction", "literary fiction", "contemporary fiction", "historical fiction", + "classics", "short stories", "young adult", "middle grade", "children", + "picture books", "romance", "romantic suspense", "new adult", + "fantasy", "epic fantasy", "urban fantasy", "high fantasy", + "magical realism", "mythology", "fairy tales", "folklore", "legends", + "science fiction", "space opera", "cyberpunk", "steampunk", "time travel", + "dystopian", "utopian", "post-apocalyptic", "alternate history", "speculative", + "mystery", "cozy mystery", "detective", "noir", "thriller", "psychological thriller", + "crime", "true crime", "spy", "espionage", "legal thriller", "political thriller", + "horror", "gothic", "paranormal", "supernatural", "occult", + "adventure", "survival", "western", "war", "naval", "pirate", + "biography", "memoir", "autobiography", "history", "military history", + "political history", "social history", "philosophy", "ethics", "religion", + "spirituality", "theology", "self-help", "personal development", "productivity", + "psychology", "psychiatry", "psychotherapy", "sociology", "anthropology", + "cultural studies", "linguistics", "language learning", "reference", + "dictionaries", "encyclopedias", "textbooks", "academic", "essays", "journalism", + "business", "entrepreneurship", "leadership", "management", "marketing", + "finance", "investing", "economics", "startups", "case studies", + "technology", "programming", "software engineering", "data science", "machine learning", + "artificial intelligence", "devops", "cybersecurity", "cryptography", "databases", + "web development", "mobile development", "cloud computing", "engineering", + "electronics", "robotics", "architecture", "urban planning", "design", + "graphic design", "photography", "film", "cinema studies", "theater", + "music", "music theory", "music history", "dance", "performance", + "comics", "graphic novels", "manga", "animation", "illustration", + "food & cooking", "baking", "wine & beverages", "nutrition", "gardening", + "nature", "wildlife", "natural history", "environment", "climate change", + "sustainability", "travel", "travel writing", "guidebooks", "geography", + "sports", "fitness", "yoga", "running", "cycling", "football", + "hobbies", "crafts", "knitting", "woodworking", "DIY", "home improvement", + "parenting", "education", "pedagogy", "children's education", "medical", + "medicine", "nursing", "public health", "nutrition science", "law", + "criminology", "policing", "legal studies", "mathematics", "statistics", + "logic", "astronomy", "physics", "chemistry", "biology", "neuroscience", + "transportation", "automotive", "aviation", "space", "astronautics", + "games", "video games", "game design", "tabletop RPGs", "board games", + "music writing", "food writing", "memoir of artists", "personal essays" + ] + + + num_categories = len(queries) + if target_count < num_categories: + categories_plan = {q: 1 for q in queries[:target_count]} + else: + base = target_count // num_categories + extra = target_count % num_categories + categories_plan = {} + for idx, q in enumerate(queries): + categories_plan[q] = base + (1 if idx < extra else 0) + + count = 0 + for query in queries: + to_fetch = categories_plan.get(query, 0) + if to_fetch == 0: + continue + fetched = 0 + logging.info(f"Scraping category: {query} ({to_fetch} books)") + for page in range(1, 10): + if fetched >= to_fetch or count >= target_count: + break + url = f"https://openlibrary.org/search.json?q={query}&page={page}" + logging.info(f"Fetching: {url}") + try: + response = requests.get(url, timeout=20).json() + docs = response.get("docs", []) + if not docs: + logging.info("No results for this query.") + break + + for item in docs: + if fetched >= to_fetch or count >= target_count: + break + + title = item.get("title", "Unknown") + authors = item.get("author_name", ["Unknown Author"]) + year = item.get("first_publish_year", 2000) + isbn_list = item.get("isbn", []) + + if isbn_list: + isbn = isbn_list[0] + else: + first_author = authors[0] if authors else "Unknown" + isbn = generate_deterministic_isbn(title, first_author) + + try: + with conn.cursor() as cur: + summary_pl = "Brak opisu." + summary_en = "Brak opisu." + work_key = item.get("key") + if work_key: + details_url = f"https://openlibrary.org{work_key}.json" + try: + details = requests.get(details_url, timeout=20).json() + summary_en = details.get("description", "Brak opisu.") + if isinstance(summary_en, dict): + summary_en = summary_en.get("value", "Brak opisu.") + + if summary_en and summary_en != "Brak opisu.": + try: + summary_pl = GoogleTranslator(source="auto", target="pl").translate( + summary_en) + except Exception as e: + logging.warning(f"Translation error for {title}: {e}") + summary_pl = "Brak opisu." + except Exception as e: + logging.warning(f"Could not fetch details from {details_url}: {e}") + summary_pl = "Brak opisu." + + if summary_pl == "Brak opisu.": + continue + + + logging.info(f"Trying to add book: {title} ({isbn})") + + cur.execute(""" + INSERT INTO books (title, published_year, isbn, summary) + VALUES (%s, %s, %s, %s) ON CONFLICT (isbn) DO NOTHING + RETURNING id + """, (title, int(year), isbn, summary_pl)) + + res = cur.fetchone() + if not res: + logging.info(f"Book already exists in database: {title} ({isbn})") + conn.commit() + continue + + book_id = res[0] + logging.info(f"Added book: {title} (id={book_id})") + + for author in authors: + parts = author.split(' ', 1) + fname = parts[0] + lname = parts[1] if len(parts) > 1 else "-" + + cur.execute( + "SELECT id FROM authors WHERE first_name = %s AND last_name = %s", + (fname, lname) + ) + author_row = cur.fetchone() + + if not author_row: + cur.execute( + "INSERT INTO authors (first_name, last_name) VALUES (%s, %s) RETURNING id", + (fname, lname) + ) + auth_id = cur.fetchone()[0] + else: + auth_id = author_row[0] + + cur.execute( + """ + INSERT INTO book_authors (book_id, author_id) + VALUES (%s, %s) ON CONFLICT DO NOTHING + """, + (book_id, auth_id) + ) + + cur.execute("SELECT id FROM categories WHERE name = %s", (query,)) + cat_row = cur.fetchone() + + if not cat_row: + cur.execute( + "INSERT INTO categories (name) VALUES (%s) RETURNING id", + (query,) + ) + cat_id = cur.fetchone()[0] + else: + cat_id = cat_row[0] + + cur.execute( + """ + INSERT INTO book_categories (book_id, category_id) + VALUES (%s, %s) ON CONFLICT DO NOTHING + """, + (book_id, cat_id) + ) + + barcode = f"SN-{isbn}" + cur.execute( + """ + INSERT INTO book_copies (book_id, barcode) + VALUES (%s, %s) ON CONFLICT (barcode) DO NOTHING + """, + (book_id, barcode) + ) + + conn.commit() + fetched += 1 + count += 1 + logging.info(f"[{count}/{target_count}] Added: {title}") + time.sleep(0.5) + + except Exception as e: + conn.rollback() + logging.error(f"Error saving '{title}': {e}") + + except Exception as e: + logging.error(f"API connection error: {e}") + time.sleep(5) + + conn.close() + logging.info("Book scraping finished.") \ No newline at end of file diff --git a/backend/uv.lock b/backend/uv.lock index 45e336c..307305c 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -41,26 +41,45 @@ name = "backend" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "deep-translator" }, { name = "dotenv" }, { name = "fastapi" }, { name = "psycopg", extra = ["binary"] }, { name = "psycopg2-binary" }, + { name = "pytest" }, { name = "python-dotenv" }, { name = "qdrant-client" }, + { name = "requests" }, { name = "uvicorn" }, ] [package.metadata] requires-dist = [ + { name = "deep-translator", specifier = ">=1.11.4" }, { name = "dotenv", specifier = ">=0.9.9" }, { name = "fastapi", specifier = ">=0.135.1" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.3.3" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "pytest", specifier = ">=9.0.3" }, { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "qdrant-client", specifier = ">=1.17.1" }, + { name = "requests", specifier = ">=2.33.1" }, { name = "uvicorn", specifier = ">=0.42.0" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -70,6 +89,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -91,6 +167,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "deep-translator" +version = "1.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/03/8fa7635c729a01de71151894cdf002ad6d245bfd6d1a731da864cf534dcf/deep_translator-1.11.4.tar.gz", hash = "sha256:801260c69231138707ea88a0955e484db7d40e210c9e0ae0f77372ffda5f4bf5", size = 36043, upload-time = "2023-06-28T19:55:23.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/61a8ef73236dbea83a1a063a8af2f8e1e41a0df64f122233938391d0f175/deep_translator-1.11.4-py3-none-any.whl", hash = "sha256:d635df037e23fa35d12fd42dab72a0b55c9dd19e6292009ee7207e3f30b9e60a", size = 42285, upload-time = "2023-06-28T19:55:20.928Z" }, +] + [[package]] name = "dotenv" version = "0.9.9" @@ -231,6 +320,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "numpy" version = "2.4.3" @@ -281,6 +379,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, ] +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "portalocker" version = "3.2.0" @@ -452,6 +568,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -492,6 +633,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" }, ] +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + [[package]] name = "starlette" version = "0.52.1" diff --git a/docker-compose.yml b/docker-compose.yml index 86a3f13..767a774 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,11 +29,8 @@ services: - "8001:8001" volumes: - ./backend:/app - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_HOST: db + env_file: + - .env depends_on: - db - qdrant