|
|
import json |
|
|
import re |
|
|
from datetime import date, datetime |
|
|
from enum import IntEnum |
|
|
from pathlib import Path |
|
|
from typing import Any, List, Optional, Callable |
|
|
from uuid import UUID |
|
|
|
|
|
import httpx |
|
|
|
|
|
FORUM_BASE_URL = "https://brestok-lab4.hf.space" |
|
|
LIBRARY_BASE_URL = "https://brestok-vika-server.hf.space" |
|
|
DOWNLOADS_DIR = Path(__file__).parent / "downloads" |
|
|
|
|
|
SEPARATOR = "=" * 70 |
|
|
SEPARATOR_60 = "=" * 60 |
|
|
SEPARATOR_90 = "-" * 90 |
|
|
DASH_LINE_60 = "-" * 60 |
|
|
PROMPT = "Select option: " |
|
|
|
|
|
TABLE_PARTICIPANTS = "participants" |
|
|
TABLE_TOPICS = "topics" |
|
|
|
|
|
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}$") |
|
|
|
|
|
|
|
|
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} is required.") |
|
|
if len(value) < 2: |
|
|
raise ValidationError(f"{field_name} must be at least 2 characters.") |
|
|
if len(value) > 50: |
|
|
raise ValidationError(f"{field_name} must be at most 50 characters.") |
|
|
if not NAME_PATTERN.match(value): |
|
|
raise ValidationError(f"{field_name} must contain only letters, spaces, hyphens, and apostrophes.") |
|
|
return value |
|
|
|
|
|
|
|
|
def validate_title(value: str, field_name: str = "Title") -> str: |
|
|
value = value.strip() |
|
|
if not value: |
|
|
raise ValidationError(f"{field_name} is required.") |
|
|
if len(value) < 2: |
|
|
raise ValidationError(f"{field_name} must be at least 2 characters.") |
|
|
if len(value) > 100: |
|
|
raise ValidationError(f"{field_name} must be at most 100 characters.") |
|
|
if not TITLE_PATTERN.match(value): |
|
|
raise ValidationError(f"{field_name} contains invalid characters.") |
|
|
return value |
|
|
|
|
|
|
|
|
def validate_author(value: str) -> str: |
|
|
value = value.strip() |
|
|
if not value: |
|
|
raise ValidationError("Author is required.") |
|
|
if len(value) < 2: |
|
|
raise ValidationError("Author must be at least 2 characters.") |
|
|
if len(value) > 100: |
|
|
raise ValidationError("Author must be at most 100 characters.") |
|
|
if not NAME_PATTERN.match(value): |
|
|
raise ValidationError("Author must contain only letters, spaces, hyphens, and apostrophes.") |
|
|
return value |
|
|
|
|
|
|
|
|
def validate_pages(value: int) -> int: |
|
|
if value < 1: |
|
|
raise ValidationError("Pages must be at least 1.") |
|
|
if value > 10000: |
|
|
raise ValidationError("Pages must be at most 10000.") |
|
|
return value |
|
|
|
|
|
|
|
|
def validate_year(value: int) -> int: |
|
|
current_year = date.today().year |
|
|
if value < 1000: |
|
|
raise ValidationError("Year must be at least 1000.") |
|
|
if value > current_year: |
|
|
raise ValidationError(f"Year cannot be greater than {current_year}.") |
|
|
return value |
|
|
|
|
|
|
|
|
def validate_genre(value: str) -> str: |
|
|
if value not in GENRES: |
|
|
raise ValidationError(f"Genre must be one of: {', '.join(GENRES)}") |
|
|
return value |
|
|
|
|
|
|
|
|
def validate_status(value: str) -> str: |
|
|
if value not in BOOK_STATUSES: |
|
|
raise ValidationError(f"Status must be one of: {', '.join(BOOK_STATUSES)}") |
|
|
return value |
|
|
|
|
|
|
|
|
def validate_experience(value: int) -> int: |
|
|
if value < 0: |
|
|
raise ValidationError("Experience cannot be negative.") |
|
|
if value > 60: |
|
|
raise ValidationError("Experience must be at most 60 years.") |
|
|
return value |
|
|
|
|
|
|
|
|
def validate_work_days(days: List[str]) -> List[str]: |
|
|
if not days: |
|
|
raise ValidationError("At least one work day is required.") |
|
|
invalid = [d for d in days if d not in WORK_DAYS] |
|
|
if invalid: |
|
|
raise ValidationError(f"Invalid work days: {', '.join(invalid)}. Valid options: {', '.join(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} is required.") |
|
|
if not UUID_PATTERN.match(value): |
|
|
raise ValidationError(f"{field_name} must be a valid UUID format.") |
|
|
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("Date must be in YYYY-MM-DD format.") |
|
|
|
|
|
|
|
|
def validate_ids_list(ids: List[str]) -> List[str]: |
|
|
if not ids: |
|
|
raise ValidationError("At least one item is required.") |
|
|
validated = [] |
|
|
for i, id_val in enumerate(ids, 1): |
|
|
id_val = id_val.strip() |
|
|
if not id_val: |
|
|
raise ValidationError(f"Item #{i} is empty.") |
|
|
if not UUID_PATTERN.match(id_val): |
|
|
raise ValidationError(f"Item #{i} ({id_val}) is not a valid UUID.") |
|
|
validated.append(id_val) |
|
|
return validated |
|
|
|
|
|
|
|
|
def validate_activity_rating(value: float) -> float: |
|
|
if value < 0.1 or value > 5.0: |
|
|
raise ValidationError("Activity rating must be between 0.1 and 5.0.") |
|
|
return value |
|
|
|
|
|
|
|
|
class ForumClient: |
|
|
def __init__(self, base_url: str = FORUM_BASE_URL): |
|
|
self.base_url = base_url |
|
|
self.client = httpx.Client(timeout=30.0) |
|
|
|
|
|
@staticmethod |
|
|
def _link(table: str, value: str) -> dict: |
|
|
return {"table": table, "value": value} |
|
|
|
|
|
def get_participants(self) -> list: |
|
|
response = self.client.get(f"{self.base_url}/participants/") |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def get_participant(self, participant_id: str) -> dict: |
|
|
response = self.client.get(f"{self.base_url}/participants/{participant_id}") |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def create_participant(self, first_name: str, last_name: str, nickname: str, activity_rating: float) -> dict: |
|
|
payload = { |
|
|
"first_name": first_name, |
|
|
"last_name": last_name, |
|
|
"nickname": nickname, |
|
|
"activity_rating": activity_rating, |
|
|
} |
|
|
response = self.client.post(f"{self.base_url}/participants/", json=payload) |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def get_topics(self) -> list: |
|
|
response = self.client.get(f"{self.base_url}/topics/") |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def get_topic(self, topic_id: str) -> dict: |
|
|
response = self.client.get(f"{self.base_url}/topics/{topic_id}") |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def get_messages(self) -> list: |
|
|
response = self.client.get(f"{self.base_url}/messages/") |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def create_topic(self, title: str, description: str, participants: list = None) -> dict: |
|
|
payload = { |
|
|
"title": title, |
|
|
"description": description, |
|
|
"participants": [self._link(TABLE_PARTICIPANTS, p) for p in (participants or [])], |
|
|
"messages": [], |
|
|
} |
|
|
response = self.client.post(f"{self.base_url}/topics/", json=payload) |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def publish_message(self, topic_id: str, participant_id: str, content: str) -> dict | str: |
|
|
payload = { |
|
|
"participant_id": self._link(TABLE_PARTICIPANTS, participant_id), |
|
|
"content": content, |
|
|
} |
|
|
response = self.client.post(f"{self.base_url}/topics/{topic_id}/messages", json=payload) |
|
|
if response.status_code == 400: |
|
|
error_detail = response.json().get("detail", "Unknown error") |
|
|
return f"ERROR: {error_detail}" |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def download_participant(self, participant_id: str) -> dict: |
|
|
response = self.client.get(f"{self.base_url}/participants/{participant_id}/download") |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def download_topic(self, topic_id: str) -> dict: |
|
|
response = self.client.get(f"{self.base_url}/topics/{topic_id}/download") |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def download_message(self, message_id: str) -> dict: |
|
|
response = self.client.get(f"{self.base_url}/messages/{message_id}/download") |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
|
|
|
def close(self): |
|
|
self.client.close() |
|
|
|
|
|
|
|
|
class LibraryClient: |
|
|
def __init__(self, base_url: str = LIBRARY_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 ForumMenuChoice(IntEnum): |
|
|
LIST_PARTICIPANTS = 1 |
|
|
GET_PARTICIPANT = 2 |
|
|
CREATE_PARTICIPANT = 3 |
|
|
DOWNLOAD_PARTICIPANT = 4 |
|
|
LIST_TOPICS = 5 |
|
|
GET_TOPIC = 6 |
|
|
CREATE_TOPIC = 7 |
|
|
DOWNLOAD_TOPIC = 8 |
|
|
PUBLISH_MESSAGE = 9 |
|
|
LIST_MESSAGES = 10 |
|
|
DOWNLOAD_MESSAGE = 11 |
|
|
BACK = 0 |
|
|
|
|
|
|
|
|
FORUM_MENU_TEXT = { |
|
|
ForumMenuChoice.LIST_PARTICIPANTS: "List all participants", |
|
|
ForumMenuChoice.GET_PARTICIPANT: "Get participant by ID", |
|
|
ForumMenuChoice.CREATE_PARTICIPANT: "Create new participant", |
|
|
ForumMenuChoice.DOWNLOAD_PARTICIPANT: "Download participant as file", |
|
|
ForumMenuChoice.LIST_TOPICS: "List all topics", |
|
|
ForumMenuChoice.GET_TOPIC: "Get topic by ID", |
|
|
ForumMenuChoice.CREATE_TOPIC: "Create new topic", |
|
|
ForumMenuChoice.DOWNLOAD_TOPIC: "Download topic as file", |
|
|
ForumMenuChoice.PUBLISH_MESSAGE: "Publish message to topic", |
|
|
ForumMenuChoice.LIST_MESSAGES: "List all messages", |
|
|
ForumMenuChoice.DOWNLOAD_MESSAGE: "Download message as file", |
|
|
ForumMenuChoice.BACK: "Back to server selection", |
|
|
} |
|
|
|
|
|
|
|
|
class LibraryMenuChoice(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 |
|
|
BACK = 0 |
|
|
|
|
|
|
|
|
LIBRARY_MENU_TEXT = { |
|
|
LibraryMenuChoice.LIST_BOOKS: "List books", |
|
|
LibraryMenuChoice.VIEW_BOOK: "Get book by ID", |
|
|
LibraryMenuChoice.CREATE_BOOK: "Create book", |
|
|
LibraryMenuChoice.UPDATE_BOOK: "Update book", |
|
|
LibraryMenuChoice.DELETE_BOOK: "Delete book", |
|
|
LibraryMenuChoice.BORROW_BOOKS: "Borrow books", |
|
|
LibraryMenuChoice.RETURN_BOOKS: "Return books", |
|
|
LibraryMenuChoice.DOWNLOAD_BOOK: "Download book as file", |
|
|
LibraryMenuChoice.LIST_VISITORS: "List visitors", |
|
|
LibraryMenuChoice.VIEW_VISITOR: "Get visitor by ID", |
|
|
LibraryMenuChoice.CREATE_VISITOR: "Create visitor", |
|
|
LibraryMenuChoice.UPDATE_VISITOR: "Update visitor", |
|
|
LibraryMenuChoice.DELETE_VISITOR: "Delete visitor", |
|
|
LibraryMenuChoice.DOWNLOAD_VISITOR: "Download visitor as file", |
|
|
LibraryMenuChoice.LIST_WORKERS: "List workers", |
|
|
LibraryMenuChoice.VIEW_WORKER: "Get worker by ID", |
|
|
LibraryMenuChoice.CREATE_WORKER: "Create worker", |
|
|
LibraryMenuChoice.UPDATE_WORKER: "Update worker", |
|
|
LibraryMenuChoice.DELETE_WORKER: "Delete worker", |
|
|
LibraryMenuChoice.WORKERS_BY_DAYS: "Find workers by work days", |
|
|
LibraryMenuChoice.DOWNLOAD_WORKER: "Download worker as file", |
|
|
LibraryMenuChoice.BACK: "Back to server selection", |
|
|
} |
|
|
|
|
|
|
|
|
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(" Error: Enter a valid integer.") |
|
|
except ValidationError as e: |
|
|
print(f" Error: {e}") |
|
|
|
|
|
|
|
|
def prompt_float_with_validation(label: str, validator: Callable[[float], float]) -> float: |
|
|
while True: |
|
|
raw = input(label).strip() |
|
|
try: |
|
|
value = float(raw) |
|
|
return validator(value) |
|
|
except ValueError: |
|
|
print(" Error: Enter a valid number.") |
|
|
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(" Error: Enter a 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("Pick number: ").strip() |
|
|
try: |
|
|
selected = int(raw) |
|
|
if 1 <= selected <= len(options): |
|
|
return options[selected - 1] |
|
|
except ValueError: |
|
|
pass |
|
|
print(" 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("Pick number (0 to skip): ").strip() |
|
|
try: |
|
|
selected = int(raw) |
|
|
if selected == 0: |
|
|
return None |
|
|
if 1 <= selected <= len(options): |
|
|
return options[selected - 1] |
|
|
except ValueError: |
|
|
pass |
|
|
print(" Error: Invalid choice.") |
|
|
|
|
|
|
|
|
def prompt_multi_choice(label: str, options: List[str]) -> List[str]: |
|
|
print(label) |
|
|
print("Use comma to separate values or type * for all.") |
|
|
for idx, option in enumerate(options, 1): |
|
|
print(f" {idx}. {option}") |
|
|
while True: |
|
|
raw = input("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(f" Error: Invalid number {p}.") |
|
|
valid = False |
|
|
break |
|
|
else: |
|
|
print(f" Error: Invalid value '{p}'.") |
|
|
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(" Error: At least one value is required.") |
|
|
|
|
|
|
|
|
def prompt_optional_multi_choice(label: str, options: List[str]) -> Optional[List[str]]: |
|
|
print(label) |
|
|
print("Use comma to separate values, * for all, or leave empty to skip.") |
|
|
for idx, option in enumerate(options, 1): |
|
|
print(f" {idx}. {option}") |
|
|
while True: |
|
|
raw = input("Enter numbers or names (empty to 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(f" Error: Invalid number {p}.") |
|
|
valid = False |
|
|
break |
|
|
else: |
|
|
print(f" Error: Invalid value '{p}'.") |
|
|
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(f" Error: Book with ID {book_id} 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(f" Error: Book with ID {validated} not found.") |
|
|
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(f" Error: Visitor with ID {validated} 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(f" Error: Worker with ID {validated} 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("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("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("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 print_participants(participants: list): |
|
|
if not participants: |
|
|
print("No participants found.") |
|
|
return |
|
|
header = f"{'ID':<40} {'Name':<25} {'Nickname':<15} {'Rating':<10}" |
|
|
print(header) |
|
|
print(SEPARATOR_90) |
|
|
for p in participants: |
|
|
full_name = f"{p['first_name']} {p['last_name']}" |
|
|
print(f"{p['id']:<40} {full_name:<25} {p['nickname']:<15} {p['activity_rating']:<10}") |
|
|
|
|
|
|
|
|
def print_topics(topics: list): |
|
|
if not topics: |
|
|
print("No topics found.") |
|
|
return |
|
|
for topic in topics: |
|
|
print(f"\nTopic: {topic['title']}") |
|
|
print(f" ID: {topic['id']}") |
|
|
print(f" Description: {topic['description']}") |
|
|
print(f" Created: {topic['created_at']}") |
|
|
print(f" Messages ({len(topic['messages'])}):") |
|
|
for msg in topic["messages"]: |
|
|
print(f" {msg.get('order_in_topic', 0)}. [{msg['participant_name']}]: {msg['content']}") |
|
|
|
|
|
|
|
|
def print_topic_detail(topic: dict): |
|
|
print(f"\nTopic: {topic['title']}") |
|
|
print(f" ID: {topic['id']}") |
|
|
print(f" Description: {topic['description']}") |
|
|
print(f" Created: {topic['created_at']}") |
|
|
print(f" Participants: {len(topic['participants'])}") |
|
|
print(f"\n Messages ({len(topic['messages'])}):") |
|
|
if not topic["messages"]: |
|
|
print(" No messages yet.") |
|
|
for msg in topic["messages"]: |
|
|
print(f" {msg.get('order_in_topic', 0)}. [{msg['participant_name']}]: {msg['content']}") |
|
|
|
|
|
|
|
|
def print_messages(messages: list): |
|
|
if not messages: |
|
|
print(" No messages yet.") |
|
|
return |
|
|
for msg in messages: |
|
|
print(f"{msg['topic_title']} | #{msg['order_in_topic']} [{msg['participant_name']}]: {msg['content']}") |
|
|
|
|
|
|
|
|
def server_selection_menu() -> Optional[int]: |
|
|
print("\n" + SEPARATOR) |
|
|
print(" UNIFIED CLIENT - SERVER SELECTION") |
|
|
print(SEPARATOR) |
|
|
print("1. Forum Server (Topics, Participants, Messages)") |
|
|
print("2. Library Server (Books, Visitors, Workers)") |
|
|
print("0. Exit") |
|
|
print(DASH_LINE_60) |
|
|
raw = input(PROMPT).strip() |
|
|
try: |
|
|
choice = int(raw) |
|
|
if choice in (0, 1, 2): |
|
|
return choice |
|
|
except ValueError: |
|
|
pass |
|
|
return None |
|
|
|
|
|
|
|
|
def forum_menu() -> Optional[ForumMenuChoice]: |
|
|
print("\n" + SEPARATOR_60) |
|
|
print(" FORUM CLIENT - MAIN MENU") |
|
|
print(SEPARATOR_60) |
|
|
for choice in ForumMenuChoice: |
|
|
print(f"{choice.value}. {FORUM_MENU_TEXT[choice]}") |
|
|
print(DASH_LINE_60) |
|
|
raw = input(PROMPT).strip() |
|
|
try: |
|
|
return ForumMenuChoice(int(raw)) |
|
|
except Exception: |
|
|
return None |
|
|
|
|
|
|
|
|
def library_menu() -> Optional[LibraryMenuChoice]: |
|
|
print("\n" + SEPARATOR) |
|
|
for choice in LibraryMenuChoice: |
|
|
print(f"{choice.value}. {LIBRARY_MENU_TEXT[choice]}") |
|
|
raw = input(PROMPT).strip() |
|
|
try: |
|
|
return LibraryMenuChoice(int(raw)) |
|
|
except Exception: |
|
|
return None |
|
|
|
|
|
|
|
|
def run_forum_client(): |
|
|
client = ForumClient() |
|
|
try: |
|
|
while True: |
|
|
choice = forum_menu() |
|
|
if choice is None: |
|
|
print("Invalid option. Please try again.") |
|
|
continue |
|
|
if choice == ForumMenuChoice.BACK: |
|
|
break |
|
|
|
|
|
try: |
|
|
if choice == ForumMenuChoice.LIST_PARTICIPANTS: |
|
|
print(SEPARATOR_60) |
|
|
print("ALL PARTICIPANTS:") |
|
|
print_participants(client.get_participants()) |
|
|
|
|
|
elif choice == ForumMenuChoice.GET_PARTICIPANT: |
|
|
print(SEPARATOR_60) |
|
|
participant_id = input("Enter participant ID: ").strip() |
|
|
try: |
|
|
UUID(participant_id) |
|
|
participant = client.get_participant(participant_id) |
|
|
print(f"\nParticipant: {participant['first_name']} {participant['last_name']}") |
|
|
print(f" ID: {participant['id']}") |
|
|
print(f" Nickname: {participant['nickname']}") |
|
|
print(f" Rating: {participant['activity_rating']}") |
|
|
print(f" Registered: {participant['registered_at']}") |
|
|
except ValueError: |
|
|
print("Invalid UUID format.") |
|
|
|
|
|
elif choice == ForumMenuChoice.CREATE_PARTICIPANT: |
|
|
print(SEPARATOR_60) |
|
|
print("CREATE NEW PARTICIPANT:") |
|
|
first_name = prompt_with_validation(" First name: ", |
|
|
lambda v: v if re.fullmatch(r"[A-Za-zА-Яа-яЁё-]+", v) else (_ for _ in ()).throw( |
|
|
ValidationError("First name must contain only letters and '-'."))) |
|
|
last_name = prompt_with_validation(" Last name: ", |
|
|
lambda v: v if re.fullmatch(r"[A-Za-zА-Яа-яЁё-]+", v) else (_ for _ in ()).throw( |
|
|
ValidationError("Last name must contain only letters and '-'."))) |
|
|
nickname = prompt_with_validation(" Nickname: ", |
|
|
lambda v: v if re.fullmatch(r"[A-Za-zА-Яа-яЁё-]+", v) else (_ for _ in ()).throw( |
|
|
ValidationError("Nickname must contain only letters and '-'."))) |
|
|
activity_rating = prompt_float_with_validation(" Activity rating: ", validate_activity_rating) |
|
|
participant = client.create_participant(first_name, last_name, nickname, activity_rating) |
|
|
print("\nParticipant created successfully!") |
|
|
print(f" ID: {participant['id']}") |
|
|
|
|
|
elif choice == ForumMenuChoice.LIST_TOPICS: |
|
|
print(SEPARATOR_60) |
|
|
print("ALL TOPICS:") |
|
|
print_topics(client.get_topics()) |
|
|
|
|
|
elif choice == ForumMenuChoice.GET_TOPIC: |
|
|
print(SEPARATOR_60) |
|
|
topic_id = input("Enter topic ID: ").strip() |
|
|
try: |
|
|
UUID(topic_id) |
|
|
topic = client.get_topic(topic_id) |
|
|
print_topic_detail(topic) |
|
|
except ValueError: |
|
|
print("Invalid UUID format.") |
|
|
|
|
|
elif choice == ForumMenuChoice.CREATE_TOPIC: |
|
|
print(SEPARATOR_60) |
|
|
print("CREATE NEW TOPIC:") |
|
|
title = input(" Title: ").strip() |
|
|
description = input(" Description: ").strip() |
|
|
topic = client.create_topic(title, description) |
|
|
print("\nTopic created successfully!") |
|
|
print(f" ID: {topic['id']}") |
|
|
|
|
|
elif choice == ForumMenuChoice.PUBLISH_MESSAGE: |
|
|
print(SEPARATOR_60) |
|
|
print("PUBLISH MESSAGE TO TOPIC:") |
|
|
while True: |
|
|
topic_id = input(" Topic ID: ").strip() |
|
|
if not topic_id: |
|
|
print("Topic ID is required.") |
|
|
continue |
|
|
try: |
|
|
client.get_topic(topic_id) |
|
|
break |
|
|
except httpx.HTTPStatusError as e: |
|
|
if e.response.status_code in (404, 422): |
|
|
print("Topic not found.") |
|
|
else: |
|
|
print(f"Error: {e.response.status_code} - {e.response.text}") |
|
|
|
|
|
while True: |
|
|
participant_id = input(" Participant ID: ").strip() |
|
|
if not participant_id: |
|
|
print("Participant ID is required.") |
|
|
continue |
|
|
try: |
|
|
client.get_participant(participant_id) |
|
|
break |
|
|
except httpx.HTTPStatusError as e: |
|
|
if e.response.status_code in (404, 422): |
|
|
print("Participant not found.") |
|
|
else: |
|
|
print(f"Error: {e.response.status_code} - {e.response.text}") |
|
|
|
|
|
content = input(" Message content: ").strip() |
|
|
result = client.publish_message(topic_id, participant_id, content) |
|
|
if isinstance(result, str): |
|
|
print(f"\n{result}") |
|
|
else: |
|
|
print("\nMessage published successfully!") |
|
|
print(f"Topic now has {len(result['messages'])} message(s).") |
|
|
|
|
|
elif choice == ForumMenuChoice.LIST_MESSAGES: |
|
|
print(SEPARATOR_60) |
|
|
print("ALL MESSAGES:") |
|
|
print_messages(client.get_messages()) |
|
|
|
|
|
elif choice == ForumMenuChoice.DOWNLOAD_PARTICIPANT: |
|
|
print(SEPARATOR_60) |
|
|
participant_id = input("Enter participant ID: ").strip() |
|
|
try: |
|
|
UUID(participant_id) |
|
|
content = client.download_participant(participant_id) |
|
|
filepath = save_to_file("participants", participant_id, content) |
|
|
print(f"File saved to: {filepath}") |
|
|
except ValueError: |
|
|
print("Invalid UUID format.") |
|
|
|
|
|
elif choice == ForumMenuChoice.DOWNLOAD_TOPIC: |
|
|
print(SEPARATOR_60) |
|
|
topic_id = input("Enter topic ID: ").strip() |
|
|
try: |
|
|
UUID(topic_id) |
|
|
content = client.download_topic(topic_id) |
|
|
filepath = save_to_file("topics", topic_id, content) |
|
|
print(f"File saved to: {filepath}") |
|
|
except ValueError: |
|
|
print("Invalid UUID format.") |
|
|
|
|
|
elif choice == ForumMenuChoice.DOWNLOAD_MESSAGE: |
|
|
print(SEPARATOR_60) |
|
|
message_id = input("Enter message ID: ").strip() |
|
|
try: |
|
|
UUID(message_id) |
|
|
content = client.download_message(message_id) |
|
|
filepath = save_to_file("messages", message_id, content) |
|
|
print(f"File saved to: {filepath}") |
|
|
except ValueError: |
|
|
print("Invalid UUID format.") |
|
|
|
|
|
except httpx.HTTPStatusError as e: |
|
|
print(f"Error: {e.response.status_code} - {e.response.text}") |
|
|
finally: |
|
|
client.close() |
|
|
|
|
|
|
|
|
def run_library_client(): |
|
|
client = LibraryClient() |
|
|
try: |
|
|
while True: |
|
|
choice = library_menu() |
|
|
if choice is None: |
|
|
print("Invalid option.") |
|
|
continue |
|
|
if choice == LibraryMenuChoice.BACK: |
|
|
break |
|
|
|
|
|
try: |
|
|
if choice == LibraryMenuChoice.LIST_BOOKS: |
|
|
print_books(client.list_books()) |
|
|
|
|
|
elif choice == LibraryMenuChoice.VIEW_BOOK: |
|
|
book_id = prompt_book_id("Book ID: ", client) |
|
|
print_books([client.get_book(book_id)]) |
|
|
|
|
|
elif choice == LibraryMenuChoice.CREATE_BOOK: |
|
|
title = prompt_with_validation("Title: ", validate_title) |
|
|
author = prompt_with_validation("Author: ", validate_author) |
|
|
pages = prompt_int_with_validation("Pages: ", validate_pages) |
|
|
year = prompt_int_with_validation("Year: ", validate_year) |
|
|
genre = prompt_choice("Genre:", GENRES) |
|
|
status = prompt_choice("Status:", BOOK_STATUSES) |
|
|
print_books([client.create_book(title, author, pages, year, genre, status)]) |
|
|
|
|
|
elif choice == LibraryMenuChoice.UPDATE_BOOK: |
|
|
book_id = prompt_book_id("Book ID: ", client) |
|
|
print("Leave fields empty to skip them.") |
|
|
title = prompt_optional_with_validation("Title (empty to skip): ", validate_title) |
|
|
author = prompt_optional_with_validation("Author (empty to skip): ", validate_author) |
|
|
pages = prompt_optional_int_with_validation("Pages (empty to skip): ", validate_pages) |
|
|
year = prompt_optional_int_with_validation("Year (empty to skip): ", validate_year) |
|
|
genre = prompt_optional_choice("Genre:", GENRES) |
|
|
status = prompt_optional_choice("Status:", BOOK_STATUSES) |
|
|
if not any([title, author, pages, year, genre, status]): |
|
|
print("No fields to update.") |
|
|
continue |
|
|
print_books([client.update_book(book_id, title, author, pages, year, genre, status)]) |
|
|
|
|
|
elif choice == LibraryMenuChoice.DELETE_BOOK: |
|
|
book_id = prompt_book_id("Book ID: ", client) |
|
|
result = client.delete_book(book_id) |
|
|
print(result.get("message") if isinstance(result, dict) else "Deleted.") |
|
|
|
|
|
elif choice == LibraryMenuChoice.BORROW_BOOKS: |
|
|
book_ids = prompt_book_ids("Book IDs (comma separated): ", client) |
|
|
visitor_id = prompt_visitor_id("Visitor ID: ", client) |
|
|
worker_id = prompt_worker_id("Worker ID: ", client) |
|
|
borrow_date = prompt_date("Borrow date (YYYY-MM-DD)") |
|
|
result = client.borrow_books(book_ids, visitor_id, worker_id, borrow_date) |
|
|
print(result.get("message") if isinstance(result, dict) else result) |
|
|
|
|
|
elif choice == LibraryMenuChoice.RETURN_BOOKS: |
|
|
book_ids = prompt_book_ids("Book IDs (comma separated): ", client) |
|
|
visitor_id = prompt_visitor_id("Visitor ID: ", client) |
|
|
worker_id = prompt_worker_id("Worker ID: ", client) |
|
|
return_date = prompt_date("Return date (YYYY-MM-DD)") |
|
|
result = client.return_books(book_ids, visitor_id, worker_id, return_date) |
|
|
print(result.get("message") if isinstance(result, dict) else result) |
|
|
|
|
|
elif choice == LibraryMenuChoice.DOWNLOAD_BOOK: |
|
|
book_id = prompt_book_id("Book ID: ", client) |
|
|
content = client.download_book(book_id) |
|
|
filepath = save_to_file("books", book_id, content) |
|
|
print(f"File saved to: {filepath}") |
|
|
|
|
|
elif choice == LibraryMenuChoice.LIST_VISITORS: |
|
|
print_visitors(client.list_visitors()) |
|
|
|
|
|
elif choice == LibraryMenuChoice.VIEW_VISITOR: |
|
|
visitor_id = prompt_visitor_id("Visitor ID: ", client) |
|
|
visitor = client.get_visitor(visitor_id) |
|
|
print_visitors([visitor]) |
|
|
current = visitor.get("currentBooks") or [] |
|
|
history = visitor.get("history") or [] |
|
|
if current: |
|
|
print("\nCurrent books:") |
|
|
print_books(current) |
|
|
if history: |
|
|
print("\nHistory:") |
|
|
print_books(history) |
|
|
|
|
|
elif choice == LibraryMenuChoice.CREATE_VISITOR: |
|
|
name = prompt_with_validation("Name: ", lambda v: validate_name(v, "Name")) |
|
|
surname = prompt_with_validation("Surname: ", lambda v: validate_name(v, "Surname")) |
|
|
print_visitors([client.create_visitor(name, surname)]) |
|
|
|
|
|
elif choice == LibraryMenuChoice.UPDATE_VISITOR: |
|
|
visitor_id = prompt_visitor_id("Visitor ID: ", client) |
|
|
print("Leave fields empty to skip them.") |
|
|
name = prompt_optional_with_validation("Name (empty to skip): ", lambda v: validate_name(v, "Name")) |
|
|
surname = prompt_optional_with_validation("Surname (empty to skip): ", lambda v: validate_name(v, "Surname")) |
|
|
if not name and not surname: |
|
|
print("No fields to update.") |
|
|
continue |
|
|
print_visitors([client.update_visitor(visitor_id, name, surname)]) |
|
|
|
|
|
elif choice == LibraryMenuChoice.DELETE_VISITOR: |
|
|
visitor_id = prompt_visitor_id("Visitor ID: ", client) |
|
|
client.delete_visitor(visitor_id) |
|
|
print("Visitor deleted.") |
|
|
|
|
|
elif choice == LibraryMenuChoice.DOWNLOAD_VISITOR: |
|
|
visitor_id = prompt_visitor_id("Visitor ID: ", client) |
|
|
content = client.download_visitor(visitor_id) |
|
|
filepath = save_to_file("visitors", visitor_id, content) |
|
|
print(f"File saved to: {filepath}") |
|
|
|
|
|
elif choice == LibraryMenuChoice.LIST_WORKERS: |
|
|
print_workers(client.list_workers()) |
|
|
|
|
|
elif choice == LibraryMenuChoice.VIEW_WORKER: |
|
|
worker_id = prompt_worker_id("Worker ID: ", 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 == LibraryMenuChoice.CREATE_WORKER: |
|
|
name = prompt_with_validation("Name: ", lambda v: validate_name(v, "Name")) |
|
|
surname = prompt_with_validation("Surname: ", lambda v: validate_name(v, "Surname")) |
|
|
experience = prompt_int_with_validation("Experience (years): ", validate_experience) |
|
|
work_days = prompt_multi_choice("Work days:", WORK_DAYS) |
|
|
print_workers([client.create_worker(name, surname, experience, work_days)]) |
|
|
|
|
|
elif choice == LibraryMenuChoice.UPDATE_WORKER: |
|
|
worker_id = prompt_worker_id("Worker ID: ", client) |
|
|
print("Leave fields empty to skip them.") |
|
|
name = prompt_optional_with_validation("Name (empty to skip): ", lambda v: validate_name(v, "Name")) |
|
|
surname = prompt_optional_with_validation("Surname (empty to skip): ", lambda v: validate_name(v, "Surname")) |
|
|
experience = prompt_optional_int_with_validation("Experience (empty to skip): ", validate_experience) |
|
|
work_days = prompt_optional_multi_choice("Work days:", WORK_DAYS) |
|
|
if not any([name, surname, experience is not None, work_days]): |
|
|
print("No fields to update.") |
|
|
continue |
|
|
print_workers([client.update_worker(worker_id, name, surname, experience, work_days)]) |
|
|
|
|
|
elif choice == LibraryMenuChoice.DELETE_WORKER: |
|
|
worker_id = prompt_worker_id("Worker ID: ", client) |
|
|
client.delete_worker(worker_id) |
|
|
print("Worker deleted.") |
|
|
|
|
|
elif choice == LibraryMenuChoice.WORKERS_BY_DAYS: |
|
|
work_days = prompt_multi_choice("Select work days:", WORK_DAYS) |
|
|
print_workers(client.workers_by_days(work_days)) |
|
|
|
|
|
elif choice == LibraryMenuChoice.DOWNLOAD_WORKER: |
|
|
worker_id = prompt_worker_id("Worker ID: ", client) |
|
|
content = client.download_worker(worker_id) |
|
|
filepath = save_to_file("workers", worker_id, content) |
|
|
print(f"File saved to: {filepath}") |
|
|
|
|
|
except (RuntimeError, httpx.HTTPError, ValueError) as error: |
|
|
print(f"Error: {error}") |
|
|
finally: |
|
|
client.close() |
|
|
|
|
|
|
|
|
def main(): |
|
|
try: |
|
|
while True: |
|
|
server_choice = server_selection_menu() |
|
|
if server_choice is None: |
|
|
print("Invalid option. Please try again.") |
|
|
continue |
|
|
if server_choice == 0: |
|
|
print("Goodbye!") |
|
|
break |
|
|
elif server_choice == 1: |
|
|
run_forum_client() |
|
|
elif server_choice == 2: |
|
|
run_library_client() |
|
|
except KeyboardInterrupt: |
|
|
print("\nInterrupted. Goodbye!") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |
|
|
|
|
|
|