lab4 / client2.py
brestok's picture
init
a08f988
import json
import re
from datetime import date, datetime
from enum import IntEnum
from pathlib import Path
from typing import Any, List, Optional, Callable
import httpx
BASE_URL = "https://brestok-vika-server.hf.space"
DOWNLOADS_DIR = Path(__file__).parent / "downloads"
SEPARATOR = "=" * 70
PROMPT = "Select option: "
GENRES = [
"Fiction",
"Non-Fiction",
"Mystery",
"Sci-Fi",
"Fantasy",
"Biography",
"History",
"Romance",
"Thriller",
"Horror",
"Poetry",
"Drama",
"Comics",
"Other",
]
BOOK_STATUSES = ["Available", "Borrowed"]
WORK_DAYS = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]
NAME_PATTERN = re.compile(r"^[A-Za-zА-Яа-яЁёІіЇїЄєҐґ\s\-']+$")
TITLE_PATTERN = re.compile(r"^[A-Za-zА-Яа-яЁёІіЇїЄєҐґ0-9\s\-'.,!?:;\"()]+$")
UUID_PATTERN = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
# Validation error messages
MSG_IS_REQUIRED = "is required."
MSG_AT_LEAST_2_CHARS = "must be at least 2 characters."
MSG_AT_MOST_50_CHARS = "must be at most 50 characters."
MSG_ONLY_LETTERS_SPACES_HYPHENS_APOSTROPHES = "must contain only letters, spaces, hyphens, and apostrophes."
MSG_AT_MOST_100_CHARS = "must be at most 100 characters."
MSG_CONTAINS_INVALID_CHARS = "contains invalid characters."
MSG_AUTHOR_REQUIRED = "Author is required."
MSG_AUTHOR_AT_LEAST_2_CHARS = "Author must be at least 2 characters."
MSG_AUTHOR_AT_MOST_100_CHARS = "Author must be at most 100 characters."
MSG_AUTHOR_ONLY_LETTERS = "Author must contain only letters, spaces, hyphens, and apostrophes."
MSG_PAGES_AT_LEAST_1 = "Pages must be at least 1."
MSG_PAGES_AT_MOST_10000 = "Pages must be at most 10000."
MSG_YEAR_AT_LEAST_1000 = "Year must be at least 1000."
MSG_YEAR_CANNOT_BE_GREATER = "Year cannot be greater than {current_year}."
MSG_GENRE_MUST_BE_ONE_OF = "Genre must be one of: {', '.join(GENRES)}"
MSG_STATUS_MUST_BE_ONE_OF = "Status must be one of: {', '.join(BOOK_STATUSES)}"
MSG_EXPERIENCE_NEGATIVE = "Experience cannot be negative."
MSG_EXPERIENCE_AT_MOST_60 = "Experience must be at most 60 years."
MSG_AT_LEAST_ONE_WORK_DAY = "At least one work day is required."
MSG_INVALID_WORK_DAYS = "Invalid work days: {', '.join(invalid)}. Valid options: {', '.join(WORK_DAYS)}"
MSG_MUST_BE_VALID_UUID = "must be a valid UUID format."
MSG_DATE_YYYY_MM_DD = "Date must be in YYYY-MM-DD format."
MSG_AT_LEAST_ONE_ITEM_REQUIRED = "At least one item is required."
MSG_ITEM_EMPTY = "Item #{i} is empty."
MSG_ITEM_INVALID_UUID = "Item #{i} ({id_val}) is not a valid UUID."
# Error and info messages
MSG_ERROR_ENTER_VALID_INTEGER = " Error: Enter a valid integer."
MSG_ERROR_INVALID_CHOICE = " Error: Invalid choice."
MSG_ERROR_INVALID_NUMBER = " Error: Invalid number {p}."
MSG_ERROR_INVALID_VALUE = " Error: Invalid value '{p}'."
MSG_ERROR_AT_LEAST_ONE_VALUE = " Error: At least one value is required."
MSG_ERROR_BOOK_NOT_FOUND = " Error: Book with ID {book_id} not found."
MSG_ERROR_BOOK_NOT_FOUND_VALIDATED = " Error: Book with ID {validated} not found."
MSG_ERROR_VISITOR_NOT_FOUND = " Error: Visitor with ID {validated} not found."
MSG_ERROR_WORKER_NOT_FOUND = " Error: Worker with ID {validated} not found."
MSG_NO_BOOKS_FOUND = "No books found."
MSG_NO_VISITORS_FOUND = "No visitors found."
MSG_NO_WORKERS_FOUND = "No workers found."
MSG_ERROR_PREFIX = "Error: "
MSG_INVALID_OPTION = "Invalid option."
MSG_GOODBYE = "Goodbye."
MSG_NO_FIELDS_TO_UPDATE = "No fields to update."
MSG_DELETED = "Deleted."
MSG_FILE_SAVED_TO = "File saved to: "
MSG_CURRENT_BOOKS = "\nCurrent books:"
MSG_HISTORY = "\nHistory:"
MSG_VISITOR_DELETED = "Visitor deleted."
MSG_WORKER_DELETED = "Worker deleted."
MSG_INTERRUPTED_GOODBYE = "\nInterrupted. Goodbye."
# Prompts and labels
MSG_USE_COMMA_OR_STAR = "Use comma to separate values or type * for all."
MSG_ENTER_NUMBERS_OR_NAMES = "Enter numbers or names: "
MSG_USE_COMMA_STAR_OR_EMPTY = "Use comma to separate values, * for all, or leave empty to skip."
MSG_ENTER_NUMBERS_OR_NAMES_EMPTY_SKIP = "Enter numbers or names (empty to skip): "
MSG_PICK_NUMBER = "Pick number: "
MSG_PICK_NUMBER_SKIP = "Pick number (0 to skip): "
MSG_BOOK_ID_PROMPT = "Book ID: "
MSG_BOOK_IDS_COMMA_SEPARATED = "Book IDs (comma separated): "
MSG_VISITOR_ID_PROMPT = "Visitor ID: "
MSG_WORKER_ID_PROMPT = "Worker ID: "
MSG_TITLE_PROMPT = "Title: "
MSG_AUTHOR_PROMPT = "Author: "
MSG_PAGES_PROMPT = "Pages: "
MSG_YEAR_PROMPT = "Year: "
MSG_GENRE_PROMPT = "Genre:"
MSG_STATUS_PROMPT = "Status:"
MSG_NAME_PROMPT = "Name: "
MSG_SURNAME_PROMPT = "Surname: "
MSG_EXPERIENCE_PROMPT = "Experience (years): "
MSG_WORK_DAYS_PROMPT = "Work days:"
MSG_SELECT_WORK_DAYS = "Select work days:"
MSG_TITLE_SKIP_PROMPT = "Title (empty to skip): "
MSG_AUTHOR_SKIP_PROMPT = "Author (empty to skip): "
MSG_PAGES_SKIP_PROMPT = "Pages (empty to skip): "
MSG_YEAR_SKIP_PROMPT = "Year (empty to skip): "
MSG_NAME_SKIP_PROMPT = "Name (empty to skip): "
MSG_SURNAME_SKIP_PROMPT = "Surname (empty to skip): "
MSG_EXPERIENCE_SKIP_PROMPT = "Experience (empty to skip): "
MSG_LEAVE_EMPTY_TO_SKIP = "Leave fields empty to skip them."
MSG_BORROW_DATE_PROMPT = "Borrow date (YYYY-MM-DD)"
MSG_RETURN_DATE_PROMPT = "Return date (YYYY-MM-DD)"
class ValidationError(Exception):
pass
def ensure_downloads_dir():
DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True)
def save_to_file(table: str, entity_id: str, content: str) -> str:
ensure_downloads_dir()
safe_id = entity_id.replace("/", "_").replace("\\", "_")
filename = f"{table}-{safe_id}.json"
filepath = DOWNLOADS_DIR / filename
if isinstance(content, (dict, list)):
text = json.dumps(content, ensure_ascii=False, indent=2)
else:
try:
parsed = json.loads(content)
text = json.dumps(parsed, ensure_ascii=False, indent=2)
except Exception:
text = content
filepath.write_text(text, encoding="utf-8")
return str(filepath)
def validate_name(value: str, field_name: str = "Name") -> str:
value = value.strip()
if not value:
raise ValidationError(f"{field_name} {MSG_IS_REQUIRED}")
if len(value) < 2:
raise ValidationError(f"{field_name} {MSG_AT_LEAST_2_CHARS}")
if len(value) > 50:
raise ValidationError(f"{field_name} {MSG_AT_MOST_50_CHARS}")
if not NAME_PATTERN.match(value):
raise ValidationError(f"{field_name} {MSG_ONLY_LETTERS_SPACES_HYPHENS_APOSTROPHES}")
return value
def validate_title(value: str, field_name: str = "Title") -> str:
value = value.strip()
if not value:
raise ValidationError(f"{field_name} {MSG_IS_REQUIRED}")
if len(value) < 2:
raise ValidationError(f"{field_name} {MSG_AT_LEAST_2_CHARS}")
if len(value) > 100:
raise ValidationError(f"{field_name} {MSG_AT_MOST_100_CHARS}")
if not TITLE_PATTERN.match(value):
raise ValidationError(f"{field_name} {MSG_CONTAINS_INVALID_CHARS}")
return value
def validate_author(value: str) -> str:
value = value.strip()
if not value:
raise ValidationError(MSG_AUTHOR_REQUIRED)
if len(value) < 2:
raise ValidationError(MSG_AUTHOR_AT_LEAST_2_CHARS)
if len(value) > 100:
raise ValidationError(MSG_AUTHOR_AT_MOST_100_CHARS)
if not NAME_PATTERN.match(value):
raise ValidationError(MSG_AUTHOR_ONLY_LETTERS)
return value
def validate_pages(value: int) -> int:
if value < 1:
raise ValidationError(MSG_PAGES_AT_LEAST_1)
if value > 10000:
raise ValidationError(MSG_PAGES_AT_MOST_10000)
return value
def validate_year(value: int) -> int:
current_year = date.today().year
if value < 1000:
raise ValidationError(MSG_YEAR_AT_LEAST_1000)
if value > current_year:
raise ValidationError(MSG_YEAR_CANNOT_BE_GREATER)
return value
def validate_genre(value: str) -> str:
if value not in GENRES:
raise ValidationError(MSG_GENRE_MUST_BE_ONE_OF)
return value
def validate_status(value: str) -> str:
if value not in BOOK_STATUSES:
raise ValidationError(MSG_STATUS_MUST_BE_ONE_OF)
return value
def validate_experience(value: int) -> int:
if value < 0:
raise ValidationError(MSG_EXPERIENCE_NEGATIVE)
if value > 60:
raise ValidationError(MSG_EXPERIENCE_AT_MOST_60)
return value
def validate_work_days(days: List[str]) -> List[str]:
if not days:
raise ValidationError(MSG_AT_LEAST_ONE_WORK_DAY)
invalid = [d for d in days if d not in WORK_DAYS]
if invalid:
raise ValidationError(MSG_INVALID_WORK_DAYS)
return days
def validate_uuid(value: str, field_name: str = "ID") -> str:
value = value.strip()
if not value:
raise ValidationError(f"{field_name} {MSG_IS_REQUIRED}")
if not UUID_PATTERN.match(value):
raise ValidationError(f"{field_name} {MSG_MUST_BE_VALID_UUID}")
return value
def validate_date(value: str) -> str:
value = value.strip()
if not value:
return date.today().isoformat()
try:
datetime.strptime(value, "%Y-%m-%d")
return value
except ValueError:
raise ValidationError(MSG_DATE_YYYY_MM_DD)
def validate_ids_list(ids: List[str]) -> List[str]:
if not ids:
raise ValidationError(MSG_AT_LEAST_ONE_ITEM_REQUIRED)
validated = []
for i, id_val in enumerate(ids, 1):
id_val = id_val.strip()
if not id_val:
raise ValidationError(MSG_ITEM_EMPTY)
if not UUID_PATTERN.match(id_val):
raise ValidationError(MSG_ITEM_INVALID_UUID)
validated.append(id_val)
return validated
class LibraryClient:
def __init__(self, base_url: str = BASE_URL):
self.client = httpx.Client(base_url=base_url, timeout=30.0)
def close(self):
self.client.close()
def _handle_response(self, response: httpx.Response):
content_type = response.headers.get("content-type", "").lower()
if response.status_code >= 400:
message = response.text
try:
payload = response.json()
if isinstance(payload, dict):
message = (
payload.get("error", {}).get("message")
or payload.get("message")
or str(payload)
)
except Exception:
message = response.text
raise RuntimeError(f"{response.status_code}: {message}")
if "application/json" in content_type:
payload = response.json()
if isinstance(payload, dict) and "successful" in payload:
if payload.get("successful"):
return payload.get("data")
error = payload.get("error") or {}
raise RuntimeError(error.get("message") or str(payload))
return payload
return response
def get(self, path: str, **kwargs):
response = self.client.get(path, **kwargs)
return self._handle_response(response)
def post(self, path: str, **kwargs):
response = self.client.post(path, **kwargs)
return self._handle_response(response)
def patch(self, path: str, **kwargs):
response = self.client.patch(path, **kwargs)
return self._handle_response(response)
def delete(self, path: str, **kwargs):
response = self.client.delete(path, **kwargs)
return self._handle_response(response)
def list_books(self):
return self.get("/books/all")
def get_book(self, book_id: str):
return self.get(f"/books/{book_id}")
def book_exists(self, book_id: str) -> bool:
try:
self.get_book(book_id)
return True
except RuntimeError:
return False
def create_book(
self,
title: str,
author: str,
pages: int,
year: int,
genre: str,
status: str = "Available",
):
payload = {
"title": title,
"author": author,
"pages": pages,
"year": year,
"genre": genre,
"status": status,
}
return self.post("/books/create", json=payload)
def update_book(
self,
book_id: str,
title: Optional[str] = None,
author: Optional[str] = None,
pages: Optional[int] = None,
year: Optional[int] = None,
genre: Optional[str] = None,
status: Optional[str] = None,
):
payload = {
k: v
for k, v in {
"title": title,
"author": author,
"pages": pages,
"year": year,
"genre": genre,
"status": status,
}.items()
if v not in (None, "")
}
return self.patch(f"/books/{book_id}", json=payload)
def delete_book(self, book_id: str):
return self.delete(f"/books/{book_id}")
def borrow_books(
self, book_ids: List[str], visitor_id: str, worker_id: str, borrow_date: str
):
payload = {
"bookIds": book_ids,
"visitorId": visitor_id,
"workerId": worker_id,
"borrowDate": borrow_date,
}
return self.post("/books/borrow", json=payload)
def return_books(
self, book_ids: List[str], visitor_id: str, worker_id: str, return_date: str
):
payload = {
"bookIds": book_ids,
"visitorId": visitor_id,
"workerId": worker_id,
"returnDate": return_date,
}
return self.post("/books/return", json=payload)
def download_book(self, book_id: str) -> str:
response = self.get(f"/books/{book_id}/download")
return response.text if isinstance(response, httpx.Response) else str(response)
def list_visitors(self):
return self.get("/visitors/all")
def get_visitor(self, visitor_id: str):
return self.get(f"/visitors/{visitor_id}")
def visitor_exists(self, visitor_id: str) -> bool:
try:
self.get_visitor(visitor_id)
return True
except RuntimeError:
return False
def create_visitor(self, name: str, surname: str):
payload = {"name": name, "surname": surname}
return self.post("/visitors/create", json=payload)
def update_visitor(
self,
visitor_id: str,
name: Optional[str] = None,
surname: Optional[str] = None,
):
payload = {k: v for k, v in {"name": name, "surname": surname}.items() if v}
return self.patch(f"/visitors/{visitor_id}", json=payload)
def delete_visitor(self, visitor_id: str):
return self.delete(f"/visitors/delete/{visitor_id}")
def download_visitor(self, visitor_id: str) -> str:
response = self.get(f"/visitors/{visitor_id}/download")
return response.text if isinstance(response, httpx.Response) else str(response)
def list_workers(self):
return self.get("/workers/all")
def get_worker(self, worker_id: str):
return self.get(f"/workers/{worker_id}")
def worker_exists(self, worker_id: str) -> bool:
try:
self.get_worker(worker_id)
return True
except RuntimeError:
return False
def create_worker(
self, name: str, surname: str, experience: int, work_days: List[str]
):
payload = {
"name": name,
"surname": surname,
"experience": experience,
"workDays": work_days,
}
return self.post("/workers/create", json=payload)
def update_worker(
self,
worker_id: str,
name: Optional[str] = None,
surname: Optional[str] = None,
experience: Optional[int] = None,
work_days: Optional[List[str]] = None,
):
payload = {
k: v
for k, v in {
"name": name,
"surname": surname,
"experience": experience,
"workDays": work_days,
}.items()
if v not in (None, "")
}
return self.patch(f"/workers/{worker_id}", json=payload)
def delete_worker(self, worker_id: str):
return self.delete(f"/workers/{worker_id}")
def workers_by_days(self, work_days: List[str]):
params = [("workDays", day) for day in work_days]
return self.get("/workers/by-work-days", params=params)
def download_worker(self, worker_id: str) -> str:
response = self.get(f"/workers/{worker_id}/download")
return response.text if isinstance(response, httpx.Response) else str(response)
class MenuChoice(IntEnum):
LIST_BOOKS = 1
VIEW_BOOK = 2
CREATE_BOOK = 3
UPDATE_BOOK = 4
DELETE_BOOK = 5
BORROW_BOOKS = 6
RETURN_BOOKS = 7
DOWNLOAD_BOOK = 8
LIST_VISITORS = 9
VIEW_VISITOR = 10
CREATE_VISITOR = 11
UPDATE_VISITOR = 12
DELETE_VISITOR = 13
DOWNLOAD_VISITOR = 14
LIST_WORKERS = 15
VIEW_WORKER = 16
CREATE_WORKER = 17
UPDATE_WORKER = 18
DELETE_WORKER = 19
WORKERS_BY_DAYS = 20
DOWNLOAD_WORKER = 21
EXIT = 0
MENU_TEXT = {
MenuChoice.LIST_BOOKS: "List books",
MenuChoice.VIEW_BOOK: "Get book by ID",
MenuChoice.CREATE_BOOK: "Create book",
MenuChoice.UPDATE_BOOK: "Update book",
MenuChoice.DELETE_BOOK: "Delete book",
MenuChoice.BORROW_BOOKS: "Borrow books",
MenuChoice.RETURN_BOOKS: "Return books",
MenuChoice.DOWNLOAD_BOOK: "Download book as file",
MenuChoice.LIST_VISITORS: "List visitors",
MenuChoice.VIEW_VISITOR: "Get visitor by ID",
MenuChoice.CREATE_VISITOR: "Create visitor",
MenuChoice.UPDATE_VISITOR: "Update visitor",
MenuChoice.DELETE_VISITOR: "Delete visitor",
MenuChoice.DOWNLOAD_VISITOR: "Download visitor as file",
MenuChoice.LIST_WORKERS: "List workers",
MenuChoice.VIEW_WORKER: "Get worker by ID",
MenuChoice.CREATE_WORKER: "Create worker",
MenuChoice.UPDATE_WORKER: "Update worker",
MenuChoice.DELETE_WORKER: "Delete worker",
MenuChoice.WORKERS_BY_DAYS: "Find workers by work days",
MenuChoice.DOWNLOAD_WORKER: "Download worker as file",
MenuChoice.EXIT: "Exit",
}
def prompt_with_validation(label: str, validator: Callable[[str], str]) -> str:
while True:
raw = input(label).strip()
try:
return validator(raw)
except ValidationError as e:
print(f" Error: {e}")
def prompt_int_with_validation(label: str, validator: Callable[[int], int]) -> int:
while True:
raw = input(label).strip()
try:
value = int(raw)
return validator(value)
except ValueError:
print(MSG_ERROR_ENTER_VALID_INTEGER)
except ValidationError as e:
print(f" Error: {e}")
def prompt_optional_with_validation(label: str, validator: Callable[[str], str]) -> Optional[str]:
while True:
raw = input(label).strip()
if not raw:
return None
try:
return validator(raw)
except ValidationError as e:
print(f" Error: {e}")
def prompt_optional_int_with_validation(label: str, validator: Callable[[int], int]) -> Optional[int]:
while True:
raw = input(label).strip()
if not raw:
return None
try:
value = int(raw)
return validator(value)
except ValueError:
print(MSG_ERROR_ENTER_VALID_INTEGER)
except ValidationError as e:
print(f" Error: {e}")
def prompt_choice(label: str, options: List[str]) -> str:
while True:
print(label)
for idx, option in enumerate(options, 1):
print(f" {idx}. {option}")
raw = input(MSG_PICK_NUMBER).strip()
try:
selected = int(raw)
if 1 <= selected <= len(options):
return options[selected - 1]
except ValueError:
pass
print(MSG_ERROR_INVALID_CHOICE)
def prompt_optional_choice(label: str, options: List[str]) -> Optional[str]:
while True:
print(label)
print(" 0. Skip")
for idx, option in enumerate(options, 1):
print(f" {idx}. {option}")
raw = input(MSG_PICK_NUMBER_SKIP).strip()
try:
selected = int(raw)
if selected == 0:
return None
if 1 <= selected <= len(options):
return options[selected - 1]
except ValueError:
pass
print(MSG_ERROR_INVALID_CHOICE)
def prompt_multi_choice(label: str, options: List[str]) -> List[str]:
print(label)
print(MSG_USE_COMMA_OR_STAR)
for idx, option in enumerate(options, 1):
print(f" {idx}. {option}")
while True:
raw = input(MSG_ENTER_NUMBERS_OR_NAMES).strip()
if raw == "*":
return options[:]
parts = [p.strip() for p in raw.split(",") if p.strip()]
result = []
valid = True
for p in parts:
if p in options:
result.append(p)
elif p.isdigit():
idx = int(p)
if 1 <= idx <= len(options):
result.append(options[idx - 1])
else:
print(MSG_ERROR_INVALID_NUMBER)
valid = False
break
else:
print(MSG_ERROR_INVALID_VALUE)
valid = False
break
if valid and result:
try:
return validate_work_days(result)
except ValidationError as e:
print(f" Error: {e}")
elif valid and not result:
print(MSG_ERROR_AT_LEAST_ONE_VALUE)
def prompt_optional_multi_choice(label: str, options: List[str]) -> Optional[List[str]]:
print(label)
print(MSG_USE_COMMA_STAR_OR_EMPTY)
for idx, option in enumerate(options, 1):
print(f" {idx}. {option}")
while True:
raw = input(MSG_ENTER_NUMBERS_OR_NAMES_EMPTY_SKIP).strip()
if not raw:
return None
if raw == "*":
return options[:]
parts = [p.strip() for p in raw.split(",") if p.strip()]
result = []
valid = True
for p in parts:
if p in options:
result.append(p)
elif p.isdigit():
idx = int(p)
if 1 <= idx <= len(options):
result.append(options[idx - 1])
else:
print(MSG_ERROR_INVALID_NUMBER)
valid = False
break
else:
print(MSG_ERROR_INVALID_VALUE)
valid = False
break
if valid and result:
try:
return validate_work_days(result)
except ValidationError as e:
print(f" Error: {e}")
elif valid and not result:
return None
def prompt_book_ids(label: str, client: LibraryClient) -> List[str]:
while True:
raw = input(label).strip()
parts = [p.strip() for p in raw.split(",") if p.strip()]
try:
validated = validate_ids_list(parts)
for book_id in validated:
if not client.book_exists(book_id):
print(MSG_ERROR_BOOK_NOT_FOUND)
raise ValidationError("Book not found")
return validated
except ValidationError as e:
if "not found" not in str(e):
print(f" Error: {e}")
def prompt_book_id(label: str, client: LibraryClient, check_exists: bool = True) -> str:
while True:
raw = input(label).strip()
try:
validated = validate_uuid(raw, "Book ID")
if check_exists and not client.book_exists(validated):
print(MSG_ERROR_BOOK_NOT_FOUND_VALIDATED)
continue
return validated
except ValidationError as e:
print(f" Error: {e}")
def prompt_visitor_id(label: str, client: LibraryClient, check_exists: bool = True) -> str:
while True:
raw = input(label).strip()
try:
validated = validate_uuid(raw, "Visitor ID")
if check_exists and not client.visitor_exists(validated):
print(MSG_ERROR_VISITOR_NOT_FOUND)
continue
return validated
except ValidationError as e:
print(f" Error: {e}")
def prompt_worker_id(label: str, client: LibraryClient, check_exists: bool = True) -> str:
while True:
raw = input(label).strip()
try:
validated = validate_uuid(raw, "Worker ID")
if check_exists and not client.worker_exists(validated):
print(MSG_ERROR_WORKER_NOT_FOUND)
continue
return validated
except ValidationError as e:
print(f" Error: {e}")
def prompt_date(label: str) -> str:
today = date.today().isoformat()
while True:
raw = input(f"{label} [{today}]: ").strip()
try:
return validate_date(raw)
except ValidationError as e:
print(f" Error: {e}")
def print_books(books: Any):
if not books:
print(MSG_NO_BOOKS_FOUND)
return
for book in books:
print(SEPARATOR)
print(f"ID: {book.get('id')}")
print(f"Title: {book.get('title')}")
print(f"Author: {book.get('author')}")
print(f"Pages: {book.get('pages')}")
print(f"Year: {book.get('year')}")
print(f"Genre: {book.get('genre')}")
print(f"Status: {book.get('status')}")
def print_visitors(visitors: Any):
if not visitors:
print(MSG_NO_VISITORS_FOUND)
return
for visitor in visitors:
print(SEPARATOR)
print(f"ID: {visitor.get('id')}")
print(f"Name: {visitor.get('name')} {visitor.get('surname')}")
print(f"Registered: {visitor.get('registrationDate')}")
current = visitor.get("currentBooks") or []
history = visitor.get("history") or []
print(f"Current books: {len(current)}")
print(f"History: {len(history)}")
def print_workers(workers: Any):
if not workers:
print(MSG_NO_WORKERS_FOUND)
return
for worker in workers:
print(SEPARATOR)
print(f"ID: {worker.get('id')}")
print(f"Name: {worker.get('name')} {worker.get('surname')}")
print(f"Experience: {worker.get('experience')} years")
print(f"Work days: {', '.join(worker.get('workDays', []))}")
issued = worker.get("issuedBooks") or []
print(f"Issued books: {len(issued)}")
def main_menu() -> Optional[MenuChoice]:
print("\n" + SEPARATOR)
for choice in MenuChoice:
print(f"{choice.value}. {MENU_TEXT[choice]}")
raw = input(PROMPT).strip()
try:
return MenuChoice(int(raw))
except Exception:
return None
def handle_error(error: Exception):
print(f"{MSG_ERROR_PREFIX}{error}")
def main():
client = LibraryClient()
try:
while True:
choice = main_menu()
if choice is None:
print(MSG_INVALID_OPTION)
continue
if choice == MenuChoice.EXIT:
print(MSG_GOODBYE)
break
try:
if choice == MenuChoice.LIST_BOOKS:
print_books(client.list_books())
elif choice == MenuChoice.VIEW_BOOK:
book_id = prompt_book_id(MSG_BOOK_ID_PROMPT, client)
print_books([client.get_book(book_id)])
elif choice == MenuChoice.CREATE_BOOK:
title = prompt_with_validation(MSG_TITLE_PROMPT, validate_title)
author = prompt_with_validation(MSG_AUTHOR_PROMPT, validate_author)
pages = prompt_int_with_validation(MSG_PAGES_PROMPT, validate_pages)
year = prompt_int_with_validation(MSG_YEAR_PROMPT, validate_year)
genre = prompt_choice(MSG_GENRE_PROMPT, GENRES)
status = prompt_choice(MSG_STATUS_PROMPT, BOOK_STATUSES)
print_books([client.create_book(title, author, pages, year, genre, status)])
elif choice == MenuChoice.UPDATE_BOOK:
book_id = prompt_book_id(MSG_BOOK_ID_PROMPT, client)
print(MSG_LEAVE_EMPTY_TO_SKIP)
title = prompt_optional_with_validation(MSG_TITLE_SKIP_PROMPT, validate_title)
author = prompt_optional_with_validation(MSG_AUTHOR_SKIP_PROMPT, validate_author)
pages = prompt_optional_int_with_validation(MSG_PAGES_SKIP_PROMPT, validate_pages)
year = prompt_optional_int_with_validation(MSG_YEAR_SKIP_PROMPT, validate_year)
genre = prompt_optional_choice(MSG_GENRE_PROMPT, GENRES)
status = prompt_optional_choice(MSG_STATUS_PROMPT, BOOK_STATUSES)
if not any([title, author, pages, year, genre, status]):
print(MSG_NO_FIELDS_TO_UPDATE)
continue
updated = client.update_book(book_id, title, author, pages, year, genre, status)
print_books([updated])
elif choice == MenuChoice.DELETE_BOOK:
book_id = prompt_book_id(MSG_BOOK_ID_PROMPT, client)
result = client.delete_book(book_id)
print(result.get("message") if isinstance(result, dict) else MSG_DELETED)
elif choice == MenuChoice.BORROW_BOOKS:
book_ids = prompt_book_ids(MSG_BOOK_IDS_COMMA_SEPARATED, client)
visitor_id = prompt_visitor_id(MSG_VISITOR_ID_PROMPT, client)
worker_id = prompt_worker_id(MSG_WORKER_ID_PROMPT, client)
borrow_date = prompt_date(MSG_BORROW_DATE_PROMPT)
result = client.borrow_books(book_ids, visitor_id, worker_id, borrow_date)
print(result.get("message") if isinstance(result, dict) else result)
elif choice == MenuChoice.RETURN_BOOKS:
book_ids = prompt_book_ids(MSG_BOOK_IDS_COMMA_SEPARATED, client)
visitor_id = prompt_visitor_id(MSG_VISITOR_ID_PROMPT, client)
worker_id = prompt_worker_id(MSG_WORKER_ID_PROMPT, client)
return_date = prompt_date(MSG_RETURN_DATE_PROMPT)
result = client.return_books(book_ids, visitor_id, worker_id, return_date)
print(result.get("message") if isinstance(result, dict) else result)
elif choice == MenuChoice.DOWNLOAD_BOOK:
book_id = prompt_book_id(MSG_BOOK_ID_PROMPT, client)
content = client.download_book(book_id)
filepath = save_to_file("books", book_id, content)
print(f"{MSG_FILE_SAVED_TO}{filepath}")
elif choice == MenuChoice.LIST_VISITORS:
print_visitors(client.list_visitors())
elif choice == MenuChoice.VIEW_VISITOR:
visitor_id = prompt_visitor_id(MSG_VISITOR_ID_PROMPT, client)
visitor = client.get_visitor(visitor_id)
print_visitors([visitor])
current = visitor.get("currentBooks") or []
history = visitor.get("history") or []
if current:
print(MSG_CURRENT_BOOKS)
print_books(current)
if history:
print(MSG_HISTORY)
print_books(history)
elif choice == MenuChoice.CREATE_VISITOR:
name = prompt_with_validation(MSG_NAME_PROMPT, lambda v: validate_name(v, "Name"))
surname = prompt_with_validation(MSG_SURNAME_PROMPT, lambda v: validate_name(v, "Surname"))
print_visitors([client.create_visitor(name, surname)])
elif choice == MenuChoice.UPDATE_VISITOR:
visitor_id = prompt_visitor_id(MSG_VISITOR_ID_PROMPT, client)
print(MSG_LEAVE_EMPTY_TO_SKIP)
name = prompt_optional_with_validation(MSG_NAME_PROMPT + " (empty to skip): ",
lambda v: validate_name(v, "Name"))
surname = prompt_optional_with_validation(MSG_SURNAME_PROMPT + " (empty to skip): ",
lambda v: validate_name(v, "Surname"))
if not name and not surname:
print(MSG_NO_FIELDS_TO_UPDATE)
continue
visitor = client.update_visitor(visitor_id, name, surname)
print_visitors([visitor])
elif choice == MenuChoice.DELETE_VISITOR:
visitor_id = prompt_visitor_id(MSG_VISITOR_ID_PROMPT, client)
client.delete_visitor(visitor_id)
print(MSG_VISITOR_DELETED)
elif choice == MenuChoice.DOWNLOAD_VISITOR:
visitor_id = prompt_visitor_id(MSG_VISITOR_ID_PROMPT, client)
content = client.download_visitor(visitor_id)
filepath = save_to_file("visitors", visitor_id, content)
print(f"{MSG_FILE_SAVED_TO}{filepath}")
elif choice == MenuChoice.LIST_WORKERS:
print_workers(client.list_workers())
elif choice == MenuChoice.VIEW_WORKER:
worker_id = prompt_worker_id(MSG_WORKER_ID_PROMPT, client)
worker = client.get_worker(worker_id)
print_workers([worker])
issued = worker.get("issuedBooks") or []
if issued:
print("\nIssued books:")
print_books(issued)
elif choice == MenuChoice.CREATE_WORKER:
name = prompt_with_validation(MSG_NAME_PROMPT, lambda v: validate_name(v, "Name"))
surname = prompt_with_validation(MSG_SURNAME_PROMPT, lambda v: validate_name(v, "Surname"))
experience = prompt_int_with_validation(MSG_EXPERIENCE_PROMPT, validate_experience)
work_days = prompt_multi_choice(MSG_WORK_DAYS_PROMPT, WORK_DAYS)
print_workers([client.create_worker(name, surname, experience, work_days)])
elif choice == MenuChoice.UPDATE_WORKER:
worker_id = prompt_worker_id(MSG_WORKER_ID_PROMPT, client)
print(MSG_LEAVE_EMPTY_TO_SKIP)
name = prompt_optional_with_validation(MSG_NAME_PROMPT + " (empty to skip): ",
lambda v: validate_name(v, "Name"))
surname = prompt_optional_with_validation(MSG_SURNAME_PROMPT + " (empty to skip): ",
lambda v: validate_name(v, "Surname"))
experience = prompt_optional_int_with_validation(MSG_EXPERIENCE_PROMPT + " (empty to skip): ",
validate_experience)
work_days = prompt_optional_multi_choice(MSG_WORK_DAYS_PROMPT, WORK_DAYS)
if not any([name, surname, experience is not None, work_days]):
print(MSG_NO_FIELDS_TO_UPDATE)
continue
worker = client.update_worker(worker_id, name, surname, experience, work_days)
print_workers([worker])
elif choice == MenuChoice.DELETE_WORKER:
worker_id = prompt_worker_id(MSG_WORKER_ID_PROMPT, client)
client.delete_worker(worker_id)
print(MSG_WORKER_DELETED)
elif choice == MenuChoice.WORKERS_BY_DAYS:
work_days = prompt_multi_choice(MSG_SELECT_WORK_DAYS, WORK_DAYS)
print_workers(client.workers_by_days(work_days))
elif choice == MenuChoice.DOWNLOAD_WORKER:
worker_id = prompt_worker_id(MSG_WORKER_ID_PROMPT, client)
content = client.download_worker(worker_id)
filepath = save_to_file("workers", worker_id, content)
print(f"{MSG_FILE_SAVED_TO}{filepath}")
else:
print(MSG_INVALID_OPTION)
except (RuntimeError, httpx.HTTPError, ValueError) as error:
handle_error(error)
except KeyboardInterrupt:
print(MSG_INTERRUPTED_GOODBYE)
finally:
client.close()
if __name__ == "__main__":
main()