diff --git a/.secrets.baseline b/.secrets.baseline index 7ef43f793b..d89543ebbe 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -124,15 +124,35 @@ }, { "path": "detect_secrets.filters.heuristic.is_templated_secret" - }, - { - "path": "detect_secrets.filters.regex.should_exclude_file", - "pattern": [ - ".secrets.baseline" - ] } ], "results": { + "src/aws-location-mcp-server/tests/test_server.py": [ + { + "type": "AWS Access Key", + "filename": "src/aws-location-mcp-server/tests/test_server.py", + "hashed_secret": "d70eab08607a4d05faa2d0d6647206599e9abc65", + "is_verified": false, + "line_number": 757, + "is_secret": false + }, + { + "type": "Base64 High Entropy String", + "filename": "src/aws-location-mcp-server/tests/test_server.py", + "hashed_secret": "d70eab08607a4d05faa2d0d6647206599e9abc65", + "is_verified": false, + "line_number": 757, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "src/aws-location-mcp-server/tests/test_server.py", + "hashed_secret": "d70eab08607a4d05faa2d0d6647206599e9abc65", + "is_verified": false, + "line_number": 767, + "is_secret": false + } + ], "src/bedrock-kb-retrieval-mcp-server/README.md": [ { "type": "Base64 High Entropy String", @@ -170,5 +190,5 @@ } ] }, - "generated_at": "2025-05-05T14:50:32Z" + "generated_at": "2025-05-07T18:33:23Z" } diff --git a/README.md b/README.md index 22763b1361..6825cf239f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ A suite of specialized MCP servers that help you get the most out of AWS, wherev - [AWS Diagram MCP Server](#aws-diagram-mcp-server) - [AWS Lambda MCP Server](#aws-lambda-mcp-server) - [AWS Terraform MCP Server](#aws-terraform-mcp-server) + - [AWS Location Service MCP Server](#aws-location-service-mcp-server) - [Git Repo Research MCP Server](#git-repo-research-mcp-server) - [Use Cases for the Servers](#use-cases-for-the-servers) - [Installation and Setup](#installation-and-setup) @@ -183,6 +184,22 @@ A server for AWS Terraform best practices. [Learn more](src/terraform-mcp-server/README.md) | [Documentation](https://awslabs.github.io/mcp/servers/terraform-mcp-server/) +### AWS Location Service MCP Server + +[![PyPI version](https://img.shields.io/pypi/v/awslabs.aws-location-mcp-server.svg)](https://pypi.org/project/awslabs.aws-location-mcp-server/) + +A server for accessing AWS Location Service capabilities, focusing on place search, geographical coordinates, and route planning. + +- Search for places using geocoding +- Get details for specific places by PlaceId +- Reverse geocode coordinates to addresses +- Search for places near a location +- Search for places that are currently open +- Calculate routes between locations with turn-by-turn directions +- Optimize waypoints for efficient routing + +[Learn more](src/aws-location-mcp-server/README.md) + ### Git Repo Research MCP Server [![PyPI version](https://img.shields.io/pypi/v/awslabs.git-repo-research-mcp-server.svg)](https://pypi.org/project/awslabs.git-repo-research-mcp-server/) @@ -285,6 +302,17 @@ Example configuration for Amazon Q CLI MCP (`~/.aws/amazonq/mcp.json`): }, "disabled": false, "autoApprove": [] + }, + "awslabs.aws-location-mcp-server": { + "command": "uvx", + "args": ["awslabs.aws-location-mcp-server@latest"], + "env": { + "AWS_PROFILE": "your-aws-profile", + "AWS_REGION": "us-east-1", + "FASTMCP_LOG_LEVEL": "ERROR" + }, + "disabled": false, + "autoApprove": [] }, "awslabs.git-repo-research-mcp-server": { "command": "uvx", diff --git a/src/aws-location-mcp-server/.gitignore b/src/aws-location-mcp-server/.gitignore new file mode 100644 index 0000000000..21a43cb007 --- /dev/null +++ b/src/aws-location-mcp-server/.gitignore @@ -0,0 +1,88 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ +.env/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Testing +.coverage +htmlcov/ +.pytest_cache/ +.tox/ +.nox/ +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local development +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Cache +.ruff_cache/ +__pycache__/ +.mypy_cache/ +.pytest_cache/ diff --git a/src/aws-location-mcp-server/.pre-commit-config.yaml b/src/aws-location-mcp-server/.pre-commit-config.yaml new file mode 100644 index 0000000000..87014f242a --- /dev/null +++ b/src/aws-location-mcp-server/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.7 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + exclude: ^mkdocs\.yml$ + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: debug-statements + - id: check-toml + + + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.20.0 + hooks: + - id: commitizen + stages: [commit-msg] diff --git a/src/aws-location-mcp-server/.python-version b/src/aws-location-mcp-server/.python-version new file mode 100644 index 0000000000..c8cfe39591 --- /dev/null +++ b/src/aws-location-mcp-server/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/src/aws-location-mcp-server/CHANGELOG.md b/src/aws-location-mcp-server/CHANGELOG.md new file mode 100644 index 0000000000..cf8a00d7e2 --- /dev/null +++ b/src/aws-location-mcp-server/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-04-17 + +### Added +- Initial release of the AWS Location Service MCP Server +- Added `search_places` tool for geocoding and place search +- Added `get_coordinates` tool for retrieving location coordinates +- Support for AWS credentials via environment variables or AWS CLI profiles +- Support for custom place index configuration + +### Changed +- Implemented using FastMCP framework for MCP protocol handling +- Structured project to match other MCP servers + +## [1.1.0] - 2025-05-06 + +### Added +- `GeoPlacesClient`: Client for AWS Location Service geo-places API. +- `GeoRoutesClient`: Client for AWS Location Service route calculation API. +- `search_places` tool: Search for places using text queries. +- `get_place` tool: Retrieve details for a specific place by PlaceId. +- `reverse_geocode` tool: Convert coordinates to a human-readable address. +- `search_nearby` tool: Find places near a given location, with radius expansion. +- `search_places_open_now` tool: Find places currently open, with support for opening hours and radius expansion. +- `get_coordinates` tool: Get coordinates for a location name or address. +- `calculate_route` tool: Calculate routes between two locations, supporting travel modes (`Car`, `Truck`, `Walking`, `Bicycle`) and route optimization (`FastestRoute`, `ShortestRoute`). +- `optimize_waypoints` tool: Optimize the order of waypoints for a route using AWS Location Service. + +### Changed +- Refactored `calculate_route` to expose only `departure_position`, `destination_position`, `travel_mode`, and `optimize_for` as parameters. Internal options are now local variables. +- Updated tests and documentation to match the new tool signatures and AWS documentation. +- Improved error handling and output consistency for route calculation and waypoint optimization tools. diff --git a/src/aws-location-mcp-server/Dockerfile b/src/aws-location-mcp-server/Dockerfile new file mode 100644 index 0000000000..3b9a9be577 --- /dev/null +++ b/src/aws-location-mcp-server/Dockerfile @@ -0,0 +1,76 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance +# with the License. A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# and limitations under the License. + +FROM public.ecr.aws/sam/build-python3.10@sha256:04cdbe84bec08d17d621192bc3f0a9e4a85a83f2ac99aa9241659dfac0d845ea AS uv + +# Install the project into `/app` +WORKDIR /app + +# Enable bytecode compilation +ENV UV_COMPILE_BYTECODE=1 + +# Copy from the cache instead of linking since it's a mounted volume +ENV UV_LINK_MODE=copy + +# Prefer the system python +ENV UV_PYTHON_PREFERENCE=only-system + +# Run without updating the uv.lock file like running with `--frozen` +ENV UV_FROZEN=true + +# Install the project's dependencies using the lockfile and settings +COPY pyproject.toml uv.lock ./ +RUN --mount=type=cache,target=/root/.cache/uv \ + pip install uv && \ + uv sync --frozen --no-install-project --no-dev --no-editable + +# Then, add the rest of the project source code and install it +# Installing separately from its dependencies allows optimal layer caching +COPY . /app +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev --no-editable + +# Make the directory just in case it doesn't exist +RUN mkdir -p /root/.local + +FROM public.ecr.aws/sam/build-python3.10@sha256:04cdbe84bec08d17d621192bc3f0a9e4a85a83f2ac99aa9241659dfac0d845ea + +# Place executables in the environment at the front of the path and include other binaries +ENV PATH="/app/.venv/bin:$PATH:/usr/sbin" + +# Install lsof for the healthcheck +# Install AWS CLI for AWS credentials management +# Add non-root user and ability to change directory into /root +RUN yum update -y && \ + yum install -y lsof && \ + yum clean all -y && \ + rm -rf /var/cache/yum && \ + groupadd --force --system app && \ + useradd app -g app -d /app && \ + chmod o+x /root + +# Get the project from the uv layer +COPY --from=uv --chown=app:app /root/.local /root/.local +COPY --from=uv --chown=app:app /app/.venv /app/.venv + +# Get healthcheck script +COPY ./docker-healthcheck.sh /usr/local/bin/docker-healthcheck.sh +RUN chmod +x /usr/local/bin/docker-healthcheck.sh + +# Run as non-root +USER app + +# AWS_REGION should be provided at runtime +# Example: docker run -e AWS_REGION=us-east-1 image_name + +# Health check and entrypoint +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "docker-healthcheck.sh" ] +ENTRYPOINT ["awslabs.aws-location-mcp-server"] diff --git a/src/aws-location-mcp-server/LICENSE b/src/aws-location-mcp-server/LICENSE new file mode 100644 index 0000000000..b09cd7856d --- /dev/null +++ b/src/aws-location-mcp-server/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/aws-location-mcp-server/NOTICE b/src/aws-location-mcp-server/NOTICE new file mode 100644 index 0000000000..feb732a16b --- /dev/null +++ b/src/aws-location-mcp-server/NOTICE @@ -0,0 +1,2 @@ +AWS Location Service MCP Server +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/src/aws-location-mcp-server/README.md b/src/aws-location-mcp-server/README.md new file mode 100644 index 0000000000..544583160a --- /dev/null +++ b/src/aws-location-mcp-server/README.md @@ -0,0 +1,221 @@ +# AWS Location Service MCP Server + +Model Context Protocol (MCP) server for AWS Location Service + +This MCP server provides tools to access AWS Location Service capabilities, focusing on place search, geographical coordinates, and route planning. + +## Features + +- **Search for Places**: Search for places using geocoding +- **Get Place Details**: Get details for specific places by PlaceId +- **Reverse Geocode**: Convert coordinates to addresses +- **Search Nearby**: Search for places near a specified location +- **Open Now Search**: Search for places that are currently open +- **Route Calculation**: Calculate routes between locations with turn-by-turn directions +- **Waypoint Optimization**: Optimize the order of waypoints for efficient routing + +## Prerequisites + +### Requirements + +1. Have an AWS account with AWS Location Service enabled +2. Install `uv` from [Astral](https://docs.astral.sh/uv/getting-started/installation/) or the [GitHub README](https://github.com/astral-sh/uv#installation) +3. Install Python 3.10 or newer using `uv python install 3.10` (or a more recent version) + +## Installation + +Here are the ways you can work with the AWS Location MCP server: + +## Configuration + +Configure the server in your MCP configuration file. Here are some ways you can work with MCP across AWS, and we'll be adding support to more products soon: (e.g. for Amazon Q Developer CLI MCP, `~/.aws/amazonq/mcp.json`): + +```json +{ + "mcpServers": { + "awslabs.aws-location-mcp-server": { + "command": "uvx", + "args": ["awslabs.aws-location-mcp-server@latest"], + "env": { + "AWS_PROFILE": "your-aws-profile", + "AWS_REGION": "us-east-1", + "FASTMCP_LOG_LEVEL": "ERROR" + }, + "disabled": false, + "autoApprove": [] + } + } +} +``` + +### Docker Configuration + +After building with `docker build -t awslabs/aws-location-mcp-server .`: + +```json +{ + "mcpServers": { + "awslabs.aws-location-mcp-server": { + "command": "docker", + "args": [ + "run", + "--rm", + "-i", + "awslabs/aws-location-mcp-server" + ], + "env": { + "AWS_PROFILE": "your-aws-profile", + "AWS_REGION": "us-east-1" + }, + "disabled": false, + "autoApprove": [] + } + } +} +``` + +### Environment Variables + +- `AWS_PROFILE`: AWS CLI profile to use for credentials +- `AWS_REGION`: AWS region to use (default: us-east-1) +- `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`: Explicit AWS credentials (alternative to AWS_PROFILE) +- `FASTMCP_LOG_LEVEL`: Logging level (ERROR, WARNING, INFO, DEBUG) + +## Tools + +The server exposes the following tools through the MCP interface: + +### search_places + +Search for places using AWS Location Service geocoding capabilities. + +```python +search_places(query: str, max_results: int = 5, mode: str = 'summary') -> dict +``` + +### get_place + +Get details for a specific place using its unique place ID. + +```python +get_place(place_id: str, mode: str = 'summary') -> dict +``` + +### reverse_geocode + +Convert coordinates to an address using reverse geocoding. + +```python +reverse_geocode(longitude: float, latitude: float) -> dict +``` + +### search_nearby + +Search for places near a specific location with optional radius expansion. + +```python +search_nearby(longitude: float, latitude: float, radius: int = 500, max_results: int = 5, + query: str = None, max_radius: int = 10000, expansion_factor: float = 2.0, + mode: str = 'summary') -> dict +``` + +### search_places_open_now + +Search for places that are currently open, with radius expansion if needed. + +```python +search_places_open_now(query: str, max_results: int = 5, initial_radius: int = 500, + max_radius: int = 50000, expansion_factor: float = 2.0) -> dict +``` + +### calculate_route + +Calculate a route between two locations with turn-by-turn directions. + +```python +calculate_route( + departure_position: list, # [longitude, latitude] + destination_position: list, # [longitude, latitude] + travel_mode: str = 'Car', # 'Car', 'Truck', 'Walking', or 'Bicycle' + optimize_for: str = 'FastestRoute' # 'FastestRoute' or 'ShortestRoute' +) -> dict +``` + +Returns: +- `distance_meters`: Total route distance in meters +- `duration_seconds`: Estimated travel time in seconds +- `legs`: List of route legs with distance and duration +- `turn_by_turn`: List of navigation instructions with: + - `distance_meters`: Distance for this step + - `duration_seconds`: Duration for this step + - `type`: Maneuver type (e.g., 'Straight', 'Turn') + - `road_name`: Name of the road for this step + +Example usage: +```python +route = await calculate_route( + ctx, + departure_position=[-122.335167, 47.608013], # Seattle + destination_position=[-122.200676, 47.610149], # Bellevue + travel_mode='Car', + optimize_for='FastestRoute' +) +``` + +### optimize_waypoints + +Optimize the order of waypoints for efficient routing. + +```python +optimize_waypoints( + origin_position: list, # [longitude, latitude] + destination_position: list, # [longitude, latitude] + waypoints: list, # List of waypoints, each as a dict with 'Id' and 'Position' [longitude, latitude] + travel_mode: str = 'Car', + mode: str = 'summary' +) -> dict +``` + +Returns: +- `optimized_order`: List of waypoint IDs in optimized order +- `total_distance_meters`: Total route distance in meters +- `total_duration_seconds`: Total estimated travel time in seconds +- `waypoints`: List of waypoints with arrival and departure times + +Example usage: +```python +result = await optimize_waypoints( + ctx, + origin_position=[-122.335167, 47.608013], # Seattle + destination_position=[-122.121513, 47.673988], # Redmond + waypoints=[ + {'Id': 'bellevue', 'Position': [-122.200676, 47.610149]}, + {'Id': 'kirkland', 'Position': [-122.209032, 47.676607]} + ], + travel_mode='Car' +) +``` + +### get_coordinates + +Get coordinates for a location name or address. + +```python +get_coordinates(location: str) -> dict +``` + +## AWS Location Service Resources + +This server uses the AWS Location Service APIs for: +- Geocoding (converting addresses to coordinates) +- Reverse geocoding (converting coordinates to addresses) +- Place search (finding places by name, category, etc.) +- Place details (getting information about specific places) +- Route calculation (finding routes between locations with turn-by-turn directions) +- Waypoint optimization (determining the most efficient order to visit multiple locations) + +## Security Considerations + +- Use AWS profiles for credential management +- Use IAM policies to restrict access to only the required AWS Location Service resources +- Consider using temporary credentials or AWS IAM roles for enhanced security diff --git a/src/aws-location-mcp-server/awslabs/__init__.py b/src/aws-location-mcp-server/awslabs/__init__.py new file mode 100644 index 0000000000..0c8a9f0aca --- /dev/null +++ b/src/aws-location-mcp-server/awslabs/__init__.py @@ -0,0 +1,21 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance +# with the License. A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# and limitations under the License. +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance +# with the License. A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# and limitations under the License. +# This file is required to make the directory a Python package diff --git a/src/aws-location-mcp-server/awslabs/aws_location_server/__init__.py b/src/aws-location-mcp-server/awslabs/aws_location_server/__init__.py new file mode 100644 index 0000000000..9bb5270a00 --- /dev/null +++ b/src/aws-location-mcp-server/awslabs/aws_location_server/__init__.py @@ -0,0 +1,14 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance +# with the License. A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# and limitations under the License. + +"""AWS Location Service MCP Server package.""" + +__version__ = '1.0.0' diff --git a/src/aws-location-mcp-server/awslabs/aws_location_server/server.py b/src/aws-location-mcp-server/awslabs/aws_location_server/server.py new file mode 100644 index 0000000000..243cfdb8c2 --- /dev/null +++ b/src/aws-location-mcp-server/awslabs/aws_location_server/server.py @@ -0,0 +1,790 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance +# with the License. A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# and limitations under the License. + +"""AWS Location Service MCP Server implementation using geo-places client only.""" + +import argparse +import asyncio +import boto3 +import botocore.config +import botocore.exceptions +import os +import sys +from loguru import logger +from mcp.server.fastmcp import Context, FastMCP +from pydantic import Field +from typing import Dict, Optional + + +# Set up logging +logger.remove() +logger.add(sys.stderr, level=os.getenv('FASTMCP_LOG_LEVEL', 'WARNING')) + +# Initialize FastMCP server +mcp = FastMCP( + 'awslabs.aws-location-mcp-server', + instructions=""" + # AWS Location Service MCP Server (geo-places) + + This server provides tools to interact with AWS Location Service geo-places capabilities, focusing on place search, details, and geocoding. + + ## Features + - Search for places using text queries + - Get place details by PlaceId + - Reverse geocode coordinates + - Search for places nearby a location + - Search for places open now (extension) + + ## Prerequisites + 1. Have an AWS account with AWS Location Service enabled + 2. Configure AWS CLI with your credentials and profile + 3. Set AWS_REGION environment variable if not using default + + ## Best Practices + - Provide specific location details for more accurate results + - Use the search_places tool for general search + - Use get_place for details on a specific place + - Use reverse_geocode for lat/lon to address + - Use search_nearby for places near a point + - Use search_places_open_now to find currently open places (if supported by data) + """, + dependencies=[ + 'boto3', + 'pydantic', + ], +) + + +class GeoPlacesClient: + """AWS Location Service geo-places client wrapper.""" + + def __init__(self): + """Initialize the AWS geo-places client.""" + self.aws_region = os.environ.get('AWS_REGION', 'us-east-1') + self.geo_places_client = None + config = botocore.config.Config( + connect_timeout=15, read_timeout=15, retries={'max_attempts': 3} + ) + aws_access_key = os.environ.get('AWS_ACCESS_KEY_ID') + aws_secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') + try: + if aws_access_key and aws_secret_key: + client_args = { + 'aws_access_key_id': aws_access_key, + 'aws_secret_access_key': aws_secret_key, + 'region_name': self.aws_region, + 'config': config, + } + self.geo_places_client = boto3.client('geo-places', **client_args) + else: + self.geo_places_client = boto3.client( + 'geo-places', region_name=self.aws_region, config=config + ) + logger.debug(f'AWS geo-places client initialized for region {self.aws_region}') + except Exception as e: + logger.error(f'Failed to initialize AWS geo-places client: {str(e)}') + self.geo_places_client = None + + +class GeoRoutesClient: + """AWS Location Service geo-routes client wrapper.""" + + def __init__(self): + """Initialize the AWS geo-routes client.""" + self.aws_region = os.environ.get('AWS_REGION', 'us-east-1') + self.geo_routes_client = None + config = botocore.config.Config( + connect_timeout=15, read_timeout=15, retries={'max_attempts': 3} + ) + aws_access_key = os.environ.get('AWS_ACCESS_KEY_ID') + aws_secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') + try: + if aws_access_key and aws_secret_key: + client_args = { + 'aws_access_key_id': aws_access_key, + 'aws_secret_access_key': aws_secret_key, + 'region_name': self.aws_region, + 'config': config, + } + self.geo_routes_client = boto3.client('geo-routes', **client_args) + else: + self.geo_routes_client = boto3.client( + 'geo-routes', region_name=self.aws_region, config=config + ) + logger.debug(f'AWS geo-routes client initialized for region {self.aws_region}') + except Exception as e: + logger.error(f'Failed to initialize AWS geo-routes client: {str(e)}') + self.geo_routes_client = None + + +# Initialize the geo-places client +geo_places_client = GeoPlacesClient() + +# Initialize the geo-routes client +geo_routes_client = GeoRoutesClient() + + +@mcp.tool() +async def search_places( + ctx: Context, + query: str = Field(description='Search query (address, place name, etc.)'), + max_results: int = Field( + default=5, description='Maximum number of results to return', ge=1, le=50 + ), + mode: str = Field( + default='summary', + description="Output mode: 'summary' (default) or 'raw' for all AWS fields", + ), +) -> Dict: + """Search for places using AWS Location Service geo-places search_text API. Geocode the query using the geocode API to get BiasPosition. If no results, try a bounding box filter. Includes contact info and opening hours if present. Output is standardized and includes all fields, even if empty or not available.""" + if not geo_places_client.geo_places_client: + error_msg = ( + 'AWS geo-places client not initialized. Please check AWS credentials and region.' + ) + await ctx.error(error_msg) + return {'error': error_msg} + try: + geo_response = geo_places_client.geo_places_client.geocode(QueryText=query) + geo_items = geo_response.get('ResultItems', []) + if geo_items: + geo_point = geo_items[0]['Position'] + bias_position = geo_point + response = geo_places_client.geo_places_client.search_text( + QueryText=query, + MaxResults=max_results, + BiasPosition=bias_position, + AdditionalFeatures=['Contact'], + ) + places = response.get('ResultItems', []) + if not places: + lon, lat = bias_position + bounding_box = [lon - 0.05, lat - 0.05, lon + 0.05, lat + 0.05] + response = geo_places_client.geo_places_client.search_text( + QueryText=query, + MaxResults=max_results, + Filter={'BoundingBox': bounding_box}, + AdditionalFeatures=['Contact'], + ) + places = response.get('ResultItems', []) + else: + error_msg = f'Could not geocode query "{query}" for BiasPosition.' + await ctx.error(error_msg) + return {'error': error_msg} + + def safe_list(val): + return val if isinstance(val, list) else ([] if val is None else [val]) + + def parse_contacts(contacts): + return { + 'phones': [p['Value'] for p in contacts.get('Phones', [])] if contacts else [], + 'websites': [w['Value'] for w in contacts.get('Websites', [])] if contacts else [], + 'emails': [e['Value'] for e in contacts.get('Emails', [])] if contacts else [], + 'faxes': [f['Value'] for f in contacts.get('Faxes', [])] if contacts else [], + } + + def parse_opening_hours(result): + oh = result.get('OpeningHours') + if not oh: + contacts = result.get('Contacts', {}) + oh = contacts.get('OpeningHours') if contacts else None + if not oh: + return [] + # Normalize to list of dicts with display and components + if isinstance(oh, dict): + oh = [oh] + parsed = [] + for entry in oh: + parsed.append( + { + 'display': entry.get('Display', []) or entry.get('display', []), + 'components': entry.get('Components', []) or entry.get('components', []), + 'open_now': entry.get('OpenNow', None), + 'categories': [cat.get('Name') for cat in entry.get('Categories', [])] + if 'Categories' in entry + else [], + } + ) + return parsed + + result_places = [] + for result in places: + if mode == 'raw': + place_data = result + else: + contacts = parse_contacts(result.get('Contacts', {})) + opening_hours = parse_opening_hours(result) + place_data = { + 'place_id': result.get('PlaceId', 'Not available'), + 'name': result.get('Title', 'Not available'), + 'address': result.get('Address', {}).get('Label', 'Not available'), + 'coordinates': { + 'longitude': result.get('Position', [None, None])[0], + 'latitude': result.get('Position', [None, None])[1], + }, + 'categories': [cat.get('Name') for cat in result.get('Categories', [])] + if result.get('Categories') + else [], + 'contacts': contacts, + 'opening_hours': opening_hours, + } + result_places.append(place_data) + result = {'query': query, 'places': result_places} + return result + except botocore.exceptions.ClientError as e: + error_msg = f'AWS geo-places Service error: {str(e)}' + print(error_msg) + await ctx.error(error_msg) + return {'error': error_msg} + except Exception as e: + error_msg = f'Error searching places: {str(e)}' + print(error_msg) + await ctx.error(error_msg) + return {'error': error_msg} + + +@mcp.tool() +async def get_place( + ctx: Context, + place_id: str = Field(description='The unique PlaceId for the place'), + mode: str = Field( + default='summary', + description="Output mode: 'summary' (default) or 'raw' for all AWS fields", + ), +) -> Dict: + """Get details for a place using AWS Location Service geo-places get_place API. Output is standardized and includes all fields, even if empty or not available.""" + if not geo_places_client.geo_places_client: + error_msg = ( + 'AWS geo-places client not initialized. Please check AWS credentials and region.' + ) + await ctx.error(error_msg) + return {'error': error_msg} + try: + response = geo_places_client.geo_places_client.get_place( + PlaceId=place_id, AdditionalFeatures=['Contact'] + ) + if mode == 'raw': + return response + contacts = { + 'phones': [p['Value'] for p in response.get('Contacts', {}).get('Phones', [])] + if response.get('Contacts') + else [], + 'websites': [w['Value'] for w in response.get('Contacts', {}).get('Websites', [])] + if response.get('Contacts') + else [], + 'emails': [e['Value'] for e in response.get('Contacts', {}).get('Emails', [])] + if response.get('Contacts') + else [], + 'faxes': [f['Value'] for f in response.get('Contacts', {}).get('Faxes', [])] + if response.get('Contacts') + else [], + } + + def parse_opening_hours(result): + oh = result.get('OpeningHours') + if not oh: + contacts = result.get('Contacts', {}) + oh = contacts.get('OpeningHours') if contacts else None + if not oh: + return [] + if isinstance(oh, dict): + oh = [oh] + parsed = [] + for entry in oh: + parsed.append( + { + 'display': entry.get('Display', []) or entry.get('display', []), + 'components': entry.get('Components', []) or entry.get('components', []), + 'open_now': entry.get('OpenNow', None), + 'categories': [cat.get('Name') for cat in entry.get('Categories', [])] + if 'Categories' in entry + else [], + } + ) + return parsed + + opening_hours = parse_opening_hours(response) + result = { + 'name': response.get('Title', 'Not available'), + 'address': response.get('Address', {}).get('Label', 'Not available'), + 'contacts': contacts, + 'categories': [cat.get('Name', '') for cat in response.get('Categories', [])] + if response.get('Categories') + else [], + 'coordinates': { + 'longitude': response.get('Position', [None, None])[0], + 'latitude': response.get('Position', [None, None])[1], + }, + 'opening_hours': opening_hours, + } + return result + except Exception as e: + print(f'get_place error: {e}') + await ctx.error(f'get_place error: {e}') + return {'error': str(e)} + + +@mcp.tool() +async def reverse_geocode( + ctx: Context, + longitude: float = Field(description='Longitude of the location'), + latitude: float = Field(description='Latitude of the location'), +) -> Dict: + """Reverse geocode coordinates to an address using AWS Location Service geo-places reverse_geocode API.""" + if not geo_places_client.geo_places_client: + error_msg = ( + 'AWS geo-places client not initialized. Please check AWS credentials and region.' + ) + logger.error(error_msg) + await ctx.error(error_msg) + return {'error': error_msg} + logger.debug(f'Reverse geocoding for longitude: {longitude}, latitude: {latitude}') + try: + response = geo_places_client.geo_places_client.reverse_geocode( + QueryPosition=[longitude, latitude] + ) + print(f'reverse_geocode raw response: {response}') + place = response.get('Place', {}) + if not place: + return {'raw_response': response} + result = { + 'name': place.get('Label') or place.get('Title', 'Unknown'), + 'coordinates': { + 'longitude': place.get('Geometry', {}).get('Point', [0, 0])[0], + 'latitude': place.get('Geometry', {}).get('Point', [0, 0])[1], + }, + 'categories': [cat.get('Name') for cat in place.get('Categories', [])], + 'address': place.get('Address', {}).get('Label', ''), + } + logger.debug(f'Reverse geocoded address for coordinates: {longitude}, {latitude}') + return result + except botocore.exceptions.ClientError as e: + error_msg = f'AWS geo-places Service error: {str(e)}' + logger.error(error_msg) + await ctx.error(error_msg) + return {'error': error_msg} + except Exception as e: + error_msg = f'Error in reverse geocoding: {str(e)}' + logger.error(error_msg) + await ctx.error(error_msg) + return {'error': error_msg} + + +@mcp.tool() +async def search_nearby( + ctx: Context, + longitude: float = Field(description='Longitude of the center point'), + latitude: float = Field(description='Latitude of the center point'), + max_results: int = Field( + default=5, description='Maximum number of results to return', ge=1, le=50 + ), + query: Optional[str] = Field(default=None, description='Optional search query'), + radius: int = Field(default=500, description='Search radius in meters', ge=1, le=50000), +) -> Dict: + """Search for places near a location using AWS Location Service geo-places search_nearby API. If no results, expand the radius up to max_radius. Output is standardized and includes all fields, even if empty or not available.""" + # Moved from parameters to local variables + max_results = 5 # Maximum number of results to return + max_radius = 10000 # Maximum search radius in meters for expansion + expansion_factor = 2.0 # Factor to expand radius by if no results + mode = 'summary' # Output mode: 'summary' (default) or 'raw' for all AWS fields + # Descriptions: + # max_results: Maximum number of results to return (default=5, ge=1, le=50) + # max_radius: Maximum search radius in meters for expansion (default=10000, ge=1, le=50000) + # expansion_factor: Factor to expand radius by if no results (default=2.0, ge=1.1, le=10.0) + # mode: Output mode: 'summary' (default) or 'raw' for all AWS fields + if not geo_places_client.geo_places_client: + error_msg = ( + 'AWS geo-places client not initialized. Please check AWS credentials and region.' + ) + await ctx.error(error_msg) + return {'error': error_msg} + try: + current_radius = radius + while current_radius <= max_radius: + params = { + 'QueryPosition': [longitude, latitude], + 'MaxResults': max_results, + 'QueryRadius': int(current_radius), + 'AdditionalFeatures': ['Contact'], + } + response = geo_places_client.geo_places_client.search_nearby(**params) + items = response.get('ResultItems', []) + results = [] + for item in items: + if mode == 'raw': + results.append(item) + else: + contacts = { + 'phones': [p['Value'] for p in item.get('Contacts', {}).get('Phones', [])] + if item.get('Contacts') + else [], + 'websites': [ + w['Value'] for w in item.get('Contacts', {}).get('Websites', []) + ] + if item.get('Contacts') + else [], + 'emails': [e['Value'] for e in item.get('Contacts', {}).get('Emails', [])] + if item.get('Contacts') + else [], + 'faxes': [f['Value'] for f in item.get('Contacts', {}).get('Faxes', [])] + if item.get('Contacts') + else [], + } + + def parse_opening_hours(result): + oh = result.get('OpeningHours') + if not oh: + contacts = result.get('Contacts', {}) + oh = contacts.get('OpeningHours') if contacts else None + if not oh: + return [] + if isinstance(oh, dict): + oh = [oh] + parsed = [] + for entry in oh: + parsed.append( + { + 'display': entry.get('Display', []) + or entry.get('display', []), + 'components': entry.get('Components', []) + or entry.get('components', []), + 'open_now': entry.get('OpenNow', None), + 'categories': [ + cat.get('Name') for cat in entry.get('Categories', []) + ] + if 'Categories' in entry + else [], + } + ) + return parsed + + opening_hours = parse_opening_hours(item) + results.append( + { + 'place_id': item.get('PlaceId', 'Not available'), + 'name': item.get('Title', 'Not available'), + 'address': item.get('Address', {}).get('Label', 'Not available'), + 'coordinates': { + 'longitude': item.get('Position', [None, None])[0], + 'latitude': item.get('Position', [None, None])[1], + }, + 'categories': [cat.get('Name') for cat in item.get('Categories', [])] + if item.get('Categories') + else [], + 'contacts': contacts, + 'opening_hours': opening_hours, + } + ) + if results: + return {'places': results, 'radius_used': current_radius} + current_radius *= expansion_factor + return {'places': [], 'radius_used': current_radius / expansion_factor} + except Exception as e: + print(f'search_nearby error: {e}') + await ctx.error(f'search_nearby error: {e}') + return {'error': str(e)} + + +@mcp.tool() +async def search_places_open_now( + ctx: Context, + query: str = Field(description='Search query (address, place name, etc.)'), + initial_radius: int = Field( + default=500, description='Initial search radius in meters for expansion', ge=1, le=50000 + ), +) -> Dict: + """Search for places that are open now using AWS Location Service geo-places search_text API and filter by opening hours. If no open places, expand the search radius up to max_radius. Uses BiasPosition from geocode.""" + # Moved from parameters to local variables + max_results = 5 # Maximum number of results to return + max_radius = 50000 # Maximum search radius in meters for expansion + expansion_factor = 2.0 # Factor to expand radius by if no open places + # Descriptions: + # max_results: Maximum number of results to return (default=5, ge=1, le=50) + # max_radius: Maximum search radius in meters for expansion (default=50000, ge=1, le=50000) + # expansion_factor: Factor to expand radius by if no open places (default=2.0, ge=1.1, le=10.0) + if not geo_places_client.geo_places_client: + error_msg = ( + 'AWS geo-places client not initialized. Please check AWS credentials and region.' + ) + logger.error(error_msg) + await ctx.error(error_msg) + return {'error': error_msg} + logger.debug(f'Searching for places open now with query: {query}, max_results: {max_results}') + try: + geo_response = geo_places_client.geo_places_client.geocode(QueryText=query) + geo_items = geo_response.get('ResultItems', []) + if not geo_items: + error_msg = f'Could not geocode query "{query}" for BiasPosition.' + logger.error(error_msg) + await ctx.error(error_msg) + return {'error': error_msg} + bias_position = geo_items[0]['Position'] + current_radius = initial_radius + open_places = [] + all_places = [] + first_attempt = True + while current_radius <= max_radius and len(open_places) < max_results: + search_kwargs = { + 'QueryText': query, + 'MaxResults': max_results * 2, # Fetch more to allow filtering + 'AdditionalFeatures': ['Contact'], + } + if first_attempt: + # Use BiasPosition for the first (smallest) search + search_kwargs['BiasPosition'] = bias_position + first_attempt = False + else: + # Use Filter.Circle for expanded radius searches + search_kwargs['Filter'] = { + 'Circle': {'Center': bias_position, 'Radius': int(current_radius)} + } + response = geo_places_client.geo_places_client.search_text(**search_kwargs) + result_items = response.get('ResultItems', []) + for idx, result in enumerate(result_items): + opening_hours = result.get('OpeningHours') + open_now = False + opening_hours_info = [] + if isinstance(opening_hours, list): + for oh in opening_hours: + display = oh.get('Display', []) + is_open = oh.get('OpenNow', False) + categories = ( + [cat.get('Name') for cat in oh.get('Categories', [])] + if 'Categories' in oh + else [] + ) + opening_hours_info.append( + {'display': display, 'open_now': is_open, 'categories': categories} + ) + if is_open: + open_now = True + elif isinstance(opening_hours, dict): + display = opening_hours.get('Display', []) + is_open = opening_hours.get('OpenNow', False) + categories = ( + [cat.get('Name') for cat in opening_hours.get('Categories', [])] + if 'Categories' in opening_hours + else [] + ) + opening_hours_info.append( + {'display': display, 'open_now': is_open, 'categories': categories} + ) + if is_open: + open_now = True + if not open_now and 'Contacts' in result: + contacts = result['Contacts'] + ch = contacts.get('OpeningHours') + if isinstance(ch, list): + for oh in ch: + if oh.get('OpenNow', False): + open_now = True + break + elif isinstance(ch, dict): + if ch.get('OpenNow', False): + open_now = True + place_data = { + 'place_id': result.get('PlaceId', ''), + 'name': result.get('Title', 'Unknown'), + 'coordinates': { + 'longitude': result.get('Position', [0, 0])[0], + 'latitude': result.get('Position', [0, 0])[1], + }, + 'address': result.get('Address', {}).get('Label', ''), + 'country': result.get('Address', {}).get('Country', {}).get('Name', ''), + 'region': result.get('Address', {}).get('Region', {}).get('Name', ''), + 'municipality': result.get('Address', {}).get('Locality', ''), + 'categories': [cat.get('Name') for cat in result.get('Categories', [])], + 'contacts': result.get('Contacts', {}), + 'opening_hours': opening_hours_info, + 'open_now': open_now, + } + all_places.append(place_data) + if open_now and len(open_places) < max_results: + open_places.append(place_data) + if open_places: + break + current_radius *= expansion_factor + if not open_places: + print( + 'search_places_open_now: No places found open now after expanding radius. Check OpeningHours and OpenNow fields above.' + ) + result = { + 'query': query, + 'open_places': open_places, + 'all_places': all_places, + 'radius_used': current_radius / expansion_factor, + } + logger.debug(f'Found {len(open_places)} places open now for query: {query}') + return result + except botocore.exceptions.ClientError as e: + error_msg = f'AWS geo-places Service error: {str(e)}' + logger.error(error_msg) + await ctx.error(error_msg) + return {'error': error_msg} + except Exception as e: + error_msg = f'Error searching for open places: {str(e)}' + logger.error(error_msg) + await ctx.error(error_msg) + return {'error': str(e)} + + +@mcp.tool() +async def calculate_route( + ctx: Context, + departure_position: list = Field(description='Departure position as [longitude, latitude]'), + destination_position: list = Field( + description='Destination position as [longitude, latitude]' + ), + travel_mode: str = Field( + default='Car', + description="Travel mode: 'Car', 'Truck', 'Walking', or 'Bicycle' (default: 'Car')", + ), + optimize_for: str = Field( + default='FastestRoute', + description="Optimize route for 'FastestRoute' or 'ShortestRoute' (default: 'FastestRoute')", + ), +) -> dict: + """Calculate a route and return summary info and turn-by-turn directions. + + Parameters: + departure_position: [lon, lat] + destination_position: [lon, lat] + travel_mode: 'Car', 'Truck', 'Walking', or 'Bicycle' (default: 'Car') + optimize_for: 'FastestRoute' or 'ShortestRoute' (default: 'FastestRoute') + + Returns: + dict with distance, duration, and turn_by_turn directions (list of step summaries) + """ + include_leg_geometry = False + mode = 'summary' + client = GeoRoutesClient().geo_routes_client + + # Check if client is None before proceeding + if client is None: + return {'error': 'Failed to initialize AWS geo-routes client'} + + params = { + 'Origin': departure_position, + 'Destination': destination_position, + 'TravelMode': travel_mode, + 'TravelStepType': 'TurnByTurn', + 'OptimizeRoutingFor': optimize_for, + } + if include_leg_geometry: + params['LegGeometryFormat'] = 'FlexiblePolyline' + try: + response = await asyncio.to_thread(client.calculate_routes, **params) + if mode == 'raw': + return response + routes = response.get('Routes', []) + if not routes: + return {'error': 'No route found'} + route = routes[0] + distance_meters = route.get('Distance', None) + duration_seconds = route.get('DurationSeconds', None) + turn_by_turn = [] + for leg in route.get('Legs', []): + vehicle_leg_details = leg.get('VehicleLegDetails', {}) + for step in vehicle_leg_details.get('TravelSteps', []): + step_summary = { + 'distance_meters': step.get('Distance'), + 'duration_seconds': step.get('Duration'), + 'type': step.get('Type'), + 'road_name': step.get('NextRoad', {}).get('RoadName') + if step.get('NextRoad') + else None, + } + turn_by_turn.append(step_summary) + return { + 'distance_meters': distance_meters, + 'duration_seconds': duration_seconds, + 'turn_by_turn': turn_by_turn, + } + except Exception as e: + return {'error': str(e)} + + +@mcp.tool() +async def optimize_waypoints( + ctx: Context, + origin_position: list = Field(description='Origin position as [longitude, latitude]'), + destination_position: list = Field( + description='Destination position as [longitude, latitude]' + ), + waypoints: list = Field( + description='List of intermediate waypoints, each as a dict with at least Position [longitude, latitude], optionally Id' + ), + travel_mode: str = Field( + default='Car', + description="Travel mode: 'Car', 'Truck', 'Walking', or 'Bicycle' (default: 'Car')", + ), + mode: str = Field( + default='summary', + description="Output mode: 'summary' (default) or 'raw' for all AWS fields", + ), +) -> Dict: + """Optimize the order of waypoints using AWS Location Service geo-routes optimize_waypoints API (V2). + + Returns summary (optimized order, total distance, duration, etc.) or full response if mode='raw'. + """ + client = GeoRoutesClient().geo_routes_client + + # Check if client is None before proceeding + if client is None: + return {'error': 'Failed to initialize AWS geo-routes client'} + + params = { + 'Origin': origin_position, + 'Destination': destination_position, + 'Waypoints': [{'Position': wp['Position']} for wp in waypoints], + 'TravelMode': travel_mode, + } + try: + response = await asyncio.to_thread(client.optimize_waypoints, **params) + if mode == 'raw': + return response + routes = response.get('Routes', []) + if not routes: + return {'error': 'No route found'} + route = routes[0] + distance_meters = route.get('Distance', None) + duration_seconds = route.get('DurationSeconds', None) + optimized_order = [wp.get('Position') for wp in route.get('Waypoints', [])] + return { + 'distance_meters': distance_meters, + 'duration_seconds': duration_seconds, + 'optimized_order': optimized_order, + } + except Exception as e: + # import traceback + # return {'error': str(e), 'traceback': traceback.format_exc()} + return {'error': str(e)} + + +def main(): + """Run the MCP server with CLI argument support.""" + parser = argparse.ArgumentParser( + description='An AWS Labs Model Context Protocol (MCP) server for AWS Location Service (geo-places)' + ) + parser.add_argument('--sse', action='store_true', help='Use SSE transport') + parser.add_argument('--port', type=int, default=8888, help='Port to run the server on') + args = parser.parse_args() + logger.info('Starting AWS Location Service MCP Server (geo-places)') + if args.sse: + logger.info(f'Using SSE transport on port {args.port}') + mcp.settings.port = args.port + mcp.run(transport='sse') + else: + logger.info('Using standard stdio transport') + mcp.run() + + +if __name__ == '__main__': + main() diff --git a/src/aws-location-mcp-server/docker-healthcheck.sh b/src/aws-location-mcp-server/docker-healthcheck.sh new file mode 100755 index 0000000000..836c99765f --- /dev/null +++ b/src/aws-location-mcp-server/docker-healthcheck.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# Check if the AWS Location MCP server process is running and has open socket connections +if [ "$(lsof +c 0 -p 1 | grep -e grep -e "^awslabs\..*\s1\s.*\unix\s.*socket$" | wc -l)" -ne "0" ]; then + echo -n "$(lsof +c 0 -p 1 | grep -e grep -e "^awslabs\..*\s1\s.*\unix\s.*socket$" | wc -l) awslabs.* streams found"; + exit 0; +else + echo -n "Zero awslabs.* streams found"; + exit 1; +fi; + +echo -n "Never should reach here"; +exit 99; diff --git a/src/aws-location-mcp-server/pyproject.toml b/src/aws-location-mcp-server/pyproject.toml new file mode 100644 index 0000000000..e94d32d257 --- /dev/null +++ b/src/aws-location-mcp-server/pyproject.toml @@ -0,0 +1,123 @@ +[project] +name = "awslabs.aws-location-mcp-server" +version = "1.0.0" +description = "An AWS Labs Model Context Protocol (MCP) server for AWS Location Service" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "boto3>=1.34.0", + "mcp[cli]>=1.6.0", + "pydantic>=2.10.6", + "loguru>=0.7.0", +] +license = {text = "Apache-2.0"} +license-files = ["LICENSE", "NOTICE" ] + +authors = [ + {name = "Amazon Web Services"}, + {name = "AWSLabs MCP", email="203918161+awslabs-mcp@users.noreply.github.com"}, +] +classifiers = [ + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[project.scripts] +"awslabs.aws-location-mcp-server" = "awslabs.aws_location_server.server:main" + +[project.urls] +Homepage = "https://awslabs.github.io/mcp/" +Documentation = "https://awslabs.github.io/mcp/servers/aws-location-mcp-server/" +Source = "https://github.com/awslabs/mcp.git" +"Bug Tracker" = "https://github.com/awslabs/mcp/issues" +Changelog = "https://github.com/awslabs/mcp/blob/main/src/aws-location-mcp-server/CHANGELOG.md" + +[dependency-groups] +dev = [ + "commitizen>=4.2.2", + "pre-commit>=4.1.0", + "ruff>=0.9.7", + "pyright>=1.1.398", + "pytest>=7.4.0", + "pytest-cov>=4.1.0", + "pytest-mock>=3.11.1", + "pytest-asyncio>=0.26.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.ruff] +line-length = 99 +extend-include = ["*.ipynb"] +exclude = [ + ".venv", + "**/__pycache__", + "**/node_modules", + "**/dist", + "**/build", + "**/env", + "**/.ruff_cache", + "**/.venv", + "**/.ipynb_checkpoints" +] +force-exclude = true + +[tool.ruff.lint] +exclude = ["__init__.py"] +select = ["C", "D", "E", "F", "I", "W"] +ignore = ["C901", "E501", "E741", "F402", "F823", "D100", "D106"] + +[tool.ruff.lint.isort] +lines-after-imports = 2 +no-sections = true + +[tool.ruff.lint.per-file-ignores] +"**/*.ipynb" = ["F704"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = true + +[tool.commitizen] +name = "cz_conventional_commits" +version = "1.0.0" +tag_format = "v$version" +version_files = [ + "pyproject.toml:version", + "awslabs/aws_location_server/__init__.py:__version__" +] +update_changelog_on_bump = true + +[tool.hatch.build.targets.wheel] +packages = ["awslabs"] + +[tool.pytest.ini_options] +markers = [ + "live: marks tests that make live API calls (deselect with '-m \"not live\"')", + "asyncio: marks tests that use asyncio" +] +asyncio_mode = "strict" +asyncio_default_fixture_loop_scope = "function" + +[tool.bandit] +exclude_dirs = [".venv", "venv", "tests"] + +[tool.coverage.run] +source = ["awslabs"] diff --git a/src/aws-location-mcp-server/tests/__init__.py b/src/aws-location-mcp-server/tests/__init__.py new file mode 100644 index 0000000000..849e56a588 --- /dev/null +++ b/src/aws-location-mcp-server/tests/__init__.py @@ -0,0 +1 @@ +"""AWS Location Service MCP Server tests package.""" diff --git a/src/aws-location-mcp-server/tests/conftest.py b/src/aws-location-mcp-server/tests/conftest.py new file mode 100644 index 0000000000..04bb1a2c28 --- /dev/null +++ b/src/aws-location-mcp-server/tests/conftest.py @@ -0,0 +1,51 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance +# with the License. A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# and limitations under the License. +"""Pytest configuration for AWS Location Service MCP Server tests.""" + +import pytest +from unittest.mock import MagicMock, patch + + +@pytest.fixture +def mock_boto3_client(): + """Create a mock boto3 client for testing.""" + mock_client = MagicMock() + + # Mock search_place_index_for_text response + mock_client.search_place_index_for_text.return_value = { + 'Results': [ + { + 'Place': { + 'Label': 'Seattle, WA, USA', + 'Geometry': {'Point': [-122.3321, 47.6062]}, + 'Country': 'USA', + 'Region': 'Washington', + 'Municipality': 'Seattle', + } + } + ] + } + + with patch('boto3.client', return_value=mock_client): + yield mock_client + + +@pytest.fixture +def mock_context(): + """Create a mock MCP context for testing.""" + context = MagicMock() + + # Make the error method awaitable + async def async_error(*args, **kwargs): + return None + + context.error = MagicMock(side_effect=async_error) + return context diff --git a/src/aws-location-mcp-server/tests/run_integration_tests.sh b/src/aws-location-mcp-server/tests/run_integration_tests.sh new file mode 100755 index 0000000000..4c52d95b24 --- /dev/null +++ b/src/aws-location-mcp-server/tests/run_integration_tests.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")" + +VENV_DIR=".venv" + +if [ ! -d "$VENV_DIR" ]; then + echo "Creating Python virtual environment..." + python3 -m venv "$VENV_DIR" +fi + +source "$VENV_DIR/bin/activate" + +# Install dependencies using uv if available, else fallback to pip +if command -v uv &> /dev/null; then + echo "Installing dependencies with uv..." + uv pip install -r ../uv.lock || uv pip install -r ../pyproject.toml +else + echo "Installing dependencies with pip..." + pip install --upgrade pip + pip install -r ../requirements.txt || pip install -r ../pyproject.toml +fi + +# Run the integration test script +export PYTHONPATH="$(cd .. && pwd):$PYTHONPATH" +echo "Running integration tests (test_server_integration.py)..." +python test_server_integration.py + +RESULT=$? +if [ $RESULT -eq 0 ]; then + echo "Integration tests completed successfully." +else + echo "Integration tests failed with exit code $RESULT." +fi diff --git a/src/aws-location-mcp-server/tests/test_server.py b/src/aws-location-mcp-server/tests/test_server.py new file mode 100644 index 0000000000..617a31dd17 --- /dev/null +++ b/src/aws-location-mcp-server/tests/test_server.py @@ -0,0 +1,984 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance +# with the License. A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions +# and limitations under the License. +"""Tests for AWS Location Service MCP Server.""" + +import pytest + +# Import the functions directly to avoid Field validation issues +from awslabs.aws_location_server.server import ( + GeoPlacesClient, + GeoRoutesClient, + calculate_route, + get_place, + main, + optimize_waypoints, + reverse_geocode, + search_places, + search_places_open_now, +) +from unittest.mock import MagicMock, patch + + +@pytest.mark.asyncio +async def test_search_places(mock_boto3_client, mock_context): + """Test the search_places tool.""" + # Set up test data + query = 'Seattle' + max_results = 5 + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places(mock_context, query=query, max_results=max_results) + + # Verify the result + assert result['query'] == query + assert 'places' in result + + +@pytest.mark.asyncio +async def test_search_places_error_no_client(mock_context): + """Test search_places when client is not initialized.""" + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = None + result = await search_places(mock_context, query='Seattle') + + assert 'error' in result + assert 'AWS geo-places client not initialized' in result['error'] + + +@pytest.mark.asyncio +async def test_search_places_geocode_error(mock_boto3_client, mock_context): + """Test search_places when geocode returns no results.""" + # Set up geocode to return empty results + mock_boto3_client.geocode.return_value = {'ResultItems': []} + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places(mock_context, query='NonexistentPlace') + + assert 'error' in result + assert 'Could not geocode query' in result['error'] + + +@pytest.mark.asyncio +async def test_search_places_client_error(mock_boto3_client, mock_context): + """Test search_places when boto3 client raises an error.""" + from botocore.exceptions import ClientError + + # Set up boto3 client to raise ClientError + mock_boto3_client.geocode.side_effect = ClientError( + {'Error': {'Code': 'TestException', 'Message': 'Test error message'}}, 'geocode' + ) + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places(mock_context, query='Seattle') + + assert 'error' in result + assert 'AWS geo-places Service error' in result['error'] + + +@pytest.mark.asyncio +async def test_search_places_general_exception(mock_boto3_client, mock_context): + """Test search_places when a general exception occurs.""" + # Set up boto3 client to raise a general exception + mock_boto3_client.geocode.side_effect = Exception('Test general exception') + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places(mock_context, query='Seattle') + + assert 'error' in result + assert 'Error searching places' in result['error'] + + +@pytest.mark.asyncio +async def test_get_place(mock_boto3_client, mock_context): + """Test the get_place tool.""" + # Set up mock response + mock_boto3_client.get_place.return_value = { + 'Title': 'Test Place', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + 'Categories': [{'Name': 'Restaurant'}], + 'Contacts': { + 'Phones': [{'Value': '123-456-7890'}], + 'Websites': [{'Value': 'https://example.com'}], + 'Emails': [], + 'Faxes': [], + }, + } + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await get_place(mock_context, place_id='test-place-id') + + # Verify the result + assert result['name'] == 'Test Place' + assert result['address'] == '123 Test St, Test City, TS' + assert result['coordinates']['longitude'] == -122.3321 + assert result['coordinates']['latitude'] == 47.6062 + assert result['categories'] == ['Restaurant'] + assert result['contacts']['phones'] == ['123-456-7890'] + assert result['contacts']['websites'] == ['https://example.com'] + + +@pytest.mark.asyncio +async def test_get_place_raw_mode(mock_boto3_client, mock_context): + """Test the get_place tool with raw mode.""" + # Set up mock response + mock_response = { + 'Title': 'Test Place', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + } + mock_boto3_client.get_place.return_value = mock_response + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await get_place(mock_context, place_id='test-place-id', mode='raw') + + # Verify the raw result is returned + assert result == mock_response + + +@pytest.mark.asyncio +async def test_get_place_error_no_client(mock_context): + """Test get_place when client is not initialized.""" + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = None + result = await get_place(mock_context, place_id='test-place-id') + + assert 'error' in result + assert 'AWS geo-places client not initialized' in result['error'] + + +@pytest.mark.asyncio +async def test_get_place_exception(mock_boto3_client, mock_context): + """Test get_place when an exception occurs.""" + # Set up boto3 client to raise an exception + mock_boto3_client.get_place.side_effect = Exception('Test exception') + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await get_place(mock_context, place_id='test-place-id') + + assert 'error' in result + assert 'Test exception' in result['error'] + + +@pytest.mark.asyncio +async def test_reverse_geocode(mock_boto3_client, mock_context): + """Test the reverse_geocode tool.""" + # Set up mock response + mock_boto3_client.reverse_geocode.return_value = { + 'Place': { + 'Label': '123 Test St, Test City, TS', + 'Title': 'Test Place', + 'Geometry': {'Point': [-122.3321, 47.6062]}, + 'Categories': [{'Name': 'Restaurant'}], + 'Address': {'Label': '123 Test St, Test City, TS'}, + } + } + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await reverse_geocode(mock_context, longitude=-122.3321, latitude=47.6062) + + # Verify the result + assert result['name'] == '123 Test St, Test City, TS' + assert result['address'] == '123 Test St, Test City, TS' + assert result['coordinates']['longitude'] == -122.3321 + assert result['coordinates']['latitude'] == 47.6062 + assert result['categories'] == ['Restaurant'] + + +@pytest.mark.asyncio +async def test_reverse_geocode_no_place(mock_boto3_client, mock_context): + """Test reverse_geocode when no place is found.""" + # Set up mock response with no Place + mock_boto3_client.reverse_geocode.return_value = {'SomeOtherField': 'value'} + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await reverse_geocode(mock_context, longitude=-122.3321, latitude=47.6062) + + # Verify the raw response is returned + assert 'raw_response' in result + assert result['raw_response'] == {'SomeOtherField': 'value'} + + +@pytest.mark.asyncio +async def test_reverse_geocode_error_no_client(mock_context): + """Test reverse_geocode when client is not initialized.""" + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = None + result = await reverse_geocode(mock_context, longitude=-122.3321, latitude=47.6062) + + assert 'error' in result + assert 'AWS geo-places client not initialized' in result['error'] + + +@pytest.mark.asyncio +async def test_reverse_geocode_client_error(mock_boto3_client, mock_context): + """Test reverse_geocode when boto3 client raises a ClientError.""" + from botocore.exceptions import ClientError + + # Set up boto3 client to raise ClientError + mock_boto3_client.reverse_geocode.side_effect = ClientError( + {'Error': {'Code': 'TestException', 'Message': 'Test error message'}}, 'reverse_geocode' + ) + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await reverse_geocode(mock_context, longitude=-122.3321, latitude=47.6062) + + assert 'error' in result + assert 'AWS geo-places Service error' in result['error'] + + +@pytest.mark.asyncio +async def test_reverse_geocode_general_exception(mock_boto3_client, mock_context): + """Test reverse_geocode when a general exception occurs.""" + # Set up boto3 client to raise a general exception + mock_boto3_client.reverse_geocode.side_effect = Exception('Test general exception') + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await reverse_geocode(mock_context, longitude=-122.3321, latitude=47.6062) + + assert 'error' in result + assert 'Error in reverse geocoding' in result['error'] + + +@pytest.mark.asyncio +async def test_search_nearby(mock_boto3_client, mock_context): + """Test the search_nearby tool.""" + # Set up mock response + mock_boto3_client.search_nearby.return_value = { + 'ResultItems': [ + { + 'PlaceId': 'test-place-id', + 'Title': 'Test Place', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + 'Categories': [{'Name': 'Restaurant'}], + 'Contacts': { + 'Phones': [{'Value': '123-456-7890'}], + 'Websites': [{'Value': 'https://example.com'}], + 'Emails': [], + 'Faxes': [], + }, + } + ] + } + + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import search_nearby as search_nearby_func + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_nearby_func( + mock_context, + longitude=-122.3321, + latitude=47.6062, + radius=500, + ) + + # Verify the result + assert 'places' in result + assert len(result['places']) == 1 + assert result['places'][0]['name'] == 'Test Place' + assert result['places'][0]['address'] == '123 Test St, Test City, TS' + assert result['places'][0]['coordinates']['longitude'] == -122.3321 + assert result['places'][0]['coordinates']['latitude'] == 47.6062 + assert result['places'][0]['categories'] == ['Restaurant'] + assert result['places'][0]['contacts']['phones'] == ['123-456-7890'] + assert result['places'][0]['contacts']['websites'] == ['https://example.com'] + assert 'radius_used' in result + + +@pytest.mark.asyncio +async def test_search_nearby_raw_mode(mock_boto3_client, mock_context): + """Test the search_nearby tool with raw mode.""" + # Set up mock response + mock_boto3_client.search_nearby.return_value = { + 'ResultItems': [ + { + 'PlaceId': 'test-place-id', + 'Title': 'Test Place', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + } + ] + } + + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import search_nearby as search_nearby_func + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_nearby_func( + mock_context, + longitude=-122.3321, + latitude=47.6062, + radius=500, + ) + + # Verify the raw result is returned + assert 'places' in result + assert len(result['places']) == 1 + assert result['places'][0]['place_id'] == 'test-place-id' + assert result['places'][0]['name'] == 'Test Place' + + +@pytest.mark.asyncio +async def test_search_nearby_no_results_expansion(mock_boto3_client, mock_context): + """Test search_nearby with radius expansion when no results are found.""" + # Set up mock response to return empty results first, then results on second call + mock_boto3_client.search_nearby.side_effect = [ + {'ResultItems': []}, # First call with initial radius + { # Second call with expanded radius + 'ResultItems': [ + { + 'PlaceId': 'test-place-id', + 'Title': 'Test Place', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + } + ] + }, + ] + + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import search_nearby as search_nearby_func + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_nearby_func( + mock_context, + longitude=-122.3321, + latitude=47.6062, + radius=500, + ) + + # Verify the result with expanded radius + assert 'places' in result + assert len(result['places']) == 1 + assert result['radius_used'] == 1000 # 500 * 2.0 + + +@pytest.mark.asyncio +async def test_search_nearby_error_no_client(mock_context): + """Test search_nearby when client is not initialized.""" + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import search_nearby as search_nearby_func + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = None + result = await search_nearby_func( + mock_context, + longitude=-122.3321, + latitude=47.6062, + radius=500, + ) + + assert 'error' in result + assert 'AWS geo-places client not initialized' in result['error'] + + +@pytest.mark.asyncio +async def test_search_nearby_exception(mock_boto3_client, mock_context): + """Test search_nearby when an exception occurs.""" + # Set up boto3 client to raise an exception + mock_boto3_client.search_nearby.side_effect = Exception('Test exception') + + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import search_nearby as search_nearby_func + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_nearby_func( + mock_context, + longitude=-122.3321, + latitude=47.6062, + radius=500, + ) + + assert 'error' in result + assert 'Test exception' in result['error'] + + +@pytest.mark.asyncio +async def test_search_places_open_now(mock_boto3_client, mock_context): + """Test the search_places_open_now tool.""" + # Set up mock responses + mock_boto3_client.geocode.return_value = {'ResultItems': [{'Position': [-122.3321, 47.6062]}]} + mock_boto3_client.search_text.return_value = { + 'ResultItems': [ + { + 'PlaceId': 'test-place-id', + 'Title': 'Test Place', + 'Address': { + 'Label': '123 Test St, Test City, TS', + 'Country': {'Name': 'USA'}, + 'Region': {'Name': 'WA'}, + 'Locality': 'Seattle', + }, + 'Position': [-122.3321, 47.6062], + 'Categories': [{'Name': 'Restaurant'}], + 'Contacts': { + 'Phones': [{'Value': '123-456-7890'}], + 'OpeningHours': {'Display': ['Mon-Fri: 9AM-5PM'], 'OpenNow': True}, + }, + } + ] + } + + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import ( + search_places_open_now as search_places_open_now_func, + ) + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places_open_now_func( + mock_context, + query='restaurants Seattle', + initial_radius=500, + ) + + # Verify the result + assert 'query' in result + assert 'open_places' in result + assert len(result['open_places']) == 1 + assert result['open_places'][0]['name'] == 'Test Place' + assert result['open_places'][0]['open_now'] is True + assert 'all_places' in result + assert 'radius_used' in result + + +@pytest.mark.asyncio +async def test_search_places_open_now_no_geocode_results(mock_boto3_client, mock_context): + """Test search_places_open_now when geocode returns no results.""" + # Set up geocode to return empty results + mock_boto3_client.geocode.return_value = {'ResultItems': []} + + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import ( + search_places_open_now as search_places_open_now_func, + ) + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places_open_now_func( + mock_context, + query='NonexistentPlace', + initial_radius=500, + ) + + assert 'error' in result + assert 'Could not geocode query' in result['error'] + + +@pytest.mark.asyncio +async def test_search_places_open_now_error_no_client(mock_context): + """Test search_places_open_now when client is not initialized.""" + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import ( + search_places_open_now as search_places_open_now_func, + ) + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = None + result = await search_places_open_now_func( + mock_context, + query='restaurants Seattle', + initial_radius=500, + ) + + assert 'error' in result + assert 'AWS geo-places client not initialized' in result['error'] + + +@pytest.mark.asyncio +async def test_search_places_open_now_client_error(mock_boto3_client, mock_context): + """Test search_places_open_now when boto3 client raises a ClientError.""" + from botocore.exceptions import ClientError + + # Set up boto3 client to raise ClientError + mock_boto3_client.geocode.side_effect = ClientError( + {'Error': {'Code': 'TestException', 'Message': 'Test error message'}}, 'geocode' + ) + + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import ( + search_places_open_now as search_places_open_now_func, + ) + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places_open_now_func( + mock_context, + query='restaurants Seattle', + initial_radius=500, + ) + + assert 'error' in result + assert 'AWS geo-places Service error' in result['error'] + + +@pytest.mark.asyncio +async def test_search_places_open_now_general_exception(mock_boto3_client, mock_context): + """Test search_places_open_now when a general exception occurs.""" + # Set up boto3 client to raise a general exception + mock_boto3_client.geocode.side_effect = Exception('Test general exception') + + # Import the function directly to avoid Field validation issues + from awslabs.aws_location_server.server import ( + search_places_open_now as search_places_open_now_func, + ) + + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places_open_now_func( + mock_context, + query='restaurants Seattle', + initial_radius=500, + ) + + assert 'error' in result + assert 'Test general exception' in result['error'] + + +def test_geo_places_client_initialization(monkeypatch): + """Test the GeoPlacesClient initialization.""" + # NOTE: No AWS credentials are set or required for this test. All AWS calls are mocked. + monkeypatch.setenv('AWS_REGION', 'us-west-2') + with patch('boto3.client') as mock_boto3_client: + _ = GeoPlacesClient() + mock_boto3_client.assert_called_once() + args, kwargs = mock_boto3_client.call_args + assert args[0] == 'geo-places' + assert kwargs['region_name'] == 'us-west-2' + + +@pytest.mark.asyncio +async def test_calculate_route(mock_boto3_client, mock_context): + """Test the calculate_route tool.""" + # Set up mock response + mock_response = { + 'Routes': [ + { + 'Distance': 100.0, + 'DurationSeconds': 300, + 'Legs': [ + { + 'Distance': 100.0, + 'DurationSeconds': 300, + 'VehicleLegDetails': { + 'TravelSteps': [ + { + 'Distance': 50.0, + 'Duration': 150, + 'StartPosition': [-122.335167, 47.608013], + 'EndPosition': [-122.300000, 47.600000], + 'Type': 'Straight', + 'NextRoad': {'RoadName': 'Test Road'}, + }, + { + 'Distance': 50.0, + 'Duration': 150, + 'StartPosition': [-122.300000, 47.600000], + 'EndPosition': [-122.200676, 47.610149], + 'Type': 'Turn', + 'NextRoad': {'RoadName': 'Another Road'}, + }, + ] + }, + } + ], + } + ] + } + + # Create a mock for the calculate_route function + with patch('awslabs.aws_location_server.server.GeoRoutesClient') as mock_geo_client: + # Set up the mock to return our mock_boto3_client + mock_geo_client.return_value.geo_routes_client = mock_boto3_client + + # Mock the asyncio.to_thread function to return the mock response directly + with patch('asyncio.to_thread', return_value=mock_response): + # Call the function + result = await calculate_route( + mock_context, + departure_position=[-122.335167, 47.608013], + destination_position=[-122.200676, 47.610149], + travel_mode='Car', + optimize_for='FastestRoute', + ) + + # Verify the result + assert 'distance_meters' in result + assert 'duration_seconds' in result + assert 'turn_by_turn' in result + assert len(result['turn_by_turn']) == 2 + assert result['turn_by_turn'][0]['road_name'] == 'Test Road' + assert result['turn_by_turn'][1]['road_name'] == 'Another Road' + + +@pytest.mark.asyncio +async def test_calculate_route_error(mock_boto3_client, mock_context): + """Test the calculate_route tool when an error occurs.""" + # Set up boto3 client to raise ClientError + mock_boto3_client.calculate_routes.side_effect = Exception('Test error') + + # Patch the geo_routes_client in the server module + with patch('awslabs.aws_location_server.server.GeoRoutesClient') as mock_geo_client: + mock_geo_client.return_value.geo_routes_client = mock_boto3_client + + # Mock asyncio.to_thread to propagate the exception + with patch('asyncio.to_thread', side_effect=Exception('Test error')): + result = await calculate_route( + mock_context, + departure_position=[-122.335167, 47.608013], + destination_position=[-122.200676, 47.610149], + travel_mode='Car', + optimize_for='FastestRoute', + ) + + # Verify the result + assert 'error' in result + assert 'Test error' in result['error'] + + +@pytest.mark.asyncio +async def test_optimize_waypoints(mock_boto3_client, mock_context): + """Test the optimize_waypoints tool.""" + # Set up mock response + mock_boto3_client.optimize_waypoints.return_value = { + 'Routes': [ + { + 'Distance': 150.0, + 'DurationSeconds': 450, + 'Waypoints': [ + {'Position': [-122.200676, 47.610149]}, + ], + } + ], + } + + # Patch the geo_routes_client in the server module + with patch('awslabs.aws_location_server.server.GeoRoutesClient') as mock_geo_client: + mock_geo_client.return_value.geo_routes_client = mock_boto3_client + + # Mock asyncio.to_thread to return the mock response directly + with patch( + 'asyncio.to_thread', return_value=mock_boto3_client.optimize_waypoints.return_value + ): + result = await optimize_waypoints( + mock_context, + origin_position=[-122.335167, 47.608013], + destination_position=[-122.121513, 47.673988], + waypoints=[{'Position': [-122.200676, 47.610149]}], + travel_mode='Car', + mode='summary', + ) + + # Verify the result + assert 'distance_meters' in result + assert 'duration_seconds' in result + assert 'optimized_order' in result + assert len(result['optimized_order']) == 1 + + +@pytest.mark.asyncio +async def test_optimize_waypoints_error(mock_boto3_client, mock_context): + """Test the optimize_waypoints tool when an error occurs.""" + # Set up boto3 client to raise Exception + mock_boto3_client.optimize_waypoints.side_effect = Exception('Test error') + + # Patch the geo_routes_client in the server module + with patch('awslabs.aws_location_server.server.GeoRoutesClient') as mock_geo_client: + mock_geo_client.return_value.geo_routes_client = mock_boto3_client + + # Mock asyncio.to_thread to propagate the exception + with patch('asyncio.to_thread', side_effect=Exception('Test error')): + result = await optimize_waypoints( + mock_context, + origin_position=[-122.335167, 47.608013], + destination_position=[-122.121513, 47.673988], + waypoints=[{'Position': [-122.200676, 47.610149]}], + travel_mode='Car', + mode='summary', + ) + + # Verify the result + assert 'error' in result + assert 'Test error' in result['error'] + + +def test_geo_routes_client_initialization(monkeypatch): + """Test the GeoRoutesClient initialization.""" + monkeypatch.setenv('AWS_REGION', 'us-west-2') + + with patch('boto3.client') as mock_boto3_client: + _ = GeoRoutesClient() + mock_boto3_client.assert_called_once() + args, kwargs = mock_boto3_client.call_args + assert args[0] == 'geo-routes' + assert kwargs['region_name'] == 'us-west-2' + + +def test_geo_routes_client_initialization_with_credentials(monkeypatch): + """Test the GeoRoutesClient initialization with explicit credentials.""" + monkeypatch.setenv('AWS_REGION', 'us-west-2') + monkeypatch.setenv( + 'AWS_ACCESS_KEY_ID', 'AKIAIOSFODNN7EXAMPLE' + ) # pragma: allowlist secret - Test credential for unit tests only + monkeypatch.setenv( + 'AWS_SECRET_ACCESS_KEY', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' + ) # pragma: allowlist secret - Test credential for unit tests only + + with patch('boto3.client') as mock_boto3_client: + _ = GeoRoutesClient() + mock_boto3_client.assert_called_once() + args, kwargs = mock_boto3_client.call_args + assert args[0] == 'geo-routes' + assert kwargs['region_name'] == 'us-west-2' + assert kwargs['aws_access_key_id'] == 'AKIAIOSFODNN7EXAMPLE' + assert kwargs['aws_secret_access_key'] == 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' + + +def test_geo_routes_client_initialization_exception(): + """Test the GeoRoutesClient initialization when an exception occurs.""" + with patch('boto3.client', side_effect=Exception('Test exception')): + geo_client = GeoRoutesClient() + assert geo_client.geo_routes_client is None + + +def test_main_stdio(): + """Test the main function with stdio transport.""" + with patch('awslabs.aws_location_server.server.mcp.run') as mock_run: + with patch('argparse.ArgumentParser.parse_args') as mock_parse_args: + mock_parse_args.return_value = MagicMock(sse=False, port=8888) + main() + mock_run.assert_called_once() + args, kwargs = mock_run.call_args + assert kwargs.get('transport') is None + + +def test_main_sse(): + """Test the main function with SSE transport.""" + with patch('awslabs.aws_location_server.server.mcp.run') as mock_run: + with patch('argparse.ArgumentParser.parse_args') as mock_parse_args: + mock_parse_args.return_value = MagicMock(sse=True, port=9999) + main() + mock_run.assert_called_once() + args, kwargs = mock_run.call_args + assert kwargs.get('transport') == 'sse' + + +@pytest.mark.asyncio +async def test_search_places_with_bounding_box(mock_boto3_client, mock_context): + """Test search_places with bounding box filter when initial search returns no results.""" + # Set up mock responses + mock_boto3_client.geocode.return_value = {'ResultItems': [{'Position': [-122.3321, 47.6062]}]} + + # First search_text call returns empty results, second call with bounding box returns results + mock_boto3_client.search_text.side_effect = [ + {'ResultItems': []}, # First call returns empty + { # Second call with bounding box returns results + 'ResultItems': [ + { + 'PlaceId': 'test-place-id', + 'Title': 'Test Place', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + 'Categories': [{'Name': 'Restaurant'}], + } + ] + }, + ] + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places(mock_context, query='Seattle', max_results=5) + + # Verify the result + assert 'places' in result + assert len(result['places']) == 1 + assert result['places'][0]['name'] == 'Test Place' + + +@pytest.mark.asyncio +async def test_search_places_with_opening_hours(mock_boto3_client, mock_context): + """Test search_places with opening hours in the response.""" + # Set up mock responses + mock_boto3_client.geocode.return_value = {'ResultItems': [{'Position': [-122.3321, 47.6062]}]} + mock_boto3_client.search_text.return_value = { + 'ResultItems': [ + { + 'PlaceId': 'test-place-id', + 'Title': 'Test Place', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + 'Categories': [{'Name': 'Restaurant'}], + 'Contacts': { + 'OpeningHours': { + 'Display': ['Mon-Fri: 9AM-5PM'], + 'OpenNow': True, + 'Components': [{'DayOfWeek': 'Monday', 'Hours': '9AM-5PM'}], + } + }, + } + ] + } + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places(mock_context, query='Seattle', max_results=5) + + # Verify the result + assert 'places' in result + assert len(result['places']) == 1 + assert result['places'][0]['name'] == 'Test Place' + assert len(result['places'][0]['opening_hours']) == 1 + assert result['places'][0]['opening_hours'][0]['open_now'] is True + assert result['places'][0]['opening_hours'][0]['display'] == ['Mon-Fri: 9AM-5PM'] + + +@pytest.mark.asyncio +async def test_search_places_open_now_with_contacts_opening_hours(mock_boto3_client, mock_context): + """Test search_places_open_now with opening hours in Contacts.""" + # Set up mock responses + mock_boto3_client.geocode.return_value = {'ResultItems': [{'Position': [-122.3321, 47.6062]}]} + mock_boto3_client.search_text.return_value = { + 'ResultItems': [ + { + 'PlaceId': 'test-place-id', + 'Title': 'Test Place', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + 'Categories': [{'Name': 'Restaurant'}], + 'Contacts': { + 'OpeningHours': { + 'Display': ['Mon-Fri: 9AM-5PM'], + 'OpenNow': True, + } + }, + } + ] + } + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places_open_now( + mock_context, + query='restaurants Seattle', + initial_radius=500, + ) + + # Verify the result + assert 'open_places' in result + assert len(result['open_places']) == 1 + assert result['open_places'][0]['name'] == 'Test Place' + assert result['open_places'][0]['open_now'] is True + + +@pytest.mark.asyncio +async def test_search_places_open_now_with_expanded_radius(mock_boto3_client, mock_context): + """Test search_places_open_now with radius expansion.""" + # Set up mock responses + mock_boto3_client.geocode.return_value = {'ResultItems': [{'Position': [-122.3321, 47.6062]}]} + + # First search returns no open places, second search with expanded radius returns open places + mock_boto3_client.search_text.side_effect = [ + { # First call returns places but none are open + 'ResultItems': [ + { + 'PlaceId': 'test-place-id-1', + 'Title': 'Test Place 1', + 'Address': {'Label': '123 Test St, Test City, TS'}, + 'Position': [-122.3321, 47.6062], + 'Categories': [{'Name': 'Restaurant'}], + 'OpeningHours': {'Display': ['Mon-Fri: 9AM-5PM'], 'OpenNow': False}, + } + ] + }, + { # Second call with expanded radius returns open places + 'ResultItems': [ + { + 'PlaceId': 'test-place-id-2', + 'Title': 'Test Place 2', + 'Address': {'Label': '456 Test St, Test City, TS'}, + 'Position': [-122.3421, 47.6162], + 'Categories': [{'Name': 'Restaurant'}], + 'OpeningHours': {'Display': ['Mon-Fri: 9AM-5PM'], 'OpenNow': True}, + } + ] + }, + ] + + # Patch the geo_places_client in the server module + with patch('awslabs.aws_location_server.server.geo_places_client') as mock_geo_client: + mock_geo_client.geo_places_client = mock_boto3_client + result = await search_places_open_now( + mock_context, + query='restaurants Seattle', + initial_radius=500, + ) + + # Verify the result + assert 'open_places' in result + assert len(result['open_places']) == 1 + assert result['open_places'][0]['name'] == 'Test Place 2' + assert result['open_places'][0]['open_now'] is True + assert result['radius_used'] == 500.0 # Initial radius + + +def test_geo_places_client_initialization_with_credentials(monkeypatch): + """Test the GeoPlacesClient initialization with explicit credentials.""" + monkeypatch.setenv('AWS_REGION', 'us-west-2') + monkeypatch.setenv( + 'AWS_ACCESS_KEY_ID', 'AKIAIOSFODNN7EXAMPLE' + ) # pragma: allowlist secret - Test credential for unit tests only + monkeypatch.setenv( + 'AWS_SECRET_ACCESS_KEY', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' + ) # pragma: allowlist secret - Test credential for unit tests only + + with patch('boto3.client') as mock_boto3_client: + _ = GeoPlacesClient() + mock_boto3_client.assert_called_once() + args, kwargs = mock_boto3_client.call_args + assert args[0] == 'geo-places' + assert kwargs['region_name'] == 'us-west-2' + assert kwargs['aws_access_key_id'] == 'AKIAIOSFODNN7EXAMPLE' + assert kwargs['aws_secret_access_key'] == 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' + + +def test_geo_places_client_initialization_exception(): + """Test the GeoPlacesClient initialization when an exception occurs.""" + with patch('boto3.client', side_effect=Exception('Test exception')): + geo_client = GeoPlacesClient() + assert geo_client.geo_places_client is None diff --git a/src/aws-location-mcp-server/tests/test_server_integration.py b/src/aws-location-mcp-server/tests/test_server_integration.py new file mode 100644 index 0000000000..82e9dd1611 --- /dev/null +++ b/src/aws-location-mcp-server/tests/test_server_integration.py @@ -0,0 +1,366 @@ +import asyncio +import logging +import os +import pytest +from awslabs.aws_location_server.server import ( + calculate_route, + get_place, + optimize_waypoints, + reverse_geocode, + search_nearby, + search_places, + search_places_open_now, +) +from mcp.server.fastmcp import Context + + +# Set up a logger instead of using print for sensitive data +logger = logging.getLogger('integration_tests') +logger.setLevel(logging.INFO) +# Only log to console during development, not in production +handler = logging.StreamHandler() +formatter = logging.Formatter('[%(levelname)s] %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) + + +class DummyContext(Context): + """Dummy context for testing.""" + + async def error(self, message=None, **extra): + """Handle error messages for DummyContext.""" + logger.error(message) + + async def run_in_threadpool(self, func, *args, **kwargs): + """Run a function in a threadpool.""" + return func(*args, **kwargs) + + +def log_place(place): + """Log details of a place for integration test output.""" + # Avoid logging potentially sensitive information + # Only log non-sensitive fields + if place: + logger.info('Place details:') + if 'name' in place: + logger.info(f'Name: {place.get("name")}') + if 'address' in place: + # Address could contain PII, so we'll just log that it exists + logger.info('Address: [Address information available]') + + # Log categories as they're generally not sensitive + if 'categories' in place and place.get('categories'): + logger.info(f'Categories: {", ".join(place.get("categories", []))}') + + # Log that coordinates exist but not their values + if 'coordinates' in place and place.get('coordinates'): + logger.info('Coordinates: [Coordinate information available]') + + # Log that contact info exists but not the actual values + if 'contacts' in place and place.get('contacts'): + logger.info('Contact information: [Available]') + + logger.info('-') + + +@pytest.fixture +def ctx(): + """Create a dummy context for testing.""" + return DummyContext(_request_context=None, _fastmcp=None) + + +@pytest.mark.skipif( + not (os.environ.get('AWS_ACCESS_KEY_ID') or os.environ.get('AWS_PROFILE')), + reason='AWS credentials not set', +) +@pytest.mark.asyncio +async def test_calculate_route_princeton_to_columbus(ctx): + """Test route calculation between Princeton, NJ and Columbus, OH.""" + logger.info('\n=== calculate_route (Princeton, NJ to Columbus, OH) ===') + departure = [-74.66446, 40.36076] # Princeton, NJ + destination = [-83.00275, 39.96199] # Columbus, OH + route_result = await calculate_route( + ctx, + departure_position=departure, + destination_position=destination, + travel_mode='Car', + optimize_for='FastestRoute', + ) + if 'error' in route_result: + logger.info(f'calculate_route error: {route_result["error"]}') + if 'traceback' in route_result: + logger.info(f'Traceback: {route_result["traceback"]}') + else: + logger.info(f'Route distance: {route_result.get("distance_meters")}') + logger.info(f'Route duration: {route_result.get("duration_seconds")}') + logger.info(f'Legs: {route_result.get("legs")}') + turn_by_turn = route_result.get('turn_by_turn', []) + if turn_by_turn: + logger.info(f'Turn-by-turn directions ({len(turn_by_turn)} steps):') + for i, step in enumerate(turn_by_turn[:10]): + logger.info(f'Step {i + 1}: {step}') + else: + logger.warning('No turn-by-turn directions found in route result!') + + +@pytest.mark.skipif( + not (os.environ.get('AWS_ACCESS_KEY_ID') or os.environ.get('AWS_PROFILE')), + reason='AWS credentials not set', +) +@pytest.mark.asyncio +async def test_calculate_route_and_optimize_waypoints(ctx): + """Test route calculation and waypoint optimization between Seattle, Bellevue, and Redmond.""" + logger.info('\n=== calculate_route ===') + # Example: Seattle to Bellevue + departure = [-122.335167, 47.608013] # Seattle + destination = [-122.200676, 47.610149] # Bellevue + route_result = await calculate_route( + ctx, + departure_position=departure, + destination_position=destination, + travel_mode='Car', + optimize_for='FastestRoute', + ) + if 'error' in route_result: + logger.info(f'calculate_route error: {route_result["error"]}') + if 'traceback' in route_result: + logger.info(f'Traceback: {route_result["traceback"]}') + else: + logger.info(f'Route distance: {route_result.get("distance_meters")}') + logger.info(f'Route duration: {route_result.get("duration_seconds")}') + logger.info(f'Legs: {route_result.get("legs")}') + turn_by_turn = route_result.get('turn_by_turn', []) + if turn_by_turn: + logger.info(f'Turn-by-turn directions ({len(turn_by_turn)} steps):') + for i, step in enumerate(turn_by_turn[:10]): + logger.info(f'Step {i + 1}: {step}') + else: + logger.warning('No turn-by-turn directions found in route result!') + # New: Check steps in each leg + for leg_idx, leg in enumerate(route_result.get('legs', [])): + steps = leg.get('steps', []) + logger.info(f'Leg {leg_idx + 1} has {len(steps)} steps.') + for step in steps[:3]: # Show first 3 steps for brevity + logger.info(f' Step: {step.get("instruction")}') + + logger.info('\n=== optimize_waypoints ===') + # Example: Seattle (origin), Bellevue (waypoint), Redmond (destination) + origin = [-122.335167, 47.608013] # Seattle + waypoint = {'Id': 'bellevue', 'Position': [-122.200676, 47.610149]} + destination = [-122.121513, 47.673988] # Redmond + optimize_result = await optimize_waypoints( + ctx, + origin_position=origin, + destination_position=destination, + waypoints=[waypoint], + travel_mode='Car', + mode='summary', + ) + if 'error' in optimize_result: + logger.info(f'optimize_waypoints error: {optimize_result["error"]}') + else: + logger.info(f'Optimized order: {optimize_result.get("optimized_order")}') + logger.info(f'Total distance: {optimize_result.get("total_distance_meters")} meters') + logger.info(f'Total duration: {optimize_result.get("total_duration_seconds")} seconds') + for wp in optimize_result.get('waypoints', []): + logger.info( + f'Waypoint: {wp["id"]} at {wp["position"]} (Arrival: {wp["arrival_time"]}, Departure: {wp["departure_time"]})' + ) + + +async def main(): + """Run integration tests for AWS Location MCP server.""" + # Skip the main function since we're using fixtures now + if not (os.environ.get('AWS_ACCESS_KEY_ID') or os.environ.get('AWS_PROFILE')): + logger.error('AWS credentials not set.') + return + if not os.environ.get('AWS_REGION'): + logger.error('AWS_REGION not set.') + return + + logger.info('\n=== search_places (POI query) ===') + search_result = await search_places(ctx, query='Starbucks, Seattle', max_results=3) + places = search_result.get('places', []) + if not places: + logger.info('No places found in search_places.') + return + + logger.info(f'Found {len(places)} places') + for place in places: + log_place(place) + + # Use the first place_id and coordinates for further tests + first_place = places[0] + place_id = first_place.get('place_id', '') + # Don't log the actual place_id as it could be considered sensitive + has_place_id = bool(place_id) + + # Store coordinates for testing but don't log them + longitude = first_place.get('coordinates', {}).get('longitude', None) + latitude = first_place.get('coordinates', {}).get('latitude', None) + has_coordinates = ( + longitude is not None and latitude is not None and longitude != 0 and latitude != 0 + ) + + logger.info('\n=== get_place ===') + if has_place_id: + get_place_result = await get_place(ctx, place_id=place_id) + if get_place_result.get('name') == 'Unknown' or not get_place_result.get('address'): + logger.info('No valid data found in get_place.') + else: + log_place(get_place_result) + else: + logger.info('No valid place_id found for get_place test.') + + logger.info('\n=== reverse_geocode ===') + if has_coordinates: + reverse_geocode_result = await reverse_geocode(ctx, longitude=longitude, latitude=latitude) + logger.info('Reverse geocode result:') + # Don't log the actual address or coordinates + if 'address' in reverse_geocode_result: + logger.info('Address: [Address information available]') + if 'coordinates' in reverse_geocode_result: + logger.info('Coordinates: [Coordinate information available]') + else: + logger.info('No valid coordinates found for reverse_geocode test.') + + logger.info('\n=== search_nearby (with radius expansion) ===') + if has_coordinates: + # Start with a very small radius to force expansion + search_nearby_result = await search_nearby( + ctx, + longitude=longitude, + latitude=latitude, + max_results=3, + radius=10, + ) + nearby_places = search_nearby_result.get('places', []) + radius_used = search_nearby_result.get('radius_used', None) + if not nearby_places: + logger.info( + f'No places found in search_nearby after expanding radius up to {radius_used}m.' + ) + else: + logger.info(f'Found {len(nearby_places)} places with radius {radius_used}m:') + for place in nearby_places: + log_place(place) + else: + logger.info('No valid coordinates found for search_nearby test.') + + logger.info('\n=== search_places_open_now (with radius expansion) ===') + query = 'Starbucks, Seattle' + open_now_result = await search_places_open_now(ctx, query=query, initial_radius=10) + logger.info(f'Query: {query}') + open_places = open_now_result.get('open_places', []) + radius_used = open_now_result.get('radius_used', None) + if not open_places: + logger.info(f'No places found open now after expanding radius up to {radius_used}m.') + else: + logger.info(f'{len(open_places)} places open now (radius used: {radius_used}m):') + for place in open_places: + logger.info(f'Name: {place.get("name")}') + # Don't log the actual address + logger.info('Address: [Address information available]') + logger.info(f'Open Now: {place.get("open_now")}') + + # Log opening hours without specific details + if place.get('opening_hours'): + logger.info( + f'Opening Hours: [Available - {len(place.get("opening_hours"))} entries]' + ) + logger.info('-') + + logger.info('\n=== search_places_open_now (7-Eleven, New York, with radius expansion) ===') + query_7e = '7-Eleven, New York' + open_now_result_7e = await search_places_open_now( + ctx, + query=query_7e, + initial_radius=10, + ) + logger.info(f'Query: {query_7e}') + open_places_7e = open_now_result_7e.get('open_places', []) + radius_used_7e = open_now_result_7e.get('radius_used', None) + if not open_places_7e: + logger.info(f'No places found open now after expanding radius up to {radius_used_7e}m.') + else: + logger.info(f'{len(open_places_7e)} places open now (radius used: {radius_used_7e}m):') + for place in open_places_7e: + logger.info(f'Name: {place.get("name")}') + # Don't log the actual address + logger.info('Address: [Address information available]') + logger.info(f'Open Now: {place.get("open_now")}') + + # Log opening hours without specific details + if place.get('opening_hours'): + logger.info( + f'Opening Hours: [Available - {len(place.get("opening_hours"))} entries]' + ) + logger.info('-') + + logger.info('\n=== search_places_open_now (mall, Princeton, NJ, with radius expansion) ===') + query_mall = 'mall, Princeton, NJ' + open_now_result_mall = await search_places_open_now( + ctx, + query=query_mall, + initial_radius=10, + ) + logger.info(f'Query: {query_mall}') + open_places_mall = open_now_result_mall.get('open_places', []) + radius_used_mall = open_now_result_mall.get('radius_used', None) + if not open_places_mall: + logger.info(f'No malls found open now after expanding radius up to {radius_used_mall}m.') + else: + logger.info(f'{len(open_places_mall)} malls open now (radius used: {radius_used_mall}m):') + for place in open_places_mall: + logger.info(f'Name: {place.get("name")}') + # Don't log the actual address + logger.info('Address: [Address information available]') + logger.info(f'Open Now: {place.get("open_now")}') + + # Log opening hours without specific details + if place.get('opening_hours'): + logger.info( + f'Opening Hours: [Available - {len(place.get("opening_hours"))} entries]' + ) + logger.info('-') + + logger.info('\n=== search_places (mall, Princeton, NJ, with operating hours) ===') + query_mall = 'mall, Princeton, NJ' + search_result_mall = await search_places(ctx, query=query_mall, max_results=3) + places_mall = search_result_mall.get('places', []) + if not places_mall: + logger.info('No malls found in search_places.') + else: + logger.info(f'{len(places_mall)} malls found:') + for place in places_mall: + log_place(place) + + # Additional POI test cases + test_cases = [ + ('hospital, Boston, MA', 5), + ('school, Palo Alto, CA', 5), + ('restaurant, Paris, France', 5), + ('gas station, Houston, TX', 5), + ('pharmacy, Tokyo, Japan', 5), + ('cafe, London, UK', 2), # To confirm optional result count + ] + for query, max_results in test_cases: + logger.info(f'\n=== search_places ({query}, max_results={max_results}) ===') + search_result = await search_places( + ctx, query=query, max_results=max_results, mode='summary' + ) + places = search_result.get('places', []) + if not places: + logger.info(f"No places found for query '{query}'.") + else: + logger.info(f'{len(places)} places found:') + for place in places: + log_place(place) + await test_calculate_route_and_optimize_waypoints(ctx) + await test_calculate_route_princeton_to_columbus(ctx) + + logger.info('Integration tests completed successfully.') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/src/aws-location-mcp-server/uv.lock b/src/aws-location-mcp-server/uv.lock new file mode 100644 index 0000000000..64ba55bb92 --- /dev/null +++ b/src/aws-location-mcp-server/uv.lock @@ -0,0 +1,1124 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "argcomplete" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/be/6c23d80cb966fb8f83fb1ebfb988351ae6b0554d0c3a613ee4531c026597/argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392", size = 72999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/08/2a4db06ec3d203124c967fc89295e85a202e5cbbcdc08fd6a64b65217d1e/argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61", size = 43569 }, +] + +[[package]] +name = "awslabs-aws-location-mcp-server" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "boto3" }, + { name = "loguru" }, + { name = "mcp", extra = ["cli"] }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "commitizen" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = ">=1.34.0" }, + { name = "loguru", specifier = ">=0.7.0" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, + { name = "pydantic", specifier = ">=2.10.6" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "commitizen", specifier = ">=4.2.2" }, + { name = "pre-commit", specifier = ">=4.1.0" }, + { name = "pyright", specifier = ">=1.1.398" }, + { name = "pytest", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", specifier = ">=0.26.0" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "pytest-mock", specifier = ">=3.11.1" }, + { name = "ruff", specifier = ">=0.9.7" }, +] + +[[package]] +name = "boto3" +version = "1.37.35" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/5f/e356ecd2f236e6ddc7711eaf3f075c15b13e2d044cfdb47719d49c4ae7dd/boto3-1.37.35.tar.gz", hash = "sha256:751ed599c8fd9ca24896edcd6620e8a32b3db1b68efea3a90126312240e668a2", size = 111640 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/e4/00958f65ac74ab0a76af33f16c8fdf5726a5c6f0d3c0d0c058ff0dd00fd7/boto3-1.37.35-py3-none-any.whl", hash = "sha256:5a90d674830adbaf86456d6b27a18f5f11378277da5286511fa860d2e7b14261", size = 139922 }, +] + +[[package]] +name = "botocore" +version = "1.37.35" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/0b/d281d74d53f7d4733402aed7a536275084fa344a2672f7ea4dbc8ebe1f1b/botocore-1.37.35.tar.gz", hash = "sha256:197a9bf8251c45b9d882c405ec0d0ab40c10e2d2a55ee66960185daec4beb6ec", size = 13821053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/00/bf9c894f5af8e35b06ecf757d4a95883408e71c48642dc7f8760580584fd/botocore-1.37.35-py3-none-any.whl", hash = "sha256:50839212e90650d0b0fa6b8f7514876bf802f6164f2775f3abcd4d53c98bb73c", size = 13485892 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "commitizen" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/82/fd3037d648a1735377f1912c96569c0c3159a2db3a37972e659d2c9ed1ea/commitizen-4.6.0.tar.gz", hash = "sha256:cc1c9f8937e59a7c54321443aa49dd246e07b829e305c7cbff1d7f7e32e449fe", size = 53118 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/06/7745bf8a3621d74afd52c542658baf1fcd888a90ea9d3014450598356c46/commitizen-4.6.0-py3-none-any.whl", hash = "sha256:d8861707b553c03c8b1993b7abd9e036384fdd1c57f95f6f38d5f215c53041a9", size = 75692 }, +] + +[[package]] +name = "coverage" +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379 }, + { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814 }, + { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937 }, + { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986 }, + { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896 }, + { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613 }, + { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909 }, + { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948 }, + { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844 }, + { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, + { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, + { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, + { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, + { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, + { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, + { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, + { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, + { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, + { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, + { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, + { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, + { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, + { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, + { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, + { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, + { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, + { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, + { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, + { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, + { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "decli" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/a0/a4658f93ecb589f479037b164dc13c68d108b50bf6594e54c820749f97ac/decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f", size = 7424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/70/3ea48dc9e958d7d66c44c9944809181f1ca79aaef25703c023b5092d34ff/decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed", size = 7854 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "identify" +version = "2.6.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mcp" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, +] + +[[package]] +name = "pydantic" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, + { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, + { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, + { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, + { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, + { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, + { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, + { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, + { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, + { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, + { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, + { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, + { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, + { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, + { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, + { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, + { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, + { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, + { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, + { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, + { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, + { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, + { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, + { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, + { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, + { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, + { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, + { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, + { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, + { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, + { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, + { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, + { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, + { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, + { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, + { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, + { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, + { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, + { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, + { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, + { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, + { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, + { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, + { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pyright" +version = "1.1.399" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "questionary" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747 }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, +] + +[[package]] +name = "ruff" +version = "0.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/11/bcef6784c7e5d200b8a1f5c2ddf53e5da0efec37e6e5a44d163fb97e04ba/ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79", size = 4010053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/1f/8848b625100ebcc8740c8bac5b5dd8ba97dd4ee210970e98832092c1635b/ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1", size = 10248105 }, + { url = "https://files.pythonhosted.org/packages/e0/47/c44036e70c6cc11e6ee24399c2a1e1f1e99be5152bd7dff0190e4b325b76/ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de", size = 11001494 }, + { url = "https://files.pythonhosted.org/packages/ed/5b/170444061650202d84d316e8f112de02d092bff71fafe060d3542f5bc5df/ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a", size = 10352151 }, + { url = "https://files.pythonhosted.org/packages/ff/91/f02839fb3787c678e112c8865f2c3e87cfe1744dcc96ff9fc56cfb97dda2/ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193", size = 10541951 }, + { url = "https://files.pythonhosted.org/packages/9e/f3/c09933306096ff7a08abede3cc2534d6fcf5529ccd26504c16bf363989b5/ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e", size = 10079195 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/a87f8933fccbc0d8c653cfbf44bedda69c9582ba09210a309c066794e2ee/ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308", size = 11698918 }, + { url = "https://files.pythonhosted.org/packages/52/7d/8eac0bd083ea8a0b55b7e4628428203441ca68cd55e0b67c135a4bc6e309/ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55", size = 12319426 }, + { url = "https://files.pythonhosted.org/packages/c2/dc/d0c17d875662d0c86fadcf4ca014ab2001f867621b793d5d7eef01b9dcce/ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc", size = 11791012 }, + { url = "https://files.pythonhosted.org/packages/f9/f3/81a1aea17f1065449a72509fc7ccc3659cf93148b136ff2a8291c4bc3ef1/ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2", size = 13949947 }, + { url = "https://files.pythonhosted.org/packages/61/9f/a3e34de425a668284e7024ee6fd41f452f6fa9d817f1f3495b46e5e3a407/ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6", size = 11471753 }, + { url = "https://files.pythonhosted.org/packages/df/c5/4a57a86d12542c0f6e2744f262257b2aa5a3783098ec14e40f3e4b3a354a/ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2", size = 10417121 }, + { url = "https://files.pythonhosted.org/packages/58/3f/a3b4346dff07ef5b862e2ba06d98fcbf71f66f04cf01d375e871382b5e4b/ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03", size = 10073829 }, + { url = "https://files.pythonhosted.org/packages/93/cc/7ed02e0b86a649216b845b3ac66ed55d8aa86f5898c5f1691797f408fcb9/ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b", size = 11076108 }, + { url = "https://files.pythonhosted.org/packages/39/5e/5b09840fef0eff1a6fa1dea6296c07d09c17cb6fb94ed5593aa591b50460/ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9", size = 11512366 }, + { url = "https://files.pythonhosted.org/packages/6f/4c/1cd5a84a412d3626335ae69f5f9de2bb554eea0faf46deb1f0cb48534042/ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287", size = 10485900 }, + { url = "https://files.pythonhosted.org/packages/42/46/8997872bc44d43df986491c18d4418f1caff03bc47b7f381261d62c23442/ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e", size = 11558592 }, + { url = "https://files.pythonhosted.org/packages/d7/6a/65fecd51a9ca19e1477c3879a7fda24f8904174d1275b419422ac00f6eee/ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79", size = 10682766 }, +] + +[[package]] +name = "s3transfer" +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/ec/aa1a215e5c126fe5decbee2e107468f51d9ce190b9763cb649f76bb45938/s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679", size = 148419 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/62/8d3fc3ec6640161a5649b2cddbbf2b9fa39c92541225b33f117c37c5a2eb/s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d", size = 84412 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sse-starlette" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, +] + +[[package]] +name = "termcolor" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + +[[package]] +name = "typer" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, +] + +[[package]] +name = "virtualenv" +version = "20.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, +]