diff --git a/README.md b/README.md index 37e9c73..ca2d2fa 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ A Model Context Protocol (MCP) server for searching and downloading academic pap ## Features -- **Multi-Source Support**: Search and download papers from arXiv, PubMed, bioRxiv, medRxiv, Google Scholar, IACR ePrint Archive, Semantic Scholar. +- **Multi-Source Support**: Search and download papers from arXiv, PubMed, bioRxiv, medRxiv, Google Scholar, IACR ePrint Archive, Semantic Scholar, and Scopus. - **Standardized Output**: Papers are returned in a consistent dictionary format via the `Paper` class. - **Asynchronous Tools**: Efficiently handles network requests using `httpx`. - **MCP Integration**: Compatible with MCP clients for LLM context enhancement. @@ -78,7 +78,8 @@ For users who want to quickly run the server: "paper_search_mcp.server" ], "env": { - "SEMANTIC_SCHOLAR_API_KEY": "" // Optional: For enhanced Semantic Scholar features + "SEMANTIC_SCHOLAR_API_KEY": "", // Optional: For enhanced Semantic Scholar features + "SCOPUS_API_KEY": "" // Add your Scopus API Key here } } } @@ -157,13 +158,13 @@ We welcome contributions! Here's how to get started: - [√] Google Scholar - [√] IACR ePrint Archive - [√] Semantic Scholar +- [√] Scopus - [ ] PubMed Central (PMC) - [ ] Science Direct - [ ] Springer Link - [ ] IEEE Xplore - [ ] ACM Digital Library - [ ] Web of Science -- [ ] Scopus - [ ] JSTOR - [ ] ResearchGate - [ ] CORE diff --git a/paper_search_mcp/academic_platforms/__init__.py b/paper_search_mcp/academic_platforms/__init__.py index e69de29..db0e102 100644 --- a/paper_search_mcp/academic_platforms/__init__.py +++ b/paper_search_mcp/academic_platforms/__init__.py @@ -0,0 +1,30 @@ +# paper_search_mcp/academic_platforms/__init__.py + +""" +This package provides modules for searching various academic platforms. +Each module should contain a searcher class that implements a common interface +(e.g., inheriting from a base PaperSource class and having a 'search' method). +""" + +from .arxiv import ArxivSearcher +from .biorxiv import BioRxivSearcher # Corrected capitalization +from .google_scholar import GoogleScholarSearcher +# hub.py is not a searcher, so it's not imported here for direct use as a platform +from .iacr import IACRSearcher +from .medrxiv import MedRxivSearcher # Corrected capitalization +from .pubmed import PubMedSearcher +from .scopus import ScopusSearcher +from .semantic import SemanticSearcher +from .shodhganga import ShodhgangaSearcher + +__all__ = [ + "ArxivSearcher", + "BioRxivSearcher", # Corrected capitalization + "GoogleScholarSearcher", + "IACRSearcher", + "MedRxivSearcher", # Corrected capitalization + "PubMedSearcher", + "ScopusSearcher", + "SemanticSearcher", + "ShodhgangaSearcher", +] diff --git a/paper_search_mcp/academic_platforms/hub.py b/paper_search_mcp/academic_platforms/hub.py index e69de29..a341223 100644 --- a/paper_search_mcp/academic_platforms/hub.py +++ b/paper_search_mcp/academic_platforms/hub.py @@ -0,0 +1,74 @@ +# paper_search_mcp/academic_platforms/hub.py + +""" +Central hub for accessing different academic platform searchers. +This allows for dynamic instantiation of searchers based on a key. +""" + +from .arxiv import ArxivSearcher +from .biorxiv import BiorxivSearcher +from .google_scholar import GoogleScholarSearcher +from .iacr import IACRSearcher +from .medrxiv import MedrxivSearcher +from .pubmed import PubMedSearcher +from .scopus import ScopusSearcher +from .semantic import SemanticSearcher +from .shodhganga import ShodhgangaSearcher + +# A dictionary mapping platform names (keys) to their searcher classes. +# This allows for easy lookup and instantiation of searchers. +AVAILABLE_SEARCHERS = { + "arxiv": ArxivSearcher, + "biorxiv": BiorxivSearcher, + "google_scholar": GoogleScholarSearcher, + "iacr": IACRSearcher, + "medrxiv": MedrxivSearcher, + "pubmed": PubMedSearcher, + "scopus": ScopusSearcher, + "semantic_scholar": SemanticSearcher, # Assuming 'semantic_scholar' as key for SemanticSearcher + "shodhganga": ShodhgangaSearcher, +} + +def get_searcher(platform_name: str): + """ + Returns an instance of the searcher for the given platform name. + + Args: + platform_name (str): The key for the desired platform + (e.g., "arxiv", "pubmed", "shodhganga"). + + Returns: + An instance of the searcher class if found, otherwise None. + + Raises: + ValueError: If the platform_name is not recognized. + """ + platform_name = platform_name.lower() + searcher_class = AVAILABLE_SEARCHERS.get(platform_name) + if searcher_class: + return searcher_class() # Instantiate the class + else: + raise ValueError(f"Unknown platform: {platform_name}. Available platforms are: {list(AVAILABLE_SEARCHERS.keys())}") + +if __name__ == '__main__': + # Example usage: + print(f"Available searcher platforms: {list(AVAILABLE_SEARCHERS.keys())}") + + try: + arxiv_searcher = get_searcher("arxiv") + print(f"Successfully got searcher for 'arxiv': {type(arxiv_searcher)}") + + shodhganga_searcher = get_searcher("shodhganga") + print(f"Successfully got searcher for 'shodhganga': {type(shodhganga_searcher)}") + + # Test a non-existent platform + # get_searcher("nonexistent_platform") + + except ValueError as e: + print(f"Error: {e}") + except ImportError as e: + print(f"ImportError: {e}. This might indicate an issue with the class names in __init__.py or the files themselves.") + print("Please ensure all Searcher classes (e.g., ArxivSearcher, PubMedSearcher) are correctly defined and imported.") + +# TODO: Consider adding a more robust plugin system if the number of platforms grows significantly. +# TODO: Potentially load API keys or configurations here if needed by searchers in the future. diff --git a/paper_search_mcp/academic_platforms/pmc.py b/paper_search_mcp/academic_platforms/pmc.py new file mode 100644 index 0000000..52da135 --- /dev/null +++ b/paper_search_mcp/academic_platforms/pmc.py @@ -0,0 +1,246 @@ +# paper_search_mcp/academic_platforms/pmc.py +from typing import List, Optional +import requests +from xml.etree import ElementTree as ET +from datetime import datetime +from ..paper import Paper +import os +from .pubmed import PaperSource # Reusing PaperSource +import PyPDF2 +import io + +class PMCSearcher(PaperSource): + """Searcher for PubMed Central (PMC) papers.""" + ESEARCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi" + EFETCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi" + PMC_PDF_URL = "https://www.ncbi.nlm.nih.gov/pmc/articles/{pmcid}/pdf" + + def search(self, query: str, max_results: int = 10) -> List[Paper]: + """Search PMC for papers.""" + search_params = { + 'db': 'pmc', + 'term': query, + 'retmax': max_results, + 'retmode': 'xml' + } + try: + search_response = requests.get(self.ESEARCH_URL, params=search_params) + search_response.raise_for_status() + search_root = ET.fromstring(search_response.content) + except requests.RequestException as e: + print(f"Error during PMC esearch request: {e}") + return [] + except ET.ParseError as e: + print(f"Error parsing PMC esearch XML response: {e}") + return [] + + ids = [id_node.text for id_node in search_root.findall('.//Id') if id_node.text] + if not ids: + return [] + + fetch_params = { + 'db': 'pmc', + 'id': ','.join(ids), + 'retmode': 'xml' + } + try: + fetch_response = requests.get(self.EFETCH_URL, params=fetch_params) + fetch_response.raise_for_status() + fetch_root = ET.fromstring(fetch_response.content) + except requests.RequestException as e: + print(f"Error during PMC efetch request: {e}") + return [] + except ET.ParseError as e: + print(f"Error parsing PMC efetch XML response: {e}") + return [] + + papers = [] + for article_node in fetch_root.findall('.//article'): # PMC uses
+ try: + # Extract PMCID + pmcid_node = article_node.find(".//article-id[@pub-id-type='pmc']") + pmcid = pmcid_node.text if pmcid_node is not None else None + if not pmcid: + continue # Skip if no PMCID + + # Extract title + title_node = article_node.find(".//article-title") + title = title_node.text if title_node is not None else "N/A" + + # Extract authors + authors = [] + for contrib_node in article_node.findall(".//contrib[@contrib-type='author']"): + surname_node = contrib_node.find(".//name/surname") + given_names_node = contrib_node.find(".//name/given-names") + surname = surname_node.text if surname_node is not None else "" + given_names = given_names_node.text if given_names_node is not None else "" + authors.append(f"{given_names} {surname}".strip()) + + # Extract abstract + abstract_text = "N/A" + abstract_element = article_node.find("./front/article-meta/abstract") + + if abstract_element is not None: + # Check for structured abstract (sections) + sections = abstract_element.findall("./sec") + if sections: # If tags are present, parse them + abstract_parts = [] + for sec_node in sections: + # Get all

text within this + sec_content_parts = [p_node.text.strip() for p_node in sec_node.findall(".//p") if p_node.text and p_node.text.strip()] + if sec_content_parts: + abstract_parts.append(" ".join(sec_content_parts)) + if abstract_parts: + abstract_text = "\n".join(abstract_parts) + else: + # Try to find a single

directly under + p_nodes = abstract_element.findall("./p") + if p_nodes: + abstract_text_parts = [p.text.strip() for p in p_nodes if p.text and p.text.strip()] + if abstract_text_parts: + abstract_text = "\n".join(abstract_text_parts) + # If no

directly under abstract, but abstract_element itself has text (less common) + elif abstract_element.text and abstract_element.text.strip(): + abstract_text = abstract_element.text.strip() + + abstract = abstract_text # Assign to the variable used later + + + # Extract publication date + pub_date_node = article_node.find(".//pub-date[@pub-type='epub']") # Prefer electronic pub date + if pub_date_node is None: # Fallback to print or other pub-types if epub not found + pub_date_node = article_node.find(".//pub-date[@pub-type='ppub']") + if pub_date_node is None: + pub_date_node = article_node.find(".//pub-date") # Generic fallback + + year, month, day = "N/A", "N/A", "N/A" + if pub_date_node is not None: + year_node = pub_date_node.find("./year") # Use relative path + month_node = pub_date_node.find("./month") + day_node = pub_date_node.find("./day") + year = year_node.text if year_node is not None and year_node.text else "N/A" + month = month_node.text if month_node is not None and month_node.text else "01" # Default month/day + day = day_node.text if day_node is not None and day_node.text else "01" + + try: + # Handle cases where year might be invalid + if year == "N/A" or not year.isdigit(): + year_int = 1900 # Default year + else: + year_int = int(year) + + if month == "N/A" or not month.isdigit() or not (1 <= int(month) <= 12): + month_int = 1 + else: + month_int = int(month) + + if day == "N/A" or not day.isdigit() or not (1 <= int(day) <= 31): + day_int = 1 + else: + day_int = int(day) + + published = datetime(year_int, month_int, day_int) + except ValueError: + published = datetime(1900, 1, 1) # Default for parsing errors + + # Extract DOI + doi_node = article_node.find(".//article-id[@pub-id-type='doi']") + doi = doi_node.text if doi_node is not None and doi_node.text else "" + + papers.append(Paper( + paper_id=pmcid, # Use PMCID as the primary ID + title=title, + authors=authors, + abstract=abstract, + url=f"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC{pmcid}/", # PMC uses PMCID in URL + pdf_url=self.PMC_PDF_URL.format(pmcid=f"PMC{pmcid}"), + published_date=published, + updated_date=published, # Assuming same as published for now + source='pmc', + categories=[], # PMC API doesn't easily provide categories + keywords=[], # PMC API doesn't easily provide keywords + doi=doi + )) + except Exception as e: + print(f"Error parsing PMC article XML (PMCID: {pmcid if 'pmcid' in locals() else 'unknown'}): {e}") + return papers + + def download_pdf(self, paper_id: str, save_path: str = "./downloads") -> str: + """Download the PDF for a PMC paper.""" + if not paper_id.startswith("PMC"): + paper_id = f"PMC{paper_id}" + + pdf_url = self.PMC_PDF_URL.format(pmcid=paper_id) + + os.makedirs(save_path, exist_ok=True) + file_path = os.path.join(save_path, f"{paper_id}.pdf") + + try: + response = requests.get(pdf_url, stream=True) + response.raise_for_status() + with open(file_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return file_path + except requests.RequestException as e: + raise ConnectionError(f"Failed to download PDF from {pdf_url}: {e}") + + def read_paper(self, paper_id: str, save_path: str = "./downloads") -> str: + """Download and read text from a PMC paper's PDF.""" + pdf_path = self.download_pdf(paper_id, save_path) + + try: + with open(pdf_path, 'rb') as f: + reader = PyPDF2.PdfReader(f) + text = "" + for page_num in range(len(reader.pages)): + text += reader.pages[page_num].extract_text() or "" + return text + except Exception as e: + print(f"Error reading PDF {pdf_path}: {e}") + return "" # Return empty string on error + +if __name__ == "__main__": + searcher = PMCSearcher() + + print("Testing PMC search functionality...") + query = "crispr gene editing" + max_results = 3 + try: + papers = searcher.search(query, max_results=max_results) + print(f"Found {len(papers)} papers for query '{query}':") + for i, paper in enumerate(papers, 1): + print(f"{i}. ID: {paper.paper_id} - {paper.title}") + print(f" Authors: {', '.join(paper.authors)}") + print(f" DOI: {paper.doi}") + print(f" URL: {paper.url}") + print(f" PDF URL: {paper.pdf_url}\n") + + if papers: + # Test PDF download and read + test_paper = papers[0] + print(f"\nTesting PDF download and read for PMCID: {test_paper.paper_id}") + try: + pdf_file_path = searcher.download_pdf(test_paper.paper_id) + print(f"PDF downloaded to: {pdf_file_path}") + + # Check if file exists and is not empty + if os.path.exists(pdf_file_path) and os.path.getsize(pdf_file_path) > 0: + print("PDF file seems valid.") + paper_text = searcher.read_paper(test_paper.paper_id) + if paper_text: + print(f"Successfully read paper. First 500 chars:\n{paper_text[:500]}...") + else: + print("Could not extract text from PDF, or PDF was empty.") + else: + print(f"PDF file at {pdf_file_path} is missing or empty.") + + except ConnectionError as e: + print(f"Connection error during PDF download/read test: {e}") + except Exception as e: + print(f"Error during PDF download/read test: {e}") + else: + print("No papers found to test download/read functionality.") + + except Exception as e: + print(f"Error during PMC search test: {e}") diff --git a/paper_search_mcp/academic_platforms/scopus.py b/paper_search_mcp/academic_platforms/scopus.py new file mode 100644 index 0000000..803c0ff --- /dev/null +++ b/paper_search_mcp/academic_platforms/scopus.py @@ -0,0 +1,131 @@ +# paper_search_mcp/academic_platforms/scopus.py +from typing import List +from datetime import datetime +import os +from elsapy.elsclient import ElsClient +from elsapy.elsprofile import ElsAuthor, ElsAffil +from elsapy.elsdoc import AbsDoc, FullDoc +from elsapy.elssearch import ElsSearch +import json +import os # Import os module +from ..paper import Paper + +# API_KEY will now be fetched from environment variable + +class PaperSource: + """Abstract base class for paper sources""" + def search(self, query: str, **kwargs) -> List[Paper]: + raise NotImplementedError + + def download_pdf(self, paper_id: str, save_path: str) -> str: + raise NotImplementedError + + def read_paper(self, paper_id: str, save_path: str) -> str: + raise NotImplementedError + +class ScopusSearcher(PaperSource): + """Searcher for Scopus papers""" + def __init__(self, api_key: str = None): + env_api_key = os.environ.get("SCOPUS_API_KEY") + final_api_key = api_key if api_key is not None else env_api_key + + if not final_api_key: + raise ValueError("Scopus API key not provided. Set SCOPUS_API_KEY environment variable or pass it during instantiation.") + self.client = ElsClient(final_api_key) + + def search(self, query: str, max_results: int = 10) -> List[Paper]: + doc_srch = ElsSearch(query, 'scopus') + doc_srch.execute(self.client, get_all=False, count=max_results) + + papers = [] + for result in doc_srch.results: + try: + # Extract basic metadata + paper_id = result.get('dc:identifier', '').replace('SCOPUS_ID:', '') + title = result.get('dc:title', '') + authors = [author['authname'] for author in result.get('author', [])] + abstract = result.get('dc:description', '') # Or 'prism:teaser' + doi = result.get('prism:doi', '') + url = result.get('prism:url', '') # Link to Scopus page + pdf_url = '' # Scopus API does not typically provide direct PDF links + + # Publication date + published_date_str = result.get('prism:coverDate', '') + published_date = None + if published_date_str: + try: + published_date = datetime.strptime(published_date_str, '%Y-%m-%d') + except ValueError: + pass # Handle other date formats if necessary + + papers.append(Paper( + paper_id=paper_id, + title=title, + authors=authors, + abstract=abstract, + url=url, + pdf_url=pdf_url, + published_date=published_date, + updated_date=None, # Scopus API might not provide this directly + source='scopus', + categories=[], # Scopus API might provide subject areas + keywords=[], # Scopus API might provide keywords + doi=doi + )) + except Exception as e: + print(f"Error parsing Scopus entry: {e}") + return papers + + def download_pdf(self, paper_id: str, save_path: str) -> str: + """ + Scopus API does not provide direct PDF download links. + This method might need to guide users to the Scopus website or + integrate with browser automation tools if PDF download is critical. + """ + raise NotImplementedError("Direct PDF download from Scopus is not supported via this API.") + + def read_paper(self, paper_id: str, save_path: str = "./downloads") -> str: + """ + Reading paper content directly is not supported as Scopus API + does not provide full text or direct PDF links. + """ + raise NotImplementedError("Reading paper content directly from Scopus is not supported.") + +if __name__ == "__main__": + # Test ScopusSearcher functionality + searcher = ScopusSearcher() + + # Test search functionality + print("Testing search functionality...") + query = "machine learning" + max_results = 5 + try: + papers = searcher.search(query, max_results=max_results) + print(f"Found {len(papers)} papers for query '{query}':") + for i, paper in enumerate(papers, 1): + print(f"{i}. {paper.title} (ID: {paper.paper_id}, DOI: {paper.doi})") + # print(f" Abstract: {paper.abstract[:100]}...") # Uncomment to see abstracts + except Exception as e: + print(f"Error during search: {e}") + + # Test download_pdf (expected to raise NotImplementedError) + if papers: + print("\nTesting PDF download functionality (expecting NotImplementedError)...") + paper_id = papers[0].paper_id + try: + searcher.download_pdf(paper_id, "./downloads") + except NotImplementedError as e: + print(f"Caught expected error: {e}") + except Exception as e: + print(f"Caught unexpected error: {e}") + + # Test read_paper (expected to raise NotImplementedError) + if papers: + print("\nTesting paper reading functionality (expecting NotImplementedError)...") + paper_id = papers[0].paper_id + try: + searcher.read_paper(paper_id) + except NotImplementedError as e: + print(f"Caught expected error: {e}") + except Exception as e: + print(f"Caught unexpected error: {e}") diff --git a/paper_search_mcp/academic_platforms/shodhganga.py b/paper_search_mcp/academic_platforms/shodhganga.py new file mode 100644 index 0000000..94d2e90 --- /dev/null +++ b/paper_search_mcp/academic_platforms/shodhganga.py @@ -0,0 +1,273 @@ +from typing import List, Optional +from datetime import datetime +import requests +from bs4 import BeautifulSoup +import time +import random +import re # Added for regex in date parsing +from ..paper import Paper # Assuming Paper class is in parent directory +import logging + +logger = logging.getLogger(__name__) + +# Define a base class for paper sources, similar to what's seen in other modules. +# If a central PaperSource exists, this searcher should inherit from it. +class PaperSource: + """Abstract base class for paper sources""" + def search(self, query: str, **kwargs) -> List[Paper]: + raise NotImplementedError + + def download_pdf(self, paper_id: str, save_path: str) -> str: + raise NotImplementedError + + def read_paper(self, paper_id: str, save_path: str) -> str: + raise NotImplementedError + +class ShodhgangaSearcher(PaperSource): + """ + Searcher for theses and dissertations on Shodhganga (https://shodhganga.inflibnet.ac.in/). + + NOTE: This implementation is based on assumed HTML structures and search parameter patterns + due to limitations in directly accessing the website during development. + It will require validation and potential adjustments with actual website responses. + """ + + BASE_URL = "https://shodhganga.inflibnet.ac.in" + SEARCH_PATH = "/simple-search" # Assuming this is the correct path for simple search + + # Common browser user agents to rotate + BROWSERS = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0" + ] + + def __init__(self): + """Initialize the session with a random user agent.""" + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': random.choice(self.BROWSERS), + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5' + }) + + def _parse_single_item(self, item_soup: BeautifulSoup, base_url: str) -> Optional[Paper]: + """ + Parses a single search result item from Shodhganga. + ASSUMPTION: This method is based on a hypothetical HTML structure. + Actual class names and tags will need to be verified. + """ + try: + # --- ASSUMED HTML STRUCTURE --- + title_tag = item_soup.select_one('h4.discovery-result-title a') # Example:

Title

+ if not title_tag: + logger.warning("Could not find title tag in item.") + return None + + title = title_tag.get_text(strip=True) + item_url = title_tag.get('href') + if not item_url: + logger.warning(f"Could not find URL for title: {title}") + return None + + # Ensure URL is absolute + if item_url.startswith('/'): + item_url = base_url + item_url + + # Authors - Example:
Author One, Author Two
+ # Or authors might be in or

+ # We will look for a div with "author" in its class name or specific metadata fields + author_tags = item_soup.select('div.authors span[title="author"], meta[name="DC.creator"]') + authors = [] + if author_tags: + authors = [tag.get_text(strip=True) if tag.name == 'span' else tag.get('content', '') for tag in author_tags] + authors = [a for a in authors if a] # Filter out empty strings + + if not authors: # Fallback if specific tags not found + author_div = item_soup.select_one('div[class*="author"]') # Generic author div + if author_div: + authors = [a.strip() for a in author_div.get_text(strip=True).split(';')] + + + # Abstract/Description - Example:

This is the abstract...
+ # Or
+ abstract_tag = item_soup.select_one('div.abstract-full, div.item-abstract') + abstract = abstract_tag.get_text(strip=True) if abstract_tag else "No abstract available." + + # Publication Date (Year) - Example:
2023
or + # Or + date_tag = item_soup.select_one('div.dateinfo, span.date, meta[name="DC.date.issued"]') + year = None + if date_tag: + date_text = date_tag.get_text(strip=True) if date_tag.name != 'meta' else date_tag.get('content', '') + # Try to extract a 4-digit year + match = re.search(r'\b(\d{4})\b', date_text) + if match: + year = int(match.group(1)) + + published_date = datetime(year, 1, 1) if year else None + + # Paper ID - can be derived from the URL or a specific metadata field + paper_id = f"shodhganga_{item_url.split('/')[-1]}" if item_url else f"shodhganga_{hash(title)}" + + return Paper( + paper_id=paper_id, + title=title, + authors=authors if authors else ["Unknown Author"], + abstract=abstract, + url=item_url, + pdf_url="", # Shodhganga links to landing pages, not direct PDFs + published_date=published_date, + updated_date=None, # Shodhganga may not provide this + source='shodhganga', + categories=[], # May need to parse if available + keywords=[], # May need to parse if available + doi="" # Shodhganga items are theses, may not always have DOIs + ) + except Exception as e: + logger.error(f"Error parsing Shodhganga item: {e}", exc_info=True) + return None + + def search(self, query: str, max_results: int = 10) -> List[Paper]: + """ + Search Shodhganga for theses and dissertations. + + ASSUMPTION: This method relies on assumed URL parameters and HTML structure + for Shodhganga's search results. These need to be verified. + """ + papers: List[Paper] = [] + # ASSUMPTION: Search parameters. Common ones are 'query' or 'rpp' (results per page). + # Shodhganga's simple search form uses 'query', 'filter_field_1', 'filter_type_1', 'filter_value_1' + # For a simple keyword search, 'query' might be enough, or it might be 'filter_value_1' with 'filter_field_1=all' + + # Let's try a structure based on typical DSpace simple search + # Example: /simple-search?query=myquery&sort_by=score&order=desc&rpp=10&etal=0&start=0 + search_url = self.BASE_URL + self.SEARCH_PATH + + # Pagination: DSpace typically uses 'start' for the offset. + # 'rpp' for results per page. + results_to_fetch_this_page = min(max_results, 20) # Shodhganga might cap results per page (e.g. at 20) + current_start_index = 0 + + while len(papers) < max_results: + params = { + 'query': query, + 'rpp': results_to_fetch_this_page, + 'sort_by': 'score', # Or 'dc.date.issued' for newest + 'order': 'desc', + 'start': current_start_index + } + + logger.info(f"Searching Shodhganga: {search_url} with params: {params}") + + try: + # Add a small delay to be polite to the server + time.sleep(random.uniform(1.0, 3.0)) + response = self.session.get(search_url, params=params) + response.raise_for_status() # Raise an exception for HTTP errors + + soup = BeautifulSoup(response.content, 'html.parser') + + # --- ASSUMED HTML STRUCTURE for results list --- + # Example:
...
...
+ # Or
  • ...
+ # Looking for elements that seem to contain individual search results. + # Common DSpace class for a list of items: 'ds-artifact-list' or 'discovery-result-results' + # Common DSpace class for one item: 'ds-artifact-item' or 'artifact-description' + result_items = soup.select('div.ds-artifact-item, div.artifact-description, li.ds-artifact-item') + + if not result_items: + logger.info("No more results found on Shodhganga or page structure not recognized.") + break + + found_on_page = 0 + for item_soup in result_items: + if len(papers) >= max_results: + break + paper = self._parse_single_item(item_soup, self.BASE_URL) + if paper: + papers.append(paper) + found_on_page +=1 + + logger.info(f"Found {found_on_page} items on this page. Total papers collected: {len(papers)}.") + + if found_on_page == 0: # No items parsed on this page, stop. + logger.info("No parsable items found on this page, stopping pagination.") + break + + # Pagination: Look for a 'next' link + # ASSUMPTION: Next page link has class 'next-page' or text 'Next'. + # DSpace pagination usually updates the 'start' parameter. + # If we successfully got `results_to_fetch_this_page` items, we assume there might be more. + # A more robust way is to check for an explicit "next" link. + # For DSpace, if current_start_index + results_to_fetch_this_page < total_hits, there's a next page. + # Total hits might be displayed as: 1-10 of 123 + + # Simple pagination: increment start index + current_start_index += results_to_fetch_this_page + + # Check if there's a clear 'next' button to decide if we should continue + next_page_tag = soup.select_one('a.next-page, a:contains("Next"), a[title="next"]') + if not next_page_tag and found_on_page < results_to_fetch_this_page : + logger.info("No 'next page' link found or fewer results than requested, assuming end of results.") + break + + + except requests.exceptions.RequestException as e: + logger.error(f"HTTP request to Shodhganga failed: {e}") + break # Stop searching if there's a request error + except Exception as e: + logger.error(f"An error occurred during Shodhganga search: {e}", exc_info=True) + break # Stop on other errors + + return papers[:max_results] + + def download_pdf(self, paper_id: str, save_path: str) -> str: + """ + Shodhganga typically links to thesis pages which may contain PDFs. + Direct PDF download via a simple ID is not assumed to be supported. + """ + raise NotImplementedError( + "Shodhganga does not provide direct PDF downloads via this interface. " + "Please use the paper URL from the search results to navigate to the thesis page and find download options." + ) + + def read_paper(self, paper_id: str, save_path: str = "./downloads") -> str: + """ + Reading papers directly from Shodhganga is not supported. + Metadata and links are provided; full text access is via the website. + """ + return ( + "Shodhganga papers cannot be read directly through this tool. " + "Please use the paper's URL to access the full text on the Shodhganga website." + ) + +if __name__ == '__main__': + # This section can be used for basic testing once the search method is implemented. + # For now, it will just demonstrate class instantiation. + searcher = ShodhgangaSearcher() + print("ShodhgangaSearcher initialized.") + + # Example of how search might be called (will currently return empty list and warning): + # try: + # papers = searcher.search("artificial intelligence", max_results=5) + # if not papers: + # print("Search returned no results (as expected for now).") + # for paper in papers: + # print(paper.title) + # except Exception as e: + # print(f"Error during search: {e}") + + # Test not implemented methods + try: + searcher.download_pdf("some_id", "./") + except NotImplementedError as e: + print(f"Caught expected error for download_pdf: {e}") + + try: + message = searcher.read_paper("some_id") + print(f"Response from read_paper: {message}") + except Exception as e: # Should not happen if it returns a message + print(f"Error during read_paper: {e}") diff --git a/paper_search_mcp/server.py b/paper_search_mcp/server.py index 7a371df..77a7e8f 100644 --- a/paper_search_mcp/server.py +++ b/paper_search_mcp/server.py @@ -9,6 +9,8 @@ from .academic_platforms.google_scholar import GoogleScholarSearcher from .academic_platforms.iacr import IACRSearcher from .academic_platforms.semantic import SemanticSearcher +from .academic_platforms.scopus import ScopusSearcher +from .academic_platforms.pmc import PMCSearcher # Added PMC # from .academic_platforms.hub import SciHubSearcher from .paper import Paper @@ -22,8 +24,15 @@ biorxiv_searcher = BioRxivSearcher() medrxiv_searcher = MedRxivSearcher() google_scholar_searcher = GoogleScholarSearcher() +pmc_searcher = PMCSearcher() # Added PMC +import os # Import os module + iacr_searcher = IACRSearcher() semantic_searcher = SemanticSearcher() +# Initialize ScopusSearcher, it will try to get API key from os.environ.get("SCOPUS_API_KEY") +# or it can be passed explicitly: ScopusSearcher(api_key=os.environ.get("SCOPUS_API_KEY")) +# For simplicity, relying on the constructor's default behavior to pick up the env var. +scopus_searcher = ScopusSearcher() # scihub_searcher = SciHubSearcher() @@ -341,5 +350,111 @@ async def read_semantic_paper(paper_id: str, save_path: str = "./downloads") -> return "" +@mcp.tool() +async def search_scopus(query: str, max_results: int = 10) -> List[Dict]: + """Search academic papers from Scopus. + + Args: + query: Search query string (e.g., 'machine learning'). + max_results: Maximum number of papers to return (default: 10). + Returns: + List of paper metadata in dictionary format. + """ + papers = await async_search(scopus_searcher, query, max_results) + return papers if papers else [] + + +@mcp.tool() +async def download_scopus(paper_id: str, save_path: str = "./downloads") -> str: + """Attempt to download PDF of a Scopus paper. + Note: Direct PDF download from Scopus is generally not supported via API. + + Args: + paper_id: Scopus paper ID. + save_path: Directory to save the PDF (default: './downloads'). + Returns: + Message indicating that direct PDF download is not supported or path to file if successful (unlikely). + """ + try: + return scopus_searcher.download_pdf(paper_id, save_path) + except NotImplementedError as e: + return str(e) + except Exception as e: + print(f"Error during Scopus download attempt for {paper_id}: {e}") + return f"An unexpected error occurred: {e}" + + +@mcp.tool() +async def read_scopus_paper(paper_id: str, save_path: str = "./downloads") -> str: + """Attempt to read/extract text from a Scopus paper. + Note: Direct full-text access from Scopus is generally not supported via API. + + Args: + paper_id: Scopus paper ID. + save_path: Directory where the PDF would be saved (default: './downloads'). + Returns: + Message indicating that direct paper reading is not supported or extracted text if successful (unlikely). + """ + try: + return scopus_searcher.read_paper(paper_id, save_path) + except NotImplementedError as e: + return str(e) + except Exception as e: + print(f"Error during Scopus read attempt for {paper_id}: {e}") + return f"An unexpected error occurred: {e}" + +@mcp.tool() +async def search_pmc(query: str, max_results: int = 10) -> List[Dict]: + """Search academic papers from PubMed Central (PMC). + + Args: + query: Search query string (e.g., 'crispr gene editing'). + max_results: Maximum number of papers to return (default: 10). + Returns: + List of paper metadata in dictionary format. + """ + papers = await async_search(pmc_searcher, query, max_results) + return papers if papers else [] + +@mcp.tool() +async def download_pmc(paper_id: str, save_path: str = "./downloads") -> str: + """Download PDF of a PubMed Central (PMC) paper. + + Args: + paper_id: PMC paper ID (e.g., 'PMC3539183' or '3539183'). + save_path: Directory to save the PDF (default: './downloads'). + Returns: + Path to the downloaded PDF file or an error message. + """ + try: + # PMCSearcher.download_pdf is synchronous, adapt if needed or call directly + # For simplicity, calling it directly assuming it's okay in this context + # If strict async is required, it should be wrapped like other searchers. + return pmc_searcher.download_pdf(paper_id, save_path) + except ConnectionError as e: + return str(e) + except Exception as e: + print(f"Error downloading PMC paper {paper_id}: {e}") + return f"An unexpected error occurred during PMC download: {e}" + +@mcp.tool() +async def read_pmc_paper(paper_id: str, save_path: str = "./downloads") -> str: + """Read and extract text content from a PubMed Central (PMC) paper PDF. + + Args: + paper_id: PMC paper ID (e.g., 'PMC3539183' or '3539183'). + save_path: Directory where the PDF is/will be saved (default: './downloads'). + Returns: + str: The extracted text content of the paper or an error message. + """ + try: + # PMCSearcher.read_paper is synchronous + return pmc_searcher.read_paper(paper_id, save_path) + except ConnectionError as e: # Catch connection errors from download_pdf + return str(e) + except Exception as e: + print(f"Error reading PMC paper {paper_id}: {e}") + return f"An unexpected error occurred during PMC paper reading: {e}" + if __name__ == "__main__": mcp.run(transport="stdio") diff --git a/pyproject.toml b/pyproject.toml index 2513f25..337cee8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ dependencies = [ "beautifulsoup4>=4.12.0", "lxml>=4.9.0", # Better HTML parser for BeautifulSoup "httpx[socks]>=0.28.1", + "elsapy", + "pandas", ] [tool.hatch.build.targets.wheel] diff --git a/tests/test_pmc.py b/tests/test_pmc.py new file mode 100644 index 0000000..0a1ce33 --- /dev/null +++ b/tests/test_pmc.py @@ -0,0 +1,221 @@ +import unittest +from unittest.mock import patch, MagicMock +import os +import requests # Added import +from datetime import datetime + +from paper_search_mcp.academic_platforms.pmc import PMCSearcher +from paper_search_mcp.paper import Paper + +class TestPMCSearcher(unittest.TestCase): + + def setUp(self): + self.searcher = PMCSearcher() + self.test_downloads_dir = "test_downloads_pmc" + os.makedirs(self.test_downloads_dir, exist_ok=True) + + def tearDown(self): + # Clean up created files and directory + if os.path.exists(self.test_downloads_dir): + for f in os.listdir(self.test_downloads_dir): + os.remove(os.path.join(self.test_downloads_dir, f)) + os.rmdir(self.test_downloads_dir) + + @patch('paper_search_mcp.academic_platforms.pmc.requests.get') + def test_search_success(self, mock_get): + # Mock ESearch response + mock_esearch_response = MagicMock() + mock_esearch_response.status_code = 200 + mock_esearch_response.content = b""" + + + PMC123 + PMC456 + + + """ + + # Mock EFetch response + mock_efetch_response = MagicMock() + mock_efetch_response.status_code = 200 + mock_efetch_response.content = b""" + +
+ + + PMC123 + 10.1000/xyz123 + + Test Paper Title 1 + + + + + Author + First + + + + + 2023 + 01 + 15 + +

This is abstract 1.

+
+
+
+
+ + + PMC456 + + Test Paper Title 2 + + + + + Tester + Another + + + + + 2022 + 12 + + BACKGROUND

Background info.

RESULTS

Results here.

+
+
+
+
+ """ + mock_get.side_effect = [mock_esearch_response, mock_efetch_response] + + papers = self.searcher.search("test query", max_results=2) + + self.assertEqual(len(papers), 2) + + paper1 = papers[0] + self.assertEqual(paper1.paper_id, "PMC123") + self.assertEqual(paper1.title, "Test Paper Title 1") + self.assertEqual(paper1.authors, ["First Author"]) + self.assertEqual(paper1.abstract, "This is abstract 1.") + self.assertEqual(paper1.doi, "10.1000/xyz123") + self.assertEqual(paper1.url, "https://www.ncbi.nlm.nih.gov/pmc/articles/PMCPMC123/") + self.assertEqual(paper1.pdf_url, "https://www.ncbi.nlm.nih.gov/pmc/articles/PMCPMC123/pdf") + self.assertEqual(paper1.published_date, datetime(2023, 1, 15)) + + paper2 = papers[1] + self.assertEqual(paper2.paper_id, "PMC456") + self.assertEqual(paper2.title, "Test Paper Title 2") + self.assertEqual(paper2.abstract, "Background info.\nResults here.") # Check structured abstract + self.assertEqual(paper2.published_date, datetime(2022, 12, 1)) # Month only, day defaults to 1 + + @patch('paper_search_mcp.academic_platforms.pmc.requests.get') + def test_search_empty_results(self, mock_get): + mock_esearch_response = MagicMock() + mock_esearch_response.status_code = 200 + mock_esearch_response.content = b"" + mock_get.return_value = mock_esearch_response + + papers = self.searcher.search("nonexistent query") + self.assertEqual(len(papers), 0) + + @patch('paper_search_mcp.academic_platforms.pmc.requests.get') + def test_search_request_exception_esearch(self, mock_get): + mock_get.side_effect = requests.RequestException("ESearch failed") + papers = self.searcher.search("test query") + self.assertEqual(len(papers), 0) + # You could also check logs or print statements if your actual code logs errors + + @patch('paper_search_mcp.academic_platforms.pmc.requests.get') + def test_search_request_exception_efetch(self, mock_get): + mock_esearch_response = MagicMock() + mock_esearch_response.status_code = 200 + mock_esearch_response.content = b"PMC123" + + mock_get.side_effect = [mock_esearch_response, requests.RequestException("EFetch failed")] + papers = self.searcher.search("test query") + self.assertEqual(len(papers), 0) + + @patch('paper_search_mcp.academic_platforms.pmc.requests.get') + def test_download_pdf_success(self, mock_get): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.iter_content = lambda chunk_size: [b"fake ", b"pdf ", b"content"] + mock_get.return_value = mock_response + + pmcid = "PMC789" + expected_path = os.path.join(self.test_downloads_dir, "PMC789.pdf") + + actual_path = self.searcher.download_pdf(pmcid, save_path=self.test_downloads_dir) + self.assertEqual(actual_path, expected_path) + self.assertTrue(os.path.exists(expected_path)) + with open(expected_path, 'rb') as f: + content = f.read() + self.assertEqual(content, b"fake pdf content") + + # Test with pmcid not starting with PMC + actual_path_no_prefix = self.searcher.download_pdf("12345", save_path=self.test_downloads_dir) + expected_path_no_prefix = os.path.join(self.test_downloads_dir, "PMC12345.pdf") + self.assertEqual(actual_path_no_prefix, expected_path_no_prefix) + + + @patch('paper_search_mcp.academic_platforms.pmc.requests.get') + def test_download_pdf_connection_error(self, mock_get): + mock_get.side_effect = requests.RequestException("Download failed") + with self.assertRaises(ConnectionError): + self.searcher.download_pdf("PMC123", save_path=self.test_downloads_dir) + + @patch('paper_search_mcp.academic_platforms.pmc.PMCSearcher.download_pdf') + @patch('paper_search_mcp.academic_platforms.pmc.PyPDF2.PdfReader') + def test_read_paper_success(self, mock_pdf_reader, mock_download_pdf): + pmcid = "PMC123" + pdf_path = os.path.join(self.test_downloads_dir, f"{pmcid}.pdf") + mock_download_pdf.return_value = pdf_path + + mock_reader_instance = MagicMock() + mock_page1 = MagicMock() + mock_page1.extract_text.return_value = "Page 1 text. " + mock_page2 = MagicMock() + mock_page2.extract_text.return_value = "Page 2 text." + mock_reader_instance.pages = [mock_page1, mock_page2] + mock_pdf_reader.return_value = mock_reader_instance + + # Create a dummy PDF file for the test, as PyPDF2 needs a real file path + os.makedirs(os.path.dirname(pdf_path), exist_ok=True) + with open(pdf_path, "w") as f: # Create empty file, content doesn't matter due to mocking + f.write("dummy") + + text = self.searcher.read_paper(pmcid, save_path=self.test_downloads_dir) + self.assertEqual(text, "Page 1 text. Page 2 text.") + mock_download_pdf.assert_called_once_with(pmcid, self.test_downloads_dir) + + @patch('paper_search_mcp.academic_platforms.pmc.PMCSearcher.download_pdf') + def test_read_paper_download_fails(self, mock_download_pdf): + mock_download_pdf.side_effect = ConnectionError("Failed to download") + + with self.assertRaises(ConnectionError): # Exception should propagate + self.searcher.read_paper("PMC123", save_path=self.test_downloads_dir) + + + @patch('paper_search_mcp.academic_platforms.pmc.PMCSearcher.download_pdf') + @patch('paper_search_mcp.academic_platforms.pmc.PyPDF2.PdfReader') + def test_read_paper_pdf_read_fails(self, mock_pdf_reader, mock_download_pdf): + pmcid = "PMC123" + pdf_path = os.path.join(self.test_downloads_dir, f"{pmcid}.pdf") + mock_download_pdf.return_value = pdf_path + + mock_pdf_reader.side_effect = Exception("PDF parsing error") + + # Create a dummy PDF file + os.makedirs(os.path.dirname(pdf_path), exist_ok=True) + with open(pdf_path, "w") as f: + f.write("dummy") + + text = self.searcher.read_paper(pmcid, save_path=self.test_downloads_dir) + self.assertEqual(text, "") # Should return empty string on PDF read error + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_scopus.py b/tests/test_scopus.py new file mode 100644 index 0000000..f6afdf9 --- /dev/null +++ b/tests/test_scopus.py @@ -0,0 +1,167 @@ +# tests/test_scopus.py +import unittest +from unittest.mock import patch, MagicMock +from datetime import datetime + +# Adjust import path based on your project structure +from paper_search_mcp.academic_platforms.scopus import ScopusSearcher +from paper_search_mcp.paper import Paper + +# Use the same API key for consistency if needed by tests, though it will be mocked +API_KEY = "84331d94db0ffe11c2b7c199fbdc8f52" # This is not used directly anymore but good for reference + +class TestScopusSearcher(unittest.TestCase): + + def setUp(self): + # We will set the API key via environment variable in each test method + # or pass it directly if needed for a specific test. + pass + + @patch.dict(os.environ, {"SCOPUS_API_KEY": "test_api_key_from_env"}) + @patch('paper_search_mcp.academic_platforms.scopus.ElsClient') + @patch('paper_search_mcp.academic_platforms.scopus.ElsSearch') + def test_search_success_from_env_key(self, MockElsSearch, MockElsClient): + searcher = ScopusSearcher() # Should pick up from mocked env + # Mock ElsClient instance + mock_client_instance = MockElsClient.return_value + + # Mock ElsSearch instance and its execute method + mock_search_instance = MockElsSearch.return_value + mock_search_instance.results = [ + { + 'dc:identifier': 'SCOPUS_ID:12345', + 'dc:title': 'Test Paper 1', + 'author': [{'authname': 'Author A'}, {'authname': 'Author B'}], + 'dc:description': 'This is a test abstract.', + 'prism:doi': '10.1000/test1', + 'prism:url': 'http://scopus.com/test1', + 'prism:coverDate': '2023-01-15', + }, + { + 'dc:identifier': 'SCOPUS_ID:67890', + 'dc:title': 'Test Paper 2', + 'author': [{'authname': 'Author C'}], + 'dc:description': 'Another test abstract.', + 'prism:doi': '10.1000/test2', + 'prism:url': 'http://scopus.com/test2', + 'prism:coverDate': '2023-02-20', + # Missing some fields to test robustness + } + ] + mock_search_instance.execute = MagicMock() + + MockElsSearch.return_value = mock_search_instance + + query = "test query" + max_results = 2 + papers = searcher.search(query, max_results=max_results) + + # Assertions + MockElsClient.assert_called_once_with("test_api_key_from_env") + MockElsSearch.assert_called_once_with(query, 'scopus') + mock_search_instance.execute.assert_called_once_with(mock_client_instance, get_all=False, count=max_results) + + self.assertEqual(len(papers), 2) + + # Check paper 1 + self.assertIsInstance(papers[0], Paper) + self.assertEqual(papers[0].paper_id, '12345') + self.assertEqual(papers[0].title, 'Test Paper 1') + self.assertEqual(papers[0].authors, ['Author A', 'Author B']) + self.assertEqual(papers[0].abstract, 'This is a test abstract.') + self.assertEqual(papers[0].doi, '10.1000/test1') + self.assertEqual(papers[0].url, 'http://scopus.com/test1') + self.assertEqual(papers[0].published_date, datetime(2023, 1, 15)) + self.assertEqual(papers[0].source, 'scopus') + + # Check paper 2 (testing defaults for missing fields) + self.assertIsInstance(papers[1], Paper) + self.assertEqual(papers[1].paper_id, '67890') + self.assertEqual(papers[1].title, 'Test Paper 2') + + @patch('paper_search_mcp.academic_platforms.scopus.ElsClient') + @patch('paper_search_mcp.academic_platforms.scopus.ElsSearch') + def test_search_success_with_direct_key(self, MockElsSearch, MockElsClient): + # Test with API key passed directly, should override env var if present + with patch.dict(os.environ, {"SCOPUS_API_KEY": "env_key_to_be_overridden"}, clear=True): + searcher = ScopusSearcher(api_key="direct_test_key") + + mock_client_instance = MockElsClient.return_value + mock_search_instance = MockElsSearch.return_value + mock_search_instance.results = [ + { + 'dc:identifier': 'SCOPUS_ID:12345', + 'dc:title': 'Test Paper Direct Key', + 'prism:doi': '10.1000/testdirect', + } + ] + mock_search_instance.execute = MagicMock() + MockElsSearch.return_value = mock_search_instance + + papers = searcher.search("direct key query", max_results=1) + MockElsClient.assert_called_once_with("direct_test_key") + self.assertEqual(len(papers), 1) + self.assertEqual(papers[0].title, 'Test Paper Direct Key') + + + @patch.dict(os.environ, {"SCOPUS_API_KEY": "test_api_key_for_error"}) + @patch('paper_search_mcp.academic_platforms.scopus.ElsClient') + @patch('paper_search_mcp.academic_platforms.scopus.ElsSearch') + def test_search_api_error(self, MockElsSearch, MockElsClient): + searcher = ScopusSearcher() + # Mock ElsClient instance + mock_client_instance = MockElsClient.return_value + + # Mock ElsSearch instance to simulate an API error during execute + mock_search_instance = MockElsSearch.return_value + mock_search_instance.execute = MagicMock(side_effect=Exception("API Communication Error")) + + MockElsSearch.return_value = mock_search_instance + + query = "error query" + with self.assertRaises(Exception) as context: # Or a more specific exception if ElsPy raises one + searcher.search(query, max_results=5) + + self.assertTrue("API Communication Error" in str(context.exception)) + MockElsClient.assert_called_once_with("test_api_key_for_error") + + + @patch.dict(os.environ, {"SCOPUS_API_KEY": "test_api_key_no_results"}) + @patch('paper_search_mcp.academic_platforms.scopus.ElsClient') + @patch('paper_search_mcp.academic_platforms.scopus.ElsSearch') + def test_search_no_results(self, MockElsSearch, MockElsClient): + searcher = ScopusSearcher() + # Mock ElsClient instance + mock_client_instance = MockElsClient.return_value + + # Mock ElsSearch instance with empty results + mock_search_instance = MockElsSearch.return_value + mock_search_instance.results = [] + mock_search_instance.execute = MagicMock() + + MockElsSearch.return_value = mock_search_instance + + papers = searcher.search("empty query", max_results=5) + self.assertEqual(len(papers), 0) + MockElsClient.assert_called_once_with("test_api_key_no_results") + + def test_api_key_missing(self): + # Ensure SCOPUS_API_KEY is not set for this test + with patch.dict(os.environ, {}, clear=True): + with self.assertRaisesRegex(ValueError, "Scopus API key not provided"): + ScopusSearcher() + + @patch.dict(os.environ, {"SCOPUS_API_KEY": "test_api_key_for_download"}) + def test_download_pdf_not_implemented(self): + searcher = ScopusSearcher() + with self.assertRaisesRegex(NotImplementedError, "Direct PDF download from Scopus is not supported via this API."): + searcher.download_pdf("some_id", "./downloads") + + @patch.dict(os.environ, {"SCOPUS_API_KEY": "test_api_key_for_read"}) + def test_read_paper_not_implemented(self): + searcher = ScopusSearcher() + with self.assertRaisesRegex(NotImplementedError, "Reading paper content directly from Scopus is not supported."): + searcher.read_paper("some_id") + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_shodhganga.py b/tests/test_shodhganga.py new file mode 100644 index 0000000..0ba4d7c --- /dev/null +++ b/tests/test_shodhganga.py @@ -0,0 +1,235 @@ +import unittest +from unittest.mock import patch, MagicMock +from datetime import datetime + +# Ensure the local path is prioritized for imports, especially for 'paper' +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from paper_search_mcp.paper import Paper +from paper_search_mcp.academic_platforms.shodhganga import ShodhgangaSearcher + +# --- Sample HTML Snippets based on DSpace structure assumptions --- + +# This is a HYPOTHETICAL HTML structure. It needs to be validated against the actual Shodhganga website. +SAMPLE_SHODHGANGA_RESULT_ITEM_HTML = """ +
+
+
+

