diff --git a/demo/demo_template 1.pptx b/demo/demo_template 1.pptx index b6c2514..b44b993 100644 Binary files a/demo/demo_template 1.pptx and b/demo/demo_template 1.pptx differ diff --git a/src/agent/attractions.ipynb b/src/agent/attractions.ipynb index 7d7a7de..dda8a44 100644 --- a/src/agent/attractions.ipynb +++ b/src/agent/attractions.ipynb @@ -935,7 +935,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.3" + "version": "3.13.7" } }, "nbformat": 4, diff --git a/src/agent/cinema.ipynb b/src/agent/cinema.ipynb new file mode 100644 index 0000000..876384b --- /dev/null +++ b/src/agent/cinema.ipynb @@ -0,0 +1,2137 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 24, + "id": "a26927c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "python: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Scripts\\python.exe\n", + "uv: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Scripts\\uv.EXE\n" + ] + } + ], + "source": [ + "# Step to ensure that the venv is being used for the project not local copies, should point at .venv in project.\n", + "import sys, shutil\n", + "print(\"python:\", sys.executable)\n", + "print(\"uv:\", shutil.which(\"uv\")) " + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "aba67892", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: uv in c:\\users\\andrzejpytel\\source\\hackathon-2025-ap-fork\\.venv\\lib\\site-packages (0.8.22)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[2mUsing Python 3.13.7 environment at: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\u001b[0m\n", + "\u001b[2mAudited \u001b[1m13 packages\u001b[0m \u001b[2min 23ms\u001b[0m\u001b[0m\n" + ] + } + ], + "source": [ + "# installs into the current Jupyter kernel environment\n", + "%pip install -U uv \n", + "#! to run shell commands\n", + "!uv pip install -r requirements.txt" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "b86c12d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Updated imports with official MCP adapter loaded successfully!\n" + ] + } + ], + "source": [ + "# LangChain + MCP Setup for Cinema Booking (HTTP-based for Jupyter)\n", + "import os\n", + "from dotenv import load_dotenv\n", + "import asyncio\n", + "import json\n", + "import requests\n", + "from typing import Dict, Any, List\n", + "\n", + "# LangChain imports\n", + "from langchain_openai import ChatOpenAI, AzureChatOpenAI\n", + "from langchain_core.messages import HumanMessage, SystemMessage\n", + "from langchain.agents import AgentExecutor, create_tool_calling_agent\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain.memory import ConversationBufferMemory\n", + "\n", + "# Official MCP adapter imports for HTTP transport\n", + "from langchain_mcp_adapters.client import MultiServerMCPClient\n", + "from langchain_mcp_adapters.tools import load_mcp_tools\n", + "\n", + "# Load environment variables\n", + "load_dotenv()\n", + "\n", + "print(\"✅ Updated imports with official MCP adapter loaded successfully!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "a60c8345", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔗 Cinema & Movie Reviews MCP HTTP adapter setup ready!\n" + ] + } + ], + "source": [ + "# MCP Client Setup using Official Adapter with HTTP Transport\n", + "import subprocess\n", + "import time\n", + "\n", + "# Global MCP client for HTTP\n", + "mcp_client = None\n", + "\n", + "async def create_mcp_tools():\n", + " \"\"\"Create MCP tools using the official LangChain MCP adapter with HTTP transport\"\"\"\n", + " global mcp_client\n", + " \n", + " try:\n", + " # Create MultiServerMCPClient with streamable_http transport for cinema and movie reviews\n", + " mcp_client = MultiServerMCPClient({\n", + " \"cinema\": {\n", + " \"transport\": \"streamable_http\",\n", + " \"url\": os.getenv(\"CINEMA_MCP_URL\") # Cinema MCP server with /mcp/ path\n", + " },\n", + " \"movie_reviews\": {\n", + " \"transport\": \"streamable_http\",\n", + " \"url\": os.getenv(\"MOVIES_MCP_URL\") # Movie Reviews MCP server\n", + " },\n", + " })\n", + " \n", + " # Get tools from the MCP servers\n", + " tools = await mcp_client.get_tools()\n", + " print(f\"Loaded {len(tools)} MCP tools: {[tool.name for tool in tools]}\")\n", + " return tools\n", + " \n", + " except Exception as e:\n", + " print(f\"Error connecting to MCP HTTP servers: {e}\")\n", + " print(\"Make sure the MCP servers are running:\")\n", + " print(\"Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\")\n", + " print(\"Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\")\n", + " return []\n", + "\n", + "print(\"🔗 Cinema & Movie Reviews MCP HTTP adapter setup ready!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "57e4f778", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🛠️ Ready to load Cinema & Movie Reviews MCP tools via official adapter!\n" + ] + } + ], + "source": [ + "# Load MCP Tools using Official Adapter\n", + "# The tools will be loaded dynamically when setting up the agent\n", + "# No need to manually create tool wrappers - the adapter handles this automatically\n", + "print(\"🛠️ Ready to load Cinema & Movie Reviews MCP tools via official adapter!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "501c5c58", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🤖 Cinema & Movie Reviews agent setup function ready! Run the next cell to initialize.\n" + ] + } + ], + "source": [ + "async def setup_agent():\n", + " \"\"\"Setup LangChain agent with Cinema and Movie Reviews MCP tools using official adapter\"\"\"\n", + " \n", + " # Initialize LLM for Azure OpenAI\n", + " # can get this from Azure Open AI service -> Azure AI Foundry Portal\n", + " from langchain_openai import AzureChatOpenAI\n", + " \n", + " llm = AzureChatOpenAI(\n", + " deployment_name=os.getenv(\"DEPLOYMENT_NAME\"), # Your Azure deployment name\n", + " api_key=os.getenv(\"AZURE_OPENAI_API_KEY\"),\n", + " azure_endpoint=os.getenv(\"AZURE_OPENAI_ENDPOINT\"), \n", + " api_version=os.getenv(\"AZURE_API_VERSION\"), \n", + " temperature=1\n", + " )\n", + " \n", + " # Load MCP tools using official adapter\n", + " tools = await create_mcp_tools()\n", + " \n", + " if not tools:\n", + " print(\"No MCP tools loaded. Make sure the Cinema and Movie Reviews MCP servers are accessible.\")\n", + " return None\n", + " \n", + " print(f\"Loaded {len(tools)} MCP tools: {[tool.name for tool in tools]}\")\n", + " \n", + " # Create system prompt for cinema and movie reviews assistant\n", + " system_prompt = \"\"\"You are a helpful movie and cinema assistant that can help users discover movies, get reviews, and make reservations.\n", + " \n", + " You have access to both cinema and movie reviews MCP tools including:\n", + " \n", + " Cinema tools:\n", + " - Getting current movies playing with showtimes and availability\n", + " - Searching for movies by title, genre, date, room, and seat availability\n", + " - Getting detailed information about specific movie showings\n", + " - Making movie reservations for customers\n", + " - Viewing customer reservations\n", + " - Canceling reservations\n", + " \n", + " Movie reviews tools:\n", + " - Getting detailed movie information including plot, cast, ratings\n", + " - Searching for movies across different databases\n", + " - Getting movie reviews and ratings from various sources\n", + " - Getting movie genres and categories\n", + " - Finding random movie recommendations\n", + " \n", + " When helping users:\n", + " 1. Use movie reviews tools to get detailed movie information, ratings, and reviews\n", + " 2. Use cinema tools to check showtimes and availability\n", + " 3. For reservations, always collect: customer name, email, number of seats wanted\n", + " 4. Use make_reservation with the exact movie details from search results\n", + " 5. Provide comprehensive movie information including reviews and cinema availability\n", + " 6. Be helpful and guide users through both movie discovery and booking process\n", + " \n", + " Always be friendly and provide rich information about movies including reviews, ratings, and booking options.\"\"\"\n", + " \n", + " # Create prompt template\n", + " prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", system_prompt),\n", + " MessagesPlaceholder(variable_name=\"chat_history\"),\n", + " (\"human\", \"{input}\"),\n", + " MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n", + " ])\n", + " \n", + " # Create agent\n", + " agent = create_tool_calling_agent(llm, tools, prompt)\n", + "\n", + " # memory\n", + " memory = ConversationBufferMemory(memory_key=\"chat_history\", return_messages=True)\n", + "\n", + " # Create agent executor with tool logging callback and verbose output\n", + " agent_executor = AgentExecutor(agent=agent, tools=tools, memory=memory, verbose=True)\n", + " \n", + " return agent_executor\n", + "\n", + "# Initialize the agent (now async)\n", + "agent_executor = None\n", + "print(\"🤖 Cinema & Movie Reviews agent setup function ready! Run the next cell to initialize.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "688da57b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initializing cinema and movie reviews agent with MCP tools...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 14 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "Loaded 14 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "LangChain cinema and movie reviews agent with MCP tools ready!\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\AndrzejPytel\\AppData\\Local\\Temp\\ipykernel_38108\\1472502583.py:67: LangChainDeprecationWarning: Please see the migration guide at: https://python.langchain.com/docs/versions/migrating_memory/\n", + " memory = ConversationBufferMemory(memory_key=\"chat_history\", return_messages=True)\n" + ] + } + ], + "source": [ + "# Initialize the agent with Cinema and Movie Reviews MCP tools\n", + "async def initialize_agent():\n", + " \"\"\"Initialize the agent with Cinema and Movie Reviews MCP tools\"\"\"\n", + " global agent_executor\n", + " print(\"Initializing cinema and movie reviews agent with MCP tools...\")\n", + " agent_executor = await setup_agent()\n", + " if agent_executor:\n", + " print(\"LangChain cinema and movie reviews agent with MCP tools ready!\")\n", + " else:\n", + " print(\"Failed to initialize agent. Check Cinema and Movie Reviews MCP server connections.\")\n", + "\n", + "# Run the initialization\n", + "await initialize_agent()" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "dc7277da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "💬 Cinema and Movie Reviews user input handler ready!\n" + ] + } + ], + "source": [ + "# User Input Handler + logged agent steps\n", + "async def process_user_input(user_input: str) -> str:\n", + " \"\"\"Process user input and return LLM response using Cinema and Movie Reviews MCP tools\"\"\"\n", + " if not agent_executor:\n", + " return \"Agent not initialized. Please run the initialization cell first.\"\n", + " \n", + " try:\n", + " # Use the agent to process the input and get intermediate steps\n", + " result = await agent_executor.ainvoke({\"input\": user_input})\n", + " output = result.get(\"output\") or result.get(\"final_output\") or \"\"\n", + "\n", + " # Print intermediate steps if present\n", + " steps = result.get(\"intermediate_steps\") or []\n", + " for step in steps:\n", + " action = None\n", + " observation = None\n", + " if isinstance(step, tuple) and len(step) == 2:\n", + " action, observation = step\n", + " elif isinstance(step, dict) and \"action\" in step:\n", + " action = step.get(\"action\")\n", + " observation = step.get(\"observation\")\n", + " else:\n", + " continue\n", + "\n", + " tool_name = getattr(action, \"tool\", getattr(action, \"tool_name\", \"unknown\"))\n", + " tool_args = getattr(action, \"tool_input\", getattr(action, \"input\", None))\n", + " print(f\"\\n--- Tool: {tool_name}\")\n", + " print(f\"args: {tool_args}\")\n", + " if observation is not None:\n", + " print(f\"result: {observation}\")\n", + " print(\"---\\n\")\n", + "\n", + " return output\n", + " except Exception as e:\n", + " return f\"Error processing request: {str(e)}\"\n", + "\n", + "# Interactive function for easy testing\n", + "async def ask_assistant(question: str):\n", + " \"\"\"Easy-to-use function for asking the cinema and movie reviews assistant\"\"\"\n", + " print(f\"🎬 User: {question}\")\n", + " print(\"🤖 Assistant:\")\n", + " \n", + " response = await process_user_input(question)\n", + " print(response)\n", + " return response\n", + "\n", + "print(\"💬 Cinema and Movie Reviews user input handler ready!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "3c70994e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 14 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "Cinema and Movie Reviews MCP HTTP servers connected successfully!\n", + "Available tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + " - get_current_movies: Get all currently playing movies with their showtimes and availability\n", + "\n", + "Returns:\n", + " List of all movies currently being shown with details including:\n", + " - Movie title, description, and basic info\n", + " - Showtimes and theater room assignments \n", + " - Seat availability and pricing\n", + " - Genre, rating, and duration\n", + " - Cast and director information\n", + "\n", + " - get_movie_details: Get detailed information about a specific movie showing\n", + "\n", + "Args:\n", + " title: Exact movie title\n", + " date: Date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " time: Time in HH:MM format (e.g., \"19:30\")\n", + " room: Room identifier (e.g., \"theater_a\", \"theater_b\", \"theater_c\", \"imax\")\n", + " \n", + "Returns:\n", + " Detailed movie information including plot, cast, theater details, and availability\n", + " Use search_movies first to find the exact title, date, time, and room values needed\n", + "\n", + " - search_movies: Search for movie presentations with optional filters\n", + "\n", + "Args:\n", + " title: Filter by movie title (partial match, case-insensitive)\n", + " genre: Filter by movie genre (action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family)\n", + " date: Filter by date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " room: Filter by cinema room (theater_a, theater_b, theater_c, imax)\n", + " available_seats_min: Minimum number of available seats required\n", + " limit: Maximum number of results to return (default: 20, max: 100)\n", + " \n", + "Returns:\n", + " Filtered list of movie presentations matching the search criteria\n", + "\n", + " - make_reservation: Create a movie reservation for a specific showing\n", + "\n", + "Args:\n", + " title: Exact movie title (use get_movie_details or search_movies to find exact title)\n", + " date: Date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " time: Time in HH:MM format (e.g., \"19:30\")\n", + " room: Room identifier (e.g., \"theater_a\", \"theater_b\", \"theater_c\", \"imax\")\n", + " seats_count: Number of seats to reserve (must be > 0)\n", + " customer_name: Customer's full name\n", + " customer_email: Customer's email address\n", + " customer_phone: Customer's phone number (optional)\n", + " special_requests: Any special requests or accessibility needs (optional)\n", + " \n", + "Returns:\n", + " Reservation confirmation with booking details and pricing information\n", + " Use the exact title, date, time, and room values from search_movies or get_current_movies results\n", + "\n", + " - get_my_reservations: Get all reservations for a customer\n", + "\n", + "Args:\n", + " customer_email: Customer's email address used for reservations\n", + " \n", + "Returns:\n", + " List of all reservations made by the customer\n", + "\n", + " - cancel_reservation: Cancel a movie reservation\n", + "\n", + "Args:\n", + " customer_email: Customer's email address\n", + " title: Exact movie title of the reservation to cancel\n", + " date: Date in YYYY-MM-DD format\n", + " time: Time in HH:MM format\n", + " room: Room identifier\n", + " \n", + "Returns:\n", + " Cancellation confirmation and details about freed seats\n", + " Use get_my_reservations first to find the exact details of reservations to cancel\n", + "\n", + " - get_all_reservations: Get all reservations for cinema employee review\n", + "\n", + "Args:\n", + " status_filter: Filter by status (\"confirmed\", \"completed\", \"cancelled\") or None for all active reservations\n", + " \n", + "Returns:\n", + " List of all reservations matching the filter for employee management\n", + " Use this to review current reservations that need attention\n", + "\n", + " - acknowledge_reservation: Acknowledge/complete a reservation (cinema employee function)\n", + "\n", + "Args:\n", + " customer_email: Customer's email address\n", + " title: Exact movie title of the reservation\n", + " date: Date in YYYY-MM-DD format\n", + " time: Time in HH:MM format\n", + " room: Room identifier\n", + " \n", + "Returns:\n", + " Acknowledgment confirmation - marks reservation as completed\n", + " Use when customer has attended the showing\n", + "\n", + " - revoke_reservation: Revoke/cancel a reservation (cinema employee function)\n", + "\n", + "Args:\n", + " customer_email: Customer's email address\n", + " title: Exact movie title of the reservation to revoke\n", + " date: Date in YYYY-MM-DD format\n", + " time: Time in HH:MM format\n", + " room: Room identifier\n", + " reason: Reason for revocation (optional, for employee records)\n", + " \n", + "Returns:\n", + " Revocation confirmation and details about freed seats\n", + " Use when a reservation needs to be cancelled by cinema staff\n", + "\n", + " - get_movie_details: Get detailed information about a specific movie\n", + "\n", + "Args:\n", + " movie_id: Unique ID of the movie\n", + "\n", + "Returns:\n", + " MovieDetails object as dictionary with movie info, reviews, and related movies\n", + "\n", + " - search_movies: Search for movies with optional filters\n", + "\n", + "Args:\n", + " genre: Genre filter - \"action\", \"comedy\", \"drama\", etc.\n", + " limit: Maximum number of results (1-100, default: 20)\n", + "\n", + "Returns:\n", + " MoviesList object as dictionary with matching movies\n", + "\n", + " - get_random_movie: Get a random movie for inspiration\n", + "\n", + "Args:\n", + " genre: Genre type - \"famous\" for world famous movies, \"india\" for Indian movies\n", + "\n", + "Returns:\n", + " Movie object as dictionary with random movie details\n", + "\n", + " - search_and_format_movies: Search for movies and return formatted results for easy reading\n", + "\n", + "Args:\n", + " genre: Genre filter (e.g., \"action\", \"comedy\", \"drama\")\n", + " limit: Maximum number of results (1-20, default: 10)\n", + "\n", + "Returns:\n", + " Formatted string with movie search results\n", + "\n", + " - get_movie_reviews_by_title: Get reviews for a movie by its title and return formatted results\n", + "\n", + "Args:\n", + " title: Title of the movie\n", + "\n", + "Returns:\n", + " Formatted string with movie reviews\n", + "\n" + ] + } + ], + "source": [ + "# Test Cinema and Movie Reviews MCP server connectivity and tools\n", + "async def test_mcp_connection():\n", + " \"\"\"Test Cinema and Movie Reviews MCP server connections and list available tools\"\"\"\n", + " tools = await create_mcp_tools()\n", + " if tools:\n", + " print(f\"Cinema and Movie Reviews MCP HTTP servers connected successfully!\")\n", + " print(f\"Available tools: {[tool.name for tool in tools]}\")\n", + " for tool in tools:\n", + " print(f\" - {tool.name}: {tool.description}\")\n", + " else:\n", + " print(\"Failed to connect to MCP HTTP servers\")\n", + " print(\"Make sure to start both MCP servers:\")\n", + " print(\"Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\")\n", + " print(\"Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\")\n", + "\n", + "# Test MCP HTTP connections\n", + "await test_mcp_connection()" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "de62dd60", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔍 Debugging Cinema and Movie Reviews MCP connections...\n", + "✅ Cinema HTTP connection works: 404\n", + "✅ Cinema HTTP connection works: 404\n", + "✅ Movie Reviews HTTP connection works: 404\n", + "✅ MCP client created successfully\n", + "✅ Movie Reviews HTTP connection works: 404\n", + "✅ MCP client created successfully\n", + "✅ Got 14 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", + "Full traceback:\n", + "✅ Got 14 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", + "Full traceback:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Temp\\ipykernel_38108\\2853036564.py\", line 46, in debug_mcp_connection\n", + " await client.close()\n", + " ^^^^^^^^^^^^\n", + "AttributeError: 'MultiServerMCPClient' object has no attribute 'close'\n" + ] + } + ], + "source": [ + "# Detailed debug of MCP connections\n", + "import traceback\n", + "import httpx\n", + "\n", + "async def debug_mcp_connection():\n", + " \"\"\"Debug the Cinema and Movie Reviews MCP connections with detailed error info\"\"\"\n", + " print(\"🔍 Debugging Cinema and Movie Reviews MCP connections...\")\n", + " \n", + " # Test basic HTTP connectivity for both servers\n", + " servers = [\n", + " (\"Cinema\", \"http://localhost:8010\"),\n", + " (\"Movie Reviews\", \"http://localhost:8011\")\n", + " ]\n", + " \n", + " for server_name, url in servers:\n", + " try:\n", + " async with httpx.AsyncClient() as client:\n", + " response = await client.get(url, timeout=5.0)\n", + " print(f\"✅ {server_name} HTTP connection works: {response.status_code}\")\n", + " except Exception as e:\n", + " print(f\"❌ {server_name} HTTP connection failed: {e}\")\n", + " \n", + " # Test MCP client creation with detailed error tracking\n", + " try:\n", + " from langchain_mcp_adapters.client import MultiServerMCPClient\n", + " \n", + " client = MultiServerMCPClient({\n", + " \"cinema\": {\n", + " \"transport\": \"streamable_http\",\n", + " \"url\": os.getenv(\"CINEMA_MCP_URL\")\n", + " },\n", + " \"movie_reviews\": {\n", + " \"transport\": \"streamable_http\",\n", + " \"url\": os.getenv(\"MOVIES_MCP_URL\")\n", + " }\n", + " })\n", + " \n", + " print(\"✅ MCP client created successfully\")\n", + " \n", + " # Test getting tools with full error details\n", + " try:\n", + " tools = await client.get_tools()\n", + " print(f\"✅ Got {len(tools)} tools: {[tool.name for tool in tools]}\")\n", + " \n", + " # Test closing connection\n", + " await client.close()\n", + " print(\"✅ Connection closed successfully\")\n", + " \n", + " except Exception as e:\n", + " print(f\"❌ Error getting tools: {e}\")\n", + " print(\"Full traceback:\")\n", + " traceback.print_exc()\n", + " \n", + " except Exception as e:\n", + " print(f\"❌ Error creating MCP client: {e}\")\n", + " print(\"Full traceback:\")\n", + " traceback.print_exc()\n", + "\n", + "# Run detailed debug\n", + "await debug_mcp_connection()" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "1551f227", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🎬 User: What movies are currently playing?\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 33,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 67.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 33,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 67.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are the movies currently playing at MovieMagic Cinema. Want more details or to book tickets? I can help with either.\n", + "\n", + "1) Galactic Adventures — Theater A — 2025-09-25 at 14:30\n", + "- Genre: Science Fiction | Rating: PG-13 | Duration: 142 min\n", + "- Price: $12.50 per seat | Seats left: 105/150\n", + "- Description: A space epic about a crew racing through distant galaxies to save humanity from an alien threat.\n", + "- Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + "- Director: Sarah Johnson\n", + "\n", + "2) The Midnight Mystery — Theater B — 2025-09-25 at 19:15\n", + "- Genre: Thriller | Rating: R | Duration: 118 min\n", + "- Price: $14.00 | Seats left: 75/200\n", + "- Description: A small-town sheriff investigates mysterious disappearances that all point to midnight.\n", + "- Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + "- Director: Michael Davis\n", + "\n", + "3) Laugh Out Loud — Theater A — 2025-09-25 at 21:45\n", + "- Genre: Comedy | Rating: PG-13 | Duration: 95 min\n", + "- Price: $12.50 | Seats left: 61/150\n", + "- Description: Three friends become viral internet sensations and navigate the chaos of fame.\n", + "- Cast: Tom Martinez, Sarah Kim, David Brown\n", + "- Director: Jennifer Lee\n", + "\n", + "4) Dragon's Heart — Theater C — 2025-09-26 at 10:00\n", + "- Genre: Animation | Rating: G | Duration: 103 min\n", + "- Price: $10.00 | Seats left: 77/100\n", + "- Description: A young girl talks with dragons to save her village from an ancient curse.\n", + "- Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "- Director: Animation Studios Inc.\n", + "\n", + "5) City of Shadows — Theater B — 2025-09-26 at 16:20\n", + "- Genre: Drama | Rating: R | Duration: 134 min\n", + "- Price: $14.00 | Seats left: 44/200\n", + "- Description: A noir about a detective uncovering corruption in 1940s NYC.\n", + "- Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + "- Director: Vincent Romano\n", + "\n", + "6) Ocean's Edge — IMAX Theater — 2025-09-26 at 20:00\n", + "- Genre: Documentary | Rating: G | Duration: 87 min\n", + "- Price: $18.00 | Seats left: 222/300\n", + "- Description: IMAX documentary exploring the deepest oceans and their creatures.\n", + "- Cast: Narrator: David Attenborough\n", + "- Director: Ocean Explorer Films\n", + "\n", + "7) Love in Paris — Theater C — 2025-09-27 at 15:45\n", + "- Genre: Romance | Rating: PG | Duration: 108 min\n", + "- Price: $11.50 | Seats left: 33/100\n", + "- Description: An American tourist finds love in Paris with a local café owner.\n", + "- Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "- Director: Claire Dubois\n", + "\n", + "8) Nightmare Manor — Theater A — 2025-09-27 at 22:30\n", + "- Genre: Horror | Rating: R | Duration: 106 min\n", + "- Price: $13.00 | Seats left: 58/150\n", + "- Description: A family inherits a haunted mansion with spirits of previous owners.\n", + "- Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "- Director: Horror Productions\n", + "\n", + "Would you like me to:\n", + "- fetch more detailed information or reviews for any of these titles\n", + "- check real-time seat availability for a specific showing\n", + "- start a reservation (I’ll need your name, email, and how many seats you want)\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the movies currently playing at MovieMagic Cinema. Want more details or to book tickets? I can help with either.\n", + "\n", + "1) Galactic Adventures — Theater A — 2025-09-25 at 14:30\n", + "- Genre: Science Fiction | Rating: PG-13 | Duration: 142 min\n", + "- Price: $12.50 per seat | Seats left: 105/150\n", + "- Description: A space epic about a crew racing through distant galaxies to save humanity from an alien threat.\n", + "- Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + "- Director: Sarah Johnson\n", + "\n", + "2) The Midnight Mystery — Theater B — 2025-09-25 at 19:15\n", + "- Genre: Thriller | Rating: R | Duration: 118 min\n", + "- Price: $14.00 | Seats left: 75/200\n", + "- Description: A small-town sheriff investigates mysterious disappearances that all point to midnight.\n", + "- Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + "- Director: Michael Davis\n", + "\n", + "3) Laugh Out Loud — Theater A — 2025-09-25 at 21:45\n", + "- Genre: Comedy | Rating: PG-13 | Duration: 95 min\n", + "- Price: $12.50 | Seats left: 61/150\n", + "- Description: Three friends become viral internet sensations and navigate the chaos of fame.\n", + "- Cast: Tom Martinez, Sarah Kim, David Brown\n", + "- Director: Jennifer Lee\n", + "\n", + "4) Dragon's Heart — Theater C — 2025-09-26 at 10:00\n", + "- Genre: Animation | Rating: G | Duration: 103 min\n", + "- Price: $10.00 | Seats left: 77/100\n", + "- Description: A young girl talks with dragons to save her village from an ancient curse.\n", + "- Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "- Director: Animation Studios Inc.\n", + "\n", + "5) City of Shadows — Theater B — 2025-09-26 at 16:20\n", + "- Genre: Drama | Rating: R | Duration: 134 min\n", + "- Price: $14.00 | Seats left: 44/200\n", + "- Description: A noir about a detective uncovering corruption in 1940s NYC.\n", + "- Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + "- Director: Vincent Romano\n", + "\n", + "6) Ocean's Edge — IMAX Theater — 2025-09-26 at 20:00\n", + "- Genre: Documentary | Rating: G | Duration: 87 min\n", + "- Price: $18.00 | Seats left: 222/300\n", + "- Description: IMAX documentary exploring the deepest oceans and their creatures.\n", + "- Cast: Narrator: David Attenborough\n", + "- Director: Ocean Explorer Films\n", + "\n", + "7) Love in Paris — Theater C — 2025-09-27 at 15:45\n", + "- Genre: Romance | Rating: PG | Duration: 108 min\n", + "- Price: $11.50 | Seats left: 33/100\n", + "- Description: An American tourist finds love in Paris with a local café owner.\n", + "- Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "- Director: Claire Dubois\n", + "\n", + "8) Nightmare Manor — Theater A — 2025-09-27 at 22:30\n", + "- Genre: Horror | Rating: R | Duration: 106 min\n", + "- Price: $13.00 | Seats left: 58/150\n", + "- Description: A family inherits a haunted mansion with spirits of previous owners.\n", + "- Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "- Director: Horror Productions\n", + "\n", + "Would you like me to:\n", + "- fetch more detailed information or reviews for any of these titles\n", + "- check real-time seat availability for a specific showing\n", + "- start a reservation (I’ll need your name, email, and how many seats you want)\n", + "\n", + "==================================================\n", + "\n", + "\u001b[32;1m\u001b[1;3mHere are the movies currently playing at MovieMagic Cinema. Want more details or to book tickets? I can help with either.\n", + "\n", + "1) Galactic Adventures — Theater A — 2025-09-25 at 14:30\n", + "- Genre: Science Fiction | Rating: PG-13 | Duration: 142 min\n", + "- Price: $12.50 per seat | Seats left: 105/150\n", + "- Description: A space epic about a crew racing through distant galaxies to save humanity from an alien threat.\n", + "- Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + "- Director: Sarah Johnson\n", + "\n", + "2) The Midnight Mystery — Theater B — 2025-09-25 at 19:15\n", + "- Genre: Thriller | Rating: R | Duration: 118 min\n", + "- Price: $14.00 | Seats left: 75/200\n", + "- Description: A small-town sheriff investigates mysterious disappearances that all point to midnight.\n", + "- Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + "- Director: Michael Davis\n", + "\n", + "3) Laugh Out Loud — Theater A — 2025-09-25 at 21:45\n", + "- Genre: Comedy | Rating: PG-13 | Duration: 95 min\n", + "- Price: $12.50 | Seats left: 61/150\n", + "- Description: Three friends become viral internet sensations and navigate the chaos of fame.\n", + "- Cast: Tom Martinez, Sarah Kim, David Brown\n", + "- Director: Jennifer Lee\n", + "\n", + "4) Dragon's Heart — Theater C — 2025-09-26 at 10:00\n", + "- Genre: Animation | Rating: G | Duration: 103 min\n", + "- Price: $10.00 | Seats left: 77/100\n", + "- Description: A young girl talks with dragons to save her village from an ancient curse.\n", + "- Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "- Director: Animation Studios Inc.\n", + "\n", + "5) City of Shadows — Theater B — 2025-09-26 at 16:20\n", + "- Genre: Drama | Rating: R | Duration: 134 min\n", + "- Price: $14.00 | Seats left: 44/200\n", + "- Description: A noir about a detective uncovering corruption in 1940s NYC.\n", + "- Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + "- Director: Vincent Romano\n", + "\n", + "6) Ocean's Edge — IMAX Theater — 2025-09-26 at 20:00\n", + "- Genre: Documentary | Rating: G | Duration: 87 min\n", + "- Price: $18.00 | Seats left: 222/300\n", + "- Description: IMAX documentary exploring the deepest oceans and their creatures.\n", + "- Cast: Narrator: David Attenborough\n", + "- Director: Ocean Explorer Films\n", + "\n", + "7) Love in Paris — Theater C — 2025-09-27 at 15:45\n", + "- Genre: Romance | Rating: PG | Duration: 108 min\n", + "- Price: $11.50 | Seats left: 33/100\n", + "- Description: An American tourist finds love in Paris with a local café owner.\n", + "- Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "- Director: Claire Dubois\n", + "\n", + "8) Nightmare Manor — Theater A — 2025-09-27 at 22:30\n", + "- Genre: Horror | Rating: R | Duration: 106 min\n", + "- Price: $13.00 | Seats left: 58/150\n", + "- Description: A family inherits a haunted mansion with spirits of previous owners.\n", + "- Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "- Director: Horror Productions\n", + "\n", + "Would you like me to:\n", + "- fetch more detailed information or reviews for any of these titles\n", + "- check real-time seat availability for a specific showing\n", + "- start a reservation (I’ll need your name, email, and how many seats you want)\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the movies currently playing at MovieMagic Cinema. Want more details or to book tickets? I can help with either.\n", + "\n", + "1) Galactic Adventures — Theater A — 2025-09-25 at 14:30\n", + "- Genre: Science Fiction | Rating: PG-13 | Duration: 142 min\n", + "- Price: $12.50 per seat | Seats left: 105/150\n", + "- Description: A space epic about a crew racing through distant galaxies to save humanity from an alien threat.\n", + "- Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + "- Director: Sarah Johnson\n", + "\n", + "2) The Midnight Mystery — Theater B — 2025-09-25 at 19:15\n", + "- Genre: Thriller | Rating: R | Duration: 118 min\n", + "- Price: $14.00 | Seats left: 75/200\n", + "- Description: A small-town sheriff investigates mysterious disappearances that all point to midnight.\n", + "- Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + "- Director: Michael Davis\n", + "\n", + "3) Laugh Out Loud — Theater A — 2025-09-25 at 21:45\n", + "- Genre: Comedy | Rating: PG-13 | Duration: 95 min\n", + "- Price: $12.50 | Seats left: 61/150\n", + "- Description: Three friends become viral internet sensations and navigate the chaos of fame.\n", + "- Cast: Tom Martinez, Sarah Kim, David Brown\n", + "- Director: Jennifer Lee\n", + "\n", + "4) Dragon's Heart — Theater C — 2025-09-26 at 10:00\n", + "- Genre: Animation | Rating: G | Duration: 103 min\n", + "- Price: $10.00 | Seats left: 77/100\n", + "- Description: A young girl talks with dragons to save her village from an ancient curse.\n", + "- Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "- Director: Animation Studios Inc.\n", + "\n", + "5) City of Shadows — Theater B — 2025-09-26 at 16:20\n", + "- Genre: Drama | Rating: R | Duration: 134 min\n", + "- Price: $14.00 | Seats left: 44/200\n", + "- Description: A noir about a detective uncovering corruption in 1940s NYC.\n", + "- Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + "- Director: Vincent Romano\n", + "\n", + "6) Ocean's Edge — IMAX Theater — 2025-09-26 at 20:00\n", + "- Genre: Documentary | Rating: G | Duration: 87 min\n", + "- Price: $18.00 | Seats left: 222/300\n", + "- Description: IMAX documentary exploring the deepest oceans and their creatures.\n", + "- Cast: Narrator: David Attenborough\n", + "- Director: Ocean Explorer Films\n", + "\n", + "7) Love in Paris — Theater C — 2025-09-27 at 15:45\n", + "- Genre: Romance | Rating: PG | Duration: 108 min\n", + "- Price: $11.50 | Seats left: 33/100\n", + "- Description: An American tourist finds love in Paris with a local café owner.\n", + "- Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "- Director: Claire Dubois\n", + "\n", + "8) Nightmare Manor — Theater A — 2025-09-27 at 22:30\n", + "- Genre: Horror | Rating: R | Duration: 106 min\n", + "- Price: $13.00 | Seats left: 58/150\n", + "- Description: A family inherits a haunted mansion with spirits of previous owners.\n", + "- Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "- Director: Horror Productions\n", + "\n", + "Would you like me to:\n", + "- fetch more detailed information or reviews for any of these titles\n", + "- check real-time seat availability for a specific showing\n", + "- start a reservation (I’ll need your name, email, and how many seats you want)\n", + "\n", + "==================================================\n", + "\n" + ] + } + ], + "source": [ + "# 🚀 EXAMPLE USAGE - Run this cell after setting up your API key!\n", + "\n", + "# Simple question about current movies\n", + "await ask_assistant(\"What movies are currently playing?\")\n", + "print(\"\\n\" + \"=\"*50 + \"\\n\")\n", + "\n", + "# Example 2: Search for specific type of movie\n", + "# await ask_assistant(\"I'm looking for action movies playing tomorrow. What do you have?\")\n", + "# print(\"\\n\" + \"=\"*50 + \"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9295d748", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🎬 Welcome to MovieMagic Cinema & Reviews Assistant!\n", + "I can help you find movies, check reviews, get ratings, check showtimes, and make reservations.\n", + "Type 'exit' to quit. Press Enter on an empty line to skip.\n", + "\n", + "🎬 User: What are the reviews for The Midnight Mystery\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: What are the reviews for The Midnight Mystery\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_movie_reviews_by_title` with `{'title': 'The Midnight Mystery'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_movie_reviews_by_title` with `{'title': 'The Midnight Mystery'}`\n", + "\n", + "\n", + "\u001b[0mError processing request: Error executing tool get_movie_reviews_by_title: search_movies() got an unexpected keyword argument 'title'\n", + "Error processing request: Error executing tool get_movie_reviews_by_title: search_movies() got an unexpected keyword argument 'title'\n", + "🎬 User: what movies are playing in cinema currently\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: what movies are playing in cinema currently\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 33,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 67.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 33,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 67.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are the movies currently playing at MovieMagic Cinema:\n", + "\n", + "- Galactic Adventures — Theater A\n", + " - Date/Time: 2025-09-25 at 14:30\n", + " - Duration: 142 min | Genre: Science Fiction | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 105/150\n", + " - Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + " - Director: Sarah Johnson\n", + "\n", + "- The Midnight Mystery — Theater B\n", + " - Date/Time: 2025-09-25 at 19:15\n", + " - Duration: 118 min | Genre: Thriller | Rating: R\n", + " - Price: $14.00 | Seats remaining: 75/200\n", + " - Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + " - Director: Michael Davis\n", + "\n", + "- Laugh Out Loud — Theater A\n", + " - Date/Time: 2025-09-25 at 21:45\n", + " - Duration: 95 min | Genre: Comedy | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 61/150\n", + " - Cast: Tom Martinez, Sarah Kim, David Brown\n", + " - Director: Jennifer Lee\n", + "\n", + "- Dragon's Heart — Theater C\n", + " - Date/Time: 2025-09-26 at 10:00\n", + " - Duration: 103 min | Genre: Animation | Rating: G\n", + " - Price: $10.00 | Seats remaining: 77/100\n", + " - Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + " - Director: Animation Studios Inc.\n", + "\n", + "- City of Shadows — Theater B\n", + " - Date/Time: 2025-09-26 at 16:20\n", + " - Duration: 134 min | Genre: Drama | Rating: R\n", + " - Price: $14.00 | Seats remaining: 44/200\n", + " - Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + " - Director: Vincent Romano\n", + "\n", + "- Ocean's Edge — IMAX Theater\n", + " - Date/Time: 2025-09-26 at 20:00\n", + " - Duration: 87 min | Genre: Documentary | Rating: G\n", + " - Price: $18.00 | Seats remaining: 222/300\n", + " - Cast: Narrator: David Attenborough\n", + " - Director: Ocean Explorer Films\n", + "\n", + "- Love in Paris — Theater C\n", + " - Date/Time: 2025-09-27 at 15:45\n", + " - Duration: 108 min | Genre: Romance | Rating: PG\n", + " - Price: $11.50 | Seats remaining: 33/100\n", + " - Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + " - Director: Claire Dubois\n", + "\n", + "- Nightmare Manor — Theater A\n", + " - Date/Time: 2025-09-27 at 22:30\n", + " - Duration: 106 min | Genre: Horror | Rating: R\n", + " - Price: $13.00 | Seats remaining: 58/150\n", + " - Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + " - Director: Horror Productions\n", + "\n", + "Would you like:\n", + "- more details or reviews for any of these titles?\n", + "- to check real-time seat availability for a specific showing?\n", + "- to start a reservation? If so, I’ll need your name, email, and how many seats you want.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the movies currently playing at MovieMagic Cinema:\n", + "\n", + "- Galactic Adventures — Theater A\n", + " - Date/Time: 2025-09-25 at 14:30\n", + " - Duration: 142 min | Genre: Science Fiction | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 105/150\n", + " - Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + " - Director: Sarah Johnson\n", + "\n", + "- The Midnight Mystery — Theater B\n", + " - Date/Time: 2025-09-25 at 19:15\n", + " - Duration: 118 min | Genre: Thriller | Rating: R\n", + " - Price: $14.00 | Seats remaining: 75/200\n", + " - Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + " - Director: Michael Davis\n", + "\n", + "- Laugh Out Loud — Theater A\n", + " - Date/Time: 2025-09-25 at 21:45\n", + " - Duration: 95 min | Genre: Comedy | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 61/150\n", + " - Cast: Tom Martinez, Sarah Kim, David Brown\n", + " - Director: Jennifer Lee\n", + "\n", + "- Dragon's Heart — Theater C\n", + " - Date/Time: 2025-09-26 at 10:00\n", + " - Duration: 103 min | Genre: Animation | Rating: G\n", + " - Price: $10.00 | Seats remaining: 77/100\n", + " - Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + " - Director: Animation Studios Inc.\n", + "\n", + "- City of Shadows — Theater B\n", + " - Date/Time: 2025-09-26 at 16:20\n", + " - Duration: 134 min | Genre: Drama | Rating: R\n", + " - Price: $14.00 | Seats remaining: 44/200\n", + " - Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + " - Director: Vincent Romano\n", + "\n", + "- Ocean's Edge — IMAX Theater\n", + " - Date/Time: 2025-09-26 at 20:00\n", + " - Duration: 87 min | Genre: Documentary | Rating: G\n", + " - Price: $18.00 | Seats remaining: 222/300\n", + " - Cast: Narrator: David Attenborough\n", + " - Director: Ocean Explorer Films\n", + "\n", + "- Love in Paris — Theater C\n", + " - Date/Time: 2025-09-27 at 15:45\n", + " - Duration: 108 min | Genre: Romance | Rating: PG\n", + " - Price: $11.50 | Seats remaining: 33/100\n", + " - Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + " - Director: Claire Dubois\n", + "\n", + "- Nightmare Manor — Theater A\n", + " - Date/Time: 2025-09-27 at 22:30\n", + " - Duration: 106 min | Genre: Horror | Rating: R\n", + " - Price: $13.00 | Seats remaining: 58/150\n", + " - Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + " - Director: Horror Productions\n", + "\n", + "Would you like:\n", + "- more details or reviews for any of these titles?\n", + "- to check real-time seat availability for a specific showing?\n", + "- to start a reservation? If so, I’ll need your name, email, and how many seats you want.\n", + "\u001b[32;1m\u001b[1;3mHere are the movies currently playing at MovieMagic Cinema:\n", + "\n", + "- Galactic Adventures — Theater A\n", + " - Date/Time: 2025-09-25 at 14:30\n", + " - Duration: 142 min | Genre: Science Fiction | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 105/150\n", + " - Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + " - Director: Sarah Johnson\n", + "\n", + "- The Midnight Mystery — Theater B\n", + " - Date/Time: 2025-09-25 at 19:15\n", + " - Duration: 118 min | Genre: Thriller | Rating: R\n", + " - Price: $14.00 | Seats remaining: 75/200\n", + " - Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + " - Director: Michael Davis\n", + "\n", + "- Laugh Out Loud — Theater A\n", + " - Date/Time: 2025-09-25 at 21:45\n", + " - Duration: 95 min | Genre: Comedy | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 61/150\n", + " - Cast: Tom Martinez, Sarah Kim, David Brown\n", + " - Director: Jennifer Lee\n", + "\n", + "- Dragon's Heart — Theater C\n", + " - Date/Time: 2025-09-26 at 10:00\n", + " - Duration: 103 min | Genre: Animation | Rating: G\n", + " - Price: $10.00 | Seats remaining: 77/100\n", + " - Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + " - Director: Animation Studios Inc.\n", + "\n", + "- City of Shadows — Theater B\n", + " - Date/Time: 2025-09-26 at 16:20\n", + " - Duration: 134 min | Genre: Drama | Rating: R\n", + " - Price: $14.00 | Seats remaining: 44/200\n", + " - Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + " - Director: Vincent Romano\n", + "\n", + "- Ocean's Edge — IMAX Theater\n", + " - Date/Time: 2025-09-26 at 20:00\n", + " - Duration: 87 min | Genre: Documentary | Rating: G\n", + " - Price: $18.00 | Seats remaining: 222/300\n", + " - Cast: Narrator: David Attenborough\n", + " - Director: Ocean Explorer Films\n", + "\n", + "- Love in Paris — Theater C\n", + " - Date/Time: 2025-09-27 at 15:45\n", + " - Duration: 108 min | Genre: Romance | Rating: PG\n", + " - Price: $11.50 | Seats remaining: 33/100\n", + " - Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + " - Director: Claire Dubois\n", + "\n", + "- Nightmare Manor — Theater A\n", + " - Date/Time: 2025-09-27 at 22:30\n", + " - Duration: 106 min | Genre: Horror | Rating: R\n", + " - Price: $13.00 | Seats remaining: 58/150\n", + " - Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + " - Director: Horror Productions\n", + "\n", + "Would you like:\n", + "- more details or reviews for any of these titles?\n", + "- to check real-time seat availability for a specific showing?\n", + "- to start a reservation? If so, I’ll need your name, email, and how many seats you want.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the movies currently playing at MovieMagic Cinema:\n", + "\n", + "- Galactic Adventures — Theater A\n", + " - Date/Time: 2025-09-25 at 14:30\n", + " - Duration: 142 min | Genre: Science Fiction | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 105/150\n", + " - Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + " - Director: Sarah Johnson\n", + "\n", + "- The Midnight Mystery — Theater B\n", + " - Date/Time: 2025-09-25 at 19:15\n", + " - Duration: 118 min | Genre: Thriller | Rating: R\n", + " - Price: $14.00 | Seats remaining: 75/200\n", + " - Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + " - Director: Michael Davis\n", + "\n", + "- Laugh Out Loud — Theater A\n", + " - Date/Time: 2025-09-25 at 21:45\n", + " - Duration: 95 min | Genre: Comedy | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 61/150\n", + " - Cast: Tom Martinez, Sarah Kim, David Brown\n", + " - Director: Jennifer Lee\n", + "\n", + "- Dragon's Heart — Theater C\n", + " - Date/Time: 2025-09-26 at 10:00\n", + " - Duration: 103 min | Genre: Animation | Rating: G\n", + " - Price: $10.00 | Seats remaining: 77/100\n", + " - Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + " - Director: Animation Studios Inc.\n", + "\n", + "- City of Shadows — Theater B\n", + " - Date/Time: 2025-09-26 at 16:20\n", + " - Duration: 134 min | Genre: Drama | Rating: R\n", + " - Price: $14.00 | Seats remaining: 44/200\n", + " - Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + " - Director: Vincent Romano\n", + "\n", + "- Ocean's Edge — IMAX Theater\n", + " - Date/Time: 2025-09-26 at 20:00\n", + " - Duration: 87 min | Genre: Documentary | Rating: G\n", + " - Price: $18.00 | Seats remaining: 222/300\n", + " - Cast: Narrator: David Attenborough\n", + " - Director: Ocean Explorer Films\n", + "\n", + "- Love in Paris — Theater C\n", + " - Date/Time: 2025-09-27 at 15:45\n", + " - Duration: 108 min | Genre: Romance | Rating: PG\n", + " - Price: $11.50 | Seats remaining: 33/100\n", + " - Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + " - Director: Claire Dubois\n", + "\n", + "- Nightmare Manor — Theater A\n", + " - Date/Time: 2025-09-27 at 22:30\n", + " - Duration: 106 min | Genre: Horror | Rating: R\n", + " - Price: $13.00 | Seats remaining: 58/150\n", + " - Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + " - Director: Horror Productions\n", + "\n", + "Would you like:\n", + "- more details or reviews for any of these titles?\n", + "- to check real-time seat availability for a specific showing?\n", + "- to start a reservation? If so, I’ll need your name, email, and how many seats you want.\n", + "🎬 User: can I make reservation for Laugh Out Loud\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: can I make reservation for Laugh Out Loud\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mFantastic choice! I can help you reserve Laugh Out Loud.\n", + "\n", + "Showing details:\n", + "- Movie: Laugh Out Loud\n", + "- Theater: A\n", + "- Date: 2025-09-25\n", + "- Time: 21:45\n", + "- Duration: 95 minutes\n", + "- Genre: Comedy | Rating: PG-13\n", + "- Price per seat: $12.50\n", + "- Seats remaining: 61 of 150\n", + "\n", + "To book, I’ll need:\n", + "- Full name (as it should appear on the reservation)\n", + "- Email address (for the confirmation)\n", + "- Number of seats you want (1 up to 61 currently available)\n", + "\n", + "Optional:\n", + "- Phone number (for contact)\n", + "- Any special requests (e.g., accessibility needs, seating preferences)\n", + "\n", + "Once you share these, I’ll place the reservation and send you the confirmation with the booking details and total price.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Fantastic choice! I can help you reserve Laugh Out Loud.\n", + "\n", + "Showing details:\n", + "- Movie: Laugh Out Loud\n", + "- Theater: A\n", + "- Date: 2025-09-25\n", + "- Time: 21:45\n", + "- Duration: 95 minutes\n", + "- Genre: Comedy | Rating: PG-13\n", + "- Price per seat: $12.50\n", + "- Seats remaining: 61 of 150\n", + "\n", + "To book, I’ll need:\n", + "- Full name (as it should appear on the reservation)\n", + "- Email address (for the confirmation)\n", + "- Number of seats you want (1 up to 61 currently available)\n", + "\n", + "Optional:\n", + "- Phone number (for contact)\n", + "- Any special requests (e.g., accessibility needs, seating preferences)\n", + "\n", + "Once you share these, I’ll place the reservation and send you the confirmation with the booking details and total price.\n", + "\u001b[32;1m\u001b[1;3mFantastic choice! I can help you reserve Laugh Out Loud.\n", + "\n", + "Showing details:\n", + "- Movie: Laugh Out Loud\n", + "- Theater: A\n", + "- Date: 2025-09-25\n", + "- Time: 21:45\n", + "- Duration: 95 minutes\n", + "- Genre: Comedy | Rating: PG-13\n", + "- Price per seat: $12.50\n", + "- Seats remaining: 61 of 150\n", + "\n", + "To book, I’ll need:\n", + "- Full name (as it should appear on the reservation)\n", + "- Email address (for the confirmation)\n", + "- Number of seats you want (1 up to 61 currently available)\n", + "\n", + "Optional:\n", + "- Phone number (for contact)\n", + "- Any special requests (e.g., accessibility needs, seating preferences)\n", + "\n", + "Once you share these, I’ll place the reservation and send you the confirmation with the booking details and total price.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Fantastic choice! I can help you reserve Laugh Out Loud.\n", + "\n", + "Showing details:\n", + "- Movie: Laugh Out Loud\n", + "- Theater: A\n", + "- Date: 2025-09-25\n", + "- Time: 21:45\n", + "- Duration: 95 minutes\n", + "- Genre: Comedy | Rating: PG-13\n", + "- Price per seat: $12.50\n", + "- Seats remaining: 61 of 150\n", + "\n", + "To book, I’ll need:\n", + "- Full name (as it should appear on the reservation)\n", + "- Email address (for the confirmation)\n", + "- Number of seats you want (1 up to 61 currently available)\n", + "\n", + "Optional:\n", + "- Phone number (for contact)\n", + "- Any special requests (e.g., accessibility needs, seating preferences)\n", + "\n", + "Once you share these, I’ll place the reservation and send you the confirmation with the booking details and total price.\n" + ] + } + ], + "source": [ + "# Interactive chat loop — keep asking questions until you exit\n", + "\n", + "# Try these example questions:\n", + "# - \"What movies are playing today?\"\n", + "# - \"Show me action movies and their reviews\"\n", + "# - \"Tell me about Avatar - plot, cast, and reviews\"\n", + "# - \"I want to see Avatar tomorrow evening - show me reviews and showtimes\"\n", + "# - \"Get me details for Avatar on 2025-09-25 at 19:30 in theater_a\"\n", + "# - \"Book 2 seats for Avatar on 2025-09-25 at 19:30 in theater_a for John Doe, john@email.com\"\n", + "# - \"Show my reservations for john@email.com\"\n", + "# - \"Find me a random movie recommendation with good reviews\"\n", + "\n", + "async def chat_loop():\n", + " print(\"🎬 Welcome to MovieMagic Cinema & Reviews Assistant!\")\n", + " print(\"I can help you find movies, check reviews, get ratings, check showtimes, and make reservations.\")\n", + " print(\"Type 'exit' to quit. Press Enter on an empty line to skip.\\n\")\n", + " \n", + " while True:\n", + " try:\n", + " question = input(\"You: \").strip()\n", + " except (EOFError, KeyboardInterrupt):\n", + " print(\"\\nExiting.\")\n", + " break\n", + " if not question:\n", + " continue\n", + " if question.lower() in (\"exit\", \"quit\", \"q\"):\n", + " print(\"🎬 Thanks for using MovieMagic Cinema & Reviews! Goodbye!\")\n", + " break\n", + " await ask_assistant(question)\n", + "\n", + "# Start the cinema and movie reviews chat loop\n", + "await chat_loop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e70dad2d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🧹 Cleanup function ready!\n" + ] + } + ], + "source": [ + "# Cleanup function for HTTP MCP clients\n", + "async def cleanup_mcp():\n", + " \"\"\"Cleanup MCP client and server resources\"\"\"\n", + " global mcp_client\n", + " if mcp_client:\n", + " try:\n", + " await mcp_client.close()\n", + " print(\"Cinema and Movie Reviews MCP clients closed\")\n", + " except Exception as e:\n", + " print(f\"Warning: Error closing MCP clients: {e}\")\n", + " mcp_client = None\n", + "\n", + "print(\"🧹 Cleanup function ready!\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/mcp/cinema-mcp/README.md b/src/mcp/cinema-mcp/README.md new file mode 100644 index 0000000..e72b63a --- /dev/null +++ b/src/mcp/cinema-mcp/README.md @@ -0,0 +1,84 @@ +# Cinema MCP Server + +A Model Context Protocol (MCP) server for managing movie theater showings and reservations. + +## Features + +- **Current Movies**: View all currently playing movies with showtimes +- **Movie Details**: Get detailed information about specific movie showings +- **Search Movies**: Filter movies by genre, date, theater room, or seat availability +- **Daily Schedule**: View all movies for a specific date + +## Quick Start + +1. Install dependencies: + ```bash + uv pip install -r requirements.txt + ``` + +2. Run the MCP server: + ```bash + uv run mcp dev main.py + ``` + +3. The server will start on port 8009 + +## Available Tools + +### `get_current_movies()` +Returns all currently playing movies with complete details including showtimes, seat availability, and pricing. + +### `get_movie_details(movie_id: str)` +Get detailed information about a specific movie showing including cast, director, and theater details. + +### `search_movies(genre, date, room, available_seats_min, limit)` +Search and filter movies by various criteria: +- **genre**: action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family +- **date**: YYYY-MM-DD format +- **room**: theater_a, theater_b, theater_c, imax +- **available_seats_min**: minimum seats required +- **limit**: maximum results (default: 20) + +### `get_movies_by_date(date: str)` +Get all movie showings for a specific date, sorted by showtime. + +## Available Resources + +- `movie://{movie_id}` - Formatted movie details +- `cinema://current-movies` - Current movies overview + +## Mock Data + +The server uses `MOCK_MOVIE_PRESENTATIONS` from `config.py` with sample movies including: +- Galactic Adventures (Sci-Fi) +- The Midnight Mystery (Thriller) +- Laugh Out Loud (Comedy) +- Dragon's Heart (Animation) +- City of Shadows (Drama) +- Ocean's Edge (Documentary/IMAX) +- Love in Paris (Romance) +- Nightmare Manor (Horror) + +## Configuration + +Edit `config.py` to modify: +- Theater room configurations +- Movie genres and ratings +- Mock movie data +- Pricing and capacity settings + +## Usage Examples + +```python +# Get all current movies +movies = get_current_movies() + +# Search for action movies with at least 50 seats available +action_movies = search_movies(genre="action", available_seats_min=50) + +# Get movies playing tomorrow +tomorrow_movies = get_movies_by_date("2025-09-26") + +# Get details for a specific movie +movie_details = get_movie_details("movie_001") +``` \ No newline at end of file diff --git a/src/mcp/cinema-mcp/__init__.py b/src/mcp/cinema-mcp/__init__.py new file mode 100644 index 0000000..fc760c0 --- /dev/null +++ b/src/mcp/cinema-mcp/__init__.py @@ -0,0 +1,7 @@ +""" +Cinema MCP Server Package +""" + +__version__ = "1.0.0" +__author__ = "Cinema MCP Team" +__description__ = "Movie theater management and booking MCP server" \ No newline at end of file diff --git a/src/mcp/cinema-mcp/cinema_service.py b/src/mcp/cinema-mcp/cinema_service.py new file mode 100644 index 0000000..f340e4a --- /dev/null +++ b/src/mcp/cinema-mcp/cinema_service.py @@ -0,0 +1,751 @@ +""" +Cinema service with MCP tools and API logic. +""" + +from typing import Dict, Any, List, Optional +from dataclasses import asdict +from datetime import datetime, date, time + +from config import ( + MOCK_MOVIE_PRESENTATIONS, CINEMA_ROOMS, MOVIE_GENRES, + DEFAULT_SEARCH_LIMIT, POPULAR_MOVIES +) +from models import MovieShowing, ContactInfo, MovieReservation +from utils import ( + get_movie_by_id, get_movie_by_natural_id, search_movie_presentations, get_current_movies, + get_movies_by_date, search_movies_by_title, format_movie_details, parse_movie_data +) + + +def get_current_movies_data() -> Dict[str, Any]: + """Get all currently playing movies + + Returns: + List of currently playing movies with their details + """ + try: + current_movies = get_current_movies() + + if not current_movies: + return {"error": "No movies currently playing"} + + movies_list = [] + for movie_data in current_movies: + movie = parse_movie_data(movie_data) + # Only show movies with available seats + if movie.seats_remaining > 0: + movies_list.append({ + "title": movie.title, + "description": movie.description, + "date": movie.date.isoformat(), + "time": movie.time.strftime("%H:%M"), + "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), + "seats_remaining": movie.seats_remaining, + "seats_total": movie.seats_available, + "duration_minutes": movie.duration_minutes, + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "rating": movie.rating, + "price_per_seat": movie_data.get("price_per_seat"), + "director": movie_data.get("director"), + "cast": movie_data.get("cast", []), + "is_sold_out": movie.is_sold_out, + "occupancy_percentage": round(movie.occupancy_percentage, 1) + }) + + return { + "cinema_name": "MovieMagic Cinema", + "total_movies": len(movies_list), + "movies": movies_list + } + + except Exception as e: + return {"error": f"Failed to get current movies: {str(e)}"} + + +def get_movie_details_data( + title: str, + date: Optional[str] = None, + time: Optional[str] = None, + room: Optional[str] = None +) -> Dict[str, Any]: + """Get detailed information about a specific movie presentation + + Args: + title: Movie title (partial match allowed) + date: Date in YYYY-MM-DD format (optional, helps narrow results) + time: Time in HH:MM or HH:MM AM/PM format (optional, helps narrow results) + room: Room identifier like 'theater_a' or 'Theater A' (optional, helps narrow results) + + Returns: + MovieShowing object as dictionary or error dict + """ + try: + movie_data = get_movie_by_natural_id(title, date, time, room) + if not movie_data: + error_msg = f"No movie found with title containing '{title}'" + if date or time or room: + filters = [] + if date: filters.append(f"date: {date}") + if time: filters.append(f"time: {time}") + if room: filters.append(f"room: {room}") + error_msg += f" and filters: {', '.join(filters)}" + return {"error": error_msg} + + movie = parse_movie_data(movie_data) + room_info = CINEMA_ROOMS.get(movie.room, {}) + + return { + "movie": { + "title": movie.title, + "description": movie.description, + "date": movie.date.isoformat(), + "time": movie.time.strftime("%H:%M"), + "duration_minutes": movie.duration_minutes, + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "rating": movie.rating, + "director": movie_data.get("director"), + "cast": movie_data.get("cast", []), + "poster_url": movie_data.get("poster_url") + }, + "showing_details": { + "room": room_info.get("name", movie.room), + "room_type": room_info.get("type", "standard"), + "seats_total": movie.seats_available, + "seats_booked": movie.seats_booked, + "seats_remaining": movie.seats_remaining, + "is_sold_out": movie.is_sold_out, + "occupancy_percentage": round(movie.occupancy_percentage, 1), + "price_per_seat": movie_data.get("price_per_seat") + } + } + + except Exception as e: + return {"error": f"Failed to get movie details: {str(e)}"} + + +def search_movies_data( + title: Optional[str] = None, + genre: Optional[str] = None, + date: Optional[str] = None, + room: Optional[str] = None, + available_seats_min: Optional[int] = None, + limit: int = DEFAULT_SEARCH_LIMIT +) -> Dict[str, Any]: + """Search for movie presentations with filters + + Args: + title: Movie title to search for (partial match, case-insensitive) + genre: Movie genre filter (e.g., "action", "comedy", "drama") + date: Date filter in YYYY-MM-DD format + room: Cinema room filter + available_seats_min: Minimum available seats required + limit: Maximum number of results (default: 20, max: 100) + + Returns: + Filtered list of movie presentations or error dict + """ + try: + if limit > 100: + limit = 100 + elif limit < 1: + limit = 1 + + # Convert date string to date object if provided + date_filter = None + if date: + try: + date_filter = datetime.strptime(date, "%Y-%m-%d").date() + except ValueError: + return {"error": "Invalid date format. Use YYYY-MM-DD"} + + movies = search_movie_presentations( + title=title, + genre=genre, + date=date_filter, + room=room, + available_seats_min=available_seats_min, + limit=limit + ) + + if not movies: + return {"error": "No movies found matching the criteria"} + + movies_list = [] + for movie_data in movies: + movie = parse_movie_data(movie_data) + movies_list.append({ + "title": movie.title, + "date": movie.date.isoformat(), + "time": movie.time.strftime("%H:%M"), + "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "rating": movie.rating, + "seats_remaining": movie.seats_remaining, + "price_per_seat": movie_data.get("price_per_seat"), + "is_sold_out": movie.is_sold_out + }) + + # Build filter summary + filters_applied = [] + if title: + filters_applied.append(f"Title: {title}") + if genre: + filters_applied.append(f"Genre: {MOVIE_GENRES.get(genre, genre)}") + if date: + filters_applied.append(f"Date: {date}") + if room: + filters_applied.append(f"Room: {CINEMA_ROOMS.get(room, {}).get('name', room)}") + if available_seats_min: + filters_applied.append(f"Min available seats: {available_seats_min}") + + return { + "search_criteria": filters_applied if filters_applied else ["No filters applied"], + "total_found": len(movies_list), + "movies": movies_list + } + + except Exception as e: + return {"error": f"Failed to search movies: {str(e)}"} + + +def get_movies_by_date_data(date: str) -> Dict[str, Any]: + """Get all movies playing on a specific date + + Args: + date: Date in YYYY-MM-DD format + + Returns: + List of movies for the specified date + """ + try: + try: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + except ValueError: + return {"error": "Invalid date format. Use YYYY-MM-DD"} + + movies = get_movies_by_date(target_date) + + if not movies: + return {"error": f"No movies scheduled for {date}"} + + movies_list = [] + for movie_data in movies: + movie = parse_movie_data(movie_data) + movies_list.append({ + "title": movie.title, + "time": movie.time.strftime("%H:%M"), + "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "duration_minutes": movie.duration_minutes, + "seats_remaining": movie.seats_remaining, + "price_per_seat": movie_data.get("price_per_seat"), + "is_sold_out": movie.is_sold_out + }) + + # Sort by time + movies_list.sort(key=lambda x: x["time"]) + + return { + "date": date, + "total_showings": len(movies_list), + "movies": movies_list + } + + except Exception as e: + return {"error": f"Failed to get movies for date: {str(e)}"} + + +def search_movies_by_title_data(title: str) -> Dict[str, Any]: + """Search for movies by title only + + Args: + title: Movie title to search for (partial match, case-insensitive) + + Returns: + List of movies matching the title search + """ + try: + if not title or not title.strip(): + return {"error": "Title is required for search"} + + movies = search_movies_by_title(title.strip()) + + if not movies: + return {"error": f"No movies found with title containing '{title}'"} + + movies_list = [] + for movie_data in movies: + movie = parse_movie_data(movie_data) + movies_list.append({ + "title": movie.title, + "date": movie.date.isoformat(), + "time": movie.time.strftime("%H:%M"), + "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "rating": movie.rating, + "seats_remaining": movie.seats_remaining, + "price_per_seat": movie_data.get("price_per_seat"), + "director": movie_data.get("director"), + "is_sold_out": movie.is_sold_out + }) + + return { + "search_title": title, + "total_found": len(movies_list), + "movies": movies_list + } + + except Exception as e: + return {"error": f"Failed to search movies by title: {str(e)}"} + + +# In-memory storage for reservations (in production, this would be a database) +RESERVATIONS: List[MovieReservation] = [] + +def create_reservation_data( + title: str, + date: str, + time: str, + room: str, + seats_count: int, + customer_name: str, + customer_email: str, + customer_phone: Optional[str] = None, + special_requests: Optional[str] = None +) -> Dict[str, Any]: + """Create a new movie reservation + + Args: + title: Exact movie title + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + seats_count: Number of seats to reserve + customer_name: Customer's full name + customer_email: Customer's email address + customer_phone: Customer's phone number (optional) + special_requests: Any special requests (optional) + + Returns: + Reservation confirmation details or error dict + """ + try: + # Find the movie showing + movie_data = get_movie_by_natural_id(title, date, time, room) + if not movie_data: + return {"error": f"No movie found: '{title}' on {date} at {time} in {room}"} + + movie = parse_movie_data(movie_data) + + # Check seat availability + if seats_count <= 0: + return {"error": "Number of seats must be greater than 0"} + + if seats_count > movie.seats_remaining: + return {"error": f"Not enough seats available. Only {movie.seats_remaining} seats remaining"} + + # Validate contact info + if not customer_name or not customer_name.strip(): + return {"error": "Customer name is required"} + + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + # Basic email validation + if "@" not in customer_email or "." not in customer_email: + return {"error": "Please provide a valid email address"} + + # Create contact info + contact_info = ContactInfo( + name=customer_name.strip(), + email=customer_email.strip().lower(), + phone=customer_phone.strip() if customer_phone else None + ) + + # Parse date and time + try: + reservation_date = datetime.strptime(date, "%Y-%m-%d").date() + reservation_time = datetime.strptime(time, "%H:%M").time() + except ValueError as e: + return {"error": f"Invalid date or time format: {str(e)}"} + + # Create reservation + reservation = MovieReservation( + movie_title=title, + movie_date=reservation_date, + movie_time=reservation_time, + room=room, + seats_reserved=seats_count, + contact_info=contact_info, + reservation_datetime=datetime.now(), + status="confirmed", + special_requests=special_requests.strip() if special_requests else None + ) + + # Add to storage + RESERVATIONS.append(reservation) + + # Update movie booking count (simulate booking the seats) + movie_data["seats_booked"] = movie_data.get("seats_booked", 0) + seats_count + + # Generate confirmation + room_name = CINEMA_ROOMS.get(room, {}).get("name", room) + genre_name = MOVIE_GENRES.get(movie.genre, movie.genre) + + return { + "confirmation": { + "reservation_id": len(RESERVATIONS), # Simple ID based on list length + "status": "confirmed", + "reservation_datetime": reservation.reservation_datetime.isoformat() + }, + "movie_details": { + "title": title, + "date": date, + "time": time, + "room": room_name, + "genre": genre_name, + "duration_minutes": movie.duration_minutes + }, + "booking_details": { + "seats_reserved": seats_count, + "customer_name": customer_name, + "customer_email": customer_email, + "customer_phone": customer_phone, + "special_requests": special_requests + }, + "pricing": { + "price_per_seat": movie_data.get("price_per_seat", 0), + "total_price": movie_data.get("price_per_seat", 0) * seats_count + } + } + + except Exception as e: + return {"error": f"Failed to create reservation: {str(e)}"} + + +def get_customer_reservations_data(customer_email: str) -> Dict[str, Any]: + """Get all reservations for a customer by email + + Args: + customer_email: Customer's email address + + Returns: + List of customer's reservations or error dict + """ + try: + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + email = customer_email.strip().lower() + customer_reservations = [ + res for res in RESERVATIONS + if res.customer_email.lower() == email and res.status != "cancelled" + ] + + if not customer_reservations: + return {"error": f"No reservations found for {customer_email}"} + + reservations_list = [] + for i, reservation in enumerate(customer_reservations, 1): + room_name = CINEMA_ROOMS.get(reservation.room, {}).get("name", reservation.room) + + reservations_list.append({ + "reservation_number": i, + "movie_title": reservation.movie_title, + "date": reservation.movie_date.isoformat(), + "time": reservation.movie_time.strftime("%H:%M"), + "room": room_name, + "seats_reserved": reservation.seats_reserved, + "status": reservation.status, + "reservation_made": reservation.reservation_datetime.isoformat(), + "special_requests": reservation.special_requests + }) + + return { + "customer_email": customer_email, + "total_reservations": len(reservations_list), + "reservations": reservations_list + } + + except Exception as e: + return {"error": f"Failed to get customer reservations: {str(e)}"} + + +def cancel_reservation_data( + customer_email: str, + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Cancel a reservation + + Args: + customer_email: Customer's email address + title: Movie title + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + + Returns: + Cancellation confirmation or error dict + """ + try: + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + email = customer_email.strip().lower() + + # Parse date and time for comparison + try: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + target_time = datetime.strptime(time, "%H:%M").time() + except ValueError as e: + return {"error": f"Invalid date or time format: {str(e)}"} + + # Find the reservation + reservation_to_cancel = None + for reservation in RESERVATIONS: + if (reservation.customer_email.lower() == email and + reservation.movie_title.lower() == title.lower() and + reservation.movie_date == target_date and + reservation.movie_time == target_time and + reservation.room.lower() == room.lower() and + reservation.status == "confirmed"): + reservation_to_cancel = reservation + break + + if not reservation_to_cancel: + return {"error": f"No confirmed reservation found for {customer_email} for '{title}' on {date} at {time} in {room}"} + + # Cancel the reservation + reservation_to_cancel.status = "cancelled" + + # Free up the seats (simulate returning seats to availability) + movie_data = get_movie_by_natural_id(title, date, time, room) + if movie_data: + movie_data["seats_booked"] = max(0, movie_data.get("seats_booked", 0) - reservation_to_cancel.seats_reserved) + + room_name = CINEMA_ROOMS.get(room, {}).get("name", room) + + return { + "cancellation_confirmed": True, + "cancelled_reservation": { + "movie_title": title, + "date": date, + "time": time, + "room": room_name, + "seats_freed": reservation_to_cancel.seats_reserved, + "customer_email": customer_email + }, + "message": f"Reservation cancelled successfully. {reservation_to_cancel.seats_reserved} seats have been freed up." + } + + except Exception as e: + return {"error": f"Failed to cancel reservation: {str(e)}"} + + +# Employee Management Functions +def get_all_reservations_data(status_filter: Optional[str] = None) -> Dict[str, Any]: + """Get all reservations for cinema employee review + + Args: + status_filter: Filter by status ("confirmed", "completed", "cancelled") or None for all + + Returns: + List of all reservations matching the filter + """ + try: + # Filter reservations by status if specified + if status_filter: + filtered_reservations = [ + res for res in RESERVATIONS + if res.status.lower() == status_filter.lower() + ] + else: + filtered_reservations = RESERVATIONS.copy() + + if not filtered_reservations: + status_msg = f" with status '{status_filter}'" if status_filter else "" + return {"error": f"No reservations found{status_msg}"} + + reservations_list = [] + for i, reservation in enumerate(filtered_reservations, 1): + room_name = CINEMA_ROOMS.get(reservation.room, {}).get("name", reservation.room) + + reservations_list.append({ + "reservation_number": i, + "movie_title": reservation.movie_title, + "date": reservation.movie_date.isoformat(), + "time": reservation.movie_time.strftime("%H:%M"), + "room": room_name, + "seats_reserved": reservation.seats_reserved, + "customer_name": reservation.customer_name, + "customer_email": reservation.customer_email, + "customer_phone": reservation.contact_info.phone, + "status": reservation.status, + "reservation_made": reservation.reservation_datetime.isoformat(), + "special_requests": reservation.special_requests + }) + + return { + "status_filter": status_filter or "all", + "total_reservations": len(reservations_list), + "reservations": reservations_list + } + + except Exception as e: + return {"error": f"Failed to get reservations: {str(e)}"} + + +def acknowledge_reservation_data( + customer_email: str, + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Acknowledge/complete a reservation (employee function) + + Args: + customer_email: Customer's email address + title: Movie title + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + + Returns: + Acknowledgment confirmation or error dict + """ + try: + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + email = customer_email.strip().lower() + + # Parse date and time for comparison + try: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + target_time = datetime.strptime(time, "%H:%M").time() + except ValueError as e: + return {"error": f"Invalid date or time format: {str(e)}"} + + # Find the reservation + reservation_to_acknowledge = None + for reservation in RESERVATIONS: + if (reservation.customer_email.lower() == email and + reservation.movie_title.lower() == title.lower() and + reservation.movie_date == target_date and + reservation.movie_time == target_time and + reservation.room.lower() == room.lower() and + reservation.status == "confirmed"): + reservation_to_acknowledge = reservation + break + + if not reservation_to_acknowledge: + return {"error": f"No confirmed reservation found for {customer_email} for '{title}' on {date} at {time} in {room}"} + + # Acknowledge the reservation (mark as completed) + reservation_to_acknowledge.status = "completed" + + # Since the customer attended, we don't free up seats - they were actually used + # The seats remain "booked" as they were occupied during the showing + + room_name = CINEMA_ROOMS.get(room, {}).get("name", room) + + return { + "acknowledgment_confirmed": True, + "completed_reservation": { + "movie_title": title, + "date": date, + "time": time, + "room": room_name, + "seats_used": reservation_to_acknowledge.seats_reserved, + "customer_email": customer_email, + "customer_name": reservation_to_acknowledge.customer_name + }, + "message": f"Reservation acknowledged as completed. Customer {reservation_to_acknowledge.customer_name} attended the showing." + } + + except Exception as e: + return {"error": f"Failed to acknowledge reservation: {str(e)}"} + + +def revoke_reservation_data( + customer_email: str, + title: str, + date: str, + time: str, + room: str, + reason: Optional[str] = None +) -> Dict[str, Any]: + """Revoke/cancel a reservation (employee function) + + Args: + customer_email: Customer's email address + title: Movie title + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + reason: Reason for revocation (optional) + + Returns: + Revocation confirmation or error dict + """ + try: + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + email = customer_email.strip().lower() + + # Parse date and time for comparison + try: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + target_time = datetime.strptime(time, "%H:%M").time() + except ValueError as e: + return {"error": f"Invalid date or time format: {str(e)}"} + + # Find the reservation + reservation_to_revoke = None + for reservation in RESERVATIONS: + if (reservation.customer_email.lower() == email and + reservation.movie_title.lower() == title.lower() and + reservation.movie_date == target_date and + reservation.movie_time == target_time and + reservation.room.lower() == room.lower() and + reservation.status == "confirmed"): + reservation_to_revoke = reservation + break + + if not reservation_to_revoke: + return {"error": f"No confirmed reservation found for {customer_email} for '{title}' on {date} at {time} in {room}"} + + # Revoke the reservation (mark as cancelled) + reservation_to_revoke.status = "cancelled" + if reason: + reservation_to_revoke.special_requests = f"CANCELLED: {reason}" + + # Free up the seats (same as customer cancellation) + movie_data = get_movie_by_natural_id(title, date, time, room) + if movie_data: + movie_data["seats_booked"] = max(0, movie_data.get("seats_booked", 0) - reservation_to_revoke.seats_reserved) + + room_name = CINEMA_ROOMS.get(room, {}).get("name", room) + + return { + "revocation_confirmed": True, + "revoked_reservation": { + "movie_title": title, + "date": date, + "time": time, + "room": room_name, + "seats_freed": reservation_to_revoke.seats_reserved, + "customer_email": customer_email, + "customer_name": reservation_to_revoke.customer_name, + "reason": reason + }, + "message": f"Reservation revoked successfully. {reservation_to_revoke.seats_reserved} seats have been freed up. Reason: {reason or 'No reason provided'}" + } + + except Exception as e: + return {"error": f"Failed to revoke reservation: {str(e)}"} \ No newline at end of file diff --git a/src/mcp/cinema-mcp/config.py b/src/mcp/cinema-mcp/config.py new file mode 100644 index 0000000..30c5546 --- /dev/null +++ b/src/mcp/cinema-mcp/config.py @@ -0,0 +1,191 @@ +""" +Cinema MCP configuration and mock data. +""" + +from datetime import date, time + +# Cinema configuration +CINEMA_NAME = "MovieMagic Cinema" +CINEMA_LOCATION = "Downtown Plaza" + +# Room/Theater configurations +CINEMA_ROOMS = { + "theater_a": {"name": "Theater A", "capacity": 150, "type": "standard"}, + "theater_b": {"name": "Theater B", "capacity": 200, "type": "premium"}, + "theater_c": {"name": "Theater C", "capacity": 100, "type": "vip"}, + "imax": {"name": "IMAX Theater", "capacity": 300, "type": "imax"} +} + +# Movie genres +MOVIE_GENRES = { + "action": "Action", + "comedy": "Comedy", + "drama": "Drama", + "horror": "Horror", + "sci-fi": "Science Fiction", + "romance": "Romance", + "thriller": "Thriller", + "animation": "Animation", + "documentary": "Documentary", + "family": "Family" +} + +# Movie ratings +MOVIE_RATINGS = ["G", "PG", "PG-13", "R", "NC-17"] + +# Booking status codes +BOOKING_STATUS = { + "pending": "Pending Confirmation", + "confirmed": "Confirmed", + "cancelled": "Cancelled", + "completed": "Completed" +} + +# Default values +DEFAULT_SEARCH_LIMIT = 20 +MAX_SEARCH_LIMIT = 100 + +# Mock movie presentations data for demonstration +MOCK_MOVIE_PRESENTATIONS = [ + { + "id": "movie_001", + "title": "Galactic Adventures", + "description": "An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.", + "date": date(2025, 9, 25), + "time": time(14, 30), # 2:30 PM + "room": "theater_a", + "seats_available": 150, + "seats_booked": 45, + "duration_minutes": 142, + "genre": "sci-fi", + "rating": "PG-13", + "price_per_seat": 12.50, + "director": "Sarah Johnson", + "cast": ["Alex Thompson", "Maria Rodriguez", "James Chen"], + "poster_url": "https://example.com/galactic-adventures.jpg" + }, + { + "id": "movie_002", + "title": "The Midnight Mystery", + "description": "A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.", + "date": date(2025, 9, 25), + "time": time(19, 15), # 7:15 PM + "room": "theater_b", + "seats_available": 200, + "seats_booked": 125, + "duration_minutes": 118, + "genre": "thriller", + "rating": "R", + "price_per_seat": 14.00, + "director": "Michael Davis", + "cast": ["Emma Wilson", "Robert Garcia", "Lisa Park"], + "poster_url": "https://example.com/midnight-mystery.jpg" + }, + { + "id": "movie_003", + "title": "Laugh Out Loud", + "description": "A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.", + "date": date(2025, 9, 25), + "time": time(21, 45), # 9:45 PM + "room": "theater_a", + "seats_available": 150, + "seats_booked": 89, + "duration_minutes": 95, + "genre": "comedy", + "rating": "PG-13", + "price_per_seat": 12.50, + "director": "Jennifer Lee", + "cast": ["Tom Martinez", "Sarah Kim", "David Brown"], + "poster_url": "https://example.com/laugh-out-loud.jpg" + }, + { + "id": "movie_004", + "title": "Dragon's Heart", + "description": "An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.", + "date": date(2025, 9, 26), + "time": time(10, 00), # 10:00 AM + "room": "theater_c", + "seats_available": 100, + "seats_booked": 23, + "duration_minutes": 103, + "genre": "animation", + "rating": "G", + "price_per_seat": 10.00, + "director": "Animation Studios Inc.", + "cast": ["Voice Cast: Amy Johnson", "Mark Stevens", "Luna Rodriguez"], + "poster_url": "https://example.com/dragons-heart.jpg" + }, + { + "id": "movie_005", + "title": "City of Shadows", + "description": "A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.", + "date": date(2025, 9, 26), + "time": time(16, 20), # 4:20 PM + "room": "theater_b", + "seats_available": 200, + "seats_booked": 156, + "duration_minutes": 134, + "genre": "drama", + "rating": "R", + "price_per_seat": 14.00, + "director": "Vincent Romano", + "cast": ["Antonio Silva", "Catherine Moore", "Frank Williams"], + "poster_url": "https://example.com/city-of-shadows.jpg" + }, + { + "id": "movie_006", + "title": "Ocean's Edge", + "description": "A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.", + "date": date(2025, 9, 26), + "time": time(20, 00), # 8:00 PM + "room": "imax", + "seats_available": 300, + "seats_booked": 78, + "duration_minutes": 87, + "genre": "documentary", + "rating": "G", + "price_per_seat": 18.00, + "director": "Ocean Explorer Films", + "cast": ["Narrator: David Attenborough"], + "poster_url": "https://example.com/oceans-edge.jpg" + }, + { + "id": "movie_007", + "title": "Love in Paris", + "description": "A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.", + "date": date(2025, 9, 27), + "time": time(15, 45), # 3:45 PM + "room": "theater_c", + "seats_available": 100, + "seats_booked": 67, + "duration_minutes": 108, + "genre": "romance", + "rating": "PG", + "price_per_seat": 11.50, + "director": "Claire Dubois", + "cast": ["Sophie Martin", "Jean-Luc Moreau", "Isabella Jones"], + "poster_url": "https://example.com/love-in-paris.jpg" + }, + { + "id": "movie_008", + "title": "Nightmare Manor", + "description": "A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.", + "date": date(2025, 9, 27), + "time": time(22, 30), # 10:30 PM + "room": "theater_a", + "seats_available": 150, + "seats_booked": 92, + "duration_minutes": 106, + "genre": "horror", + "rating": "R", + "price_per_seat": 13.00, + "director": "Horror Productions", + "cast": ["Scary Actor 1", "Scary Actor 2", "Scary Actor 3"], + "poster_url": "https://example.com/nightmare-manor.jpg" + } +] + +# World famous movies for random selection +POPULAR_MOVIES = [ + "movie_001", "movie_002", "movie_003", "movie_006" +] \ No newline at end of file diff --git a/src/mcp/cinema-mcp/main.py b/src/mcp/cinema-mcp/main.py new file mode 100644 index 0000000..2aa7e3f --- /dev/null +++ b/src/mcp/cinema-mcp/main.py @@ -0,0 +1,279 @@ +""" +Cinema MCP Server - Discover and book movie showings. + +To run this server: + uv run mcp dev main.py + +Provides information about current movie showings, seat availability, +and reservation capabilities. +""" + +from typing import Dict, Any, Optional +from mcp.server.fastmcp import FastMCP + +from cinema_service import ( + get_current_movies_data, + get_movie_details_data, + search_movies_data, + create_reservation_data, + get_customer_reservations_data, + cancel_reservation_data, + get_all_reservations_data, + acknowledge_reservation_data, + revoke_reservation_data +) + +mcp = FastMCP("Cinema", port=8010) + +# Tools +@mcp.tool() +def get_current_movies() -> Dict[str, Any]: + """Get all currently playing movies with their showtimes and availability + + Returns: + List of all movies currently being shown with details including: + - Movie title, description, and basic info + - Showtimes and theater room assignments + - Seat availability and pricing + - Genre, rating, and duration + - Cast and director information + """ + return get_current_movies_data() + +@mcp.tool() +def get_movie_details( + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Get detailed information about a specific movie showing + + Args: + title: Exact movie title + date: Date in YYYY-MM-DD format (e.g., "2025-09-25") + time: Time in HH:MM format (e.g., "19:30") + room: Room identifier (e.g., "theater_a", "theater_b", "theater_c", "imax") + + Returns: + Detailed movie information including plot, cast, theater details, and availability + Use search_movies first to find the exact title, date, time, and room values needed + """ + return get_movie_details_data(title, date, time, room) + +@mcp.tool() +def search_movies( + title: Optional[str] = None, + genre: Optional[str] = None, + date: Optional[str] = None, + room: Optional[str] = None, + available_seats_min: Optional[int] = None, + limit: int = 20 +) -> Dict[str, Any]: + """Search for movie presentations with optional filters + + Args: + title: Filter by movie title (partial match, case-insensitive) + genre: Filter by movie genre (action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family) + date: Filter by date in YYYY-MM-DD format (e.g., "2025-09-25") + room: Filter by cinema room (theater_a, theater_b, theater_c, imax) + available_seats_min: Minimum number of available seats required + limit: Maximum number of results to return (default: 20, max: 100) + + Returns: + Filtered list of movie presentations matching the search criteria + """ + return search_movies_data(title, genre, date, room, available_seats_min, limit) + +@mcp.tool() +def make_reservation( + title: str, + date: str, + time: str, + room: str, + seats_count: int, + customer_name: str, + customer_email: str, + customer_phone: Optional[str] = None, + special_requests: Optional[str] = None +) -> Dict[str, Any]: + """Create a movie reservation for a specific showing + + Args: + title: Exact movie title (use get_movie_details or search_movies to find exact title) + date: Date in YYYY-MM-DD format (e.g., "2025-09-25") + time: Time in HH:MM format (e.g., "19:30") + room: Room identifier (e.g., "theater_a", "theater_b", "theater_c", "imax") + seats_count: Number of seats to reserve (must be > 0) + customer_name: Customer's full name + customer_email: Customer's email address + customer_phone: Customer's phone number (optional) + special_requests: Any special requests or accessibility needs (optional) + + Returns: + Reservation confirmation with booking details and pricing information + Use the exact title, date, time, and room values from search_movies or get_current_movies results + """ + return create_reservation_data( + title, date, time, room, seats_count, + customer_name, customer_email, customer_phone, special_requests + ) + +@mcp.tool() +def get_my_reservations(customer_email: str) -> Dict[str, Any]: + """Get all reservations for a customer + + Args: + customer_email: Customer's email address used for reservations + + Returns: + List of all reservations made by the customer + """ + return get_customer_reservations_data(customer_email) + +@mcp.tool() +def cancel_reservation( + customer_email: str, + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Cancel a movie reservation + + Args: + customer_email: Customer's email address + title: Exact movie title of the reservation to cancel + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + + Returns: + Cancellation confirmation and details about freed seats + Use get_my_reservations first to find the exact details of reservations to cancel + """ + return cancel_reservation_data(customer_email, title, date, time, room) + +@mcp.tool() +def get_all_reservations(status_filter: Optional[str] = None) -> Dict[str, Any]: + """Get all reservations for cinema employee review + + Args: + status_filter: Filter by status ("confirmed", "completed", "cancelled") or None for all active reservations + + Returns: + List of all reservations matching the filter for employee management + Use this to review current reservations that need attention + """ + return get_all_reservations_data(status_filter) + +@mcp.tool() +def acknowledge_reservation( + customer_email: str, + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Acknowledge/complete a reservation (cinema employee function) + + Args: + customer_email: Customer's email address + title: Exact movie title of the reservation + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + + Returns: + Acknowledgment confirmation - marks reservation as completed + Use when customer has attended the showing + """ + return acknowledge_reservation_data(customer_email, title, date, time, room) + +@mcp.tool() +def revoke_reservation( + customer_email: str, + title: str, + date: str, + time: str, + room: str, + reason: Optional[str] = None +) -> Dict[str, Any]: + """Revoke/cancel a reservation (cinema employee function) + + Args: + customer_email: Customer's email address + title: Exact movie title of the reservation to revoke + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + reason: Reason for revocation (optional, for employee records) + + Returns: + Revocation confirmation and details about freed seats + Use when a reservation needs to be cancelled by cinema staff + """ + return revoke_reservation_data(customer_email, title, date, time, room, reason) + +# Resources +@mcp.resource("movie://{title}/{date}/{time}/{room}") +def get_movie_resource(title: str, date: str, time: str, room: str) -> str: + """Get movie details as a formatted resource""" + result = get_movie_details_data(title, date, time, room) + + if "error" in result: + return f"Error: {result['error']}" + + movie = result["movie"] + showing = result["showing_details"] + + return f""" +🎬 {movie['title']} ({movie['rating']}) + +📅 Showing: {movie['date']} at {movie['time']} +🏛️ Theater: {showing['room']} ({showing['room_type']}) +🎭 Genre: {movie['genre']} | ⏱️ Duration: {movie['duration_minutes']} minutes +🎬 Director: {movie['director']} +⭐ Cast: {', '.join(movie['cast'])} + +💺 Seating: +- Total Seats: {showing['seats_total']} +- Available: {showing['seats_remaining']} +- Booked: {showing['seats_booked']} +- Occupancy: {showing['occupancy_percentage']}% +- Price: ${showing['price_per_seat']:.2f} per seat + +📝 Description: +{movie['description']} + +{"🚫 SOLD OUT" if showing['is_sold_out'] else "✅ Tickets Available"} +""" + +@mcp.resource("cinema://current-movies") +def get_current_movies_resource() -> str: + """Get current movies as a formatted resource""" + result = get_current_movies_data() + + if "error" in result: + return f"Error: {result['error']}" + + output = f"🎬 {result['cinema_name']} - Current Movies\n" + output += f"📊 Total Movies Playing: {result['total_movies']}\n\n" + + for i, movie in enumerate(result['movies'], 1): + output += f"{i}. {movie['title']} ({movie['rating']})\n" + output += f" 📅 {movie['date']} at {movie['time']} | 🏛️ {movie['room']}\n" + output += f" 🎭 {movie['genre']} | ⏱️ {movie['duration_minutes']}min | 💰 ${movie['price_per_seat']:.2f}\n" + output += f" 💺 {movie['seats_remaining']}/{movie['seats_total']} seats available" + + if movie['is_sold_out']: + output += " 🚫 SOLD OUT" + else: + output += f" ({movie['occupancy_percentage']}% full)" + + output += "\n\n" + + return output + +if __name__ == "__main__": + mcp.run(transport="streamable-http") \ No newline at end of file diff --git a/src/mcp/cinema-mcp/models.py b/src/mcp/cinema-mcp/models.py new file mode 100644 index 0000000..bb7e181 --- /dev/null +++ b/src/mcp/cinema-mcp/models.py @@ -0,0 +1,73 @@ +""" +Cinema data models - Data classes for movies and reservations. +""" + +from dataclasses import dataclass +from typing import Optional, List +from datetime import datetime, date, time + +@dataclass +class MovieShowing: + """Represents a movie showing in the cinema""" + title: str + description: str + date: date # Date of the showing + time: time # Time of the showing + room: str # Cinema room/hall identifier + seats_available: int # Total seats available for this showing + seats_booked: int = 0 # Number of seats already booked + duration_minutes: Optional[int] = None # Movie duration in minutes + genre: Optional[str] = None # Movie genre + rating: Optional[str] = None # Movie rating (PG, PG-13, R, etc.) + + @property + def seats_remaining(self) -> int: + """Calculate remaining available seats""" + return max(0, self.seats_available - self.seats_booked) + + @property + def is_sold_out(self) -> bool: + """Check if the movie showing is sold out""" + return self.seats_remaining == 0 + + @property + def occupancy_percentage(self) -> float: + """Calculate the occupancy percentage""" + if self.seats_available == 0: + return 0.0 + return (self.seats_booked / self.seats_available) * 100 + +@dataclass +class ContactInfo: + """Contact information for a reservation""" + name: str + email: str + phone: Optional[str] = None + +@dataclass +class MovieReservation: + """Represents a reservation for a movie showing""" + movie_title: str # Title of the movie (for easy reference) + movie_date: date # Date of the movie showing + movie_time: time # Time of the movie showing + room: str # Cinema room + seats_reserved: int # Number of seats reserved + contact_info: ContactInfo # Customer contact information + reservation_datetime: datetime # When the reservation was made + status: str = "confirmed" # Status: confirmed, cancelled, completed + special_requests: Optional[str] = None # Any special requests or notes + + @property + def is_active(self) -> bool: + """Check if the reservation is still active""" + return self.status == "confirmed" + + @property + def customer_name(self) -> str: + """Get customer name for easy access""" + return self.contact_info.name + + @property + def customer_email(self) -> str: + """Get customer email for easy access""" + return self.contact_info.email diff --git a/src/mcp/cinema-mcp/pyproject.toml b/src/mcp/cinema-mcp/pyproject.toml new file mode 100644 index 0000000..d62e814 --- /dev/null +++ b/src/mcp/cinema-mcp/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "cinema-mcp" +version = "1.0.0" +description = "MCP server for discovering and booking movie showings" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "mcp[cli]>=1.13.1", +] \ No newline at end of file diff --git a/src/mcp/cinema-mcp/utils.py b/src/mcp/cinema-mcp/utils.py new file mode 100644 index 0000000..d96fa84 --- /dev/null +++ b/src/mcp/cinema-mcp/utils.py @@ -0,0 +1,293 @@ +""" +Utility functions for cinema operations. +""" + +from typing import Dict, Any, Optional, List +from datetime import datetime, date, time + +from config import MOCK_MOVIE_PRESENTATIONS, CINEMA_ROOMS, MOVIE_GENRES +from models import MovieShowing + + +def get_movie_by_id(movie_id: str) -> Optional[Dict[str, Any]]: + """Get movie details by ID from mock data""" + for movie in MOCK_MOVIE_PRESENTATIONS: + if movie["id"] == movie_id: + return movie + return None + + +def get_current_movies() -> List[Dict[str, Any]]: + """Get all currently playing movies from mock data""" + today = date.today() + current_movies = [] + + for movie in MOCK_MOVIE_PRESENTATIONS: + # For demo purposes, show movies from today and next few days + if movie["date"] >= today: + current_movies.append(movie) + + return current_movies + + +def get_movies_by_date(target_date: date) -> List[Dict[str, Any]]: + """Get all movies playing on a specific date""" + movies_on_date = [] + + for movie in MOCK_MOVIE_PRESENTATIONS: + if movie["date"] == target_date: + movies_on_date.append(movie) + + return movies_on_date + + +def search_movies_by_title(title: str) -> List[Dict[str, Any]]: + """Search for movie presentations by title (partial match, case-insensitive)""" + matching_movies = [] + search_title = title.lower() + + for movie in MOCK_MOVIE_PRESENTATIONS: + movie_title = movie.get("title", "").lower() + if search_title in movie_title: + matching_movies.append(movie) + + return matching_movies + + +def get_movie_by_natural_id( + title: str, + date: Optional[str] = None, + time: Optional[str] = None, + room: Optional[str] = None +) -> Optional[Dict[str, Any]]: + """Get movie by natural identifiers (title is required, others help narrow down)""" + search_title = title.lower() + + # Parse date if provided + parsed_date = None + if date: + try: + from datetime import datetime + parsed_date = datetime.strptime(date, "%Y-%m-%d").date() + except ValueError: + return None + + # Parse time if provided + parsed_time = None + if time: + try: + from datetime import datetime + # Try different time formats + for fmt in ["%H:%M", "%I:%M %p", "%I:%M%p"]: + try: + parsed_time = datetime.strptime(time, fmt).time() + break + except ValueError: + continue + except: + pass + + matches = [] + for movie in MOCK_MOVIE_PRESENTATIONS: + # Title must match (partial, case-insensitive) + movie_title = movie.get("title", "").lower() + if search_title not in movie_title: + continue + + # Filter by date if provided + if parsed_date and movie.get("date") != parsed_date: + continue + + # Filter by time if provided + if parsed_time and movie.get("time") != parsed_time: + continue + + # Filter by room if provided (case-insensitive) + if room: + movie_room = movie.get("room", "").lower() + search_room = room.lower() + if movie_room != search_room: + continue + + matches.append(movie) + + # Return the first match, or None if no matches + return matches[0] if matches else None + + +def search_movie_presentations( + title: Optional[str] = None, + genre: Optional[str] = None, + date: Optional[date] = None, + room: Optional[str] = None, + available_seats_min: Optional[int] = None, + limit: int = 20 +) -> List[Dict[str, Any]]: + """Search movie presentations based on various criteria""" + filtered_presentations = [] + + for presentation in MOCK_MOVIE_PRESENTATIONS: + # Filter by title (partial match, case-insensitive) + if title: + presentation_title = presentation.get("title", "").lower() + search_title = title.lower() + if search_title not in presentation_title: + continue + + # Filter by genre (case-insensitive) + if genre: + presentation_genre = presentation.get("genre", "").lower() + search_genre = genre.lower() + if presentation_genre != search_genre: + continue + + # Filter by date + if date and presentation.get("date") != date: + continue + + # Filter by room (case-insensitive) + if room: + presentation_room = presentation.get("room", "").lower() + search_room = room.lower() + if presentation_room != search_room: + continue + + # Filter by minimum available seats + if available_seats_min: + seats_available = presentation.get("seats_available", 0) + seats_booked = presentation.get("seats_booked", 0) + seats_remaining = seats_available - seats_booked + if seats_remaining < available_seats_min: + continue + + filtered_presentations.append(presentation) + + # Apply limit + if len(filtered_presentations) >= limit: + break + + return filtered_presentations + + +def parse_movie_data(movie_data: Dict[str, Any]) -> MovieShowing: + """Parse movie data from mock data into MovieShowing object""" + return MovieShowing( + title=movie_data.get("title", ""), + description=movie_data.get("description", ""), + date=movie_data.get("date"), + time=movie_data.get("time"), + room=movie_data.get("room", ""), + seats_available=movie_data.get("seats_available", 0), + seats_booked=movie_data.get("seats_booked", 0), + duration_minutes=movie_data.get("duration_minutes"), + genre=movie_data.get("genre"), + rating=movie_data.get("rating") + ) + + +def format_movie_details(movie: MovieShowing, movie_data: Dict[str, Any]) -> str: + """Format movie details as a readable string""" + room_info = CINEMA_ROOMS.get(movie.room, {}) + genre_name = MOVIE_GENRES.get(movie.genre, movie.genre) if movie.genre else "Unknown" + + details = f"🎬 {movie.title}\n" + details += f"📅 Date: {movie.date.strftime('%A, %B %d, %Y')}\n" + details += f"🕒 Time: {movie.time.strftime('%I:%M %p')}\n" + details += f"🏛️ Theater: {room_info.get('name', movie.room)}\n" + details += f"🎭 Genre: {genre_name}\n" + + if movie.rating: + details += f"🏷️ Rating: {movie.rating}\n" + + if movie.duration_minutes: + hours = movie.duration_minutes // 60 + minutes = movie.duration_minutes % 60 + if hours > 0: + details += f"⏱️ Duration: {hours}h {minutes}m\n" + else: + details += f"⏱️ Duration: {minutes}m\n" + + details += f"💺 Seats: {movie.seats_remaining} available / {movie.seats_available} total\n" + + if movie.is_sold_out: + details += f"🚫 Status: SOLD OUT\n" + else: + details += f"📊 Occupancy: {movie.occupancy_percentage:.1f}%\n" + + if movie_data.get("price_per_seat"): + details += f"💰 Price: ${movie_data['price_per_seat']:.2f} per seat\n" + + if movie_data.get("director"): + details += f"🎬 Director: {movie_data['director']}\n" + + if movie_data.get("cast"): + cast_list = movie_data["cast"][:3] # Show first 3 cast members + details += f"⭐ Cast: {', '.join(cast_list)}\n" + + if movie.description: + details += f"\n📝 Description: {movie.description}\n" + + return details + + +def format_showtime(movie_time: time) -> str: + """Format time in a user-friendly way""" + return movie_time.strftime("%I:%M %p").lstrip("0") + + +def get_room_display_name(room_key: str) -> str: + """Get display name for cinema room""" + room_info = CINEMA_ROOMS.get(room_key, {}) + return room_info.get("name", room_key.replace("_", " ").title()) + + +def calculate_total_price(num_seats: int, price_per_seat: float) -> float: + """Calculate total price for reservation""" + return num_seats * price_per_seat + + +def validate_movie_date(movie_date: date) -> bool: + """Validate that movie date is not in the past""" + return movie_date >= date.today() + + +def validate_seat_availability(movie_data: Dict[str, Any], requested_seats: int) -> bool: + """Check if enough seats are available for booking""" + seats_available = movie_data.get("seats_available", 0) + seats_booked = movie_data.get("seats_booked", 0) + seats_remaining = seats_available - seats_booked + return seats_remaining >= requested_seats + + +def get_popular_movies() -> List[Dict[str, Any]]: + """Get popular movies from mock data""" + from config import POPULAR_MOVIES + popular = [] + + for movie_id in POPULAR_MOVIES: + movie_data = get_movie_by_id(movie_id) + if movie_data: + popular.append(movie_data) + + return popular + + +def format_movie_summary(movie_data: Dict[str, Any]) -> str: + """Create a short summary of a movie showing""" + movie_time = movie_data.get("time") + room_name = get_room_display_name(movie_data.get("room", "")) + genre_name = MOVIE_GENRES.get(movie_data.get("genre"), movie_data.get("genre", "")) + + seats_available = movie_data.get("seats_available", 0) + seats_booked = movie_data.get("seats_booked", 0) + seats_remaining = seats_available - seats_booked + + time_str = format_showtime(movie_time) if movie_time else "TBA" + + summary = f"{movie_data.get('title', 'Untitled')} - {time_str} in {room_name}" + summary += f" | {genre_name} | {seats_remaining} seats left" + + if movie_data.get("rating"): + summary += f" | Rated {movie_data['rating']}" + + return summary \ No newline at end of file diff --git a/src/mcp/cinema-mcp/uv.lock b/src/mcp/cinema-mcp/uv.lock new file mode 100644 index 0000000..7ee26ae --- /dev/null +++ b/src/mcp/cinema-mcp/uv.lock @@ -0,0 +1,486 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cinema-mcp" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "mcp", extra = ["cli"] }, +] + +[package.metadata] +requires-dist = [{ name = "mcp", extras = ["cli"], specifier = ">=1.13.1" }] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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 = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/e9/242096400d702924b49f8d202c6ded7efb8841cacba826b5d2e6183aef7b/mcp-1.14.1.tar.gz", hash = "sha256:31c4406182ba15e8f30a513042719c3f0a38c615e76188ee5a736aaa89e20134", size = 454944, upload-time = "2025-09-18T13:37:19.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/11/d334fbb7c2aeddd2e762b86d7a619acffae012643a5738e698f975a2a9e2/mcp-1.14.1-py3-none-any.whl", hash = "sha256:3b7a479e8e5cbf5361bdc1da8bc6d500d795dc3aff44b44077a363a7f7e945a4", size = 163809, upload-time = "2025-09-18T13:37:18.165Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[package]] +name = "typer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, +] diff --git a/src/mcp/movie-reviews-mcp/__init__.py b/src/mcp/movie-reviews-mcp/__init__.py new file mode 100644 index 0000000..d6876ce --- /dev/null +++ b/src/mcp/movie-reviews-mcp/__init__.py @@ -0,0 +1,44 @@ +""" +Movie Reviews MCP Package - Discover list of movies and reviews +""" + +from movie_service import ( + get_movie_details_data, + search_movies_data, + get_random_famous_movie_data, + get_movie_reviews_data, + format_movie_resource +) +from models import ( + Movie, MovieReview, MovieList +) +from utils import ( + get_movie_by_id, search_movies, parse_movie_data, + format_attraction_name, get_category_display_name, generate_booking_id, + validate_visit_date, validate_email, format_attraction_details +) + +__version__ = "1.0.0" +__all__ = [ + # Service functions + "parse_movie_data", + "search_movies_data", + "get_random_famous_movie_data", + "get_movie_details_data", + "get_movie_reviews_data", + "format_movie_resource", + # Models + "Movie", + "MovieList", + "MovieReview", + # Utilities + "get_movie_by_id", + "search_attractions", + "parse_attraction_data", + "format_attraction_name", + "get_category_display_name", + "generate_booking_id", + "validate_visit_date", + "validate_email", + "format_attraction_details" +] diff --git a/src/mcp/movie-reviews-mcp/config.py b/src/mcp/movie-reviews-mcp/config.py new file mode 100644 index 0000000..9c68265 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/config.py @@ -0,0 +1,503 @@ +""" +Tourist attractions MCP configuration mocks data. +""" + +MOVIE_GENRES = { + "action": "Action", + "comedy": "Comedy", + "drama": "Drama", + "horror": "Horror", + "romance": "Romance", + "thriller": "Thriller", + "sci-fi": "Science Fiction", + "fantasy": "Fantasy", + "documentary": "Documentary", + "animation": "Animation" +} + +# Default values +DEFAULT_SEARCH_LIMIT = 20 +MAX_SEARCH_LIMIT = 100 +DEFAULT_RATING_MIN = 3.0 + +# Mock movies data matching cinema-mcp for demonstration +MOCK_MOVIES = [ + { + "id": 1, + "title": "Galactic Adventures", + "synopsis": "An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.", + "rating": 8.7, + "durationMins": 142, + "genre": "sci-fi" + }, + { + "id": 2, + "title": "The Midnight Mystery", + "synopsis": "A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.", + "rating": 8.2, + "durationMins": 118, + "genre": "thriller" + }, + { + "id": 3, + "title": "Laugh Out Loud", + "synopsis": "A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.", + "rating": 7.8, + "durationMins": 95, + "genre": "comedy" + }, + { + "id": 4, + "title": "Dragon's Heart", + "synopsis": "An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.", + "rating": 8.5, + "durationMins": 103, + "genre": "animation" + }, + { + "id": 5, + "title": "City of Shadows", + "synopsis": "A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.", + "rating": 8.9, + "durationMins": 134, + "genre": "drama" + }, + { + "id": 6, + "title": "Ocean's Edge", + "synopsis": "A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.", + "rating": 9.1, + "durationMins": 87, + "genre": "documentary" + }, + { + "id": 7, + "title": "Love in Paris", + "synopsis": "A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.", + "rating": 7.6, + "durationMins": 108, + "genre": "romance" + }, + { + "id": 8, + "title": "Nightmare Manor", + "synopsis": "A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.", + "rating": 7.4, + "durationMins": 106, + "genre": "horror" + }, + { + "id": 9, + "title": "The Shawshank Redemption", + "synopsis": "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", + "rating": 9.3, + "durationMins": 142, + "genre": "drama" + }, + { + "id": 10, + "title": "Spirited Away", + "synopsis": "During her family's move to the suburbs, a sullen 10-year-old girl wanders into a world ruled by gods, witches, and spirits, where humans are changed into beasts.", + "rating": 8.6, + "durationMins": 125, + "genre": "animation" + }, + { + "id": 11, + "title": "Gladiator", + "synopsis": "A former Roman General sets out to exact vengeance against the corrupt emperor who murdered his family and sent him into slavery.", + "rating": 8.5, + "durationMins": 155, + "genre": "action" + }, + { + "id": 12, + "title": "The Matrix", + "synopsis": "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.", + "rating": 8.7, + "durationMins": 136, + "genre": "sci-fi" + }, + { + "id": 13, + "title": "Fight Club", + "synopsis": "An insomniac office worker and a devil-may-care soap maker form an underground fight club that evolves into something much more.", + "rating": 8.8, + "durationMins": 139, + "genre": "drama" + }, + { + "id": 14, + "title": "The Lord of the Rings: The Return of the King", + "synopsis": "Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.", + "rating": 8.9, + "durationMins": 201, + "genre": "fantasy" + }, + { + "id": 15, + "title": "The Lion King", + "synopsis": "Lion prince Simba and his father are targeted by his bitter uncle, who wants to ascend the throne himself.", + "rating": 8.5, + "durationMins": 88, + "genre": "animation" + }, + { + "id": 16, + "title": "Goodfellas", + "synopsis": "The story of Henry Hill and his life in the mob, covering his relationship with his wife Karen Hill and his mob partners.", + "rating": 8.7, + "durationMins": 146, + "genre": "crime" + }, + { + "id": 17, + "title": "The Silence of the Lambs", + "synopsis": "A young F.B.I. cadet must receive the help of an incarcerated and manipulative cannibal killer to catch another serial killer.", + "rating": 8.6, + "durationMins": 118, + "genre": "thriller" + }, + { + "id": 18, + "title": "Saving Private Ryan", + "synopsis": "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action.", + "rating": 8.6, + "durationMins": 169, + "genre": "war" + }, + { + "id": 19, + "title": "The Prestige", + "synopsis": "After a tragic accident, two stage magicians engage in a battle to create the ultimate illusion while sacrificing everything they have to outwit each other.", + "rating": 8.5, + "durationMins": 130, + "genre": "mystery" + }, + { + "id": 20, + "title": "Whiplash", + "synopsis": "A promising young drummer enrolls at a cut-throat music conservatory where his dreams of greatness are mentored by an instructor who will stop at nothing to realize a student's potential.", + "rating": 8.5, + "durationMins": 106, + "genre": "drama" + } +] + +MOCK_REVIEWS = [ + { + "movie_id": 1, + "reviewer": "Alice", + "rating": 5, + "comment": "An epic space adventure with stunning visuals and compelling characters. The galaxy scenes are breathtaking!", + "reviewDate": "2024-09-01" + }, + { + "movie_id": 1, + "reviewer": "Bob", + "rating": 4, + "comment": "Great sci-fi adventure, though the plot gets a bit convoluted in the middle. Overall very entertaining.", + "reviewDate": "2024-09-02" + }, + { + "movie_id": 2, + "reviewer": "Charlie", + "rating": 5, + "comment": "A masterful thriller that keeps you guessing until the very end. Perfect midnight mystery vibes!", + "reviewDate": "2024-09-03" + }, + { + "movie_id": 2, + "reviewer": "Diana", + "rating": 4, + "comment": "Excellent detective work and atmospheric tension. The midnight setting adds to the suspense.", + "reviewDate": "2024-09-04" + }, + { + "movie_id": 3, + "reviewer": "Eve", + "rating": 4, + "comment": "Hilarious from start to finish! The social media angle is spot-on and very relatable.", + "reviewDate": "2024-09-05" + }, + { + "movie_id": 3, + "reviewer": "Frank", + "rating": 5, + "comment": "Best comedy I've seen this year. The friendship dynamics are heartwarming and funny.", + "reviewDate": "2024-09-06" + }, + { + "movie_id": 4, + "reviewer": "Grace", + "rating": 5, + "comment": "Beautiful animation and a heartwarming story about dragons and friendship. Perfect for families!", + "reviewDate": "2024-09-07" + }, + { + "movie_id": 4, + "reviewer": "Henry", + "rating": 4, + "comment": "Lovely animated adventure with great voice acting and stunning visuals.", + "reviewDate": "2024-09-08" + }, + { + "movie_id": 5, + "reviewer": "Ivy", + "rating": 5, + "comment": "A masterful noir drama with incredible atmosphere. The 1940s setting is perfectly captured.", + "reviewDate": "2024-09-09" + }, + { + "movie_id": 5, + "reviewer": "Jack", + "rating": 4, + "comment": "Gripping detective story with excellent cinematography and strong performances.", + "reviewDate": "2024-09-10" + }, + { + "movie_id": 6, + "reviewer": "Karen", + "rating": 5, + "comment": "Absolutely breathtaking documentary! The ocean footage is stunning and educational.", + "reviewDate": "2024-09-11" + }, + { + "movie_id": 6, + "reviewer": "Leo", + "rating": 5, + "comment": "David Attenborough's narration combined with incredible underwater cinematography. A must-see!", + "reviewDate": "2024-09-12" + }, + { + "movie_id": 7, + "reviewer": "Mona", + "rating": 4, + "comment": "Charming romantic comedy with beautiful Parisian scenery. Predictable but delightful!", + "reviewDate": "2024-09-13" + }, + { + "movie_id": 7, + "reviewer": "Nate", + "rating": 3, + "comment": "Sweet story but a bit clichéd. The Paris setting makes up for the predictable plot.", + "reviewDate": "2024-09-14" + }, + { + "movie_id": 8, + "reviewer": "Olivia", + "rating": 4, + "comment": "Genuinely scary horror film with great atmosphere. The old mansion setting is perfect!", + "reviewDate": "2024-09-15" + }, + { + "movie_id": 8, + "reviewer": "Paul", + "rating": 3, + "comment": "Classic haunted house horror but some jump scares felt forced. Good for horror fans.", + "reviewDate": "2024-09-16" + }, + { + "movie_id": 9, + "reviewer": "Quinn", + "rating": 5, + "comment": "A moving story of hope and friendship.", + "reviewDate": "2024-03-17" + }, + { + "movie_id": 9, + "reviewer": "Rita", + "rating": 5, + "comment": "Timeless and uplifting. A true classic.", + "reviewDate": "2024-03-18" + }, + { + "movie_id": 10, + "reviewer": "Sam", + "rating": 5, + "comment": "Magical animation with a rich, imaginative world.", + "reviewDate": "2024-03-19" + }, + { + "movie_id": 10, + "reviewer": "Tina", + "rating": 4, + "comment": "Beautiful visuals and a touching story.", + "reviewDate": "2024-03-20" + }, + { + "movie_id": 11, + "reviewer": "Uma", + "rating": 5, + "comment": "Epic battles and a compelling hero.", + "reviewDate": "2024-03-21" + }, + { + "movie_id": 11, + "reviewer": "Victor", + "rating": 4, + "comment": "Intense action and strong performances.", + "reviewDate": "2024-03-22" + }, + { + "movie_id": 12, + "reviewer": "Wendy", + "rating": 5, + "comment": "Mind-blowing concept and great action.", + "reviewDate": "2024-03-23" + }, + { + "movie_id": 12, + "reviewer": "Xander", + "rating": 4, + "comment": "Innovative and entertaining sci-fi.", + "reviewDate": "2024-03-24" + }, + { + "movie_id": 13, + "reviewer": "Yara", + "rating": 5, + "comment": "Dark, clever, and unforgettable.", + "reviewDate": "2024-03-25" + }, + { + "movie_id": 13, + "reviewer": "Zane", + "rating": 4, + "comment": "Unique story with strong performances.", + "reviewDate": "2024-03-26" + }, + { + "movie_id": 14, + "reviewer": "Abby", + "rating": 5, + "comment": "Epic fantasy with breathtaking visuals.", + "reviewDate": "2024-03-27" + }, + { + "movie_id": 14, + "reviewer": "Ben", + "rating": 5, + "comment": "A fitting end to a legendary trilogy.", + "reviewDate": "2024-03-28" + }, + { + "movie_id": 15, + "reviewer": "Cara", + "rating": 5, + "comment": "A childhood favorite with memorable songs.", + "reviewDate": "2024-03-29" + }, + { + "movie_id": 15, + "reviewer": "Dan", + "rating": 4, + "comment": "Beautiful animation and a touching story.", + "reviewDate": "2024-03-30" + }, + { + "movie_id": 16, + "reviewer": "Ella", + "rating": 5, + "comment": "Gripping crime drama with stellar acting.", + "reviewDate": "2024-03-31" + }, + { + "movie_id": 16, + "reviewer": "Finn", + "rating": 4, + "comment": "Intense and well-crafted mob story.", + "reviewDate": "2024-04-01" + }, + { + "movie_id": 17, + "reviewer": "Gina", + "rating": 5, + "comment": "Chilling and suspenseful thriller.", + "reviewDate": "2024-04-02" + }, + { + "movie_id": 17, + "reviewer": "Hank", + "rating": 4, + "comment": "Great performances and a gripping plot.", + "reviewDate": "2024-04-03" + }, + { + "movie_id": 18, + "reviewer": "Iris", + "rating": 5, + "comment": "A powerful and realistic war film.", + "reviewDate": "2024-04-04" + }, + { + "movie_id": 18, + "reviewer": "Jake", + "rating": 4, + "comment": "Intense and emotional storytelling.", + "reviewDate": "2024-04-05" + }, + { + "movie_id": 19, + "reviewer": "Kara", + "rating": 5, + "comment": "A fascinating tale of rivalry and obsession.", + "reviewDate": "2024-04-06" + }, + { + "movie_id": 19, + "reviewer": "Liam", + "rating": 4, + "comment": "Intriguing plot with excellent twists.", + "reviewDate": "2024-04-07" + }, + { + "movie_id": 20, + "reviewer": "Mia", + "rating": 5, + "comment": "Electrifying performances and a compelling story.", + "reviewDate": "2024-04-08" + }, + { + "movie_id": 20, + "reviewer": "Noah", + "rating": 4, + "comment": "Intense and inspiring musical drama.", + "reviewDate": "2024-04-09" + }, + { + "movie_id": 1, + "reviewer": "Oscar", + "rating": 2, + "comment": "Too confusing and dragged on for too long.", + "reviewDate": "2024-04-10" + }, + { + "movie_id": 5, + "reviewer": "Pam", + "rating": 2, + "comment": "Overrated and slow, didn't connect with the story.", + "reviewDate": "2024-04-11" + }, + { + "movie_id": 7, + "reviewer": "Quentin", + "rating": 1, + "comment": "Found it boring and hard to follow.", + "reviewDate": "2024-04-12" + }, + { + "movie_id": 12, + "reviewer": "Ralph", + "rating": 2, + "comment": "Not as groundbreaking as people say, felt dated.", + "reviewDate": "2024-04-13" + }, + { + "movie_id": 15, + "reviewer": "Sophie", + "rating": 1, + "comment": "Didn't enjoy the animation style or the music.", + "reviewDate": "2024-04-14" + } +] \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/main.py b/src/mcp/movie-reviews-mcp/main.py new file mode 100644 index 0000000..3f24d51 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/main.py @@ -0,0 +1,168 @@ +""" +Tourist Attractions MCP Server - Discover and book attractions worldwide. + +To run this server: + uv run mcp dev main.py + +Uses World Tourist Attractions API to provide information about tourist attractions +and booking capabilities. +""" + +from typing import Dict, Any, Optional +from mcp.server.fastmcp import FastMCP + +from movie_service import ( + get_movie_genres_data, + get_random_movie_data, + parse_movie_data, + search_movies_data, + get_movie_details_data, + get_movie_reviews_data, + format_movie_resource, + format_search_results, + format_review_list +) + +mcp = FastMCP("MovieReviews", port=8011) + +# tools +@mcp.tool() +def get_movie_details(movie_id: int) -> Dict[str, Any]: + """Get detailed information about a specific movie + + Args: + movie_id: Unique ID of the movie + + Returns: + MovieDetails object as dictionary with movie info, reviews, and related movies + """ + return get_movie_details_data(movie_id) + +@mcp.tool() +def search_movies( + title: Optional[str] = None, + genre: Optional[str] = None, + limit: int = 20 +) -> Dict[str, Any]: + """Search for movies with optional filters + + Args: + genre: Genre filter - "action", "comedy", "drama", etc. + limit: Maximum number of results (1-100, default: 20) + + Returns: + MoviesList object as dictionary with matching movies + """ + return search_movies_data(title, genre, limit) + +@mcp.tool() +def get_random_movie() -> Dict[str, Any]: + """Get a random movie for inspiration + + Args: + genre: Genre type - "famous" for world famous movies, "india" for Indian movies + + Returns: + Movie object as dictionary with random movie details + """ + return get_random_movie_data() + + + +@mcp.tool() +def search_and_format_movies( + genre: Optional[str] = None, + limit: int = 10 +) -> str: + """Search for movies and return formatted results for easy reading + + Args: + genre: Genre filter (e.g., "action", "comedy", "drama") + limit: Maximum number of results (1-20, default: 10) + + Returns: + Formatted string with movie search results + """ + if limit > 20: + limit = 20 + + search_data = search_movies_data(genre, limit) + return format_search_results(search_data) + + +@mcp.tool() +def get_movie_reviews_by_title(title: str) -> str: + """Get reviews for a movie by its title and return formatted results + + Args: + title: Title of the movie + + Returns: + Formatted string with movie reviews + """ + # First, search for the movie by title to get its ID + search_data = search_movies(title=title, limit=1) + if not search_data or "movies" not in search_data or not search_data["movies"]: + return f"No movie found with title '{title}'." + + movie_id = search_data["movies"][0]["id"] + reviews_data = get_movie_reviews_data(movie_id) + return format_review_list(reviews_data) + +# resources +@mcp.resource("movie://{movie_id}") +def get_movie_resource(movie_id: int) -> str: + """Get movie information as a formatted resource""" + return format_movie_resource(movie_id) + +@mcp.resource("movies://genre/{genre}") +def get_movies_by_genre_resource(genre: str) -> str: + """Get movies by genre as a formatted resource""" + search_data = search_movies_data(genre=genre, limit=10) + return format_search_results(search_data) + +@mcp.resource("movies://genre") +def get_movies() -> str: + """Get 10 movies by formatted resource""" + search_data = search_movies_data(limit=10) + return format_search_results(search_data) + +@mcp.resource("movies://genres") +def get_movie_genres_resource() -> str: + """Get list of available movie genres as a formatted resource""" + return get_movie_genres_data() + + +# prompts + +# @mcp.prompt() +# def travel_planning_prompt(location: str, days: int = 3) -> str: +# """Generate a prompt for travel planning with attractions""" +# return f"""Please help plan a {days}-day itinerary for {location}, including: +# 1. Top must-see attractions and landmarks +# 2. Best time to visit each attraction +# 3. Recommended booking strategies and timing +# 4. Transportation between attractions +# 5. Estimated costs and budgeting tips +# 6. Cultural considerations and local customs +# 7. Alternative attractions if main ones are crowded + +# Focus on creating a balanced mix of historical, cultural, and recreational activities suitable for different interests.""" + +# @mcp.prompt() +# def attraction_comparison_prompt(attraction_ids: str) -> str: +# """Generate a prompt for comparing multiple attractions""" +# return f"""Please provide a detailed comparison of these attractions (IDs: {attraction_ids}), including: +# 1. Unique features and highlights of each +# 2. Best times to visit and crowd levels +# 3. Entry requirements and booking procedures +# 4. Approximate visit duration +# 5. Nearby attractions and activities +# 6. Accessibility and facilities +# 7. Value for money assessment +# 8. Personal recommendations based on different travel styles + +# Help decide which attractions to prioritize based on time, budget, and interests.""" + +if __name__ == "__main__": + mcp.run(transport="streamable-http") \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/models.py b/src/mcp/movie-reviews-mcp/models.py new file mode 100644 index 0000000..adce83b --- /dev/null +++ b/src/mcp/movie-reviews-mcp/models.py @@ -0,0 +1,40 @@ +""" +Movie reviews and information models +""" + +from dataclasses import dataclass +from typing import Optional, List +from datetime import datetime + +# movie reviews +@dataclass +class MovieReview: + movie_id: int + rating: int + comment: str + reviewer: str + reviewDate: datetime + +# movie +@dataclass +class Movie: + id: int + title: str + synopsis: str + rating: float + durationMins: int + year: int + genre: str + +@dataclass +class MoviesList: + genre: str + total_count: int = 0 + movies: List[Movie] = None + +@dataclass +class MovieReviewList: + movie: Movie + reviews: List[MovieReview] + total_count: int = 0 + avg_rating: Optional[float] = None \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/movie_service.py b/src/mcp/movie-reviews-mcp/movie_service.py new file mode 100644 index 0000000..5113f21 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/movie_service.py @@ -0,0 +1,232 @@ +""" +Movies service with MCP tools and API logic. +""" + +from typing import Dict, Any, List +from dataclasses import asdict +from datetime import datetime + +from config import DEFAULT_SEARCH_LIMIT, MOVIE_GENRES, MOCK_REVIEWS + +from models import ( + Movie, MovieReview, MovieReviewList, MoviesList +) +from utils import ( + format_movie_details, + get_genre_display_name, + get_movie_by_id, + parse_movie_data, + search_movies, + get_random_famous_movie +) + + +def search_movies_data( + title: str = None, + genre: str = None, + limit: int = DEFAULT_SEARCH_LIMIT +) -> Dict[str, Any]: + """Search for movies with filters + + Args: + genre: Genre of movies (e.g., "action", "comedy", "drama") + limit: Maximum number of results (default: 20, max: 100) + + Returns: + MoviesList object as dictionary or error dict + """ + try: + if limit > 100: + limit = 100 + elif limit < 1: + limit = 1 + + data = search_movies(title, genre, limit) + if not data: + return {"error": "No movies found matching the criteria"} + + movies = [] + if "movies" in data: + for item in data["movies"]: + movie = parse_movie_data(item) + movies.append(movie) + + movies_list = MoviesList( + genre=get_genre_display_name(genre) if genre else "All Genres", + total_count=data.get("total", len(movies)), + movies=movies + ) + + return asdict(movies_list) + + except Exception as e: + return {"error": f"Failed to search movies: {str(e)}"} + + +def get_random_movie_data() -> Dict[str, Any]: + """Get a random movie + + + Returns: + Movie object as dictionary or error dict + """ + try: + data = get_random_famous_movie() + + if not data: + return {"error": f"No random movie found"} + + movie = parse_movie_data(data) + return asdict(movie) + + except Exception as e: + return {"error": f"Failed to get random movie: {str(e)}"} + + +def get_movie_genres_data() -> str: + """Get list of available movie genres as formatted string + + Returns: + Formatted string with genre codes and display names + """ + try: + result = "🏛️ **Available Movie Genres**\n\n" + result += f"Total Genres: {len(MOVIE_GENRES)}\n\n" + + for code, display_name in MOVIE_GENRES.items(): + result += f"• **{code}**: {display_name}\n" + + result += "\n*Use these genre codes when searching for movies.*" + return result + + except Exception as e: + return f"Error: Failed to get categories: {str(e)}" + +def get_movie_details_data(movie_id: int): + """Get details for a specific movie + + Args: + movie_id: ID of the movie + Returns: + Movie object as dictionary or error dict + """ + try: + movie_data = get_movie_by_id(movie_id) + if not movie_data: + return {"error": f"Movie with ID {movie_id} not found"} + + movie = parse_movie_data(asdict(movie_data)) + return {"movie": asdict(movie)} + + except Exception as e: + return {"error": f"Failed to get movie details: {str(e)}"} + +def format_movie_resource(movie_id: int) -> str: + """Get movie information as a formatted resource""" + data = get_movie_details_data(movie_id) + if "error" in data: + return f"Error: {data['error']}" + + movie_data = data['movie'] + movie = parse_movie_data(movie_data) + + return format_movie_details(movie) + + +def format_search_results(search_data: Dict[str, Any]) -> str: + """Format search results as a readable string""" + if "error" in search_data: + return f"Error: {search_data['error']}" + + movies = search_data.get('movies', []) + if not movies: + return "No movies found matching your criteria." + + result = f"🎯 Found {search_data.get('total_count', len(movies))} movies" + if search_data.get('genre') != "All Genres": + result += f" ({search_data['genre']})" + result += ":\n\n" + + for i, movie_data in enumerate(movies[:10], 1): # Show first 10 + movie = parse_movie_data(movie_data) + + result += f"{i}. 🎬 **{movie.title}**\n" + result += f" 🏷️ {get_genre_display_name(movie.genre)}\n" + + if movie.rating: + result += f" ⭐ {movie.rating}/5.0\n" + if movie.durationMins: + result += f" ⏳ {movie.durationMins}\n" + + result += "\n" + + if len(movies) > 10: + result += f"... and {len(movies) - 10} more movies\n" + + return result + +def get_movie_reviews_data(movie_id: int) -> MovieReviewList: + """Get reviews for a specific movie + + Args: + movie_id: ID of the movie + Returns: + MovieReviewList object as dictionary or error dict + """ + + movie = get_movie_by_id(movie_id) + + # Filter reviews by movie_id (MOCK_REVIEWS contains dictionaries) + review_dicts = [review for review in MOCK_REVIEWS if review["movie_id"] == movie_id] + + # Convert dictionary reviews to MovieReview objects + reviews = [] + for review_dict in review_dicts: + review_date = datetime.strptime(review_dict["reviewDate"], "%Y-%m-%d") + review = MovieReview( + movie_id=review_dict["movie_id"], + rating=review_dict["rating"], + comment=review_dict["comment"], + reviewer=review_dict["reviewer"], + reviewDate=review_date + ) + reviews.append(review) + + # Create movie object if movie data exists + movie_obj = None + if movie: + movie_obj = Movie( + id=movie["id"], + title=movie["title"], + synopsis=movie["synopsis"], + rating=movie["rating"], + durationMins=movie["durationMins"], + year=2024, # Default year since not in movie data + genre=movie["genre"] + ) + + review_list = MovieReviewList( + movie=movie_obj, + total_count=len(reviews), + reviews=reviews, + avg_rating=(sum(r.rating for r in reviews) / len(reviews)) if reviews else None + ) + return review_list + +def format_review_list(review_list: MovieReviewList) -> str: + """Format movie review list as a readable string""" + if not review_list.reviews: + return f"No reviews found for movie: {review_list.movie.title}" + + result = f"📝 Reviews for **{review_list.movie.title}**\n" + result += f"Total Reviews: {review_list.total_count}\n" + if review_list.avg_rating: + result += f"Average Rating: {review_list.avg_rating:.1f}/5.0\n" + result += "\n" + + for i, review in enumerate(review_list.reviews, 1): + review_date = review.reviewDate.strftime("%Y-%m-%d") + result += f"{i}. ⭐ {review.rating}/5.0 on {review_date} by {review.reviewer}\n" + result += f" \"{review.comment}\"\n\n" + + return result.strip() \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/pyproject.toml b/src/mcp/movie-reviews-mcp/pyproject.toml new file mode 100644 index 0000000..335839c --- /dev/null +++ b/src/mcp/movie-reviews-mcp/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "movie-reviews-mcp" +version = "1.0.0" +description = "MCP server for discovering movies and reviews" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "mcp[cli]>=1.13.1", + "requests", +] diff --git a/src/mcp/movie-reviews-mcp/readme.md b/src/mcp/movie-reviews-mcp/readme.md new file mode 100644 index 0000000..f1ed6b6 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/readme.md @@ -0,0 +1,103 @@ +get_movie_details_data# Movie Reviews MCP Server + +A Model Context Protocol (MCP) server for discovering movie list and reviews using the Movie Reviews API. + +## Features + +🏛️ **Movie Discovery** +- Search movies by title and genre +- Get detailed movie information +- Discover a random famous movie +- Get a list of reviews from a movie + +## Installation + +```bash +## cd to movie reviews mcp project +cd src/mcp/movie-reviews-mcp +``` + +```bash +# Install dependencies +uv sync +``` + +### Running the Server + +```bash +uv run mcp dev main.py +``` + +```bash +# use this when wanting to consume within an agent +uv run main.py +``` + +### Available Tools + +#### 1. Get Movie Details +```python +get_movie_details_data(movie_id: int) +``` +Get comprehensive information about a specific movie and its reviews. + +#### 2. Search Movies +```python +search_movies_data(title: str = None, genre: str = None, limit: int = 20) +``` +Search for movies by title and genre. + +#### 3. Random Famous Movie +```python +get_random_famous_movie_data(region: str = "famous") +``` +Get a random famous movie. + +### Resources + +Access attraction data as resources: +- `movie://{movie_id}` - Specific movie details +- `movies://search/{location}` - movies by location +- `movies://genre/{genre}` - movies by category + +### Prompts + +- `movie_booking_prompt(title, genre)` - Booking assistance +- `travel_planning_prompt(title, days)` - Multi-day itinerary planning +- `movie_comparison_prompt(movie_ids)` - Compare multiple movies + +## Example Usage + +```python +# Search for historical movies in Rome +search_movies(title="Inception", genre="sci-fi", limit=10) + +# Get details about the movie Inception (example ID: 1) +get_movie_details(1) + +# Get formatted search results +search_and_format_movies(location="Paris", category="cultural", limit=5) +``` + +## Project Structure + +``` +src/mcp/attractions-mcp/ +├── __init__.py # Package exports +├── main.py # MCP server setup and tools +├── models.py # Data classes (Movie, Booking, etc.) +├── config.py # API URLs and constants +├── utils.py # Helper functions and validation +├── movie_service.py # Core business logic +├── pyproject.toml # Dependencies +└── README.md # This file +``` + +## Development + +The codebase follows a modular structure similar to the weather-mcp: +- **Models**: Data structures for movies and reviews +- **Config**: Mock Data for movies +- **Utils**: Helper functions for API calls and validation +- **Service**: Business logic and data processing +- **Main**: MCP server orchestration diff --git a/src/mcp/movie-reviews-mcp/utils.py b/src/mcp/movie-reviews-mcp/utils.py new file mode 100644 index 0000000..a272e70 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/utils.py @@ -0,0 +1,130 @@ +""" +Utility functions for tourist movies operations. +""" + +import requests +import random +import string +from typing import Dict, Any, Optional, List +from datetime import datetime, timedelta + +from config import MOVIE_GENRES, MOCK_MOVIES, MOCK_REVIEWS +from models import Movie + + +def make_api_request(url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Make an API request with error handling""" + try: + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"API request failed: {str(e)}") + + +def get_movie_by_id(movie_id: int) -> Optional[Dict[str, Any]]: + """Get movie details by ID from mock data""" + for movie in MOCK_MOVIES: + if movie["id"] == movie_id: + return movie + return None + + +def search_movies( + title: Optional[str] = None, + genre: Optional[str] = None, + limit: int = 20 +) -> Optional[Dict[str, Any]]: + """Search for movies with filters using mock data""" + filtered_movies = [] + + for movie in MOCK_MOVIES: + + # Filter by genre + genre_match = True + if genre: + genre_match = movie["genres"] == genre.lower() + + # Filter by title + title_match = True + if title: + title_match = title.lower() in movie["title"].lower() + + if genre_match and title_match: + filtered_movies.append(movie) + + # Apply limit + filtered_movies = filtered_movies[:limit] + + return { + "movies": filtered_movies, + "total": len(filtered_movies) + } + +def get_random_famous_movie() -> Optional[Dict[str, Any]]: + """Get a random famous movie from mock data""" + if not MOCK_MOVIES: + return None + return random.choice(MOCK_MOVIES) + + +def parse_movie_data(data: Dict[str, Any]) -> Movie: + """Parse movie data from API response""" + + return Movie( + id=data.get("id", 0), + title=data.get("title", ""), + synopsis=data.get("synopsis", ""), + genre=data.get("genre", ""), + rating=data.get("rating"), + durationMins=data.get("durationMins", 0), + year=data.get("year", 0) + ) + + +def get_genre_display_name(genre: str) -> str: + """Get display name for genre""" + return MOVIE_GENRES.get(genre.lower(), genre.title()) + + +def format_duration(minutes: int) -> str: + """Format duration from minutes to hours and minutes""" + hours = minutes // 60 + mins = minutes % 60 + if hours > 0: + return f"{hours}h {mins}m" + return f"{mins}m" + + +def get_movie_rating_from_mock_reviews(movie: Movie) -> str: + """Get movie rating from mock reviews""" + + ratings = [] + + for review in MOCK_REVIEWS: + if review["movie_id"] == movie.id: + ratings.append(review["rating"]) + + if ratings: + avg_rating = sum(ratings) / len(ratings) + return f"{avg_rating:.1f}/5.0" + + return "Not Rated" + + +def format_movie_details(movie: Movie) -> str: + """Format movie details as a readable string""" + + details = f"🎞️ {movie.title}\n" + details += f"🏷️ Genre: {get_genre_display_name(movie.genre)}\n" + + if movie.rating: + details += f"⭐ Rating: {get_movie_rating_from_mock_reviews(movie)}\n" + + if movie.synopsis: + details += f"\n📝 Synopsis: {movie.synopsis}\n" + + if movie.durationMins: + details += f"⏳ Duration: {format_duration(movie.durationMins)}\n" + + return details diff --git a/src/mcp/movie-reviews-mcp/uv.lock b/src/mcp/movie-reviews-mcp/uv.lock new file mode 100644 index 0000000..df0f880 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/uv.lock @@ -0,0 +1,545 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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 = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/3c/82c400c2d50afdac4fbefb5b4031fd327e2ad1f23ccef8eee13c5909aa48/mcp-1.13.1.tar.gz", hash = "sha256:165306a8fd7991dc80334edd2de07798175a56461043b7ae907b279794a834c5", size = 438198, upload-time = "2025-08-22T09:22:16.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/3f/d085c7f49ade6d273b185d61ec9405e672b6433f710ea64a90135a8dd445/mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df", size = 161494, upload-time = "2025-08-22T09:22:14.705Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "movie-reviews-mcp" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "mcp", extra = ["cli"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "mcp", extras = ["cli"], specifier = ">=1.13.1" }, + { name = "requests" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" }, +] + +[[package]] +name = "typer" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/82/f4bfed3bc18c6ebd6f828320811bbe4098f92a31adf4040bee59c4ae02ea/typer-0.17.3.tar.gz", hash = "sha256:0c600503d472bcf98d29914d4dcd67f80c24cc245395e2e00ba3603c9332e8ba", size = 103517, upload-time = "2025-08-30T12:35:24.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/e8/b3d537470e8404659a6335e7af868e90657efb73916ef31ddf3d8b9cb237/typer-0.17.3-py3-none-any.whl", hash = "sha256:643919a79182ab7ac7581056d93c6a2b865b026adf2872c4d02c72758e6f095b", size = 46494, upload-time = "2025-08-30T12:35:22.391Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +]