lab4 / client.py
brestok's picture
init
a08f988
import re
from enum import IntEnum
from uuid import UUID
import httpx
BASE_URL = "http://127.0.0.1:8000"
TABLE_PARTICIPANTS = "participants"
TABLE_TOPICS = "topics"
NEWLINE = "\n"
SEPARATOR_60 = "=" * 60
SEPARATOR_90 = "-" * 90
DASH_LINE_60 = "-" * 60
MAIN_MENU_TITLE = " FORUM CLIENT - MAIN MENU"
PROMPT_SELECT_OPTION = "Select option: "
MSG_GOODBYE = "Goodbye!"
LABEL_ALL_PARTICIPANTS = "ALL PARTICIPANTS:"
LABEL_ALL_TOPICS = "ALL TOPICS:"
LABEL_ALL_MESSAGES = "ALL MESSAGES:"
LABEL_CREATE_PARTICIPANT = "CREATE NEW PARTICIPANT:"
LABEL_CREATE_TOPIC = "CREATE NEW TOPIC:"
LABEL_PUBLISH_MESSAGE = "PUBLISH MESSAGE TO TOPIC:"
PROMPT_PARTICIPANT_ID = "Enter participant ID: "
PROMPT_TOPIC_ID = "Enter topic ID: "
PROMPT_TITLE = " Title: "
PROMPT_DESCRIPTION = " Description: "
PROMPT_FIRST_NAME = " First name: "
PROMPT_LAST_NAME = " Last name: "
PROMPT_NICKNAME = " Nickname: "
PROMPT_ACTIVITY = " Activity rating: "
PROMPT_TOPIC_ID_INLINE = " Topic ID: "
PROMPT_PARTICIPANT_ID_INLINE = " Participant ID: "
PROMPT_MESSAGE_CONTENT = " Message content: "
MSG_NO_PARTICIPANTS = "No participants found."
MSG_NO_TOPICS = "No topics found."
MSG_INVALID_OPTION = "Invalid option. Please try again."
MSG_INVALID_UUID = "Invalid UUID format."
MSG_GOODBYE_INTERRUPT = "\n\nInterrupted. Goodbye!"
MSG_TOPIC_ID_REQUIRED = "Topic ID is required."
MSG_PARTICIPANT_ID_REQUIRED = "Participant ID is required."
MSG_TOPIC_NOT_FOUND = "Topic not found."
MSG_PARTICIPANT_NOT_FOUND = "Participant not found."
MSG_NO_MESSAGES = " No messages yet."
MSG_PARTICIPANT_CREATED = "\nParticipant created successfully!"
MSG_TOPIC_CREATED = "\nTopic created successfully!"
MSG_MESSAGE_PUBLISHED = "\nMessage published successfully!"
MSG_ERROR_TEMPLATE = "Error: {status} - {text}"
PARTICIPANT_NAME_RULE = "First name must contain only letters and '-'."
PARTICIPANT_LAST_NAME_RULE = "Last name must contain only letters and '-'."
PARTICIPANT_NICKNAME_RULE = "Nickname must contain only letters and '-'."
ACTIVITY_RANGE_RULE = "Activity rating must be between 0.1 and 5.0."
ACTIVITY_NUMBER_RULE = "Activity rating must be a number between 0.1 and 5.0."
PARTICIPANT_TABLE_HEADER = f"{'ID':<40} {'Name':<25} {'Nickname':<15} {'Rating':<10}"
PARTICIPANT_ROW_TEMPLATE = (
"{id:<40} {full_name:<25} {nickname:<15} {rating:<10}"
)
TOPIC_TITLE_TEMPLATE = "\nTopic: {title}"
TOPIC_ID_TEMPLATE = " ID: {id}"
TOPIC_DESCRIPTION_TEMPLATE = " Description: {description}"
TOPIC_CREATED_TEMPLATE = " Created: {created_at}"
TOPIC_MESSAGES_TEMPLATE = " Messages ({count}):"
TOPIC_PARTICIPANTS_TEMPLATE = " Participants: {count}"
TOPIC_MESSAGE_TEMPLATE = " {order}. [{participant_name}]: {content}"
MESSAGE_LIST_TEMPLATE = "{topic_title} | #{order} [{participant_name}]: {content}"
PARTICIPANT_SUMMARY_TEMPLATE = "\nParticipant: {first} {last}"
PARTICIPANT_ID_TEMPLATE = " ID: {id}"
PARTICIPANT_NICKNAME_TEMPLATE = " Nickname: {nickname}"
PARTICIPANT_RATING_TEMPLATE = " Rating: {rating}"
PARTICIPANT_REGISTERED_TEMPLATE = " Registered: {registered}"
TOPIC_CREATED_ID_TEMPLATE = " ID: {id}"
PARTICIPANT_CREATED_ID_TEMPLATE = " ID: {id}"
TOPIC_MESSAGES_COUNT_TEMPLATE = "Topic now has {count} message(s)."
RESULT_MESSAGE_TEMPLATE = "\n{result}"
MENU_HEADER = NEWLINE + SEPARATOR_60
class ForumClient:
def __init__(self, base_url: str = 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 get_messages_by_topic(self, topic_id: str) -> list:
response = self.client.get(f"{self.base_url}/messages/topic/{topic_id}")
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 close(self):
self.client.close()
def print_separator():
print(SEPARATOR_60)
def print_participants(participants: list):
if not participants:
print(MSG_NO_PARTICIPANTS)
return
print(PARTICIPANT_TABLE_HEADER)
print(SEPARATOR_90)
for p in participants:
full_name = f"{p['first_name']} {p['last_name']}"
print(
PARTICIPANT_ROW_TEMPLATE.format(
id=p["id"],
full_name=full_name,
nickname=p["nickname"],
rating=p["activity_rating"],
)
)
def print_topics(topics: list):
if not topics:
print(MSG_NO_TOPICS)
return
for topic in topics:
print(TOPIC_TITLE_TEMPLATE.format(title=topic["title"]))
print(TOPIC_ID_TEMPLATE.format(id=topic["id"]))
print(TOPIC_DESCRIPTION_TEMPLATE.format(description=topic["description"]))
print(TOPIC_CREATED_TEMPLATE.format(created_at=topic["created_at"]))
print(TOPIC_MESSAGES_TEMPLATE.format(count=len(topic["messages"])))
for msg in topic["messages"]:
print(
TOPIC_MESSAGE_TEMPLATE.format(
participant_name=msg["participant_name"],
content=msg["content"],
order=msg.get("order_in_topic", 0),
)
)
def print_topic_detail(topic: dict):
print(TOPIC_TITLE_TEMPLATE.format(title=topic["title"]))
print(TOPIC_ID_TEMPLATE.format(id=topic["id"]))
print(TOPIC_DESCRIPTION_TEMPLATE.format(description=topic["description"]))
print(TOPIC_CREATED_TEMPLATE.format(created_at=topic["created_at"]))
print(TOPIC_PARTICIPANTS_TEMPLATE.format(count=len(topic["participants"])))
print(NEWLINE + TOPIC_MESSAGES_TEMPLATE.format(count=len(topic["messages"])))
if not topic["messages"]:
print(MSG_NO_MESSAGES)
for msg in topic["messages"]:
print(
TOPIC_MESSAGE_TEMPLATE.format(
participant_name=msg["participant_name"],
content=msg["content"],
order=msg.get("order_in_topic", 0),
)
)
def print_messages(messages: list):
if not messages:
print(MSG_NO_MESSAGES)
return
for msg in messages:
print(
MESSAGE_LIST_TEMPLATE.format(
topic_title=msg["topic_title"],
order=msg["order_in_topic"],
participant_name=msg["participant_name"],
content=msg["content"],
)
)
def main_menu():
print(MENU_HEADER)
print(MAIN_MENU_TITLE)
print(SEPARATOR_60)
for option in MENU_OPTIONS:
print(option)
print(DASH_LINE_60)
return input(PROMPT_SELECT_OPTION).strip()
class MenuChoice(IntEnum):
LIST_PARTICIPANTS = 1, "List all participants"
GET_PARTICIPANT = 2, "Get participant by ID"
CREATE_PARTICIPANT = 3, "Create new participant"
LIST_TOPICS = 4, "List all topics"
GET_TOPIC = 5, "Get topic by ID"
CREATE_TOPIC = 6, "Create new topic"
PUBLISH_MESSAGE = 7, "Publish message to topic"
LIST_MESSAGES = 8, "List all messages"
EXIT = 0, "Exit"
def __new__(cls, value, label=""):
obj = int.__new__(cls, value)
obj._value_ = value
obj.label = label
return obj
MENU_OPTIONS = tuple(f"{choice.value}. {choice.label}" for choice in MenuChoice)
def main():
client = ForumClient()
try:
while True:
raw_choice = main_menu()
try:
choice = MenuChoice(int(raw_choice))
except (ValueError, TypeError):
print(MSG_INVALID_OPTION)
continue
if choice == MenuChoice.EXIT:
print(MSG_GOODBYE)
break
elif choice == MenuChoice.LIST_PARTICIPANTS:
print_separator()
print(LABEL_ALL_PARTICIPANTS)
try:
participants = client.get_participants()
print_participants(participants)
except httpx.HTTPStatusError as e:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
elif choice == MenuChoice.GET_PARTICIPANT:
print_separator()
participant_id = input(PROMPT_PARTICIPANT_ID).strip()
try:
UUID(participant_id)
participant = client.get_participant(participant_id)
print(
PARTICIPANT_SUMMARY_TEMPLATE.format(
first=participant["first_name"],
last=participant["last_name"],
)
)
print(PARTICIPANT_ID_TEMPLATE.format(id=participant["id"]))
print(
PARTICIPANT_NICKNAME_TEMPLATE.format(
nickname=participant["nickname"]
)
)
print(
PARTICIPANT_RATING_TEMPLATE.format(
rating=participant["activity_rating"]
)
)
print(
PARTICIPANT_REGISTERED_TEMPLATE.format(
registered=participant["registered_at"]
)
)
except ValueError:
print(MSG_INVALID_UUID)
except httpx.HTTPStatusError as e:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
elif choice == MenuChoice.CREATE_PARTICIPANT:
print_separator()
print(LABEL_CREATE_PARTICIPANT)
while True:
first_name = input(PROMPT_FIRST_NAME).strip()
if not re.fullmatch(r"[A-Za-zА-Яа-яЁё-]+", first_name):
print(PARTICIPANT_NAME_RULE)
continue
break
while True:
last_name = input(PROMPT_LAST_NAME).strip()
if not re.fullmatch(r"[A-Za-zА-Яа-яЁё-]+", last_name):
print(PARTICIPANT_LAST_NAME_RULE)
continue
break
while True:
nickname = input(PROMPT_NICKNAME).strip()
if not re.fullmatch(r"[A-Za-zА-Яа-яЁё-]+", nickname):
print(PARTICIPANT_NICKNAME_RULE)
continue
break
while True:
activity_rating_raw = input(PROMPT_ACTIVITY).strip()
try:
activity_rating = float(activity_rating_raw)
if activity_rating < 0.1 or activity_rating > 5.0:
print(ACTIVITY_RANGE_RULE)
continue
except ValueError:
print(ACTIVITY_NUMBER_RULE)
continue
break
try:
participant = client.create_participant(
first_name, last_name, nickname, activity_rating
)
print(MSG_PARTICIPANT_CREATED)
print(
PARTICIPANT_CREATED_ID_TEMPLATE.format(id=participant["id"])
)
except httpx.HTTPStatusError as e:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
elif choice == MenuChoice.LIST_TOPICS:
print_separator()
print(LABEL_ALL_TOPICS)
try:
topics = client.get_topics()
print_topics(topics)
except httpx.HTTPStatusError as e:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
elif choice == MenuChoice.LIST_MESSAGES:
print_separator()
print(LABEL_ALL_MESSAGES)
try:
messages = client.get_messages()
print_messages(messages)
except httpx.HTTPStatusError as e:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
elif choice == MenuChoice.GET_TOPIC:
print_separator()
topic_id = input(PROMPT_TOPIC_ID).strip()
try:
UUID(topic_id)
topic = client.get_topic(topic_id)
print_topic_detail(topic)
except ValueError:
print(MSG_INVALID_UUID)
except httpx.HTTPStatusError as e:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
elif choice == MenuChoice.CREATE_TOPIC:
print_separator()
print(LABEL_CREATE_TOPIC)
title = input(PROMPT_TITLE).strip()
description = input(PROMPT_DESCRIPTION).strip()
try:
topic = client.create_topic(title, description)
print(MSG_TOPIC_CREATED)
print(TOPIC_CREATED_ID_TEMPLATE.format(id=topic["id"]))
except httpx.HTTPStatusError as e:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
elif choice == MenuChoice.PUBLISH_MESSAGE:
print_separator()
print(LABEL_PUBLISH_MESSAGE)
while True:
topic_id = input(PROMPT_TOPIC_ID_INLINE).strip()
if not topic_id:
print(MSG_TOPIC_ID_REQUIRED)
continue
try:
client.get_topic(topic_id)
except httpx.HTTPStatusError as e:
if e.response.status_code in (404, 422):
print(MSG_TOPIC_NOT_FOUND)
else:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
continue
break
while True:
participant_id = input(PROMPT_PARTICIPANT_ID_INLINE).strip()
if not participant_id:
print(MSG_PARTICIPANT_ID_REQUIRED)
continue
try:
client.get_participant(participant_id)
except httpx.HTTPStatusError as e:
if e.response.status_code in (404, 422):
print(MSG_PARTICIPANT_NOT_FOUND)
else:
print(
MSG_ERROR_TEMPLATE.format(
status=e.response.status_code, text=e.response.text
)
)
continue
break
content = input(PROMPT_MESSAGE_CONTENT).strip()
result = client.publish_message(topic_id, participant_id, content)
if isinstance(result, str):
print(RESULT_MESSAGE_TEMPLATE.format(result=result))
else:
print(MSG_MESSAGE_PUBLISHED)
print(
TOPIC_MESSAGES_COUNT_TEMPLATE.format(
count=len(result["messages"])
)
)
else:
print(MSG_INVALID_OPTION)
except KeyboardInterrupt:
print(MSG_GOODBYE_INTERRUPT)
finally:
client.close()
if __name__ == "__main__":
main()