├── .python-version ├── src └── jira_prompts_mcp_server │ ├── version.py │ ├── jira_utils │ ├── __init__.py │ ├── client.py │ ├── config.py │ ├── issues.py │ └── preprocessing.py │ ├── __main__.py │ ├── __init__.py │ ├── cli.py │ └── server.py ├── .gitignore ├── pyproject.toml ├── LICENSE ├── README.md └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/jira_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .issues import IssuesMixin 2 | 3 | 4 | class JiraFetcher(IssuesMixin): ... 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | 12 | *.txt 13 | 14 | .zed 15 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/__main__.py: -------------------------------------------------------------------------------- 1 | """This module is run when the package is executed as a script""" 2 | 3 | from jira_prompts_mcp_server import entry_point 4 | 5 | if __name__ == "__main__": 6 | entry_point() 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "jira-prompts-mcp-server" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | requires-python = ">=3.13" 6 | dependencies = [ 7 | "beautifulsoup4>=4.13.3", 8 | "fastmcp>=2.3.4", 9 | "jira>=3.8.0", 10 | "markdownify>=1.1.0", 11 | "pydantic>=2.11.3", 12 | "typer>=0.15.2", 13 | ] 14 | 15 | [[project.authors]] 16 | name = "Ceshine Lee" 17 | email = "shuanck@gmail.com" 18 | 19 | [dependency-groups] 20 | dev = [ 21 | "ipython>=9.1.0", 22 | ] 23 | 24 | [build-system] 25 | requires = ["hatchling"] 26 | build-backend = "hatchling.build" 27 | 28 | [project.scripts] 29 | jira-prompts-mcp-server = "jira_prompts_mcp_server:entry_point" 30 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from pathlib import Path 4 | from datetime import datetime 5 | 6 | import typer 7 | 8 | from .server import APP 9 | 10 | 11 | def _main(url: str, username: str, api_token: str) -> None: 12 | os.environ["JIRA_URL"] = url 13 | os.environ["JIRA_USERNAME"] = username 14 | os.environ["JIRA_API_TOKEN"] = api_token 15 | APP.run() 16 | 17 | 18 | def entry_point(): 19 | logging.basicConfig( 20 | format="[%(asctime)s][%(levelname)s][%(name)s] %(message)s", 21 | level=logging.INFO, 22 | datefmt="%Y-%m-%dT%H:%M:%S%z", 23 | ) 24 | # Export log to a temporary file under /tmp 25 | tmp_log_file = Path(f"/tmp/jira_prompts_mcp_{datetime.now().strftime('%Y%m%d%H%M%S')}.log") 26 | file_handler = logging.FileHandler(tmp_log_file) 27 | file_handler.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s][%(name)s] %(message)s")) 28 | logging.getLogger().addHandler(file_handler) 29 | typer.run(_main) 30 | 31 | 32 | if __name__ == "__main__": 33 | entry_point() 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ceshine Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/cli.py: -------------------------------------------------------------------------------- 1 | """CLI for testing the MCP methods. 2 | 3 | Prerequiesite - These environment variables need to be set: 4 | 5 | 1. `JIRA_URL` 6 | 2. `JIRA_USERNAME` 7 | 3. `JIRA_API_TOKEN` 8 | """ 9 | 10 | import asyncio 11 | 12 | import typer 13 | from fastmcp import Client 14 | 15 | from .server import APP as MCP_APP 16 | 17 | CLIENT = Client(MCP_APP) 18 | 19 | TYPER_APP = typer.Typer() 20 | 21 | 22 | @TYPER_APP.command() 23 | def jira_full(issue_key: str): 24 | async def _internal_func(): 25 | async with CLIENT: 26 | result = await CLIENT.get_prompt("jira-issue-full", arguments={"issue_key": issue_key}) 27 | print(result.messages[0].content.text) # type: ignore 28 | 29 | asyncio.run(_internal_func()) 30 | 31 | 32 | @TYPER_APP.command() 33 | def jira_brief(issue_key: str): 34 | async def _internal_func(): 35 | async with CLIENT: 36 | result = await CLIENT.get_prompt("jira-issue-brief", arguments={"issue_key": issue_key}) 37 | print(result.messages[0].content.text) # type: ignore 38 | 39 | asyncio.run(_internal_func()) 40 | 41 | 42 | if __name__ == "__main__": 43 | TYPER_APP() 44 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/jira_utils/client.py: -------------------------------------------------------------------------------- 1 | """Base client module for Jira API interactions.""" 2 | 3 | import logging 4 | 5 | from jira import JIRA 6 | 7 | from .config import JiraConfig 8 | from .preprocessing import JiraPreprocessor 9 | 10 | # Configure logging 11 | LOGGER = logging.getLogger("jira_prompts.client") 12 | 13 | 14 | class JiraClient: 15 | """Base client for Jira API interactions.""" 16 | 17 | def __init__(self, config: JiraConfig | None = None) -> None: 18 | """Initialize the Jira client with configuration options. 19 | 20 | Args: 21 | config: Optional configuration object (will use env vars if not provided) 22 | 23 | Raises: 24 | ValueError: If configuration is invalid or required credentials are missing 25 | """ 26 | # Load configuration from environment variables if not provided 27 | self.config = config or JiraConfig.from_env() 28 | 29 | # Initialize the Jira client based on auth type 30 | if self.config.auth_type == "token": 31 | raise NotImplementedError("Token authentication is not supported yet") 32 | else: # basic auth 33 | assert self.config.username and self.config.api_token, "Username and API token are required for basic auth" 34 | self.jira = JIRA( 35 | self.config.url, 36 | basic_auth=(self.config.username, self.config.api_token), 37 | ) 38 | 39 | self.preprocessor = JiraPreprocessor(base_url=self.config.url, jira_client=self.jira) 40 | 41 | # Cache for frequently used data 42 | self._field_ids: dict[str, str] | None = None 43 | self._current_user_account_id: str | None = None 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jira Prompts MCP Server 2 | 3 | This repository provides a Model Context Protocol (MCP) server that offers several commands for generating prompts or contexts from Jira content. 4 | 5 | This repository draws significant inspiration from the [MarkItDown MCP server](https://github.com/KorigamiK/markitdown_mcp_server) and the example [Git MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/git). It also incorporates design and code elements from the [mcp-atlassian](https://github.com/sooperset/mcp-atlassian) repository. The main differences between this repository and `mcp-atlassian` are that it uses [pycontribs/jira](https://github.com/pycontribs/jira) instead of [atlassian-api/atlassian-python-api](https://github.com/atlassian-api/atlassian-python-api) to interact with the Jira API, and it focuses on providing prompts rather than tools. The latter makes it especially useful when working with tools that support only MCP prompts, such as Zed's AI assistant. 6 | 7 | Here's another MCP server project of mine: [ceshine/git-prompts-mcp-server](https://github.com/ceshine/git-prompts-mcp-server) 8 | 9 | ## Changelog 10 | 11 | ### 0.1.0 12 | 13 | * Migrate from the low-level [mcp package](https://github.com/modelcontextprotocol/python-sdk) to the [FastMCP](https://github.com/jlowin/fastmcp?tab=readme-ov-file) package. 14 | * Add a CLI for testing the server. 15 | 16 | ### 0.0.1 17 | 18 | The initial release with two prompts implemented: `jira-issue-brief` and `jira-issue-full`. 19 | 20 | ## Installation 21 | 22 | ### Manual Installation 23 | 24 | 1. Clone this repository 25 | 2. Install dependencies: `uv sync --frozen` 26 | 27 | 28 | ## Usage 29 | 30 | ### As a MCP Server for Zed Editor 31 | 32 | Add the following to your `settings.json`: 33 | 34 | ```json 35 | "context_servers": { 36 | "git_prompt_mcp": { 37 | "command": { 38 | "path": "uv", 39 | "args": [ 40 | "--directory", 41 | "/path/to/local/jira_prompts_mcp_server", 42 | "run", 43 | "jira-prompts-mcp-server", 44 | "https://my-company.atlassian.net", // Jira base URL 45 | "your_jira_account@example.com", // Jira username 46 | "your_api_key" // Jira API token (https://id.atlassian.com/manage-profile/security/api-tokens) 47 | ] 48 | }, 49 | "settings": {} 50 | } 51 | } 52 | ``` 53 | 54 | #### Commands 55 | 56 | The server responds to the following commands: 57 | 58 | 1. `jira-issue-brief `: Retrieves the core fields of a Jira issue. Requires the issue key (e.g., `PROJ-123`) as an argument. 59 | 2. `jira-issue-full `: Retrieves the core fields, comments, linked issues, and subtasks of a Jira issue. Requires the issue key as an argument. 60 | 61 | Examples: 62 | 63 | 1. `/jira-issue-brief PROJ-123` 64 | 2. `/jira-issue-brief PROJ-155` 65 | 66 | ### Testing the server using the CLI 67 | 68 | Prerequisities: configuring the required environment variables (`JIRA_URL`, `JIRA_USERNAME`, `JIRA_API_TOKEN`) 69 | 70 | You can quickly test the MCP server using the CLI. Below are some example commands: 71 | 72 | * `uv run python -m jira_prompts_mcp_server.cli jira-brief BOOM-1234` 73 | * `uv run python -m jira_prompts_mcp_server.cli jira-full BOOM-1234` 74 | 75 | ## License 76 | 77 | MIT License. See [LICENSE](LICENSE) for details. 78 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/jira_utils/config.py: -------------------------------------------------------------------------------- 1 | """Configuration module for Jira API interactions.""" 2 | 3 | import os 4 | import re 5 | from urllib.parse import urlparse 6 | from dataclasses import dataclass 7 | from typing import Literal 8 | 9 | 10 | def is_atlassian_cloud_url(url: str) -> bool: 11 | """Determine if a URL belongs to Atlassian Cloud or Server/Data Center. 12 | 13 | Args: 14 | url: The URL to check 15 | 16 | Returns: 17 | True if the URL is for an Atlassian Cloud instance, False for Server/Data Center 18 | """ 19 | # Localhost and IP-based URLs are always Server/Data Center 20 | if url is None or not url: 21 | return False 22 | 23 | parsed_url = urlparse(url) 24 | hostname = parsed_url.hostname or "" 25 | 26 | # Check for localhost or IP address 27 | if ( 28 | hostname == "localhost" 29 | or re.match(r"^127\.", hostname) 30 | or re.match(r"^192\.168\.", hostname) 31 | or re.match(r"^10\.", hostname) 32 | or re.match(r"^172\.(1[6-9]|2[0-9]|3[0-1])\.", hostname) 33 | ): 34 | return False 35 | 36 | # The standard check for Atlassian cloud domains 37 | return ".atlassian.net" in hostname or ".jira.com" in hostname or ".jira-dev.com" in hostname 38 | 39 | 40 | @dataclass 41 | class JiraConfig: 42 | """Jira API configuration. 43 | 44 | Handles authentication for both Jira Cloud (using username/API token) 45 | and Jira Server/Data Center (using personal access token). 46 | """ 47 | 48 | url: str # Base URL for Jira 49 | auth_type: Literal["basic", "token"] # Authentication type 50 | username: str | None = None # Email or username (Cloud) 51 | api_token: str | None = None # API token (Cloud) 52 | personal_token: str | None = None # Personal access token (Server/DC) 53 | projects_filter: str | None = None # List of project keys to filter searches 54 | 55 | @property 56 | def is_cloud(self) -> bool: 57 | """Check if this is a cloud instance. 58 | 59 | Returns: 60 | True if this is a cloud instance (atlassian.net), False otherwise. 61 | Localhost URLs are always considered non-cloud (Server/Data Center). 62 | """ 63 | return is_atlassian_cloud_url(self.url) 64 | 65 | @classmethod 66 | def from_env(cls) -> "JiraConfig": 67 | """Create configuration from environment variables. 68 | 69 | Returns: 70 | JiraConfig with values from environment variables 71 | 72 | Raises: 73 | ValueError: If required environment variables are missing or invalid 74 | """ 75 | url = os.getenv("JIRA_URL") 76 | if not url: 77 | error_msg = "Missing required JIRA_URL environment variable" 78 | raise ValueError(error_msg) 79 | 80 | # Determine authentication type based on available environment variables 81 | username = os.getenv("JIRA_USERNAME") 82 | api_token = os.getenv("JIRA_API_TOKEN") 83 | personal_token = os.getenv("JIRA_PERSONAL_TOKEN") 84 | 85 | # Use the shared utility function directly 86 | is_cloud = is_atlassian_cloud_url(url) 87 | 88 | if is_cloud: 89 | if username and api_token: 90 | auth_type = "basic" 91 | else: 92 | error_msg = "Cloud authentication requires JIRA_USERNAME and JIRA_API_TOKEN" 93 | raise ValueError(error_msg) 94 | else: # Server/Data Center 95 | if personal_token: 96 | auth_type = "token" 97 | elif username and api_token: 98 | # Allow basic auth for Server/DC too 99 | auth_type = "basic" 100 | else: 101 | error_msg = "Server/Data Center authentication requires JIRA_PERSONAL_TOKEN" 102 | raise ValueError(error_msg) 103 | 104 | # Get the projects filter if provided 105 | projects_filter = os.getenv("JIRA_PROJECTS_FILTER") 106 | 107 | return cls( 108 | url=url, 109 | auth_type=auth_type, 110 | username=username, 111 | api_token=api_token, 112 | personal_token=personal_token, 113 | projects_filter=projects_filter, 114 | ) 115 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/jira_utils/issues.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Any 2 | 3 | from jira.resources import Issue 4 | 5 | from .client import JiraClient 6 | 7 | 8 | class IssuesMixin(JiraClient): 9 | def collect_comments( 10 | self, 11 | issue: Issue, 12 | limit: int = -1, # Set limit to -1 to collect all comments 13 | ) -> list[dict[str, Any]]: 14 | comments = sorted(issue.fields.comment.comments, key=lambda x: x.created, reverse=True) 15 | if limit > 0: 16 | comments = comments[:limit] 17 | return [ 18 | { 19 | "id": entry.id, 20 | "author": entry.author, 21 | "created": entry.created, 22 | "updated": entry.updated, 23 | "body": self.preprocessor.clean_jira_text(entry.body), 24 | } 25 | for entry in comments 26 | ] 27 | 28 | @staticmethod 29 | def collect_links(issue: Issue): 30 | links = issue.fields.issuelinks 31 | results = [] 32 | for entry in links: 33 | try: 34 | results.append( 35 | { 36 | "relationship": entry.type.inward, 37 | "key": entry.inwardIssue.key, 38 | "summary": entry.inwardIssue.fields.summary, 39 | "status": entry.inwardIssue.fields.status.name, 40 | "type": entry.inwardIssue.fields.issuetype.name, 41 | } 42 | ) 43 | except AttributeError: 44 | results.append( 45 | { 46 | "relationship": entry.type.outward, 47 | "key": entry.outwardIssue.key, 48 | "summary": entry.outwardIssue.fields.summary, 49 | "status": entry.outwardIssue.fields.status.name, 50 | "type": entry.outwardIssue.fields.issuetype.name, 51 | } 52 | ) 53 | return results 54 | 55 | @staticmethod 56 | def collect_subtasks(issue): 57 | return [ 58 | { 59 | "key": entry.key, 60 | "summary": entry.fields.summary, 61 | "status": entry.fields.status.name, 62 | "type": entry.fields.issuetype.name, 63 | } 64 | for entry in issue.fields.subtasks 65 | ] 66 | 67 | def collect_epic_children(self, issue) -> list[dict[str, str]]: 68 | assert issue.fields.issuetype.name == "Epic" 69 | results = [] 70 | for child_issue in self.jira.search_issues(f'"parent" = "{issue.key}"', maxResults=256): 71 | assert isinstance(child_issue, Issue) 72 | results.append( 73 | { 74 | "key": child_issue.key, 75 | "summary": child_issue.fields.summary, 76 | "status": child_issue.fields.status.name, 77 | "type": child_issue.fields.issuetype.name, 78 | "created": child_issue.fields.created, 79 | "updated": child_issue.fields.updated, 80 | } 81 | ) 82 | return sorted(results, key=lambda x: x["created"]) 83 | 84 | def get_issue_and_core_fields( 85 | self, 86 | issue_key: str, 87 | fields: str | Iterable[str] = ( 88 | "summary", 89 | "description", 90 | "status", 91 | "assignee", 92 | "parent", 93 | "reporter", 94 | "labels", 95 | "priority", 96 | "created", 97 | "updated", 98 | "issuetype", 99 | ), 100 | ) -> tuple[dict[str, Any], Issue]: 101 | issue = self.jira.issue(issue_key) 102 | if isinstance(fields, str): 103 | fields = fields.split(",") 104 | # Weed out any non-existent keys 105 | fields = [x for x in fields if x in issue.fields.__dict__] 106 | results = {field: getattr(issue.fields, field) for field in fields} 107 | # Special rule for "description" as it requires a conversion from the Jira markup format to the markdown format 108 | if "description" in results: 109 | results["description"] = self.preprocessor.clean_jira_text(results["description"]) 110 | return results, issue 111 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | from collections.abc import AsyncIterator 5 | from contextlib import asynccontextmanager 6 | 7 | from fastmcp import FastMCP 8 | from fastmcp.server.dependencies import get_context 9 | from mcp.types import PromptMessage, TextContent 10 | from pydantic import Field 11 | 12 | from .jira_utils import JiraFetcher 13 | 14 | LOGGER = logging.getLogger("jira_prompts") 15 | 16 | 17 | class StrFallbackEncoder(json.JSONEncoder): 18 | """A custom JSON encoder to get around restrictions on unserializable objects 19 | 20 | A custom JSON encoder that falls back to an object's __str__ representation 21 | if the object is not directly serializable by the default JSON encoder. 22 | """ 23 | 24 | def default(self, o): 25 | """ 26 | Overrides the default method of JSONEncoder. 27 | 28 | If the object `obj` is not serializable by the standard encoder, 29 | this method is called. It returns the string representation (obj.__str__()) 30 | of the object. 31 | 32 | Args: 33 | obj: The object to encode. 34 | 35 | Returns: 36 | A serializable representation of obj (its string form in this case). 37 | 38 | Raises: 39 | TypeError: If the default encoder itself encounters an issue after 40 | this method returns (though unlikely if str() succeeds). 41 | It primarily handles cases where the standard encoder fails. 42 | """ 43 | try: 44 | # Let the base class default method try first (handles dates, etc.) 45 | # Although often the check happens *before* calling default, 46 | # this is more robust if the base class had more complex logic. 47 | # However, for this specific requirement (call str() on failure), 48 | # we can directly attempt the fallback. 49 | # 50 | # If json.JSONEncoder already raises TypeError for obj, 51 | # this 'default' method will be called. 52 | return str(o) 53 | except TypeError: 54 | # If str(obj) itself fails (less common), let the base class 55 | # raise the final TypeError. 56 | # This line is technically only reached if str(obj) itself fails, 57 | # which is rare. The primary path is just `return str(obj)`. 58 | return json.JSONEncoder.default(self, o) 59 | 60 | 61 | @asynccontextmanager 62 | async def server_lifespan(server: FastMCP) -> AsyncIterator[JiraFetcher]: 63 | """Initialize and clean up application resources.""" 64 | jira_url = os.getenv("JIRA_URL") 65 | if jira_url is None: 66 | raise ValueError("JIRA_URL environment variable is not set") 67 | 68 | try: 69 | jira = JiraFetcher() 70 | 71 | # Log the startup information 72 | LOGGER.info("Starting Jira Prompts MCP server") 73 | os.system("notify-send 'Jira Prompts MCP server is starting'") 74 | 75 | jira_url = jira.config.url 76 | LOGGER.info(f"Jira URL: {jira_url}") 77 | 78 | # Provide context to the application 79 | yield jira 80 | finally: 81 | # Cleanup resources if needed 82 | pass 83 | 84 | 85 | APP = FastMCP("jira-prompts-mcp", lifespan=server_lifespan) 86 | 87 | 88 | def _postprocessing_for_issue_fields_(field_to_value): 89 | for name_field in ("status", "priority", "issuetype"): 90 | if name_field in field_to_value: 91 | field_to_value[name_field] = field_to_value[name_field].name 92 | for user_field in ("assignee", "reporter"): 93 | if user_field in field_to_value: 94 | if field_to_value[user_field] is None: 95 | field_to_value[user_field] = "N/A" 96 | else: 97 | field_to_value[user_field] = field_to_value[user_field].displayName 98 | if "parent" in field_to_value: 99 | field_to_value["parent"] = { 100 | "key": field_to_value["parent"].key, 101 | "summary": field_to_value["parent"].fields.summary, 102 | "status": field_to_value["parent"].fields.status.name, 103 | } 104 | 105 | 106 | def get_issue_and_core_fields(jira_fetcher: JiraFetcher, arguments: dict[str, str] | None): 107 | if not arguments: 108 | raise ValueError("Argument `issue_key` is required") 109 | issue_key = arguments.get("issue_key", "") 110 | assert issue_key 111 | field_to_value, issue = jira_fetcher.get_issue_and_core_fields(issue_key) 112 | field_to_value["issue_key"] = issue_key 113 | _postprocessing_for_issue_fields_(field_to_value) 114 | return field_to_value, issue 115 | 116 | 117 | @APP.prompt( 118 | name="jira-issue-brief", 119 | ) 120 | def jira_issu_brief(issue_key: str = Field(description="The key/ID of the issue")): 121 | "Get the core information about a Jira issue, including its description, parent, status, type, priority, and assignee." 122 | ctx = get_context() 123 | # TODO: this is probably not best way to get the Jira fetcher instance 124 | jira_fetcher = ctx.request_context.lifespan_context 125 | field_to_value, issue = get_issue_and_core_fields(jira_fetcher, {"issue_key": issue_key}) 126 | return PromptMessage( 127 | role="user", content=TextContent(type="text", text=json.dumps(field_to_value, cls=StrFallbackEncoder, indent=4)) 128 | ) 129 | 130 | 131 | @APP.prompt( 132 | name="jira-issue-full", 133 | ) 134 | def jira_issu_full(issue_key: str = Field(description="The key/ID of the issue")): 135 | "Get the full information about a Jira issue, including core information, linked issues, child tasks/sub tasks, and comments." 136 | ctx = get_context() 137 | # TODO: this is probably not best way to get the Jira fetcher instance 138 | jira_fetcher = ctx.request_context.lifespan_context 139 | field_to_value, issue = get_issue_and_core_fields(jira_fetcher, {"issue_key": issue_key}) 140 | field_to_value["links"] = jira_fetcher.collect_links(issue) 141 | if field_to_value["issuetype"] != "Epic": 142 | field_to_value["subtasks"] = jira_fetcher.collect_subtasks(issue) 143 | else: 144 | field_to_value["child_tasks"] = jira_fetcher.collect_epic_children(issue) 145 | field_to_value["comments"] = jira_fetcher.collect_comments(issue) 146 | return PromptMessage( 147 | role="user", content=TextContent(type="text", text=json.dumps(field_to_value, cls=StrFallbackEncoder, indent=4)) 148 | ) 149 | -------------------------------------------------------------------------------- /src/jira_prompts_mcp_server/jira_utils/preprocessing.py: -------------------------------------------------------------------------------- 1 | """Jira-specific text preprocessing module.""" 2 | 3 | import re 4 | import logging 5 | import warnings 6 | from functools import lru_cache 7 | from typing import Any 8 | 9 | 10 | import jira 11 | from markdownify import markdownify as md 12 | from bs4 import BeautifulSoup, Tag 13 | from bs4.element import NavigableString 14 | 15 | LOGGER = logging.getLogger("jira_prompts.jira.preprocessor") 16 | 17 | 18 | class BasePreprocessor: 19 | """Base class for text preprocessing operations.""" 20 | 21 | def __init__(self, base_url: str = "") -> None: 22 | """ 23 | Initialize the base text preprocessor. 24 | 25 | Args: 26 | base_url: Base URL for API server 27 | """ 28 | self.base_url = base_url.rstrip("/") if base_url else "" 29 | 30 | def process_html_content(self, html_content: str, space_key: str = "") -> tuple[str, str]: 31 | """ 32 | Process HTML content to replace user refs and page links. 33 | 34 | Args: 35 | html_content: The HTML content to process 36 | space_key: Optional space key for context 37 | 38 | Returns: 39 | Tuple of (processed_html, processed_markdown) 40 | """ 41 | try: 42 | # Parse the HTML content 43 | soup = BeautifulSoup(html_content, "html.parser") 44 | 45 | # Process user mentions 46 | self._process_user_mentions_in_soup(soup) 47 | 48 | # Convert to string and markdown 49 | processed_html = str(soup) 50 | processed_markdown = md(processed_html) 51 | 52 | return processed_html, processed_markdown 53 | 54 | except Exception as e: 55 | LOGGER.error(f"Error in process_html_content: {str(e)}") 56 | raise 57 | 58 | def _process_user_mentions_in_soup(self, soup: BeautifulSoup) -> None: 59 | """ 60 | Process user mentions in BeautifulSoup object. 61 | 62 | Args: 63 | soup: BeautifulSoup object containing HTML 64 | """ 65 | # Find all ac:link elements that might contain user mentions 66 | user_mentions = soup.find_all("ac:link") 67 | 68 | for user_element in user_mentions: 69 | # Ensure we're working with a Tag, not just any PageElement 70 | if not isinstance(user_element, Tag): 71 | continue 72 | user_ref = user_element.find("ri:user") 73 | if user_ref and isinstance(user_ref, Tag) and user_ref.has_attr("ri:account-id"): 74 | # Case 1: Direct user reference without link-body 75 | account_id = user_ref["ri:account-id"] 76 | if isinstance(account_id, str): 77 | self._replace_user_mention(user_element, account_id) 78 | continue 79 | 80 | # Case 2: User reference with link-body containing @ 81 | link_body = user_element.find("ac:link-body") 82 | if link_body and isinstance(link_body, Tag): 83 | link_text = link_body.get_text(strip=True) 84 | if "@" in link_text: 85 | user_ref = user_element.find("ri:user") 86 | if user_ref and isinstance(user_ref, Tag) and user_ref.has_attr("ri:account-id"): 87 | account_id = user_ref["ri:account-id"] 88 | if isinstance(account_id, str): 89 | self._replace_user_mention(user_element, account_id) 90 | 91 | def _replace_user_mention(self, user_element: Tag, account_id: str) -> None: 92 | """ 93 | Replace a user mention with the user's display name. 94 | 95 | Args: 96 | user_element: The HTML element containing the user mention 97 | account_id: The user's account ID 98 | """ 99 | try: 100 | # If we don't have a confluence client or couldn't get user details, 101 | # use fallback 102 | self._use_fallback_user_mention(user_element, account_id) 103 | except Exception as e: 104 | LOGGER.warning(f"Error processing user mention: {str(e)}") 105 | self._use_fallback_user_mention(user_element, account_id) 106 | 107 | def _use_fallback_user_mention(self, user_element: Tag, account_id: str) -> None: 108 | """ 109 | Replace user mention with a fallback when the API call fails. 110 | 111 | Args: 112 | user_element: The HTML element containing the user mention 113 | account_id: The user's account ID 114 | """ 115 | # Fallback: just use the account ID 116 | new_text = f"@user_{account_id}" 117 | new_element = NavigableString(new_text) 118 | user_element.replace_with(new_element) 119 | 120 | def _convert_html_to_markdown(self, text: str) -> str: 121 | """Convert HTML content to markdown if needed.""" 122 | if re.search(r"<[^>]+>", text): 123 | try: 124 | with warnings.catch_warnings(): 125 | warnings.filterwarnings("ignore", category=UserWarning) 126 | soup = BeautifulSoup(f"
{text}
", "html.parser") 127 | html = str(soup.div.decode_contents()) if soup.div else text 128 | text = md(html) 129 | except Exception as e: 130 | LOGGER.warning(f"Error converting HTML to markdown: {str(e)}") 131 | return text 132 | 133 | 134 | class JiraPreprocessor(BasePreprocessor): 135 | """Handles text preprocessing for Jira content.""" 136 | 137 | def __init__(self, jira_client: jira.JIRA, base_url: str = "", **kwargs: Any) -> None: 138 | """ 139 | Initialize the Jira text preprocessor. 140 | 141 | Args: 142 | base_url: Base URL for Jira API 143 | **kwargs: Additional arguments for the base class 144 | """ 145 | super().__init__(base_url=base_url, **kwargs) 146 | self.jira_client = jira_client 147 | # Create a method with LRU cache bound to this instance 148 | self._find_user = self._create_cached_find_user() 149 | 150 | def clean_jira_text(self, text: str) -> str: 151 | """ 152 | Clean Jira text content by: 153 | 1. Processing user mentions and links 154 | 2. Converting Jira markup to markdown 155 | 3. Converting HTML/wiki markup to markdown 156 | """ 157 | if not text: 158 | return "" 159 | 160 | # Process user mentions 161 | mention_pattern = r"\[~accountid:(.*?)\]" 162 | text = self._process_mentions(text, mention_pattern) 163 | 164 | # Process Jira smart links 165 | text = self._process_smart_links(text) 166 | 167 | # First convert any Jira markup to Markdown 168 | text = self.jira_to_markdown(text) 169 | 170 | # Then convert any remaining HTML to markdown 171 | text = self._convert_html_to_markdown(text) 172 | 173 | return text.strip() 174 | 175 | def _create_cached_find_user(self): 176 | """Create a cached version of the find_user method.""" 177 | 178 | @lru_cache(maxsize=100) 179 | def cached_find_user(account_id): 180 | """Cached version of user lookup by account ID.""" 181 | LOGGER.debug(f"Cache miss for user: {account_id}") 182 | return self.jira_client.user(account_id) 183 | 184 | return cached_find_user 185 | 186 | def _process_mentions(self, text: str, pattern: str) -> str: 187 | """ 188 | Process user mentions in text. 189 | 190 | Args: 191 | text: The text containing mentions 192 | pattern: Regular expression pattern to match mentions 193 | 194 | Returns: 195 | Text with mentions replaced with display names 196 | """ 197 | mentions = re.findall(pattern, text) 198 | for account_id in mentions: 199 | try: 200 | display_name = f"@<{self.jira_client.user(account_id).displayName}>" 201 | text = text.replace(f"[~accountid:{account_id}]", display_name) 202 | except Exception as e: 203 | LOGGER.error(f"Error processing mention for {account_id}: {str(e)}") 204 | return text 205 | 206 | def _process_smart_links(self, text: str) -> str: 207 | """Process Jira/Confluence smart links.""" 208 | # Pattern matches: [text|url|smart-link] 209 | link_pattern = r"\[(.*?)\|(.*?)\|smart-link\]" 210 | matches = re.finditer(link_pattern, text) 211 | 212 | for match in matches: 213 | full_match = match.group(0) 214 | link_text = match.group(1) 215 | link_url = match.group(2) 216 | 217 | # Extract issue key if it's a Jira issue link 218 | issue_key_match = re.search(r"browse/([A-Z]+-\d+)", link_url) 219 | # Check if it's a Confluence wiki link 220 | confluence_match = re.search(r"wiki/spaces/.+?/pages/\d+/(.+?)(?:\?|$)", link_url) 221 | 222 | if issue_key_match: 223 | issue_key = issue_key_match.group(1) 224 | clean_url = f"{self.base_url}/browse/{issue_key}" 225 | text = text.replace(full_match, f"[{issue_key}]({clean_url})") 226 | elif confluence_match: 227 | url_title = confluence_match.group(1) 228 | readable_title = url_title.replace("+", " ") 229 | readable_title = re.sub(r"^[A-Z]+-\d+\s+", "", readable_title) 230 | text = text.replace(full_match, f"[{readable_title}]({link_url})") 231 | else: 232 | clean_url = link_url.split("?")[0] 233 | text = text.replace(full_match, f"[{link_text}]({clean_url})") 234 | 235 | return text 236 | 237 | def jira_to_markdown(self, input_text: str) -> str: 238 | """ 239 | Convert Jira markup to Markdown format. 240 | 241 | Args: 242 | input_text: Text in Jira markup format 243 | 244 | Returns: 245 | Text in Markdown format 246 | """ 247 | if not input_text: 248 | return "" 249 | 250 | # Block quotes 251 | output = re.sub(r"^bq\.(.*?)$", r"> \1\n", input_text, flags=re.MULTILINE) 252 | 253 | # Text formatting (bold, italic) 254 | output = re.sub( 255 | r"(?\1", output) 283 | 284 | # Inserted text 285 | output = re.sub(r"\+([^+]*)\+", r"\1", output) 286 | 287 | # Superscript 288 | output = re.sub(r"\^([^^]*)\^", r"\1", output) 289 | 290 | # Subscript 291 | output = re.sub(r"~([^~]*)~", r"\1", output) 292 | 293 | # Strikethrough 294 | output = re.sub(r"-([^-]*)-", r"-\1-", output) 295 | 296 | # Code blocks with optional language specification 297 | output = re.sub( 298 | r"\{code(?::([a-z]+))?\}([\s\S]*?)\{code\}", 299 | r"```\1\n\2\n```", 300 | output, 301 | flags=re.MULTILINE, 302 | ) 303 | 304 | # No format 305 | output = re.sub(r"\{noformat\}([\s\S]*?)\{noformat\}", r"```\n\1\n```", output) 306 | 307 | # Quote blocks 308 | output = re.sub( 309 | r"\{quote\}([\s\S]*)\{quote\}", 310 | lambda match: "\n".join([f"> {line}" for line in match.group(1).split("\n")]), 311 | output, 312 | flags=re.MULTILINE, 313 | ) 314 | 315 | # Images with alt text 316 | output = re.sub( 317 | r"!([^|\n\s]+)\|([^\n!]*)alt=([^\n!\,]+?)(,([^\n!]*))?!", 318 | r"![\3](\1)", 319 | output, 320 | ) 321 | 322 | # Images with other parameters (ignore them) 323 | output = re.sub(r"!([^|\n\s]+)\|([^\n!]*)!", r"![](\1)", output) 324 | 325 | # Images without parameters 326 | output = re.sub(r"!([^\n\s!]+)!", r"![](\1)", output) 327 | 328 | # Links 329 | output = re.sub(r"\[([^|]+)\|(.+?)\]", r"[\1](\2)", output) 330 | output = re.sub(r"\[(.+?)\]([^\(]+)", r"<\1>\2", output) 331 | 332 | # Colored text 333 | output = re.sub( 334 | r"\{color:([^}]+)\}([\s\S]*?)\{color\}", 335 | r"\2", 336 | output, 337 | flags=re.MULTILINE, 338 | ) 339 | 340 | # Convert Jira table headers (||) to markdown table format 341 | lines = output.split("\n") 342 | i = 0 343 | while i < len(lines): 344 | line = lines[i] 345 | 346 | if "||" in line: 347 | # Replace Jira table headers 348 | lines[i] = lines[i].replace("||", "|") 349 | 350 | # Add a separator line for markdown tables 351 | header_cells = lines[i].count("|") - 1 352 | if header_cells > 0: 353 | separator_line = "|" + "---|" * header_cells 354 | lines.insert(i + 1, separator_line) 355 | i += 1 # Skip the newly inserted line in next iteration 356 | 357 | i += 1 358 | 359 | # Rejoin the lines 360 | output = "\n".join(lines) 361 | 362 | return output 363 | 364 | def markdown_to_jira(self, input_text: str) -> str: 365 | """ 366 | Convert Markdown syntax to Jira markup syntax. 367 | 368 | Args: 369 | input_text: Text in Markdown format 370 | 371 | Returns: 372 | Text in Jira markup format 373 | """ 374 | if not input_text: 375 | return "" 376 | 377 | # Save code blocks to prevent recursive processing 378 | code_blocks = [] 379 | inline_codes = [] 380 | 381 | # Extract code blocks 382 | def save_code_block(match: re.Match) -> str: 383 | """ 384 | Process and save a code block. 385 | 386 | Args: 387 | match: Regex match object containing the code block 388 | 389 | Returns: 390 | Jira-formatted code block 391 | """ 392 | syntax = match.group(1) or "" 393 | content = match.group(2) 394 | code = "{code" 395 | if syntax: 396 | code += ":" + syntax 397 | code += "}" + content + "{code}" 398 | code_blocks.append(code) 399 | return str(code) # Ensure we return a string 400 | 401 | # Extract inline code 402 | def save_inline_code(match: re.Match) -> str: 403 | """ 404 | Process and save inline code. 405 | 406 | Args: 407 | match: Regex match object containing the inline code 408 | 409 | Returns: 410 | Jira-formatted inline code 411 | """ 412 | content = match.group(1) 413 | code = "{{" + content + "}}" 414 | inline_codes.append(code) 415 | return str(code) # Ensure we return a string 416 | 417 | # Save code sections temporarily 418 | output = re.sub(r"```(\w*)\n([\s\S]+?)```", save_code_block, input_text) 419 | output = re.sub(r"`([^`]+)`", save_inline_code, output) 420 | 421 | # Headers with = or - underlines 422 | output = re.sub( 423 | r"^(.*?)\n([=-])+$", 424 | lambda match: f"h{1 if match.group(2)[0] == '=' else 2}. {match.group(1)}", 425 | output, 426 | flags=re.MULTILINE, 427 | ) 428 | 429 | # Headers with # prefix 430 | output = re.sub( 431 | r"^([#]+)(.*?)$", 432 | lambda match: f"h{len(match.group(1))}." + match.group(2), 433 | output, 434 | flags=re.MULTILINE, 435 | ) 436 | 437 | # Bold and italic 438 | output = re.sub( 439 | r"([*_]+)(.*?)\1", 440 | lambda match: ("_" if len(match.group(1)) == 1 else "*") 441 | + match.group(2) 442 | + ("_" if len(match.group(1)) == 1 else "*"), 443 | output, 444 | ) 445 | 446 | # Multi-level bulleted list 447 | output = re.sub( 448 | r"^(\s*)- (.*)$", 449 | lambda match: ( 450 | "* " + match.group(2) 451 | if not match.group(1) 452 | else " " * (len(match.group(1)) // 2) + "* " + match.group(2) 453 | ), 454 | output, 455 | flags=re.MULTILINE, 456 | ) 457 | 458 | # Multi-level numbered list 459 | output = re.sub( 460 | r"^(\s+)1\. (.*)$", 461 | lambda match: "#" * (int(len(match.group(1)) / 4) + 2) + " " + match.group(2), 462 | output, 463 | flags=re.MULTILINE, 464 | ) 465 | 466 | # HTML formatting tags to Jira markup 467 | tag_map = {"cite": "??", "del": "-", "ins": "+", "sup": "^", "sub": "~"} 468 | 469 | for tag, replacement in tag_map.items(): 470 | output = re.sub(rf"<{tag}>(.*?)<\/{tag}>", rf"{replacement}\1{replacement}", output) 471 | 472 | # Colored text 473 | output = re.sub( 474 | r"([\s\S]*?)", 475 | r"{color:\1}\2{color}", 476 | output, 477 | flags=re.MULTILINE, 478 | ) 479 | 480 | # Strikethrough 481 | output = re.sub(r"~~(.*?)~~", r"-\1-", output) 482 | 483 | # Images without alt text 484 | output = re.sub(r"!\[\]\(([^)\n\s]+)\)", r"!\1!", output) 485 | 486 | # Images with alt text 487 | output = re.sub(r"!\[([^\]\n]+)\]\(([^)\n\s]+)\)", r"!\2|alt=\1!", output) 488 | 489 | # Links 490 | output = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"[\1|\2]", output) 491 | output = re.sub(r"<([^>]+)>", r"[\1]", output) 492 | 493 | # Convert markdown tables to Jira table format 494 | lines = output.split("\n") 495 | i = 0 496 | while i < len(lines): 497 | if i < len(lines) - 1 and re.match(r"\|[-\s|]+\|", lines[i + 1]): 498 | # Convert header row to Jira format 499 | lines[i] = lines[i].replace("|", "||") 500 | # Remove the separator line 501 | lines.pop(i + 1) 502 | i += 1 503 | 504 | # Rejoin the lines 505 | output = "\n".join(lines) 506 | 507 | return output 508 | 509 | def _convert_jira_list_to_markdown(self, match: re.Match) -> str: 510 | """ 511 | Helper method to convert Jira lists to Markdown format. 512 | 513 | Args: 514 | match: Regex match object containing the Jira list markup 515 | 516 | Returns: 517 | Markdown-formatted list item 518 | """ 519 | jira_bullets = match.group(1) 520 | content = match.group(2) 521 | 522 | # Calculate indentation level based on number of symbols 523 | indent_level = len(jira_bullets) - 1 524 | indent = " " * (indent_level * 2) 525 | 526 | # Determine the marker based on the last character 527 | last_char = jira_bullets[-1] 528 | prefix = "1." if last_char == "#" else "-" 529 | 530 | return f"{indent}{prefix} {content}" 531 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.13" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.9.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "idna" }, 19 | { name = "sniffio" }, 20 | ] 21 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, 24 | ] 25 | 26 | [[package]] 27 | name = "asttokens" 28 | version = "3.0.0" 29 | source = { registry = "https://pypi.org/simple" } 30 | sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, 33 | ] 34 | 35 | [[package]] 36 | name = "beautifulsoup4" 37 | version = "4.13.3" 38 | source = { registry = "https://pypi.org/simple" } 39 | dependencies = [ 40 | { name = "soupsieve" }, 41 | { name = "typing-extensions" }, 42 | ] 43 | sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } 44 | wheels = [ 45 | { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, 46 | ] 47 | 48 | [[package]] 49 | name = "certifi" 50 | version = "2025.1.31" 51 | source = { registry = "https://pypi.org/simple" } 52 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 53 | wheels = [ 54 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 55 | ] 56 | 57 | [[package]] 58 | name = "charset-normalizer" 59 | version = "3.4.1" 60 | source = { registry = "https://pypi.org/simple" } 61 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 62 | wheels = [ 63 | { 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 }, 64 | { 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 }, 65 | { 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 }, 66 | { 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 }, 67 | { 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 }, 68 | { 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 }, 69 | { 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 }, 70 | { 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 }, 71 | { 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 }, 72 | { 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 }, 73 | { 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 }, 74 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 75 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 76 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 77 | ] 78 | 79 | [[package]] 80 | name = "click" 81 | version = "8.1.8" 82 | source = { registry = "https://pypi.org/simple" } 83 | dependencies = [ 84 | { name = "colorama", marker = "platform_system == 'Windows'" }, 85 | ] 86 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 87 | wheels = [ 88 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 89 | ] 90 | 91 | [[package]] 92 | name = "colorama" 93 | version = "0.4.6" 94 | source = { registry = "https://pypi.org/simple" } 95 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 96 | wheels = [ 97 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 98 | ] 99 | 100 | [[package]] 101 | name = "decorator" 102 | version = "5.2.1" 103 | source = { registry = "https://pypi.org/simple" } 104 | sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } 105 | wheels = [ 106 | { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, 107 | ] 108 | 109 | [[package]] 110 | name = "defusedxml" 111 | version = "0.7.1" 112 | source = { registry = "https://pypi.org/simple" } 113 | sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } 114 | wheels = [ 115 | { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, 116 | ] 117 | 118 | [[package]] 119 | name = "exceptiongroup" 120 | version = "1.3.0" 121 | source = { registry = "https://pypi.org/simple" } 122 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } 123 | wheels = [ 124 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, 125 | ] 126 | 127 | [[package]] 128 | name = "executing" 129 | version = "2.2.0" 130 | source = { registry = "https://pypi.org/simple" } 131 | sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } 132 | wheels = [ 133 | { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, 134 | ] 135 | 136 | [[package]] 137 | name = "fastmcp" 138 | version = "2.3.4" 139 | source = { registry = "https://pypi.org/simple" } 140 | dependencies = [ 141 | { name = "exceptiongroup" }, 142 | { name = "httpx" }, 143 | { name = "mcp" }, 144 | { name = "openapi-pydantic" }, 145 | { name = "python-dotenv" }, 146 | { name = "rich" }, 147 | { name = "typer" }, 148 | { name = "websockets" }, 149 | ] 150 | sdist = { url = "https://files.pythonhosted.org/packages/75/d9/cc3eb61c59fec834a9492ea21df134381b4be76c35faa18cd2b0249278b8/fastmcp-2.3.4.tar.gz", hash = "sha256:f3fe004b8735b365a65ec2547eeb47db8352d5613697254854bc7c9c3c360eea", size = 998315 } 151 | wheels = [ 152 | { url = "https://files.pythonhosted.org/packages/a0/e6/310d1fe6708b7338e1f48915a13d8bf00fd0599acdc7bf98da4fd20fcb66/fastmcp-2.3.4-py3-none-any.whl", hash = "sha256:12a45f72dd95aeaa1a6a56281fff96ca46929def3ccd9f9eb125cb97b722fbab", size = 96393 }, 153 | ] 154 | 155 | [[package]] 156 | name = "h11" 157 | version = "0.14.0" 158 | source = { registry = "https://pypi.org/simple" } 159 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 160 | wheels = [ 161 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 162 | ] 163 | 164 | [[package]] 165 | name = "httpcore" 166 | version = "1.0.8" 167 | source = { registry = "https://pypi.org/simple" } 168 | dependencies = [ 169 | { name = "certifi" }, 170 | { name = "h11" }, 171 | ] 172 | sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 } 173 | wheels = [ 174 | { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, 175 | ] 176 | 177 | [[package]] 178 | name = "httpx" 179 | version = "0.28.1" 180 | source = { registry = "https://pypi.org/simple" } 181 | dependencies = [ 182 | { name = "anyio" }, 183 | { name = "certifi" }, 184 | { name = "httpcore" }, 185 | { name = "idna" }, 186 | ] 187 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 188 | wheels = [ 189 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 190 | ] 191 | 192 | [[package]] 193 | name = "httpx-sse" 194 | version = "0.4.0" 195 | source = { registry = "https://pypi.org/simple" } 196 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 197 | wheels = [ 198 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 199 | ] 200 | 201 | [[package]] 202 | name = "idna" 203 | version = "3.10" 204 | source = { registry = "https://pypi.org/simple" } 205 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 206 | wheels = [ 207 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 208 | ] 209 | 210 | [[package]] 211 | name = "ipython" 212 | version = "9.1.0" 213 | source = { registry = "https://pypi.org/simple" } 214 | dependencies = [ 215 | { name = "colorama", marker = "sys_platform == 'win32'" }, 216 | { name = "decorator" }, 217 | { name = "ipython-pygments-lexers" }, 218 | { name = "jedi" }, 219 | { name = "matplotlib-inline" }, 220 | { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, 221 | { name = "prompt-toolkit" }, 222 | { name = "pygments" }, 223 | { name = "stack-data" }, 224 | { name = "traitlets" }, 225 | ] 226 | sdist = { url = "https://files.pythonhosted.org/packages/70/9a/6b8984bedc990f3a4aa40ba8436dea27e23d26a64527de7c2e5e12e76841/ipython-9.1.0.tar.gz", hash = "sha256:a47e13a5e05e02f3b8e1e7a0f9db372199fe8c3763532fe7a1e0379e4e135f16", size = 4373688 } 227 | wheels = [ 228 | { url = "https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl", hash = "sha256:2df07257ec2f84a6b346b8d83100bcf8fa501c6e01ab75cd3799b0bb253b3d2a", size = 604053 }, 229 | ] 230 | 231 | [[package]] 232 | name = "ipython-pygments-lexers" 233 | version = "1.1.1" 234 | source = { registry = "https://pypi.org/simple" } 235 | dependencies = [ 236 | { name = "pygments" }, 237 | ] 238 | sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } 239 | wheels = [ 240 | { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, 241 | ] 242 | 243 | [[package]] 244 | name = "jedi" 245 | version = "0.19.2" 246 | source = { registry = "https://pypi.org/simple" } 247 | dependencies = [ 248 | { name = "parso" }, 249 | ] 250 | sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } 251 | wheels = [ 252 | { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, 253 | ] 254 | 255 | [[package]] 256 | name = "jira" 257 | version = "3.8.0" 258 | source = { registry = "https://pypi.org/simple" } 259 | dependencies = [ 260 | { name = "defusedxml" }, 261 | { name = "packaging" }, 262 | { name = "pillow" }, 263 | { name = "requests" }, 264 | { name = "requests-oauthlib" }, 265 | { name = "requests-toolbelt" }, 266 | { name = "typing-extensions" }, 267 | ] 268 | sdist = { url = "https://files.pythonhosted.org/packages/78/b4/557e4c80c0ea12164ffeec0e29372c085bfb263faad53cef5e1455523bec/jira-3.8.0.tar.gz", hash = "sha256:63719c529a570aaa01c3373dbb5a104dab70381c5be447f6c27f997302fa335a", size = 102927 } 269 | wheels = [ 270 | { url = "https://files.pythonhosted.org/packages/4f/52/bb617020064261ba31cc965e932943458b7facfd9691ad7f76a2b631f44f/jira-3.8.0-py3-none-any.whl", hash = "sha256:12190dc84dad00b8a6c0341f7e8a254b0f38785afdec022bd5941e1184a5a3fb", size = 77505 }, 271 | ] 272 | 273 | [[package]] 274 | name = "jira-prompts-mcp-server" 275 | version = "0.1.0" 276 | source = { editable = "." } 277 | dependencies = [ 278 | { name = "beautifulsoup4" }, 279 | { name = "fastmcp" }, 280 | { name = "jira" }, 281 | { name = "markdownify" }, 282 | { name = "pydantic" }, 283 | { name = "typer" }, 284 | ] 285 | 286 | [package.dev-dependencies] 287 | dev = [ 288 | { name = "ipython" }, 289 | ] 290 | 291 | [package.metadata] 292 | requires-dist = [ 293 | { name = "beautifulsoup4", specifier = ">=4.13.3" }, 294 | { name = "fastmcp", specifier = ">=2.3.4" }, 295 | { name = "jira", specifier = ">=3.8.0" }, 296 | { name = "markdownify", specifier = ">=1.1.0" }, 297 | { name = "pydantic", specifier = ">=2.11.3" }, 298 | { name = "typer", specifier = ">=0.15.2" }, 299 | ] 300 | 301 | [package.metadata.requires-dev] 302 | dev = [{ name = "ipython", specifier = ">=9.1.0" }] 303 | 304 | [[package]] 305 | name = "markdown-it-py" 306 | version = "3.0.0" 307 | source = { registry = "https://pypi.org/simple" } 308 | dependencies = [ 309 | { name = "mdurl" }, 310 | ] 311 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 312 | wheels = [ 313 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 314 | ] 315 | 316 | [[package]] 317 | name = "markdownify" 318 | version = "1.1.0" 319 | source = { registry = "https://pypi.org/simple" } 320 | dependencies = [ 321 | { name = "beautifulsoup4" }, 322 | { name = "six" }, 323 | ] 324 | sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127 } 325 | wheels = [ 326 | { url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901 }, 327 | ] 328 | 329 | [[package]] 330 | name = "matplotlib-inline" 331 | version = "0.1.7" 332 | source = { registry = "https://pypi.org/simple" } 333 | dependencies = [ 334 | { name = "traitlets" }, 335 | ] 336 | sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } 337 | wheels = [ 338 | { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, 339 | ] 340 | 341 | [[package]] 342 | name = "mcp" 343 | version = "1.9.0" 344 | source = { registry = "https://pypi.org/simple" } 345 | dependencies = [ 346 | { name = "anyio" }, 347 | { name = "httpx" }, 348 | { name = "httpx-sse" }, 349 | { name = "pydantic" }, 350 | { name = "pydantic-settings" }, 351 | { name = "python-multipart" }, 352 | { name = "sse-starlette" }, 353 | { name = "starlette" }, 354 | { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, 355 | ] 356 | sdist = { url = "https://files.pythonhosted.org/packages/bc/8d/0f4468582e9e97b0a24604b585c651dfd2144300ecffd1c06a680f5c8861/mcp-1.9.0.tar.gz", hash = "sha256:905d8d208baf7e3e71d70c82803b89112e321581bcd2530f9de0fe4103d28749", size = 281432 } 357 | wheels = [ 358 | { url = "https://files.pythonhosted.org/packages/a5/d5/22e36c95c83c80eb47c83f231095419cf57cf5cca5416f1c960032074c78/mcp-1.9.0-py3-none-any.whl", hash = "sha256:9dfb89c8c56f742da10a5910a1f64b0d2ac2c3ed2bd572ddb1cfab7f35957178", size = 125082 }, 359 | ] 360 | 361 | [[package]] 362 | name = "mdurl" 363 | version = "0.1.2" 364 | source = { registry = "https://pypi.org/simple" } 365 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 366 | wheels = [ 367 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 368 | ] 369 | 370 | [[package]] 371 | name = "oauthlib" 372 | version = "3.2.2" 373 | source = { registry = "https://pypi.org/simple" } 374 | sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } 375 | wheels = [ 376 | { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, 377 | ] 378 | 379 | [[package]] 380 | name = "openapi-pydantic" 381 | version = "0.5.1" 382 | source = { registry = "https://pypi.org/simple" } 383 | dependencies = [ 384 | { name = "pydantic" }, 385 | ] 386 | sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 } 387 | wheels = [ 388 | { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 }, 389 | ] 390 | 391 | [[package]] 392 | name = "packaging" 393 | version = "24.2" 394 | source = { registry = "https://pypi.org/simple" } 395 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 398 | ] 399 | 400 | [[package]] 401 | name = "parso" 402 | version = "0.8.4" 403 | source = { registry = "https://pypi.org/simple" } 404 | sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } 405 | wheels = [ 406 | { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, 407 | ] 408 | 409 | [[package]] 410 | name = "pexpect" 411 | version = "4.9.0" 412 | source = { registry = "https://pypi.org/simple" } 413 | dependencies = [ 414 | { name = "ptyprocess" }, 415 | ] 416 | sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } 417 | wheels = [ 418 | { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, 419 | ] 420 | 421 | [[package]] 422 | name = "pillow" 423 | version = "11.2.1" 424 | source = { registry = "https://pypi.org/simple" } 425 | sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } 426 | wheels = [ 427 | { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, 428 | { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, 429 | { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, 430 | { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, 431 | { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, 432 | { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, 433 | { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, 434 | { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, 435 | { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, 436 | { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, 437 | { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, 438 | { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, 439 | { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, 440 | { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, 441 | { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, 442 | { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, 443 | { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, 444 | { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, 445 | { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, 446 | { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, 447 | { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, 448 | { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, 449 | ] 450 | 451 | [[package]] 452 | name = "prompt-toolkit" 453 | version = "3.0.50" 454 | source = { registry = "https://pypi.org/simple" } 455 | dependencies = [ 456 | { name = "wcwidth" }, 457 | ] 458 | sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } 459 | wheels = [ 460 | { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, 461 | ] 462 | 463 | [[package]] 464 | name = "ptyprocess" 465 | version = "0.7.0" 466 | source = { registry = "https://pypi.org/simple" } 467 | sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } 468 | wheels = [ 469 | { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, 470 | ] 471 | 472 | [[package]] 473 | name = "pure-eval" 474 | version = "0.2.3" 475 | source = { registry = "https://pypi.org/simple" } 476 | sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } 477 | wheels = [ 478 | { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, 479 | ] 480 | 481 | [[package]] 482 | name = "pydantic" 483 | version = "2.11.3" 484 | source = { registry = "https://pypi.org/simple" } 485 | dependencies = [ 486 | { name = "annotated-types" }, 487 | { name = "pydantic-core" }, 488 | { name = "typing-extensions" }, 489 | { name = "typing-inspection" }, 490 | ] 491 | sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } 492 | wheels = [ 493 | { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, 494 | ] 495 | 496 | [[package]] 497 | name = "pydantic-core" 498 | version = "2.33.1" 499 | source = { registry = "https://pypi.org/simple" } 500 | dependencies = [ 501 | { name = "typing-extensions" }, 502 | ] 503 | sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } 504 | wheels = [ 505 | { 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 }, 506 | { 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 }, 507 | { 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 }, 508 | { 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 }, 509 | { 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 }, 510 | { 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 }, 511 | { 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 }, 512 | { 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 }, 513 | { 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 }, 514 | { 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 }, 515 | { 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 }, 516 | { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, 517 | { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, 518 | { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, 519 | { 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 }, 520 | { 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 }, 521 | { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, 522 | ] 523 | 524 | [[package]] 525 | name = "pydantic-settings" 526 | version = "2.8.1" 527 | source = { registry = "https://pypi.org/simple" } 528 | dependencies = [ 529 | { name = "pydantic" }, 530 | { name = "python-dotenv" }, 531 | ] 532 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 533 | wheels = [ 534 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 535 | ] 536 | 537 | [[package]] 538 | name = "pygments" 539 | version = "2.19.1" 540 | source = { registry = "https://pypi.org/simple" } 541 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 542 | wheels = [ 543 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 544 | ] 545 | 546 | [[package]] 547 | name = "python-dotenv" 548 | version = "1.1.0" 549 | source = { registry = "https://pypi.org/simple" } 550 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } 551 | wheels = [ 552 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, 553 | ] 554 | 555 | [[package]] 556 | name = "python-multipart" 557 | version = "0.0.20" 558 | source = { registry = "https://pypi.org/simple" } 559 | sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } 560 | wheels = [ 561 | { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, 562 | ] 563 | 564 | [[package]] 565 | name = "requests" 566 | version = "2.32.3" 567 | source = { registry = "https://pypi.org/simple" } 568 | dependencies = [ 569 | { name = "certifi" }, 570 | { name = "charset-normalizer" }, 571 | { name = "idna" }, 572 | { name = "urllib3" }, 573 | ] 574 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 575 | wheels = [ 576 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 577 | ] 578 | 579 | [[package]] 580 | name = "requests-oauthlib" 581 | version = "2.0.0" 582 | source = { registry = "https://pypi.org/simple" } 583 | dependencies = [ 584 | { name = "oauthlib" }, 585 | { name = "requests" }, 586 | ] 587 | sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } 588 | wheels = [ 589 | { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, 590 | ] 591 | 592 | [[package]] 593 | name = "requests-toolbelt" 594 | version = "1.0.0" 595 | source = { registry = "https://pypi.org/simple" } 596 | dependencies = [ 597 | { name = "requests" }, 598 | ] 599 | sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } 600 | wheels = [ 601 | { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, 602 | ] 603 | 604 | [[package]] 605 | name = "rich" 606 | version = "14.0.0" 607 | source = { registry = "https://pypi.org/simple" } 608 | dependencies = [ 609 | { name = "markdown-it-py" }, 610 | { name = "pygments" }, 611 | ] 612 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } 613 | wheels = [ 614 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, 615 | ] 616 | 617 | [[package]] 618 | name = "shellingham" 619 | version = "1.5.4" 620 | source = { registry = "https://pypi.org/simple" } 621 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 622 | wheels = [ 623 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 624 | ] 625 | 626 | [[package]] 627 | name = "six" 628 | version = "1.17.0" 629 | source = { registry = "https://pypi.org/simple" } 630 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } 631 | wheels = [ 632 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, 633 | ] 634 | 635 | [[package]] 636 | name = "sniffio" 637 | version = "1.3.1" 638 | source = { registry = "https://pypi.org/simple" } 639 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 640 | wheels = [ 641 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 642 | ] 643 | 644 | [[package]] 645 | name = "soupsieve" 646 | version = "2.6" 647 | source = { registry = "https://pypi.org/simple" } 648 | sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } 649 | wheels = [ 650 | { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, 651 | ] 652 | 653 | [[package]] 654 | name = "sse-starlette" 655 | version = "2.2.1" 656 | source = { registry = "https://pypi.org/simple" } 657 | dependencies = [ 658 | { name = "anyio" }, 659 | { name = "starlette" }, 660 | ] 661 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 662 | wheels = [ 663 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 664 | ] 665 | 666 | [[package]] 667 | name = "stack-data" 668 | version = "0.6.3" 669 | source = { registry = "https://pypi.org/simple" } 670 | dependencies = [ 671 | { name = "asttokens" }, 672 | { name = "executing" }, 673 | { name = "pure-eval" }, 674 | ] 675 | sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } 676 | wheels = [ 677 | { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, 678 | ] 679 | 680 | [[package]] 681 | name = "starlette" 682 | version = "0.46.1" 683 | source = { registry = "https://pypi.org/simple" } 684 | dependencies = [ 685 | { name = "anyio" }, 686 | ] 687 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } 688 | wheels = [ 689 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, 690 | ] 691 | 692 | [[package]] 693 | name = "traitlets" 694 | version = "5.14.3" 695 | source = { registry = "https://pypi.org/simple" } 696 | sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } 697 | wheels = [ 698 | { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, 699 | ] 700 | 701 | [[package]] 702 | name = "typer" 703 | version = "0.15.2" 704 | source = { registry = "https://pypi.org/simple" } 705 | dependencies = [ 706 | { name = "click" }, 707 | { name = "rich" }, 708 | { name = "shellingham" }, 709 | { name = "typing-extensions" }, 710 | ] 711 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } 712 | wheels = [ 713 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, 714 | ] 715 | 716 | [[package]] 717 | name = "typing-extensions" 718 | version = "4.13.2" 719 | source = { registry = "https://pypi.org/simple" } 720 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } 721 | wheels = [ 722 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, 723 | ] 724 | 725 | [[package]] 726 | name = "typing-inspection" 727 | version = "0.4.0" 728 | source = { registry = "https://pypi.org/simple" } 729 | dependencies = [ 730 | { name = "typing-extensions" }, 731 | ] 732 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } 733 | wheels = [ 734 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, 735 | ] 736 | 737 | [[package]] 738 | name = "urllib3" 739 | version = "2.4.0" 740 | source = { registry = "https://pypi.org/simple" } 741 | sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } 742 | wheels = [ 743 | { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, 744 | ] 745 | 746 | [[package]] 747 | name = "uvicorn" 748 | version = "0.34.0" 749 | source = { registry = "https://pypi.org/simple" } 750 | dependencies = [ 751 | { name = "click" }, 752 | { name = "h11" }, 753 | ] 754 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 755 | wheels = [ 756 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 757 | ] 758 | 759 | [[package]] 760 | name = "wcwidth" 761 | version = "0.2.13" 762 | source = { registry = "https://pypi.org/simple" } 763 | sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } 764 | wheels = [ 765 | { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, 766 | ] 767 | 768 | [[package]] 769 | name = "websockets" 770 | version = "15.0.1" 771 | source = { registry = "https://pypi.org/simple" } 772 | sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } 773 | wheels = [ 774 | { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, 775 | { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, 776 | { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, 777 | { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, 778 | { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, 779 | { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, 780 | { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, 781 | { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, 782 | { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, 783 | { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, 784 | { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, 785 | { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, 786 | ] 787 | --------------------------------------------------------------------------------