+ A Study on Fictional Technology +

+
+ Researcher, Anand R.; + Guide, Priya S. +
+
University of Example, Department of Studies
+
Issued Date: 2023-01-15
+
+ This thesis explores the impact of fictional technology on modern society. + It covers various aspects and provides detailed analysis. +
+ +
+
+
+""" + +SAMPLE_SHODHGANGA_SEARCH_PAGE_HTML_TEMPLATE = """ + +Search Results + +
+ {items_html} +
+ {pagination_html} + + +""" + +SAMPLE_SHODHGANGA_PAGINATION_NEXT = 'Next' +NO_RESULTS_HTML = """ +
+""" + + +class TestShodhgangaSearcher(unittest.TestCase): + + def setUp(self): + self.searcher = ShodhgangaSearcher() + # Keep a reference to the original session for tests that might need it (though not typical for unit tests) + self.original_session = self.searcher.session + + def tearDown(self): + # Restore original session if it was modified + self.searcher.session = self.original_session + + @patch('requests.Session.get') + def test_search_url_construction_and_basic_call(self, mock_get): + """Test if the search method constructs the URL and params correctly.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = NO_RESULTS_HTML # No results needed for this test + mock_get.return_value = mock_response + + self.searcher.search("test query", max_results=5) + + expected_url = "https://shodhganga.inflibnet.ac.in/simple-search" + expected_params = { + 'query': "test query", + 'rpp': 5, + 'sort_by': 'score', + 'order': 'desc', + 'start': 0 + } + mock_get.assert_called_once() + args, kwargs = mock_get.call_args + self.assertEqual(args[0], expected_url) + self.assertDictEqual(kwargs['params'], expected_params) + + def test_parse_single_item_success(self): + """Test parsing a single valid HTML item.""" + from bs4 import BeautifulSoup + item_soup = BeautifulSoup(SAMPLE_SHODHGANGA_RESULT_ITEM_HTML, 'html.parser') + + paper = self.searcher._parse_single_item(item_soup.select_one('div.ds-artifact-item'), self.searcher.BASE_URL) + + self.assertIsNotNone(paper) + self.assertEqual(paper.title, "A Study on Fictional Technology") + self.assertListEqual(paper.authors, ["Researcher, Anand R.", "Guide, Priya S."]) + self.assertEqual(paper.url, "https://shodhganga.inflibnet.ac.in/jspui/handle/10603/12345") + self.assertEqual(paper.abstract[:50], "This thesis explores the impact of fictional tech") + self.assertEqual(paper.published_date, datetime(2023, 1, 1)) + self.assertEqual(paper.source, "shodhganga") + self.assertTrue(paper.paper_id.startswith("shodhganga_")) + + def test_parse_single_item_missing_fields(self): + """Test parsing an item with some missing fields.""" + from bs4 import BeautifulSoup + html_missing_author_abstract = """ +
+

Minimal Item

