diff --git a/.github/workflows/build-exe.yml b/.github/workflows/build-exe.yml deleted file mode 100644 index 67f414d..0000000 --- a/.github/workflows/build-exe.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Build Executable - -on: - workflow_run: - workflows: ["Python Package"] - types: - - completed - branches: - - "main" - -jobs: - build: - # Only run if the python-publish workflow succeeded - if: ${{ github.event.workflow_run.conclusion == 'success' }} - strategy: - fail-fast: false - matrix: - target: - - os: windows-2022 - triple: x86_64-pc-windows-msvc - - os: ubuntu-22.04 - triple: x86_64-unknown-linux-gnu - - os: macos-12 - triple: x86_64-apple-darwin - runs-on: ${{ matrix.target.os }} - permissions: - contents: write - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: olegtarasov/get-tag@v2.1.4 - id: get_tag_name - with: - tagRegex: "v(?.*)" - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - cache: pip - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install nox - - - name: Build exe - run: | - nox -s build-exe -- --release --version ${{ steps.get_tag_name.outputs.version }} - - - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - updateOnlyUnreleased: false - omitBody: true - artifacts: ".zip/*.zip" - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} diff --git a/.github/workflows/bumpversion.yml b/.github/workflows/bumpversion.yml deleted file mode 100644 index 9aa25f4..0000000 --- a/.github/workflows/bumpversion.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Bump version - -on: - push: - branches: - - main - -jobs: - bump-version: - if: "!startsWith(github.event.head_commit.message, 'bump:')" - runs-on: ubuntu-latest - name: "Bump version and create changelog with commitizen" - steps: - - name: Check out - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - token: '${{ secrets.PERSONAL_ACCESS_TOKEN }}' - - name: Create bump and changelog - uses: commitizen-tools/commitizen-action@master - with: - github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - branch: main diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml deleted file mode 100644 index 6103542..0000000 --- a/.github/workflows/codecov.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Codecov -on: [push, pull_request] -jobs: - run: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Get repository name - id: repo-name - uses: MariachiBear/get-repo-name-action@v1.3.0 - with: - with-owner: 'true' - string-case: 'uppercase' - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install -r requirements-dev.txt - poetry --version - - name: Run tests and collect coverage - run: | - nox -s pytest - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 - with: - slug: loonghao/${{ steps.repo-name.outputs.repository-name }} - files: 'coverage.xml' - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/issue-translator.yml b/.github/workflows/issue-translator.yml deleted file mode 100644 index 83c127b..0000000 --- a/.github/workflows/issue-translator.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: 'issue-translator' -on: - issue_comment: - types: [created] - issues: - types: [opened] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: usthe/issues-translate-action@v2.7 - with: - IS_MODIFY_TITLE: false - # not require, default false, . Decide whether to modify the issue title - # if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot. - CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. πŸ‘―πŸ‘­πŸ»πŸ§‘β€πŸ€β€πŸ§‘πŸ‘«πŸ§‘πŸΏβ€πŸ€β€πŸ§‘πŸ»πŸ‘©πŸΎβ€πŸ€β€πŸ‘¨πŸΏπŸ‘¬πŸΏ - # not require. Customize the translation robot prefix message. diff --git a/.github/workflows/mr-test.yml b/.github/workflows/mr-test.yml deleted file mode 100644 index 9d86cf0..0000000 --- a/.github/workflows/mr-test.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: MR Checks -on: [ pull_request ] - -jobs: - python-check: - strategy: - max-parallel: 3 - matrix: - target: - - os: 'ubuntu-22.04' - triple: 'x86_64-unknown-linux-gnu' - - os: 'macos-12' - triple: 'x86_64-apple-darwin' - - os: 'windows-2022' - triple: 'x86_64-pc-windows-msvc' - python-version: ["3.10"] - fail-fast: false - runs-on: ${{ matrix.target.os }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install -r requirements-dev.txt - poetry --version - - name: lint - run: | - nox -s lint - - name: test build - run: | - nox -s build-exe -- --test diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 09e3b26..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Upload Python Package - -on: - push: - tags: - - "v*" - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - # IMPORTANT: this permission is mandatory for trusted publishing - id-token: write - contents: write - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - token: "${{ secrets.GITHUB_TOKEN }}" - fetch-depth: 0 - ref: main - - uses: olegtarasov/get-tag@v2.1.4 - id: get_tag_name - with: - tagRegex: "v(?.*)" - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install -r requirements-dev.txt - poetry --version - poetry build - # Note that we don't need credentials. - # We rely on https://docs.pypi.org/trusted-publishers/. - - name: Upload to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist - - name: Generate changelog - id: changelog - uses: jaywcjlove/changelog-generator@main - with: - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - filter-author: (|dependabot|renovate\\[bot\\]|dependabot\\[bot\\]|Renovate Bot) - filter: '[R|r]elease[d]\s+[v|V]\d(\.\d+){0,2}' - template: | - ## Bugs - {{fix}} - ## Feature - {{feat}} - ## Improve - {{refactor,perf,clean}} - ## Misc - {{chore,style,ci||πŸ”Ά Nothing change}} - ## Unknown - {{__unknown__}} - - uses: ncipollo/release-action@v1 - with: - artifacts: "dist/*" - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - body: | - Comparing Changes: ${{ steps.changelog.outputs.compareurl }} - - ${{ steps.changelog.outputs.changelog }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..602e639 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,102 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +jobs: + test: + uses: ./.github/workflows/test.yml + + build: + needs: test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + run: | + python -m pip install --upgrade pip + pip install uv + + - name: Install dependencies + run: | + uvx poetry install + + - name: Build package + run: | + uvx poetry build + + - name: Check package + run: | + uvx poetry run twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + + github-release: + needs: publish + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate changelog + id: changelog + run: | + # Extract version from tag + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Generate changelog (basic implementation) + echo "## Changes in v$VERSION" > CHANGELOG.md + echo "" >> CHANGELOG.md + git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 HEAD^)..HEAD >> CHANGELOG.md || echo "- Initial release" >> CHANGELOG.md + + - name: Create GitHub Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release v${{ steps.changelog.outputs.version }} + body_path: CHANGELOG.md + draft: false + prerelease: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fda3a2a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,104 @@ +name: Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + strategy: + max-parallel: 6 + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12"] + fail-fast: false + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: '**/pyproject.toml' + + # Cache Poetry dependencies + - name: Cache Poetry dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pypoetry + key: ${{ runner.os }}-poetry-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry-${{ matrix.python-version }}- + + # Cache nox environments + - name: Cache nox environments + uses: actions/cache@v4 + with: + path: .nox + key: ${{ runner.os }}-nox-${{ matrix.python-version }}-${{ hashFiles('**/noxfile.py') }} + restore-keys: | + ${{ runner.os }}-nox-${{ matrix.python-version }}- + + - name: Install uv + run: | + python -m pip install --upgrade pip + pip install uv + uv --version + + - name: Install dependencies + run: | + uvx poetry install + + - name: Lint with ruff + run: | + uvx nox -s lint + + - name: Type check with mypy + run: | + uvx nox -s mypy + + - name: Run tests + run: | + uvx nox -s pytest + + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + security: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + run: | + python -m pip install --upgrade pip + pip install uv + + - name: Install dependencies + run: | + uvx poetry install + + - name: Run security checks + run: | + uvx nox -s safety diff --git a/.gitignore b/.gitignore index fbedea2..599c4b4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ target/* /build/ /coverage.xml /.zip/ +.env + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b049e1e..dd0e332 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.5.0 hooks: - id: no-commit-to-branch # prevent direct commits to main branch - id: check-yaml @@ -10,3 +10,26 @@ repos: - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace + - id: check-added-large-files + - id: check-merge-conflict + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + additional_dependencies: [types-requests] + args: [--ignore-missing-imports] diff --git a/README.md b/README.md index feb16a7..b00aea9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ -# repo-template -This is a boilerplate git repository for creating new project +# PyPI Query MCP Server + +A Model Context Protocol (MCP) server for querying PyPI package information, dependencies, and compatibility checking. + +## Features + +- πŸ“¦ Query PyPI package information (name, version, description, dependencies) +- 🐍 Python version compatibility checking +- πŸ” Dependency analysis and resolution +- 🏒 Private PyPI repository support +- ⚑ Fast async operations with caching +- πŸ› οΈ Easy integration with MCP clients + +## Quick Start + +### Installation + +```bash +# Install from PyPI (coming soon) +pip install pypi-query-mcp-server + +# Or install from source +git clone https://github.com/loonghao/pypi-query-mcp-server.git +cd pypi-query-mcp-server +poetry install +``` + +### Usage + +```bash +# Start the MCP server +pypi-query-mcp + +# Or run directly with Python +python -m pypi_query_mcp.server +``` + +### Available MCP Tools + +The server provides the following MCP tools: + +1. **get_package_info** - Get comprehensive package information +2. **get_package_versions** - List all available versions for a package +3. **get_package_dependencies** - Analyze package dependencies +4. **check_package_python_compatibility** - Check Python version compatibility +5. **get_package_compatible_python_versions** - Get all compatible Python versions + +### Example Usage with MCP Client + +```python +# Example: Check if Django is compatible with Python 3.9 +result = await mcp_client.call_tool("check_package_python_compatibility", { + "package_name": "django", + "target_python_version": "3.9" +}) + +# Example: Get package information +info = await mcp_client.call_tool("get_package_info", { + "package_name": "requests" +}) +``` + +## Development Status + +πŸŽ‰ **Core functionality implemented and ready for use!** + +Current implementation status: +- βœ… Basic project structure +- βœ… PyPI API client with caching +- βœ… MCP tools implementation (package info, versions, dependencies) +- βœ… Python version compatibility checking +- βœ… CI/CD pipeline with multi-platform testing +- ⏳ Private repository support (planned) +- ⏳ Advanced dependency analysis (planned) + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/dist/pypi_query_mcp_server-0.1.0-py3-none-any.whl b/dist/pypi_query_mcp_server-0.1.0-py3-none-any.whl new file mode 100644 index 0000000..379990f Binary files /dev/null and b/dist/pypi_query_mcp_server-0.1.0-py3-none-any.whl differ diff --git a/dist/pypi_query_mcp_server-0.1.0.tar.gz b/dist/pypi_query_mcp_server-0.1.0.tar.gz new file mode 100644 index 0000000..750c5ac Binary files /dev/null and b/dist/pypi_query_mcp_server-0.1.0.tar.gz differ diff --git a/nox_actions/codetest.py b/nox_actions/codetest.py index da570f3..cf6b048 100644 --- a/nox_actions/codetest.py +++ b/nox_actions/codetest.py @@ -8,10 +8,26 @@ from nox_actions.utils import THIS_ROOT def pytest(session: nox.Session) -> None: + """Run pytest with coverage reporting.""" session.install(".") - session.install("pytest", "pytest_cov", "pytest_mock") + session.install("pytest", "pytest-cov", "pytest-mock", "pytest-asyncio") test_root = os.path.join(THIS_ROOT, "tests") session.run("pytest", f"--cov={PACKAGE_NAME}", "--cov-report=xml:coverage.xml", + "--cov-report=term-missing", f"--rootdir={test_root}", env={"PYTHONPATH": THIS_ROOT.as_posix()}) + + +def mypy(session: nox.Session) -> None: + """Run mypy type checking.""" + session.install(".") + session.install("mypy", "types-requests") + session.run("mypy", PACKAGE_NAME, "--ignore-missing-imports") + + +def safety(session: nox.Session) -> None: + """Run safety security checks.""" + session.install(".") + session.install("safety") + session.run("safety", "check", "--json") diff --git a/nox_actions/release.py b/nox_actions/release.py index 8a071a3..b3b4a56 100644 --- a/nox_actions/release.py +++ b/nox_actions/release.py @@ -10,6 +10,13 @@ from nox_actions.utils import PACKAGE_NAME from nox_actions.utils import THIS_ROOT +def build(session: nox.Session) -> None: + """Build Python package distributions.""" + session.install("build", "twine") + session.run("python", "-m", "build") + session.run("twine", "check", "dist/*") + + @nox.session(name="build-exe", reuse_venv=True) def build_exe(session: nox.Session) -> None: parser = argparse.ArgumentParser(prog="nox -s build-exe --release") diff --git a/nox_actions/utils.py b/nox_actions/utils.py index e5d0fe3..076694e 100644 --- a/nox_actions/utils.py +++ b/nox_actions/utils.py @@ -2,7 +2,7 @@ from pathlib import Path -PACKAGE_NAME = "" +PACKAGE_NAME = "pypi_query_mcp" THIS_ROOT = Path(__file__).parent.parent PROJECT_ROOT = THIS_ROOT.parent diff --git a/noxfile.py b/noxfile.py index 70752f8..30b1182 100644 --- a/noxfile.py +++ b/noxfile.py @@ -8,7 +8,7 @@ import nox ROOT = os.path.dirname(__file__) -# Ensure maya_umbrella is importable. +# Ensure pypi_query_mcp is importable. if ROOT not in sys.path: sys.path.append(ROOT) @@ -18,7 +18,10 @@ from nox_actions import lint # noqa: E402 from nox_actions import release # noqa: E402 +# Configure nox sessions nox.session(lint.lint, name="lint") nox.session(lint.lint_fix, name="lint-fix") nox.session(codetest.pytest, name="pytest") -nox.session(release.build_exe, name="build-exe") +nox.session(codetest.mypy, name="mypy") +nox.session(codetest.safety, name="safety") +nox.session(release.build, name="build") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..ac58c91 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1201 @@ +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.9.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "argcomplete" +version = "3.6.2" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591"}, + {file = "argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "certifi" +version = "2025.4.26" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "colorlog" +version = "6.9.0" +description = "Add colours to the output of Python's logging module." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff"}, + {file = "colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + +[[package]] +name = "coverage" +version = "7.8.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"}, + {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"}, + {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"}, + {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"}, + {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"}, + {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"}, + {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"}, + {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"}, + {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"}, + {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"}, + {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"}, + {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"}, + {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"}, + {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, + {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, + {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, + {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, + {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, + {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"}, + {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"}, + {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"}, + {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"}, + {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"}, + {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"}, + {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"}, + {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"}, + {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"}, + {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"}, + {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"}, + {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"}, + {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"}, + {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"}, + {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"}, + {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"}, + {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"}, + {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"}, + {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"}, + {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"}, + {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"}, + {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, + {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastmcp" +version = "0.2.0" +description = "A more ergonomic interface for MCP servers" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "fastmcp-0.2.0-py3-none-any.whl", hash = "sha256:ba6c53a6e5d7415992f6c0940fb5626c864ca8c2c310abf0e8f0951142aee3d9"}, + {file = "fastmcp-0.2.0.tar.gz", hash = "sha256:41a7ac4dff38abfb7b695cdddf551908e54ae36f9bd761d49cb1945ccc106f61"}, +] + +[package.dependencies] +httpx = ">=0.26.0" +mcp = ">=1.0.0" +pydantic = ">=2.5.3" +pydantic-settings = ">=2.6.1" +typer = ">=0.9.0" + +[[package]] +name = "filelock" +version = "3.18.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +description = "Consume Server-Sent Event (SSE) messages with HTTPX." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, + {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, +] + +[[package]] +name = "identify" +version = "2.6.12" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}, + {file = "identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mcp" +version = "1.9.1" +description = "Model Context Protocol SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "mcp-1.9.1-py3-none-any.whl", hash = "sha256:2900ded8ffafc3c8a7bfcfe8bc5204037e988e753ec398f371663e6a06ecd9a9"}, + {file = "mcp-1.9.1.tar.gz", hash = "sha256:19879cd6dde3d763297617242888c2f695a95dfa854386a6a68676a646ce75e4"}, +] + +[package.dependencies] +anyio = ">=4.5" +httpx = ">=0.27" +httpx-sse = ">=0.4" +pydantic = ">=2.7.2,<3.0.0" +pydantic-settings = ">=2.5.2" +python-multipart = ">=0.0.9" +sse-starlette = ">=1.6.1" +starlette = ">=0.27" +uvicorn = {version = ">=0.23.1", markers = "sys_platform != \"emscripten\""} + +[package.extras] +cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] +rich = ["rich (>=13.9.4)"] +ws = ["websockets (>=15.0.1)"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mypy" +version = "1.15.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, + {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, + {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, + {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, + {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, + {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, + {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, + {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, + {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, + {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, + {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, + {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, + {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "nox" +version = "2024.10.9" +description = "Flexible test automation." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "nox-2024.10.9-py3-none-any.whl", hash = "sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab"}, + {file = "nox-2024.10.9.tar.gz", hash = "sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95"}, +] + +[package.dependencies] +argcomplete = ">=1.9.4,<4" +colorlog = ">=2.6.1,<7" +packaging = ">=20.9" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} +virtualenv = ">=20.14.1" + +[package.extras] +tox-to-nox = ["jinja2", "tox"] +uv = ["uv (>=0.1.6)"] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pydantic" +version = "2.11.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, + {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"}, + {file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.3.5" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, + {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "rich" +version = "14.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, + {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sse-starlette" +version = "2.3.5" +description = "SSE plugin for Starlette" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8"}, + {file = "sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2"}, +] + +[package.dependencies] +anyio = ">=4.7.0" +starlette = ">=0.41.3" + +[package.extras] +examples = ["fastapi"] +uvicorn = ["uvicorn (>=0.34.0)"] + +[[package]] +name = "starlette" +version = "0.46.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "typer" +version = "0.16.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, + {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "uvicorn" +version = "0.34.2" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "sys_platform != \"emscripten\"" +files = [ + {file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"}, + {file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "virtualenv" +version = "20.31.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, + {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "5c6d9124f3327862ee651809a94d257a32e06a5380b37a2392c69284d432cb72" diff --git a/pypi_query_mcp/__init__.py b/pypi_query_mcp/__init__.py new file mode 100644 index 0000000..0664362 --- /dev/null +++ b/pypi_query_mcp/__init__.py @@ -0,0 +1,13 @@ +"""PyPI Query MCP Server. + +A Model Context Protocol (MCP) server for querying PyPI package information, +dependencies, and compatibility checking. +""" + +__version__ = "0.1.0" +__author__ = "Hal" +__email__ = "hal.long@outlook.com" + +from pypi_query_mcp.server import app + +__all__ = ["app", "__version__"] diff --git a/pypi_query_mcp/config/__init__.py b/pypi_query_mcp/config/__init__.py new file mode 100644 index 0000000..ad213e7 --- /dev/null +++ b/pypi_query_mcp/config/__init__.py @@ -0,0 +1,8 @@ +"""Configuration management for PyPI Query MCP Server. + +This package handles configuration loading, validation, and management +for the MCP server, including private registry settings. +""" + +# Configuration exports will be added as modules are implemented +__all__ = [] diff --git a/pypi_query_mcp/core/__init__.py b/pypi_query_mcp/core/__init__.py new file mode 100644 index 0000000..3a5ee86 --- /dev/null +++ b/pypi_query_mcp/core/__init__.py @@ -0,0 +1,27 @@ +"""Core modules for PyPI Query MCP Server. + +This package contains the core business logic for PyPI package queries, +including API clients, data processing, and utility functions. +""" + +from .exceptions import ( + InvalidPackageNameError, + NetworkError, + PackageNotFoundError, + PyPIError, + PyPIServerError, + RateLimitError, +) +from .pypi_client import PyPIClient +from .version_utils import VersionCompatibility + +__all__ = [ + "PyPIClient", + "VersionCompatibility", + "PyPIError", + "PackageNotFoundError", + "NetworkError", + "RateLimitError", + "InvalidPackageNameError", + "PyPIServerError", +] diff --git a/pypi_query_mcp/core/exceptions.py b/pypi_query_mcp/core/exceptions.py new file mode 100644 index 0000000..6cdc942 --- /dev/null +++ b/pypi_query_mcp/core/exceptions.py @@ -0,0 +1,56 @@ +"""Custom exceptions for PyPI Query MCP Server.""" + + +class PyPIError(Exception): + """Base exception for PyPI-related errors.""" + + def __init__(self, message: str, status_code: int = None): + super().__init__(message) + self.message = message + self.status_code = status_code + + +class PackageNotFoundError(PyPIError): + """Raised when a package is not found on PyPI.""" + + def __init__(self, package_name: str): + message = f"Package '{package_name}' not found on PyPI" + super().__init__(message, status_code=404) + self.package_name = package_name + + +class NetworkError(PyPIError): + """Raised when network-related errors occur.""" + + def __init__(self, message: str, original_error: Exception = None): + super().__init__(message) + self.original_error = original_error + + +class RateLimitError(PyPIError): + """Raised when API rate limit is exceeded.""" + + def __init__(self, retry_after: int = None): + message = "PyPI API rate limit exceeded" + if retry_after: + message += f". Retry after {retry_after} seconds" + super().__init__(message, status_code=429) + self.retry_after = retry_after + + +class InvalidPackageNameError(PyPIError): + """Raised when package name is invalid.""" + + def __init__(self, package_name: str): + message = f"Invalid package name: '{package_name}'" + super().__init__(message, status_code=400) + self.package_name = package_name + + +class PyPIServerError(PyPIError): + """Raised when PyPI server returns a server error.""" + + def __init__(self, status_code: int, message: str = None): + if not message: + message = f"PyPI server error (HTTP {status_code})" + super().__init__(message, status_code=status_code) diff --git a/pypi_query_mcp/core/pypi_client.py b/pypi_query_mcp/core/pypi_client.py new file mode 100644 index 0000000..5e02425 --- /dev/null +++ b/pypi_query_mcp/core/pypi_client.py @@ -0,0 +1,238 @@ +"""PyPI API client for package information retrieval.""" + +import asyncio +import logging +import re +from typing import Any +from urllib.parse import quote + +import httpx + +from .exceptions import ( + InvalidPackageNameError, + NetworkError, + PackageNotFoundError, + PyPIServerError, + RateLimitError, +) + +logger = logging.getLogger(__name__) + + +class PyPIClient: + """Async client for PyPI JSON API.""" + + def __init__( + self, + base_url: str = "https://pypi.org/pypi", + timeout: float = 30.0, + max_retries: int = 3, + retry_delay: float = 1.0, + ): + """Initialize PyPI client. + + Args: + base_url: Base URL for PyPI API + timeout: Request timeout in seconds + max_retries: Maximum number of retry attempts + retry_delay: Delay between retries in seconds + """ + self.base_url = base_url.rstrip("/") + self.timeout = timeout + self.max_retries = max_retries + self.retry_delay = retry_delay + + # Simple in-memory cache + self._cache: dict[str, dict[str, Any]] = {} + self._cache_ttl = 300 # 5 minutes + + # HTTP client configuration + self._client = httpx.AsyncClient( + timeout=httpx.Timeout(timeout), + headers={ + "User-Agent": "pypi-query-mcp-server/0.1.0", + "Accept": "application/json", + }, + follow_redirects=True, + ) + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.close() + + async def close(self): + """Close the HTTP client.""" + await self._client.aclose() + + def _validate_package_name(self, package_name: str) -> str: + """Validate and normalize package name. + + Args: + package_name: Package name to validate + + Returns: + Normalized package name + + Raises: + InvalidPackageNameError: If package name is invalid + """ + if not package_name or not package_name.strip(): + raise InvalidPackageNameError(package_name) + + # Normalize package name (convert to lowercase, replace _ with -) + normalized = re.sub(r"[-_.]+", "-", package_name.lower()) + + # Basic validation - package names should contain only alphanumeric, hyphens, dots, underscores + if not re.match(r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$", package_name): + raise InvalidPackageNameError(package_name) + + return normalized + + def _get_cache_key(self, package_name: str, endpoint: str = "info") -> str: + """Generate cache key for package data.""" + return f"{endpoint}:{package_name}" + + def _is_cache_valid(self, cache_entry: dict[str, Any]) -> bool: + """Check if cache entry is still valid.""" + import time + return time.time() - cache_entry.get("timestamp", 0) < self._cache_ttl + + async def _make_request(self, url: str) -> dict[str, Any]: + """Make HTTP request with retry logic. + + Args: + url: URL to request + + Returns: + JSON response data + + Raises: + NetworkError: For network-related errors + PackageNotFoundError: When package is not found + RateLimitError: When rate limit is exceeded + PyPIServerError: For server errors + """ + last_exception = None + + for attempt in range(self.max_retries + 1): + try: + logger.debug(f"Making request to {url} (attempt {attempt + 1})") + + response = await self._client.get(url) + + # Handle different HTTP status codes + if response.status_code == 200: + return response.json() + elif response.status_code == 404: + # Extract package name from URL for better error message + package_name = url.split("/")[-2] if "/" in url else "unknown" + raise PackageNotFoundError(package_name) + elif response.status_code == 429: + retry_after = response.headers.get("Retry-After") + retry_after_int = int(retry_after) if retry_after else None + raise RateLimitError(retry_after_int) + elif response.status_code >= 500: + raise PyPIServerError(response.status_code) + else: + raise PyPIServerError( + response.status_code, + f"Unexpected status code: {response.status_code}" + ) + + except httpx.TimeoutException as e: + last_exception = NetworkError(f"Request timeout: {e}", e) + except httpx.NetworkError as e: + last_exception = NetworkError(f"Network error: {e}", e) + except (PackageNotFoundError, RateLimitError, PyPIServerError): + # Don't retry these errors + raise + except Exception as e: + last_exception = NetworkError(f"Unexpected error: {e}", e) + + # Wait before retry (except on last attempt) + if attempt < self.max_retries: + await asyncio.sleep(self.retry_delay * (2 ** attempt)) # Exponential backoff + + # If we get here, all retries failed + raise last_exception + + async def get_package_info(self, package_name: str, use_cache: bool = True) -> dict[str, Any]: + """Get comprehensive package information from PyPI. + + Args: + package_name: Name of the package to query + use_cache: Whether to use cached data if available + + Returns: + Dictionary containing package information + + Raises: + InvalidPackageNameError: If package name is invalid + PackageNotFoundError: If package is not found + NetworkError: For network-related errors + """ + normalized_name = self._validate_package_name(package_name) + cache_key = self._get_cache_key(normalized_name, "info") + + # Check cache first + if use_cache and cache_key in self._cache: + cache_entry = self._cache[cache_key] + if self._is_cache_valid(cache_entry): + logger.debug(f"Using cached data for package: {normalized_name}") + return cache_entry["data"] + + # Make API request + url = f"{self.base_url}/{quote(normalized_name)}/json" + logger.info(f"Fetching package info for: {normalized_name}") + + try: + data = await self._make_request(url) + + # Cache the result + import time + self._cache[cache_key] = { + "data": data, + "timestamp": time.time() + } + + return data + + except Exception as e: + logger.error(f"Failed to fetch package info for {normalized_name}: {e}") + raise + + async def get_package_versions(self, package_name: str, use_cache: bool = True) -> list[str]: + """Get list of available versions for a package. + + Args: + package_name: Name of the package to query + use_cache: Whether to use cached data if available + + Returns: + List of version strings + """ + package_info = await self.get_package_info(package_name, use_cache) + releases = package_info.get("releases", {}) + return list(releases.keys()) + + async def get_latest_version(self, package_name: str, use_cache: bool = True) -> str: + """Get the latest version of a package. + + Args: + package_name: Name of the package to query + use_cache: Whether to use cached data if available + + Returns: + Latest version string + """ + package_info = await self.get_package_info(package_name, use_cache) + return package_info.get("info", {}).get("version", "") + + def clear_cache(self): + """Clear the internal cache.""" + self._cache.clear() + logger.debug("Cache cleared") diff --git a/pypi_query_mcp/core/version_utils.py b/pypi_query_mcp/core/version_utils.py new file mode 100644 index 0000000..9e327e4 --- /dev/null +++ b/pypi_query_mcp/core/version_utils.py @@ -0,0 +1,274 @@ +"""Version parsing and compatibility checking utilities.""" + +import logging +import re +from typing import Any + +from packaging.specifiers import SpecifierSet +from packaging.version import InvalidVersion, Version + +logger = logging.getLogger(__name__) + + +class VersionCompatibility: + """Utility class for Python version compatibility checking.""" + + def __init__(self): + """Initialize version compatibility checker.""" + # Common Python version patterns in classifiers + self.python_classifier_pattern = re.compile( + r"Programming Language :: Python :: (\d+(?:\.\d+)*)" + ) + + # Implementation-specific classifiers + self.implementation_pattern = re.compile( + r"Programming Language :: Python :: Implementation :: (\w+)" + ) + + def parse_requires_python(self, requires_python: str) -> SpecifierSet | None: + """Parse requires_python field into a SpecifierSet. + + Args: + requires_python: The requires_python string from package metadata + + Returns: + SpecifierSet object or None if parsing fails + """ + if not requires_python or not requires_python.strip(): + return None + + try: + # Clean up the version specification + cleaned = requires_python.strip() + return SpecifierSet(cleaned) + except Exception as e: + logger.warning(f"Failed to parse requires_python '{requires_python}': {e}") + return None + + def extract_python_versions_from_classifiers(self, classifiers: list[str]) -> set[str]: + """Extract Python version information from classifiers. + + Args: + classifiers: List of classifier strings + + Returns: + Set of Python version strings + """ + versions = set() + + for classifier in classifiers: + match = self.python_classifier_pattern.search(classifier) + if match: + version = match.group(1) + versions.add(version) + + return versions + + def extract_python_implementations(self, classifiers: list[str]) -> set[str]: + """Extract Python implementation information from classifiers. + + Args: + classifiers: List of classifier strings + + Returns: + Set of Python implementation names (CPython, PyPy, etc.) + """ + implementations = set() + + for classifier in classifiers: + match = self.implementation_pattern.search(classifier) + if match: + implementation = match.group(1) + implementations.add(implementation) + + return implementations + + def check_version_compatibility( + self, + target_version: str, + requires_python: str | None = None, + classifiers: list[str] | None = None + ) -> dict[str, Any]: + """Check if a target Python version is compatible with package requirements. + + Args: + target_version: Target Python version (e.g., "3.9", "3.10.5") + requires_python: The requires_python specification + classifiers: List of package classifiers + + Returns: + Dictionary containing compatibility information + """ + result = { + "target_version": target_version, + "is_compatible": False, + "compatibility_source": None, + "details": {}, + "warnings": [], + "suggestions": [] + } + + try: + target_ver = Version(target_version) + except InvalidVersion as e: + result["warnings"].append(f"Invalid target version format: {e}") + return result + + # Check requires_python first (more authoritative) + if requires_python: + spec_set = self.parse_requires_python(requires_python) + if spec_set: + is_compatible = target_ver in spec_set + result.update({ + "is_compatible": is_compatible, + "compatibility_source": "requires_python", + "details": { + "requires_python": requires_python, + "parsed_spec": str(spec_set), + "check_result": is_compatible + } + }) + + if not is_compatible: + result["suggestions"].append( + f"Package requires Python {requires_python}, " + f"but target is {target_version}" + ) + + return result + + # Fall back to classifiers if no requires_python + if classifiers: + supported_versions = self.extract_python_versions_from_classifiers(classifiers) + implementations = self.extract_python_implementations(classifiers) + + if supported_versions: + # Check if target version matches any supported version + target_major_minor = f"{target_ver.major}.{target_ver.minor}" + target_major = str(target_ver.major) + + is_compatible = ( + target_version in supported_versions or + target_major_minor in supported_versions or + target_major in supported_versions + ) + + result.update({ + "is_compatible": is_compatible, + "compatibility_source": "classifiers", + "details": { + "supported_versions": sorted(supported_versions), + "implementations": sorted(implementations), + "target_major_minor": target_major_minor, + "check_result": is_compatible + } + }) + + if not is_compatible: + result["suggestions"].append( + f"Package supports Python versions: {', '.join(sorted(supported_versions))}, " + f"but target is {target_version}" + ) + + return result + + # No version information available + result["warnings"].append( + "No Python version requirements found in package metadata" + ) + result["suggestions"].append( + "Consider checking package documentation for Python version compatibility" + ) + + return result + + def get_compatible_versions( + self, + requires_python: str | None = None, + classifiers: list[str] | None = None, + available_pythons: list[str] | None = None + ) -> dict[str, Any]: + """Get list of compatible Python versions for a package. + + Args: + requires_python: The requires_python specification + classifiers: List of package classifiers + available_pythons: List of Python versions to check against + + Returns: + Dictionary containing compatible versions and recommendations + """ + if available_pythons is None: + # Default Python versions to check + available_pythons = [ + "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" + ] + + compatible = [] + incompatible = [] + + for python_version in available_pythons: + result = self.check_version_compatibility( + python_version, requires_python, classifiers + ) + + if result["is_compatible"]: + compatible.append({ + "version": python_version, + "source": result["compatibility_source"] + }) + else: + incompatible.append({ + "version": python_version, + "reason": result["suggestions"][0] if result["suggestions"] else "Unknown" + }) + + return { + "compatible_versions": compatible, + "incompatible_versions": incompatible, + "total_checked": len(available_pythons), + "compatibility_rate": len(compatible) / len(available_pythons) if available_pythons else 0, + "recommendations": self._generate_recommendations(compatible, incompatible) + } + + def _generate_recommendations( + self, + compatible: list[dict[str, Any]], + incompatible: list[dict[str, Any]] + ) -> list[str]: + """Generate recommendations based on compatibility results. + + Args: + compatible: List of compatible versions + incompatible: List of incompatible versions + + Returns: + List of recommendation strings + """ + recommendations = [] + + if not compatible: + recommendations.append( + "⚠️ No compatible Python versions found. " + "Check package documentation for requirements." + ) + elif len(compatible) == 1: + version = compatible[0]["version"] + recommendations.append( + f"πŸ“Œ Only Python {version} is compatible with this package." + ) + else: + versions = [v["version"] for v in compatible] + latest = max(versions, key=lambda x: tuple(map(int, x.split(".")))) + recommendations.append( + f"βœ… Compatible with Python {', '.join(versions)}. " + f"Recommended: Python {latest}" + ) + + if len(incompatible) > len(compatible): + recommendations.append( + "⚠️ This package has limited Python version support. " + "Consider using a more recent version of the package if available." + ) + + return recommendations diff --git a/pypi_query_mcp/server.py b/pypi_query_mcp/server.py new file mode 100644 index 0000000..7c97a90 --- /dev/null +++ b/pypi_query_mcp/server.py @@ -0,0 +1,295 @@ +"""FastMCP server for PyPI package queries.""" + +import logging +from typing import Any + +import click +from fastmcp import FastMCP + +from .core.exceptions import InvalidPackageNameError, NetworkError, PackageNotFoundError +from .tools import ( + check_python_compatibility, + get_compatible_python_versions, + query_package_dependencies, + query_package_info, + query_package_versions, +) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# Create FastMCP application +app = FastMCP("PyPI Query MCP Server") + + +@app.tool() +async def get_package_info(package_name: str) -> dict[str, Any]: + """Query comprehensive information about a PyPI package. + + This tool retrieves detailed information about a Python package from PyPI, + including metadata, description, author information, dependencies, and more. + + Args: + package_name: The name of the PyPI package to query (e.g., 'requests', 'django') + + Returns: + Dictionary containing comprehensive package information including: + - Basic metadata (name, version, summary, description) + - Author and maintainer information + - License and project URLs + - Python version requirements + - Dependencies and classifiers + - Version history summary + + Raises: + InvalidPackageNameError: If package name is empty or invalid + PackageNotFoundError: If package is not found on PyPI + NetworkError: For network-related errors + """ + try: + logger.info(f"MCP tool: Querying package info for {package_name}") + result = await query_package_info(package_name) + logger.info(f"Successfully retrieved info for package: {package_name}") + return result + except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e: + logger.error(f"Error querying package {package_name}: {e}") + return { + "error": str(e), + "error_type": type(e).__name__, + "package_name": package_name + } + except Exception as e: + logger.error(f"Unexpected error querying package {package_name}: {e}") + return { + "error": f"Unexpected error: {e}", + "error_type": "UnexpectedError", + "package_name": package_name + } + + +@app.tool() +async def get_package_versions(package_name: str) -> dict[str, Any]: + """Get version information for a PyPI package. + + This tool retrieves comprehensive version information for a Python package, + including all available versions, release details, and distribution formats. + + Args: + package_name: The name of the PyPI package to query (e.g., 'requests', 'numpy') + + Returns: + Dictionary containing version information including: + - Latest version and total version count + - List of all available versions (sorted) + - Recent versions with release details + - Distribution format information (wheel, source) + + Raises: + InvalidPackageNameError: If package name is empty or invalid + PackageNotFoundError: If package is not found on PyPI + NetworkError: For network-related errors + """ + try: + logger.info(f"MCP tool: Querying versions for {package_name}") + result = await query_package_versions(package_name) + logger.info(f"Successfully retrieved versions for package: {package_name}") + return result + except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e: + logger.error(f"Error querying versions for {package_name}: {e}") + return { + "error": str(e), + "error_type": type(e).__name__, + "package_name": package_name + } + except Exception as e: + logger.error(f"Unexpected error querying versions for {package_name}: {e}") + return { + "error": f"Unexpected error: {e}", + "error_type": "UnexpectedError", + "package_name": package_name + } + + +@app.tool() +async def get_package_dependencies(package_name: str, version: str | None = None) -> dict[str, Any]: + """Get dependency information for a PyPI package. + + This tool retrieves comprehensive dependency information for a Python package, + including runtime dependencies, development dependencies, and optional dependencies. + + Args: + package_name: The name of the PyPI package to query (e.g., 'django', 'flask') + version: Specific version to query (optional, defaults to latest version) + + Returns: + Dictionary containing dependency information including: + - Runtime dependencies and development dependencies + - Optional dependency groups + - Python version requirements + - Dependency counts and summary statistics + + Raises: + InvalidPackageNameError: If package name is empty or invalid + PackageNotFoundError: If package is not found on PyPI + NetworkError: For network-related errors + """ + try: + logger.info(f"MCP tool: Querying dependencies for {package_name}" + + (f" version {version}" if version else " (latest)")) + result = await query_package_dependencies(package_name, version) + logger.info(f"Successfully retrieved dependencies for package: {package_name}") + return result + except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e: + logger.error(f"Error querying dependencies for {package_name}: {e}") + return { + "error": str(e), + "error_type": type(e).__name__, + "package_name": package_name, + "version": version + } + except Exception as e: + logger.error(f"Unexpected error querying dependencies for {package_name}: {e}") + return { + "error": f"Unexpected error: {e}", + "error_type": "UnexpectedError", + "package_name": package_name, + "version": version + } + + +@app.tool() +async def check_package_python_compatibility( + package_name: str, + target_python_version: str, + use_cache: bool = True +) -> dict[str, Any]: + """Check if a package is compatible with a specific Python version. + + This tool analyzes a package's Python version requirements and determines + if it's compatible with your target Python version. + + Args: + package_name: The name of the PyPI package to check (e.g., 'django', 'requests') + target_python_version: Target Python version to check (e.g., '3.9', '3.10.5', '3.11') + use_cache: Whether to use cached package data (default: True) + + Returns: + Dictionary containing detailed compatibility information including: + - Compatibility status (True/False) + - Source of compatibility information (requires_python or classifiers) + - Detailed analysis and suggestions + - Package version requirements + + Raises: + InvalidPackageNameError: If package name is empty or invalid + PackageNotFoundError: If package is not found on PyPI + NetworkError: For network-related errors + """ + try: + logger.info(f"MCP tool: Checking Python {target_python_version} compatibility for {package_name}") + result = await check_python_compatibility(package_name, target_python_version, use_cache) + logger.info(f"Compatibility check completed for {package_name}") + return result + except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e: + logger.error(f"Error checking compatibility for {package_name}: {e}") + return { + "error": str(e), + "error_type": type(e).__name__, + "package_name": package_name, + "target_python_version": target_python_version + } + except Exception as e: + logger.error(f"Unexpected error checking compatibility for {package_name}: {e}") + return { + "error": f"Unexpected error: {e}", + "error_type": "UnexpectedError", + "package_name": package_name, + "target_python_version": target_python_version + } + + +@app.tool() +async def get_package_compatible_python_versions( + package_name: str, + python_versions: list[str] | None = None, + use_cache: bool = True +) -> dict[str, Any]: + """Get all Python versions compatible with a package. + + This tool analyzes a package and returns which Python versions are + compatible with it, along with recommendations. + + Args: + package_name: The name of the PyPI package to analyze (e.g., 'numpy', 'pandas') + python_versions: List of Python versions to check (optional, defaults to common versions) + use_cache: Whether to use cached package data (default: True) + + Returns: + Dictionary containing compatibility information including: + - List of compatible Python versions + - List of incompatible versions with reasons + - Compatibility rate and recommendations + - Package version requirements + + Raises: + InvalidPackageNameError: If package name is empty or invalid + PackageNotFoundError: If package is not found on PyPI + NetworkError: For network-related errors + """ + try: + logger.info(f"MCP tool: Getting compatible Python versions for {package_name}") + result = await get_compatible_python_versions(package_name, python_versions, use_cache) + logger.info(f"Compatible versions analysis completed for {package_name}") + return result + except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e: + logger.error(f"Error getting compatible versions for {package_name}: {e}") + return { + "error": str(e), + "error_type": type(e).__name__, + "package_name": package_name + } + except Exception as e: + logger.error(f"Unexpected error getting compatible versions for {package_name}: {e}") + return { + "error": f"Unexpected error: {e}", + "error_type": "UnexpectedError", + "package_name": package_name + } + + +@click.command() +@click.option( + "--host", + default="localhost", + help="Host to bind the server to" +) +@click.option( + "--port", + default=8000, + type=int, + help="Port to bind the server to" +) +@click.option( + "--log-level", + default="INFO", + type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"]), + help="Logging level" +) +def main(host: str, port: int, log_level: str) -> None: + """Start the PyPI Query MCP Server.""" + # Set logging level + logging.getLogger().setLevel(getattr(logging, log_level)) + + logger.info(f"Starting PyPI Query MCP Server on {host}:{port}") + logger.info(f"Log level set to: {log_level}") + + # Run the FastMCP server + app.run(host=host, port=port) + + +if __name__ == "__main__": + main() diff --git a/pypi_query_mcp/tools/__init__.py b/pypi_query_mcp/tools/__init__.py new file mode 100644 index 0000000..9dd64d7 --- /dev/null +++ b/pypi_query_mcp/tools/__init__.py @@ -0,0 +1,25 @@ +"""MCP tools for PyPI package queries. + +This package contains the FastMCP tool implementations that provide +the user-facing interface for PyPI package operations. +""" + +from .compatibility_check import ( + check_python_compatibility, + get_compatible_python_versions, + suggest_python_version_for_packages, +) +from .package_query import ( + query_package_dependencies, + query_package_info, + query_package_versions, +) + +__all__ = [ + "query_package_info", + "query_package_versions", + "query_package_dependencies", + "check_python_compatibility", + "get_compatible_python_versions", + "suggest_python_version_for_packages", +] diff --git a/pypi_query_mcp/tools/compatibility_check.py b/pypi_query_mcp/tools/compatibility_check.py new file mode 100644 index 0000000..48f4224 --- /dev/null +++ b/pypi_query_mcp/tools/compatibility_check.py @@ -0,0 +1,260 @@ +"""Python version compatibility checking tools for PyPI MCP server.""" + +import logging +from typing import Any + +from ..core import ( + InvalidPackageNameError, + NetworkError, + PyPIClient, + PyPIError, +) +from ..core.version_utils import VersionCompatibility + +logger = logging.getLogger(__name__) + + +async def check_python_compatibility( + package_name: str, + target_python_version: str, + use_cache: bool = True +) -> dict[str, Any]: + """Check if a package is compatible with a specific Python version. + + Args: + package_name: Name of the package to check + target_python_version: Target Python version (e.g., "3.9", "3.10.5") + use_cache: Whether to use cached package data + + Returns: + Dictionary containing compatibility information + + Raises: + InvalidPackageNameError: If package name is invalid + PackageNotFoundError: If package is not found + NetworkError: For network-related errors + """ + if not package_name or not package_name.strip(): + raise InvalidPackageNameError(package_name) + + if not target_python_version or not target_python_version.strip(): + raise ValueError("Target Python version cannot be empty") + + logger.info(f"Checking Python {target_python_version} compatibility for package: {package_name}") + + try: + async with PyPIClient() as client: + package_data = await client.get_package_info(package_name, use_cache) + + info = package_data.get("info", {}) + requires_python = info.get("requires_python") + classifiers = info.get("classifiers", []) + + # Perform compatibility check + compat_checker = VersionCompatibility() + result = compat_checker.check_version_compatibility( + target_python_version, + requires_python, + classifiers + ) + + # Add package information to result + result.update({ + "package_name": info.get("name", package_name), + "package_version": info.get("version", ""), + "requires_python": requires_python, + "supported_implementations": compat_checker.extract_python_implementations(classifiers), + "classifier_versions": sorted(compat_checker.extract_python_versions_from_classifiers(classifiers)) + }) + + return result + + except PyPIError: + # Re-raise PyPI-specific errors + raise + except Exception as e: + logger.error(f"Unexpected error checking compatibility for {package_name}: {e}") + raise NetworkError(f"Failed to check Python compatibility: {e}", e) from e + + +async def get_compatible_python_versions( + package_name: str, + python_versions: list[str] | None = None, + use_cache: bool = True +) -> dict[str, Any]: + """Get list of Python versions compatible with a package. + + Args: + package_name: Name of the package to check + python_versions: List of Python versions to check (optional) + use_cache: Whether to use cached package data + + Returns: + Dictionary containing compatibility information for multiple versions + + Raises: + InvalidPackageNameError: If package name is invalid + PackageNotFoundError: If package is not found + NetworkError: For network-related errors + """ + if not package_name or not package_name.strip(): + raise InvalidPackageNameError(package_name) + + logger.info(f"Getting compatible Python versions for package: {package_name}") + + try: + async with PyPIClient() as client: + package_data = await client.get_package_info(package_name, use_cache) + + info = package_data.get("info", {}) + requires_python = info.get("requires_python") + classifiers = info.get("classifiers", []) + + # Get compatibility information + compat_checker = VersionCompatibility() + result = compat_checker.get_compatible_versions( + requires_python, + classifiers, + python_versions + ) + + # Add package information to result + result.update({ + "package_name": info.get("name", package_name), + "package_version": info.get("version", ""), + "requires_python": requires_python, + "supported_implementations": sorted(compat_checker.extract_python_implementations(classifiers)), + "classifier_versions": sorted(compat_checker.extract_python_versions_from_classifiers(classifiers)) + }) + + return result + + except PyPIError: + # Re-raise PyPI-specific errors + raise + except Exception as e: + logger.error(f"Unexpected error getting compatible versions for {package_name}: {e}") + raise NetworkError(f"Failed to get compatible Python versions: {e}", e) from e + + +async def suggest_python_version_for_packages( + package_names: list[str], + use_cache: bool = True +) -> dict[str, Any]: + """Suggest optimal Python version for a list of packages. + + Args: + package_names: List of package names to analyze + use_cache: Whether to use cached package data + + Returns: + Dictionary containing version suggestions and compatibility matrix + + Raises: + ValueError: If package_names is empty + NetworkError: For network-related errors + """ + if not package_names: + raise ValueError("Package names list cannot be empty") + + logger.info(f"Analyzing Python version compatibility for {len(package_names)} packages") + + # Default Python versions to analyze + python_versions = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + + compatibility_matrix = {} + package_details = {} + errors = {} + + async with PyPIClient() as client: + for package_name in package_names: + try: + package_data = await client.get_package_info(package_name, use_cache) + info = package_data.get("info", {}) + + requires_python = info.get("requires_python") + classifiers = info.get("classifiers", []) + + compat_checker = VersionCompatibility() + compat_result = compat_checker.get_compatible_versions( + requires_python, + classifiers, + python_versions + ) + + # Store compatibility for this package + compatible_versions = [v["version"] for v in compat_result["compatible_versions"]] + compatibility_matrix[package_name] = compatible_versions + + package_details[package_name] = { + "version": info.get("version", ""), + "requires_python": requires_python, + "compatible_versions": compatible_versions, + "compatibility_rate": compat_result["compatibility_rate"] + } + + except Exception as e: + logger.warning(f"Failed to analyze package {package_name}: {e}") + errors[package_name] = str(e) + compatibility_matrix[package_name] = [] + + # Find common compatible versions + if compatibility_matrix: + all_versions = set(python_versions) + common_versions = all_versions.copy() + + for _package_name, compatible in compatibility_matrix.items(): + if compatible: # Only consider packages with known compatibility + common_versions &= set(compatible) + else: + common_versions = set() + + # Generate recommendations + recommendations = [] + if common_versions: + latest_common = max(common_versions, key=lambda x: tuple(map(int, x.split(".")))) + recommendations.append( + f"βœ… Recommended Python version: {latest_common} " + f"(compatible with all {len([p for p in compatibility_matrix if compatibility_matrix[p]])} packages)" + ) + + if len(common_versions) > 1: + all_common = sorted(common_versions, key=lambda x: tuple(map(int, x.split(".")))) + recommendations.append( + f"πŸ“‹ All compatible versions: {', '.join(all_common)}" + ) + else: + recommendations.append( + "⚠️ No Python version is compatible with all packages. " + "Consider updating packages or using different versions." + ) + + # Find the version compatible with most packages + version_scores = {} + for version in python_versions: + score = sum(1 for compatible in compatibility_matrix.values() if version in compatible) + version_scores[version] = score + + if version_scores: + best_version = max(version_scores, key=version_scores.get) + best_score = version_scores[best_version] + total_packages = len([p for p in compatibility_matrix if compatibility_matrix[p]]) + + if best_score > 0: + recommendations.append( + f"πŸ“Š Best compromise: Python {best_version} " + f"(compatible with {best_score}/{total_packages} packages)" + ) + + return { + "analyzed_packages": len(package_names), + "successful_analyses": len(package_details), + "failed_analyses": len(errors), + "common_compatible_versions": sorted(common_versions), + "recommended_version": max(common_versions, key=lambda x: tuple(map(int, x.split(".")))) if common_versions else None, + "compatibility_matrix": compatibility_matrix, + "package_details": package_details, + "errors": errors, + "recommendations": recommendations, + "python_versions_analyzed": python_versions + } diff --git a/pypi_query_mcp/tools/package_query.py b/pypi_query_mcp/tools/package_query.py new file mode 100644 index 0000000..5cfee49 --- /dev/null +++ b/pypi_query_mcp/tools/package_query.py @@ -0,0 +1,253 @@ +"""Package query tools for PyPI MCP server.""" + +import logging +from typing import Any + +from ..core import ( + InvalidPackageNameError, + NetworkError, + PyPIClient, + PyPIError, +) + +logger = logging.getLogger(__name__) + + +def format_package_info(package_data: dict[str, Any]) -> dict[str, Any]: + """Format package information for MCP response. + + Args: + package_data: Raw package data from PyPI API + + Returns: + Formatted package information + """ + info = package_data.get("info", {}) + + # Extract basic information + formatted = { + "name": info.get("name", ""), + "version": info.get("version", ""), + "summary": info.get("summary", ""), + "description": info.get("description", "")[:500] + "..." if len(info.get("description", "")) > 500 else info.get("description", ""), + "author": info.get("author", ""), + "author_email": info.get("author_email", ""), + "maintainer": info.get("maintainer", ""), + "maintainer_email": info.get("maintainer_email", ""), + "license": info.get("license", ""), + "home_page": info.get("home_page", ""), + "project_url": info.get("project_url", ""), + "download_url": info.get("download_url", ""), + "requires_python": info.get("requires_python", ""), + "platform": info.get("platform", ""), + "keywords": info.get("keywords", ""), + "classifiers": info.get("classifiers", []), + "requires_dist": info.get("requires_dist", []), + "project_urls": info.get("project_urls", {}), + } + + # Add release information + releases = package_data.get("releases", {}) + formatted["total_versions"] = len(releases) + formatted["available_versions"] = list(releases.keys())[-10:] # Last 10 versions + + # Add download statistics if available + if "urls" in package_data: + urls = package_data["urls"] + if urls: + formatted["download_info"] = { + "files_count": len(urls), + "file_types": list({url.get("packagetype", "") for url in urls}), + "python_versions": list({url.get("python_version", "") for url in urls if url.get("python_version")}), + } + + return formatted + + +def format_version_info(package_data: dict[str, Any]) -> dict[str, Any]: + """Format version information for MCP response. + + Args: + package_data: Raw package data from PyPI API + + Returns: + Formatted version information + """ + info = package_data.get("info", {}) + releases = package_data.get("releases", {}) + + # Sort versions (basic sorting, could be improved with proper version parsing) + sorted_versions = sorted(releases.keys(), reverse=True) + + return { + "package_name": info.get("name", ""), + "latest_version": info.get("version", ""), + "total_versions": len(releases), + "versions": sorted_versions, + "recent_versions": sorted_versions[:20], # Last 20 versions + "version_details": { + version: { + "release_count": len(releases[version]), + "has_wheel": any(file.get("packagetype") == "bdist_wheel" for file in releases[version]), + "has_source": any(file.get("packagetype") == "sdist" for file in releases[version]), + } + for version in sorted_versions[:10] # Details for last 10 versions + } + } + + +def format_dependency_info(package_data: dict[str, Any]) -> dict[str, Any]: + """Format dependency information for MCP response. + + Args: + package_data: Raw package data from PyPI API + + Returns: + Formatted dependency information + """ + info = package_data.get("info", {}) + requires_dist = info.get("requires_dist", []) or [] + + # Parse dependencies + runtime_deps = [] + dev_deps = [] + optional_deps = {} + + for dep in requires_dist: + if not dep: + continue + + # Basic parsing - could be improved with proper dependency parsing + if "extra ==" in dep: + # Optional dependency + parts = dep.split(";") + dep_name = parts[0].strip() + extra_part = parts[1] if len(parts) > 1 else "" + + if "extra ==" in extra_part: + extra_name = extra_part.split("extra ==")[1].strip().strip('"\'') + if extra_name not in optional_deps: + optional_deps[extra_name] = [] + optional_deps[extra_name].append(dep_name) + elif "dev" in dep.lower() or "test" in dep.lower(): + dev_deps.append(dep) + else: + runtime_deps.append(dep) + + return { + "package_name": info.get("name", ""), + "version": info.get("version", ""), + "requires_python": info.get("requires_python", ""), + "runtime_dependencies": runtime_deps, + "development_dependencies": dev_deps, + "optional_dependencies": optional_deps, + "total_dependencies": len(requires_dist), + "dependency_summary": { + "runtime_count": len(runtime_deps), + "dev_count": len(dev_deps), + "optional_groups": len(optional_deps), + "total_optional": sum(len(deps) for deps in optional_deps.values()), + } + } + + +async def query_package_info(package_name: str) -> dict[str, Any]: + """Query comprehensive package information from PyPI. + + Args: + package_name: Name of the package to query + + Returns: + Formatted package information + + Raises: + InvalidPackageNameError: If package name is invalid + PackageNotFoundError: If package is not found + NetworkError: For network-related errors + """ + if not package_name or not package_name.strip(): + raise InvalidPackageNameError(package_name) + + logger.info(f"Querying package info for: {package_name}") + + try: + async with PyPIClient() as client: + package_data = await client.get_package_info(package_name) + return format_package_info(package_data) + except PyPIError: + # Re-raise PyPI-specific errors + raise + except Exception as e: + logger.error(f"Unexpected error querying package {package_name}: {e}") + raise NetworkError(f"Failed to query package information: {e}", e) from e + + +async def query_package_versions(package_name: str) -> dict[str, Any]: + """Query package version information from PyPI. + + Args: + package_name: Name of the package to query + + Returns: + Formatted version information + + Raises: + InvalidPackageNameError: If package name is invalid + PackageNotFoundError: If package is not found + NetworkError: For network-related errors + """ + if not package_name or not package_name.strip(): + raise InvalidPackageNameError(package_name) + + logger.info(f"Querying versions for package: {package_name}") + + try: + async with PyPIClient() as client: + package_data = await client.get_package_info(package_name) + return format_version_info(package_data) + except PyPIError: + # Re-raise PyPI-specific errors + raise + except Exception as e: + logger.error(f"Unexpected error querying versions for {package_name}: {e}") + raise NetworkError(f"Failed to query package versions: {e}", e) from e + + +async def query_package_dependencies(package_name: str, version: str | None = None) -> dict[str, Any]: + """Query package dependency information from PyPI. + + Args: + package_name: Name of the package to query + version: Specific version to query (optional, defaults to latest) + + Returns: + Formatted dependency information + + Raises: + InvalidPackageNameError: If package name is invalid + PackageNotFoundError: If package is not found + NetworkError: For network-related errors + """ + if not package_name or not package_name.strip(): + raise InvalidPackageNameError(package_name) + + logger.info(f"Querying dependencies for package: {package_name}" + + (f" version {version}" if version else " (latest)")) + + try: + async with PyPIClient() as client: + package_data = await client.get_package_info(package_name) + + # TODO: In future, support querying specific version dependencies + # For now, we return dependencies for the latest version + if version and version != package_data.get("info", {}).get("version"): + logger.warning(f"Specific version {version} requested but not implemented yet. " + f"Returning dependencies for latest version.") + + return format_dependency_info(package_data) + except PyPIError: + # Re-raise PyPI-specific errors + raise + except Exception as e: + logger.error(f"Unexpected error querying dependencies for {package_name}: {e}") + raise NetworkError(f"Failed to query package dependencies: {e}", e) from e diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2e405f4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,102 @@ +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "pypi-query-mcp-server" +version = "0.1.0" +description = "A Model Context Protocol (MCP) server for querying PyPI package information, dependencies, and compatibility" +authors = ["Hal "] +readme = "README.md" +license = "MIT" +homepage = "https://github.com/loonghao/pypi-query-mcp-server" +repository = "https://github.com/loonghao/pypi-query-mcp-server" +documentation = "https://github.com/loonghao/pypi-query-mcp-server" +keywords = ["mcp", "pypi", "package", "dependency", "python"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Software Distribution", +] +packages = [{include = "pypi_query_mcp"}] + +[tool.poetry.dependencies] +python = "^3.10" +fastmcp = "^0.2.0" +httpx = "^0.27.0" +packaging = "^24.0" +pydantic = "^2.0.0" +click = "^8.1.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.0.0" +pytest-asyncio = "^0.23.0" +pytest-cov = "^4.0.0" +pytest-mock = "^3.12.0" +ruff = "^0.1.0" +mypy = "^1.8.0" +pre-commit = "^3.6.0" +nox = "^2024.3.2" + +[tool.poetry.scripts] +pypi-query-mcp = "pypi_query_mcp.server:main" + +[tool.ruff] +target-version = "py310" +line-length = 88 +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.mypy] +python_version = "3.10" +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q --strict-markers --strict-config" +testpaths = ["tests"] +asyncio_mode = "auto" + +[tool.coverage.run] +source = ["pypi_query_mcp"] +omit = ["tests/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", +] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index be84a08..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -poetry -nox -pytest diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..954f644 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for PyPI Query MCP Server.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..59cdba9 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,63 @@ +"""Pytest configuration and fixtures.""" + +import pytest + + +@pytest.fixture +def sample_package_data(): + """Sample package data for testing.""" + return { + "info": { + "name": "test-package", + "version": "1.0.0", + "summary": "A test package", + "description": "This is a test package for testing purposes.", + "author": "Test Author", + "author_email": "test@example.com", + "license": "MIT", + "requires_python": ">=3.8", + "requires_dist": [ + "requests>=2.25.0", + "click>=8.0.0", + "pytest>=6.0.0; extra == 'test'" + ], + "classifiers": [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython" + ] + }, + "releases": { + "1.0.0": [ + { + "filename": "test_package-1.0.0-py3-none-any.whl", + "packagetype": "bdist_wheel", + "python_version": "py3" + }, + { + "filename": "test-package-1.0.0.tar.gz", + "packagetype": "sdist", + "python_version": "source" + } + ], + "0.9.0": [ + { + "filename": "test-package-0.9.0.tar.gz", + "packagetype": "sdist", + "python_version": "source" + } + ] + } + } + + +@pytest.fixture +def mock_pypi_response(sample_package_data): + """Mock PyPI API response.""" + return sample_package_data diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..19210aa --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,78 @@ +"""Basic tests for PyPI Query MCP Server.""" + +import pytest + +from pypi_query_mcp import __version__ + + +def test_version(): + """Test that version is defined.""" + assert __version__ == "0.1.0" + + +def test_import(): + """Test that main modules can be imported.""" + from pypi_query_mcp.core import PyPIClient, VersionCompatibility + from pypi_query_mcp.server import app + + assert PyPIClient is not None + assert VersionCompatibility is not None + assert app is not None + + +@pytest.mark.asyncio +async def test_pypi_client_basic(): + """Test basic PyPI client functionality.""" + from pypi_query_mcp.core import PyPIClient + + async with PyPIClient() as client: + # Test that client can be created and closed + assert client is not None + + # Test cache clearing + client.clear_cache() + + +def test_version_compatibility(): + """Test version compatibility utility.""" + from pypi_query_mcp.core import VersionCompatibility + + compat = VersionCompatibility() + + # Test requires_python parsing + spec = compat.parse_requires_python(">=3.8") + assert spec is not None + + # Test classifier extraction + classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: Implementation :: CPython" + ] + + versions = compat.extract_python_versions_from_classifiers(classifiers) + assert "3.8" in versions + assert "3.9" in versions + + implementations = compat.extract_python_implementations(classifiers) + assert "CPython" in implementations + + +def test_mcp_tools_import(): + """Test that MCP tools can be imported.""" + from pypi_query_mcp.tools import ( + check_python_compatibility, + get_compatible_python_versions, + query_package_dependencies, + query_package_info, + query_package_versions, + suggest_python_version_for_packages, + ) + + # Test that all tools are callable + assert callable(query_package_info) + assert callable(query_package_versions) + assert callable(query_package_dependencies) + assert callable(check_python_compatibility) + assert callable(get_compatible_python_versions) + assert callable(suggest_python_version_for_packages)