+
2021
+
+ """ + item_soup = BeautifulSoup(html_missing_author_abstract, 'html.parser') + paper = self.searcher._parse_single_item(item_soup.select_one('div.ds-artifact-item'), self.searcher.BASE_URL) + + self.assertIsNotNone(paper) + self.assertEqual(paper.title, "Minimal Item") + self.assertListEqual(paper.authors, ["Unknown Author"]) # Default value + self.assertEqual(paper.abstract, "No abstract available.") # Default value + self.assertEqual(paper.published_date, datetime(2021, 1, 1)) + self.assertEqual(paper.url, "https://shodhganga.inflibnet.ac.in/handle/123/broken") + + + @patch('requests.Session.get') + def test_search_parses_results(self, mock_get): + """Test that the search method uses _parse_single_item and returns Paper objects.""" + mock_response = MagicMock() + mock_response.status_code = 200 + # Simulate a page with one item and no next page link + mock_response.content = SAMPLE_SHODHGANGA_SEARCH_PAGE_HTML_TEMPLATE.format( + items_html=SAMPLE_SHODHGANGA_RESULT_ITEM_HTML, + pagination_html="" + ) + mock_get.return_value = mock_response + + papers = self.searcher.search("fictional technology", max_results=1) + + self.assertEqual(len(papers), 1) + self.assertIsInstance(papers[0], Paper) + self.assertEqual(papers[0].title, "A Study on Fictional Technology") + + @patch('requests.Session.get') + def test_search_handles_no_results(self, mock_get): + """Test search with a response that contains no results.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = NO_RESULTS_HTML + mock_get.return_value = mock_response + + papers = self.searcher.search("nonexistent query", max_results=10) + self.assertEqual(len(papers), 0) + + @patch('requests.Session.get') + def test_search_pagination_logic(self, mock_get): + """Test that search attempts to fetch multiple pages if max_results implies it.""" + # First response: one item + next page link + response1_html = SAMPLE_SHODHGANGA_SEARCH_PAGE_HTML_TEMPLATE.format( + items_html=SAMPLE_SHODHGANGA_RESULT_ITEM_HTML.replace("12345", "page1item"), + pagination_html=SAMPLE_SHODHGANGA_PAGINATION_NEXT + ) + mock_response1 = MagicMock(status_code=200, content=response1_html.encode('utf-8')) + + # Second response: another item, no next page link + item2_html = SAMPLE_SHODHGANGA_RESULT_ITEM_HTML.replace( + "A Study on Fictional Technology", "Another Study" + ).replace("12345", "page2item") + response2_html = SAMPLE_SHODHGANGA_SEARCH_PAGE_HTML_TEMPLATE.format( + items_html=item2_html, + pagination_html="" + ) + mock_response2 = MagicMock(status_code=200, content=response2_html.encode('utf-8')) + + # Third response (should not be strictly needed if max_results is met, but good for safety) + mock_response_empty = MagicMock(status_code=200, content=NO_RESULTS_HTML.encode('utf-8')) + + mock_get.side_effect = [mock_response1, mock_response2, mock_response_empty] + + papers = self.searcher.search("test query", max_results=2) # Request 2 results + + self.assertEqual(len(papers), 2) + self.assertEqual(mock_get.call_count, 2) # Should make two calls due to pagination + + # Check params for the second call (start index should have incremented) + args1, kwargs1 = mock_get.call_args_list[0] + args2, kwargs2 = mock_get.call_args_list[1] + + self.assertEqual(kwargs1['params']['start'], 0) + # rpp is min(max_results_remaining, configured_rpp_cap_in_shodhganga.py) + # max_results=2, so initial rpp = min(2, 20) = 2 + self.assertEqual(kwargs1['params']['rpp'], 2) + + self.assertEqual(kwargs2['params']['start'], 2) # start = 0 + 2 (rpp from first call) + self.assertEqual(kwargs2['params']['rpp'], 2) # rpp = min(2-1, 20) = 1, but code uses initial rpp throughout loop. + # This might be an area for refinement in shodhganga.py if needed, + # but current test reflects current code. + # The loop condition `len(papers) < max_results` will stop it. + + def test_download_pdf_not_implemented(self): + """Test that download_pdf raises NotImplementedError.""" + with self.assertRaisesRegex(NotImplementedError, "Shodhganga does not provide direct PDF downloads"): + self.searcher.download_pdf("some_id", "./downloads") + + def test_read_paper_returns_message(self): + """Test that read_paper returns the correct informational message.""" + message = self.searcher.read_paper("some_id") + self.assertIn("Shodhganga papers cannot be read directly", message) + + @patch('requests.Session.get') + def test_search_http_error(self, mock_get): + """Test search handling of HTTP errors.""" + mock_response = MagicMock() + mock_response.status_code = 500 + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("Server Error") + mock_get.return_value = mock_response + + papers = self.searcher.search("test query", max_results=10) + self.assertEqual(len(papers), 0) # Should return empty list on error + + def test_parse_single_item_no_title_tag(self): + """Test _parse_single_item when the main title tag is missing.""" + from bs4 import BeautifulSoup + html_no_title = """
No title here
""" + item_soup = BeautifulSoup(html_no_title, 'html.parser') + paper = self.searcher._parse_single_item(item_soup.select_one('div.ds-artifact-item'), self.searcher.BASE_URL) + self.assertIsNone(paper) + +if __name__ == '__main__': + print("NOTE: These tests for ShodhgangaSearcher rely on ASSUMED HTML structures.") + print("Actual functionality needs validation against the live Shodhganga website.\n") + unittest.main() diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 1fb9ef5..0000000 --- a/uv.lock +++ /dev/null @@ -1,665 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.10" - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, - { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, - { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, - { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, - { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, - { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, - { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, - { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, - { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, - { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, - { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, - { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, - { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, -] - -[[package]] -name = "fastmcp" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "mcp" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-dotenv" }, - { name = "typer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/84/17b549133263d7ee77141970769bbc401525526bf1af043ea6842bce1a55/fastmcp-0.4.1.tar.gz", hash = "sha256:713ad3b8e4e04841c9e2f3ca022b053adb89a286ceffad0d69ae7b56f31cbe64", size = 785575 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0b/008a340435fe8f0879e9d608f48af2737ad48440e09bd33b83b3fd03798b/fastmcp-0.4.1-py3-none-any.whl", hash = "sha256:664b42c376fb89ec90a50c9433f5a1f4d24f36696d6c41b024b427ae545f9619", size = 35282 }, -] - -[[package]] -name = "feedparser" -version = "6.0.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sgmllib3k" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/aa/7af346ebeb42a76bf108027fe7f3328bb4e57a3a96e53e21fd9ef9dd6dd0/feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5", size = 286197 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/d4/8c31aad9cc18f451c49f7f9cfb5799dadffc88177f7917bc90a66459b1d7/feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45", size = 81343 }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, -] - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, -] - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "lxml" -version = "5.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/61/d3dc048cd6c7be6fe45b80cedcbdd4326ba4d550375f266d9f4246d0f4bc/lxml-5.3.2.tar.gz", hash = "sha256:773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1", size = 3679948 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/9c/b015de0277a13d1d51924810b248b8a685a4e3dcd02d2ffb9b4e65cc37f4/lxml-5.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4b84d6b580a9625dfa47269bf1fd7fbba7ad69e08b16366a46acb005959c395", size = 8144077 }, - { url = "https://files.pythonhosted.org/packages/a7/6a/30467f6b66ae666d20b52dffa98c00f0f15e0567d1333d70db7c44a6939e/lxml-5.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4c08ecb26e4270a62f81f81899dfff91623d349e433b126931c9c4577169666", size = 4423433 }, - { url = "https://files.pythonhosted.org/packages/12/85/5a50121c0b57c8aba1beec30d324dc9272a193ecd6c24ad1efb5e223a035/lxml-5.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef926e9f11e307b5a7c97b17c5c609a93fb59ffa8337afac8f89e6fe54eb0b37", size = 5230753 }, - { url = "https://files.pythonhosted.org/packages/81/07/a62896efbb74ff23e9d19a14713fb9c808dfd89d79eecb8a583d1ca722b1/lxml-5.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017ceeabe739100379fe6ed38b033cd244ce2da4e7f6f07903421f57da3a19a2", size = 4945993 }, - { url = "https://files.pythonhosted.org/packages/74/ca/c47bffbafcd98c53c2ccd26dcb29b2de8fa0585d5afae76e5c5a9dce5f96/lxml-5.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dae97d9435dc90590f119d056d233c33006b2fd235dd990d5564992261ee7ae8", size = 5562292 }, - { url = "https://files.pythonhosted.org/packages/8f/79/f4ad46c00b72eb465be2032dad7922a14c929ae983e40cd9a179f1e727db/lxml-5.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:910f39425c6798ce63c93976ae5af5fff6949e2cb446acbd44d6d892103eaea8", size = 5000296 }, - { url = "https://files.pythonhosted.org/packages/44/cb/c974078e015990f83d13ef00dac347d74b1d62c2e6ec6e8eeb40ec9a1f1a/lxml-5.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9780de781a0d62a7c3680d07963db3048b919fc9e3726d9cfd97296a65ffce1", size = 5114822 }, - { url = "https://files.pythonhosted.org/packages/1b/c4/dde5d197d176f232c018e7dfd1acadf3aeb8e9f3effa73d13b62f9540061/lxml-5.3.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1a06b0c6ba2e3ca45a009a78a4eb4d6b63831830c0a83dcdc495c13b9ca97d3e", size = 4941338 }, - { url = "https://files.pythonhosted.org/packages/eb/8b/72f8df23f6955bb0f6aca635f72ec52799104907d6b11317099e79e1c752/lxml-5.3.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:4c62d0a34d1110769a1bbaf77871a4b711a6f59c4846064ccb78bc9735978644", size = 5586914 }, - { url = "https://files.pythonhosted.org/packages/0f/93/7b5ff2971cc5cf017de8ef0e9fdfca6afd249b1e187cb8195e27ed40bb9a/lxml-5.3.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:8f961a4e82f411b14538fe5efc3e6b953e17f5e809c463f0756a0d0e8039b700", size = 5082388 }, - { url = "https://files.pythonhosted.org/packages/a3/3e/f81d28bceb4e978a3d450098bdc5364d9c58473ad2f4ded04f679dc76e7e/lxml-5.3.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3dfc78f5f9251b6b8ad37c47d4d0bfe63ceb073a916e5b50a3bf5fd67a703335", size = 5161925 }, - { url = "https://files.pythonhosted.org/packages/4d/4b/1218fcfa0dfc8917ce29c66150cc8f6962d35579f412080aec480cc1a990/lxml-5.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10e690bc03214d3537270c88e492b8612d5e41b884f232df2b069b25b09e6711", size = 5022096 }, - { url = "https://files.pythonhosted.org/packages/8c/de/8eb6fffecd9c5f129461edcdd7e1ac944f9de15783e3d89c84ed6e0374bc/lxml-5.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa837e6ee9534de8d63bc4c1249e83882a7ac22bd24523f83fad68e6ffdf41ae", size = 5652903 }, - { url = "https://files.pythonhosted.org/packages/95/79/80f4102a08495c100014593680f3f0f7bd7c1333b13520aed855fc993326/lxml-5.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:da4c9223319400b97a2acdfb10926b807e51b69eb7eb80aad4942c0516934858", size = 5491813 }, - { url = "https://files.pythonhosted.org/packages/15/f5/9b1f7edf6565ee31e4300edb1bcc61eaebe50a3cff4053c0206d8dc772f2/lxml-5.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc0e9bdb3aa4d1de703a437576007d366b54f52c9897cae1a3716bb44fc1fc85", size = 5227837 }, - { url = "https://files.pythonhosted.org/packages/5c/17/c31d94364c02e3492215658917f5590c00edce8074aeb06d05b7771465d9/lxml-5.3.2-cp310-cp310-win32.whl", hash = "sha256:5f94909a1022c8ea12711db7e08752ca7cf83e5b57a87b59e8a583c5f35016ad", size = 3477533 }, - { url = "https://files.pythonhosted.org/packages/f2/2c/397c5a9d76a7a0faf9e5b13143ae1a7e223e71d2197a45da71c21aacb3d4/lxml-5.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:d64ea1686474074b38da13ae218d9fde0d1dc6525266976808f41ac98d9d7980", size = 3805160 }, - { url = "https://files.pythonhosted.org/packages/84/b8/2b727f5a90902f7cc5548349f563b60911ca05f3b92e35dfa751349f265f/lxml-5.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d61a7d0d208ace43986a92b111e035881c4ed45b1f5b7a270070acae8b0bfb4", size = 8163457 }, - { url = "https://files.pythonhosted.org/packages/91/84/23135b2dc72b3440d68c8f39ace2bb00fe78e3a2255f7c74f7e76f22498e/lxml-5.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856dfd7eda0b75c29ac80a31a6411ca12209183e866c33faf46e77ace3ce8a79", size = 4433445 }, - { url = "https://files.pythonhosted.org/packages/c9/1c/6900ade2294488f80598af7b3229669562166384bb10bf4c915342a2f288/lxml-5.3.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a01679e4aad0727bedd4c9407d4d65978e920f0200107ceeffd4b019bd48529", size = 5029603 }, - { url = "https://files.pythonhosted.org/packages/2f/e9/31dbe5deaccf0d33ec279cf400306ad4b32dfd1a0fee1fca40c5e90678fe/lxml-5.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6b37b4c3acb8472d191816d4582379f64d81cecbdce1a668601745c963ca5cc", size = 4771236 }, - { url = "https://files.pythonhosted.org/packages/68/41/c3412392884130af3415af2e89a2007e00b2a782be6fb848a95b598a114c/lxml-5.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3df5a54e7b7c31755383f126d3a84e12a4e0333db4679462ef1165d702517477", size = 5369815 }, - { url = "https://files.pythonhosted.org/packages/34/0a/ba0309fd5f990ea0cc05aba2bea225ef1bcb07ecbf6c323c6b119fc46e7f/lxml-5.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c09a40f28dcded933dc16217d6a092be0cc49ae25811d3b8e937c8060647c353", size = 4843663 }, - { url = "https://files.pythonhosted.org/packages/b6/c6/663b5d87d51d00d4386a2d52742a62daa486c5dc6872a443409d9aeafece/lxml-5.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1ef20f1851ccfbe6c5a04c67ec1ce49da16ba993fdbabdce87a92926e505412", size = 4918028 }, - { url = "https://files.pythonhosted.org/packages/75/5f/f6a72ccbe05cf83341d4b6ad162ed9e1f1ffbd12f1c4b8bc8ae413392282/lxml-5.3.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f79a63289dbaba964eb29ed3c103b7911f2dce28c36fe87c36a114e6bd21d7ad", size = 4792005 }, - { url = "https://files.pythonhosted.org/packages/37/7b/8abd5b332252239ffd28df5842ee4e5bf56e1c613c323586c21ccf5af634/lxml-5.3.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:75a72697d95f27ae00e75086aed629f117e816387b74a2f2da6ef382b460b710", size = 5405363 }, - { url = "https://files.pythonhosted.org/packages/5a/79/549b7ec92b8d9feb13869c1b385a0749d7ccfe5590d1e60f11add9cdd580/lxml-5.3.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:b9b00c9ee1cc3a76f1f16e94a23c344e0b6e5c10bec7f94cf2d820ce303b8c01", size = 4932915 }, - { url = "https://files.pythonhosted.org/packages/57/eb/4fa626d0bac8b4f2aa1d0e6a86232db030fd0f462386daf339e4a0ee352b/lxml-5.3.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:77cbcab50cbe8c857c6ba5f37f9a3976499c60eada1bf6d38f88311373d7b4bc", size = 4983473 }, - { url = "https://files.pythonhosted.org/packages/1b/c8/79d61d13cbb361c2c45fbe7c8bd00ea6a23b3e64bc506264d2856c60d702/lxml-5.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29424058f072a24622a0a15357bca63d796954758248a72da6d512f9bd9a4493", size = 4855284 }, - { url = "https://files.pythonhosted.org/packages/80/16/9f84e1ef03a13136ab4f9482c9adaaad425c68b47556b9d3192a782e5d37/lxml-5.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7d82737a8afe69a7c80ef31d7626075cc7d6e2267f16bf68af2c764b45ed68ab", size = 5458355 }, - { url = "https://files.pythonhosted.org/packages/aa/6d/f62860451bb4683e87636e49effb76d499773337928e53356c1712ccec24/lxml-5.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:95473d1d50a5d9fcdb9321fdc0ca6e1edc164dce4c7da13616247d27f3d21e31", size = 5300051 }, - { url = "https://files.pythonhosted.org/packages/3f/5f/3b6c4acec17f9a57ea8bb89a658a70621db3fb86ea588e7703b6819d9b03/lxml-5.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2162068f6da83613f8b2a32ca105e37a564afd0d7009b0b25834d47693ce3538", size = 5033481 }, - { url = "https://files.pythonhosted.org/packages/79/bd/3c4dd7d903bb9981f4876c61ef2ff5d5473e409ef61dc7337ac207b91920/lxml-5.3.2-cp311-cp311-win32.whl", hash = "sha256:f8695752cf5d639b4e981afe6c99e060621362c416058effd5c704bede9cb5d1", size = 3474266 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/9311fa1ef75b7d601c89600fc612838ee77ad3d426184941cba9cf62641f/lxml-5.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:d1a94cbb4ee64af3ab386c2d63d6d9e9cf2e256ac0fd30f33ef0a3c88f575174", size = 3815230 }, - { url = "https://files.pythonhosted.org/packages/0d/7e/c749257a7fabc712c4df57927b0f703507f316e9f2c7e3219f8f76d36145/lxml-5.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:16b3897691ec0316a1aa3c6585f61c8b7978475587c5b16fc1d2c28d283dc1b0", size = 8193212 }, - { url = "https://files.pythonhosted.org/packages/a8/50/17e985ba162c9f1ca119f4445004b58f9e5ef559ded599b16755e9bfa260/lxml-5.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8d4b34a0eeaf6e73169dcfd653c8d47f25f09d806c010daf074fba2db5e2d3f", size = 4451439 }, - { url = "https://files.pythonhosted.org/packages/c2/b5/4960ba0fcca6ce394ed4a2f89ee13083e7fcbe9641a91166e8e9792fedb1/lxml-5.3.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cd7a959396da425022e1e4214895b5cfe7de7035a043bcc2d11303792b67554", size = 5052146 }, - { url = "https://files.pythonhosted.org/packages/5f/d1/184b04481a5d1f5758916de087430752a7b229bddbd6c1d23405078c72bd/lxml-5.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cac5eaeec3549c5df7f8f97a5a6db6963b91639389cdd735d5a806370847732b", size = 4789082 }, - { url = "https://files.pythonhosted.org/packages/7d/75/1a19749d373e9a3d08861addccdf50c92b628c67074b22b8f3c61997cf5a/lxml-5.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b5f7d77334877c2146e7bb8b94e4df980325fab0a8af4d524e5d43cd6f789d", size = 5312300 }, - { url = "https://files.pythonhosted.org/packages/fb/00/9d165d4060d3f347e63b219fcea5c6a3f9193e9e2868c6801e18e5379725/lxml-5.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13f3495cfec24e3d63fffd342cc8141355d1d26ee766ad388775f5c8c5ec3932", size = 4836655 }, - { url = "https://files.pythonhosted.org/packages/b8/e9/06720a33cc155966448a19677f079100517b6629a872382d22ebd25e48aa/lxml-5.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e70ad4c9658beeff99856926fd3ee5fde8b519b92c693f856007177c36eb2e30", size = 4961795 }, - { url = "https://files.pythonhosted.org/packages/2d/57/4540efab2673de2904746b37ef7f74385329afd4643ed92abcc9ec6e00ca/lxml-5.3.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:507085365783abd7879fa0a6fa55eddf4bdd06591b17a2418403bb3aff8a267d", size = 4779791 }, - { url = "https://files.pythonhosted.org/packages/99/ad/6056edf6c9f4fa1d41e6fbdae52c733a4a257fd0d7feccfa26ae051bb46f/lxml-5.3.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:5bb304f67cbf5dfa07edad904732782cbf693286b9cd85af27059c5779131050", size = 5346807 }, - { url = "https://files.pythonhosted.org/packages/a1/fa/5be91fc91a18f3f705ea5533bc2210b25d738c6b615bf1c91e71a9b2f26b/lxml-5.3.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:3d84f5c093645c21c29a4e972b84cb7cf682f707f8706484a5a0c7ff13d7a988", size = 4909213 }, - { url = "https://files.pythonhosted.org/packages/f3/74/71bb96a3b5ae36b74e0402f4fa319df5559a8538577f8c57c50f1b57dc15/lxml-5.3.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bdc13911db524bd63f37b0103af014b7161427ada41f1b0b3c9b5b5a9c1ca927", size = 4987694 }, - { url = "https://files.pythonhosted.org/packages/08/c2/3953a68b0861b2f97234b1838769269478ccf872d8ea7a26e911238220ad/lxml-5.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ec944539543f66ebc060ae180d47e86aca0188bda9cbfadff47d86b0dc057dc", size = 4862865 }, - { url = "https://files.pythonhosted.org/packages/e0/9a/52e48f7cfd5a5e61f44a77e679880580dfb4f077af52d6ed5dd97e3356fe/lxml-5.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:59d437cc8a7f838282df5a199cf26f97ef08f1c0fbec6e84bd6f5cc2b7913f6e", size = 5423383 }, - { url = "https://files.pythonhosted.org/packages/17/67/42fe1d489e4dcc0b264bef361aef0b929fbb2b5378702471a3043bc6982c/lxml-5.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e275961adbd32e15672e14e0cc976a982075208224ce06d149c92cb43db5b93", size = 5286864 }, - { url = "https://files.pythonhosted.org/packages/29/e4/03b1d040ee3aaf2bd4e1c2061de2eae1178fe9a460d3efc1ea7ef66f6011/lxml-5.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:038aeb6937aa404480c2966b7f26f1440a14005cb0702078c173c028eca72c31", size = 5056819 }, - { url = "https://files.pythonhosted.org/packages/83/b3/e2ec8a6378e4d87da3af9de7c862bcea7ca624fc1a74b794180c82e30123/lxml-5.3.2-cp312-cp312-win32.whl", hash = "sha256:3c2c8d0fa3277147bff180e3590be67597e17d365ce94beb2efa3138a2131f71", size = 3486177 }, - { url = "https://files.pythonhosted.org/packages/d5/8a/6a08254b0bab2da9573735725caab8302a2a1c9b3818533b41568ca489be/lxml-5.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:77809fcd97dfda3f399102db1794f7280737b69830cd5c961ac87b3c5c05662d", size = 3817134 }, - { url = "https://files.pythonhosted.org/packages/19/fe/904fd1b0ba4f42ed5a144fcfff7b8913181892a6aa7aeb361ee783d441f8/lxml-5.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:77626571fb5270ceb36134765f25b665b896243529eefe840974269b083e090d", size = 8173598 }, - { url = "https://files.pythonhosted.org/packages/97/e8/5e332877b3ce4e2840507b35d6dbe1cc33b17678ece945ba48d2962f8c06/lxml-5.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78a533375dc7aa16d0da44af3cf6e96035e484c8c6b2b2445541a5d4d3d289ee", size = 4441586 }, - { url = "https://files.pythonhosted.org/packages/de/f4/8fe2e6d8721803182fbce2325712e98f22dbc478126070e62731ec6d54a0/lxml-5.3.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6f62b2404b3f3f0744bbcabb0381c5fe186fa2a9a67ecca3603480f4846c585", size = 5038447 }, - { url = "https://files.pythonhosted.org/packages/a6/ac/fa63f86a1a4b1ba8b03599ad9e2f5212fa813223ac60bfe1155390d1cc0c/lxml-5.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea918da00091194526d40c30c4996971f09dacab032607581f8d8872db34fbf", size = 4783583 }, - { url = "https://files.pythonhosted.org/packages/1a/7a/08898541296a02c868d4acc11f31a5839d80f5b21d4a96f11d4c0fbed15e/lxml-5.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c35326f94702a7264aa0eea826a79547d3396a41ae87a70511b9f6e9667ad31c", size = 5305684 }, - { url = "https://files.pythonhosted.org/packages/0b/be/9a6d80b467771b90be762b968985d3de09e0d5886092238da65dac9c1f75/lxml-5.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3bef90af21d31c4544bc917f51e04f94ae11b43156356aff243cdd84802cbf2", size = 4830797 }, - { url = "https://files.pythonhosted.org/packages/8d/1c/493632959f83519802637f7db3be0113b6e8a4e501b31411fbf410735a75/lxml-5.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52fa7ba11a495b7cbce51573c73f638f1dcff7b3ee23697467dc063f75352a69", size = 4950302 }, - { url = "https://files.pythonhosted.org/packages/c7/13/01aa3b92a6b93253b90c061c7527261b792f5ae7724b420cded733bfd5d6/lxml-5.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ad131e2c4d2c3803e736bb69063382334e03648de2a6b8f56a878d700d4b557d", size = 4775247 }, - { url = "https://files.pythonhosted.org/packages/60/4a/baeb09fbf5c84809e119c9cf8e2e94acec326a9b45563bf5ae45a234973b/lxml-5.3.2-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:00a4463ca409ceacd20490a893a7e08deec7870840eff33dc3093067b559ce3e", size = 5338824 }, - { url = "https://files.pythonhosted.org/packages/69/c7/a05850f169ad783ed09740ac895e158b06d25fce4b13887a8ac92a84d61c/lxml-5.3.2-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:87e8d78205331cace2b73ac8249294c24ae3cba98220687b5b8ec5971a2267f1", size = 4899079 }, - { url = "https://files.pythonhosted.org/packages/de/48/18ca583aba5235582db0e933ed1af6540226ee9ca16c2ee2d6f504fcc34a/lxml-5.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bf6389133bb255e530a4f2f553f41c4dd795b1fbb6f797aea1eff308f1e11606", size = 4978041 }, - { url = "https://files.pythonhosted.org/packages/b6/55/6968ddc88554209d1dba0dca196360c629b3dfe083bc32a3370f9523a0c4/lxml-5.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3709fc752b42fb6b6ffa2ba0a5b9871646d97d011d8f08f4d5b3ee61c7f3b2b", size = 4859761 }, - { url = "https://files.pythonhosted.org/packages/2e/52/d2d3baa1e0b7d04a729613160f1562f466fb1a0e45085a33acb0d6981a2b/lxml-5.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:abc795703d0de5d83943a4badd770fbe3d1ca16ee4ff3783d7caffc252f309ae", size = 5418209 }, - { url = "https://files.pythonhosted.org/packages/d3/50/6005b297ba5f858a113d6e81ccdb3a558b95a615772e7412d1f1cbdf22d7/lxml-5.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98050830bb6510159f65d9ad1b8aca27f07c01bb3884ba95f17319ccedc4bcf9", size = 5274231 }, - { url = "https://files.pythonhosted.org/packages/fb/33/6f40c09a5f7d7e7fcb85ef75072e53eba3fbadbf23e4991ca069ab2b1abb/lxml-5.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ba465a91acc419c5682f8b06bcc84a424a7aa5c91c220241c6fd31de2a72bc6", size = 5051899 }, - { url = "https://files.pythonhosted.org/packages/8b/3a/673bc5c0d5fb6596ee2963dd016fdaefaed2c57ede82c7634c08cbda86c1/lxml-5.3.2-cp313-cp313-win32.whl", hash = "sha256:56a1d56d60ea1ec940f949d7a309e0bff05243f9bd337f585721605670abb1c1", size = 3485315 }, - { url = "https://files.pythonhosted.org/packages/8c/be/cab8dd33b0dbe3af5b5d4d24137218f79ea75d540f74eb7d8581195639e0/lxml-5.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:1a580dc232c33d2ad87d02c8a3069d47abbcdce974b9c9cc82a79ff603065dbe", size = 3814639 }, - { url = "https://files.pythonhosted.org/packages/3d/1a/480682ac974e0f8778503300a61d96c3b4d992d2ae024f9db18d5fd895d1/lxml-5.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:521ab9c80b98c30b2d987001c3ede2e647e92eeb2ca02e8cb66ef5122d792b24", size = 3937182 }, - { url = "https://files.pythonhosted.org/packages/74/e6/ac87269713e372b58c4334913601a65d7a6f3b7df9ac15a4a4014afea7ae/lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1231b0f9810289d41df1eacc4ebb859c63e4ceee29908a0217403cddce38d0", size = 4235148 }, - { url = "https://files.pythonhosted.org/packages/75/ec/7d7af58047862fb59fcdec6e3abcffc7a98f7f7560e580485169ce28b706/lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271f1a4d5d2b383c36ad8b9b489da5ea9c04eca795a215bae61ed6a57cf083cd", size = 4349974 }, - { url = "https://files.pythonhosted.org/packages/ff/de/021ef34a57a372778f44182d2043fa3cae0b0407ac05fc35834f842586f2/lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6fca8a5a13906ba2677a5252752832beb0f483a22f6c86c71a2bb320fba04f61", size = 4238656 }, - { url = "https://files.pythonhosted.org/packages/0a/96/00874cb83ebb2cf649f2a8cad191d8da64fe1cf15e6580d5a7967755d6a3/lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ea0c3b7922209160faef194a5b6995bfe7fa05ff7dda6c423ba17646b7b9de10", size = 4373836 }, - { url = "https://files.pythonhosted.org/packages/6b/40/7d49ff503cc90b03253eba0768feec909b47ce92a90591b025c774a29a95/lxml-5.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0a006390834603e5952a2ff74b9a31a6007c7cc74282a087aa6467afb4eea987", size = 3487898 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "mcp" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "uvicorn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, -] - -[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "paper-search-mcp" -version = "0.1.2" -source = { editable = "." } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "fastmcp" }, - { name = "feedparser" }, - { name = "lxml" }, - { name = "mcp", extra = ["cli"] }, - { name = "pypdf2" }, - { name = "requests" }, -] - -[package.metadata] -requires-dist = [ - { name = "beautifulsoup4", specifier = ">=4.12.0" }, - { name = "fastmcp" }, - { name = "feedparser" }, - { name = "lxml", specifier = ">=4.9.0" }, - { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, - { name = "pypdf2", specifier = ">=3.0.0" }, - { name = "requests" }, -] - -[[package]] -name = "pydantic" -version = "2.11.2" -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/b0/41/832125a41fe098b58d1fdd04ae819b4dc6b34d6b09ed78304fd93d4bc051/pydantic-2.11.2.tar.gz", hash = "sha256:2138628e050bd7a1e70b91d4bf4a91167f4ad76fdb83209b107c8d84b854917e", size = 784742 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/c2/0f3baea344d0b15e35cb3e04ad5b953fa05106b76efbf4c782a3f47f22f5/pydantic-2.11.2-py3-none-any.whl", hash = "sha256:7f17d25846bcdf89b670a86cdfe7b29a9f1c9ca23dee154221c9aa81845cfca7", size = 443295 }, -] - -[[package]] -name = "pydantic-core" -version = "2.33.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, - { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, - { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, - { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, - { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, - { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, - { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, - { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, - { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, - { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, - { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, - { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, - { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, - { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, - { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, - { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, - { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, - { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, - { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, - { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, - { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, - { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, - { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, - { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, - { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, - { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, - { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, - { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, - { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, - { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, - { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, - { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, - { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, - { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, - { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, - { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, - { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, - { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, - { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, - { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, - { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, - { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, - { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, - { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, - { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, - { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, - { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, - { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, - { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, - { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, - { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, - { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, - { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, - { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, - { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, - { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, - { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, - { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, - { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, - { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, - { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, - { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, - { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, - { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, - { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, - { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, - { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, - { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, - { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, - { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, - { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, - { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, - { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, - { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, - { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.8.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, -] - -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, -] - -[[package]] -name = "pypdf2" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/bb/18dc3062d37db6c491392007dfd1a7f524bb95886eb956569ac38a23a784/PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", size = 227419 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572 }, -] - -[[package]] -name = "python-dotenv" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "rich" -version = "14.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, -] - -[[package]] -name = "sgmllib3k" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750 } - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - -[[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "soupsieve" -version = "2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, -] - -[[package]] -name = "sse-starlette" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, -] - -[[package]] -name = "starlette" -version = "0.46.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, -] - -[[package]] -name = "typer" -version = "0.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, -] - -[[package]] -name = "typing-extensions" -version = "4.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, -] - -[[package]] -name = "uvicorn" -version = "0.34.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, -]