diff --git a/demo_comparison.py b/demo_comparison.py index b92662a..c693e1d 100644 --- a/demo_comparison.py +++ b/demo_comparison.py @@ -3,97 +3,105 @@ import asyncio import sys -import os # Add the package to Python path -sys.path.insert(0, '/tmp/a/improve-top-packages') +sys.path.insert(0, "/tmp/a/improve-top-packages") + async def demo_improvements(): """Demonstrate the improvements made to get_top_packages_by_downloads.""" - + print("๐Ÿš€ PyPI Top Packages Tool - Improvement Demonstration") print("=" * 60) - + print("\n๐Ÿ“‹ PROBLEM ANALYSIS:") print("- Original implementation relied solely on pypistats.org API") print("- When API returns 502 errors (as currently), tool returns empty results") print("- No fallback mechanism for reliability") print("- Limited package data and context") - + print("\n๐Ÿ”ง SOLUTION IMPLEMENTED:") print("โœ… Multi-tier fallback strategy:") print(" 1. Try real PyPI download stats from pypistats.org") print(" 2. Fall back to curated popular packages database") print(" 3. Enhance with real-time GitHub popularity metrics") print(" 4. Always return meaningful results") - + print("โœ… Comprehensive curated database:") print(" - 100+ popular packages across categories") print(" - Realistic download estimates based on historical data") print(" - Package metadata (category, description, use case)") - + print("โœ… GitHub API integration:") print(" - Real-time star counts and repository metrics") print(" - Popularity-based download estimate adjustments") print(" - Additional metadata (language, topics, activity)") - + print("โœ… Robust error handling:") print(" - Graceful degradation when APIs fail") print(" - Intelligent caching for performance") print(" - Detailed methodology reporting") - + # Import and test the improved function from pypi_query_mcp.tools.download_stats import get_top_packages_by_downloads - + print("\n๐Ÿงช TESTING IMPROVED IMPLEMENTATION:") print("-" * 40) - + try: # Test with current API state (likely failing) - result = await get_top_packages_by_downloads('month', 8) - + result = await get_top_packages_by_downloads("month", 8) + print(f"โœ… SUCCESS! Returned {len(result.get('top_packages', []))} packages") print(f"๐Ÿ“Š Data source: {result.get('data_source')}") print(f"๐Ÿ”ฌ Methodology: {result.get('methodology')}") - - print(f"\n๐Ÿ“ฆ Top 5 packages:") - for i, pkg in enumerate(result.get('top_packages', [])[:5]): - downloads = pkg.get('downloads', 0) - stars = pkg.get('github_stars', 'N/A') - category = pkg.get('category', 'N/A') - estimated = ' (estimated)' if pkg.get('estimated', False) else ' (real stats)' - github_enhanced = ' ๐ŸŒŸ' if pkg.get('github_enhanced', False) else '' - - print(f" {i+1}. {pkg.get('package', 'N/A')}") + + print("\n๐Ÿ“ฆ Top 5 packages:") + for i, pkg in enumerate(result.get("top_packages", [])[:5]): + downloads = pkg.get("downloads", 0) + stars = pkg.get("github_stars", "N/A") + category = pkg.get("category", "N/A") + estimated = ( + " (estimated)" if pkg.get("estimated", False) else " (real stats)" + ) + github_enhanced = " ๐ŸŒŸ" if pkg.get("github_enhanced", False) else "" + + print(f" {i + 1}. {pkg.get('package', 'N/A')}") print(f" Downloads: {downloads:,}{estimated}{github_enhanced}") print(f" Category: {category}") - if stars != 'N/A': + if stars != "N/A": print(f" GitHub: {stars:,} stars") print() - + print("\n๐Ÿ”„ TESTING DIFFERENT SCENARIOS:") print("-" * 30) - + # Test different periods periods_test = {} - for period in ['day', 'week', 'month']: + for period in ["day", "week", "month"]: result = await get_top_packages_by_downloads(period, 3) - avg_downloads = sum(p.get('downloads', 0) for p in result.get('top_packages', [])) // max(len(result.get('top_packages', [])), 1) + avg_downloads = sum( + p.get("downloads", 0) for p in result.get("top_packages", []) + ) // max(len(result.get("top_packages", [])), 1) periods_test[period] = avg_downloads - print(f"โœ… {period}: {len(result.get('top_packages', []))} packages, avg downloads: {avg_downloads:,}") - + print( + f"โœ… {period}: {len(result.get('top_packages', []))} packages, avg downloads: {avg_downloads:,}" + ) + # Verify period scaling makes sense - if periods_test['day'] < periods_test['week'] < periods_test['month']: + if periods_test["day"] < periods_test["week"] < periods_test["month"]: print("โœ… Period scaling works correctly (day < week < month)") - + # Test different limits for limit in [5, 15, 25]: - result = await get_top_packages_by_downloads('month', limit) - packages = result.get('top_packages', []) - real_count = len([p for p in packages if not p.get('estimated', False)]) - github_count = len([p for p in packages if 'github_stars' in p]) - print(f"โœ… Limit {limit}: {len(packages)} packages ({real_count} real, {github_count} GitHub-enhanced)") - + result = await get_top_packages_by_downloads("month", limit) + packages = result.get("top_packages", []) + real_count = len([p for p in packages if not p.get("estimated", False)]) + github_count = len([p for p in packages if "github_stars" in p]) + print( + f"โœ… Limit {limit}: {len(packages)} packages ({real_count} real, {github_count} GitHub-enhanced)" + ) + print("\n๐ŸŽฏ KEY IMPROVEMENTS ACHIEVED:") print("โœ… 100% reliability - always returns results even when APIs fail") print("โœ… Rich metadata - category, description, GitHub stats") @@ -101,16 +109,18 @@ async def demo_improvements(): print("โœ… Performance - intelligent caching and concurrent requests") print("โœ… Transparency - clear methodology and data source reporting") print("โœ… Scalability - supports different periods and limits") - - print(f"\n๐Ÿ† CONCLUSION:") + + print("\n๐Ÿ† CONCLUSION:") print("The improved get_top_packages_by_downloads tool now provides") print("reliable, informative results even when external APIs fail,") print("making it suitable for production use with robust fallbacks.") - + except Exception as e: print(f"โŒ Error during testing: {e}") import traceback + traceback.print_exc() -if __name__ == '__main__': - asyncio.run(demo_improvements()) \ No newline at end of file + +if __name__ == "__main__": + asyncio.run(demo_improvements()) diff --git a/example_usage.py b/example_usage.py index ee71e7e..d44fc92 100644 --- a/example_usage.py +++ b/example_usage.py @@ -10,19 +10,14 @@ This demonstrates how to use the new transitive dependency functionality. # Basic usage (backward compatible) example_1 = { "tool": "get_package_dependencies", - "parameters": { - "package_name": "requests" - } + "parameters": {"package_name": "requests"}, } # Returns: Direct dependencies only (existing behavior) # Enable transitive dependencies example_2 = { - "tool": "get_package_dependencies", - "parameters": { - "package_name": "requests", - "include_transitive": True - } + "tool": "get_package_dependencies", + "parameters": {"package_name": "requests", "include_transitive": True}, } # Returns: Complete dependency tree with analysis @@ -33,8 +28,8 @@ example_3 = { "package_name": "django", "include_transitive": True, "max_depth": 3, - "python_version": "3.11" - } + "python_version": "3.11", + }, } # Returns: Filtered dependency tree for Python 3.11, max 3 levels deep @@ -46,20 +41,18 @@ example_response = { "include_transitive": True, "max_depth": 5, "python_version": "3.10", - # Direct dependencies (same as before) "runtime_dependencies": [ "urllib3>=1.21.1,<3", "certifi>=2017.4.17", "charset-normalizer>=2,<4", - "idna>=2.5,<4" + "idna>=2.5,<4", ], "development_dependencies": [], "optional_dependencies": { "security": ["pyOpenSSL>=0.14", "cryptography>=1.3.4"], - "socks": ["PySocks>=1.5.6,!=1.5.7"] + "socks": ["PySocks>=1.5.6,!=1.5.7"], }, - # NEW: Transitive dependency information "transitive_dependencies": { "dependency_tree": { @@ -71,41 +64,41 @@ example_response = { "package_name": "urllib3", "version": "2.0.4", "depth": 1, - "children": {} + "children": {}, }, "certifi": { - "package_name": "certifi", + "package_name": "certifi", "version": "2023.7.22", "depth": 1, - "children": {} + "children": {}, }, "charset-normalizer": { "package_name": "charset-normalizer", - "version": "3.2.0", + "version": "3.2.0", "depth": 1, - "children": {} + "children": {}, }, "idna": { "package_name": "idna", "version": "3.4", "depth": 1, - "children": {} - } - } + "children": {}, + }, + }, }, "all_packages": { "requests": { "name": "requests", "version": "2.31.0", "depth": 0, - "dependency_count": {"runtime": 4, "development": 0, "total_extras": 0} + "dependency_count": {"runtime": 4, "development": 0, "total_extras": 0}, }, "urllib3": { "name": "urllib3", - "version": "2.0.4", + "version": "2.0.4", "depth": 1, - "dependency_count": {"runtime": 0, "development": 0, "total_extras": 0} - } + "dependency_count": {"runtime": 0, "development": 0, "total_extras": 0}, + }, # ... other packages }, "circular_dependencies": [], @@ -115,11 +108,10 @@ example_response = { "average_depth": 0.8, "shallow_deps": 4, "deep_deps": 0, - "leaf_packages": ["urllib3", "certifi", "charset-normalizer", "idna"] - } + "leaf_packages": ["urllib3", "certifi", "charset-normalizer", "idna"], + }, }, - - # Enhanced summary statistics + # Enhanced summary statistics "dependency_summary": { "direct_runtime_count": 4, "direct_dev_count": 0, @@ -133,27 +125,22 @@ example_response = { "score": 8.2, "level": "low", "recommendation": "Simple dependency structure, low maintenance overhead", - "factors": { - "total_packages": 5, - "max_depth": 1, - "total_dependencies": 4 - } - } + "factors": {"total_packages": 5, "max_depth": 1, "total_dependencies": 4}, + }, }, - # Performance and health analysis "analysis": { "resolution_stats": { "total_packages": 5, "total_runtime_dependencies": 4, - "max_depth": 1 + "max_depth": 1, }, "potential_conflicts": [], "maintenance_concerns": { "total_packages": 5, "packages_without_version_info": 0, "high_dependency_packages": [], - "maintenance_risk_score": {"score": 0.0, "level": "low"} + "maintenance_risk_score": {"score": 0.0, "level": "low"}, }, "performance_impact": { "estimated_install_time_seconds": 15, @@ -162,11 +149,11 @@ example_response = { "recommendations": [], "metrics": { "package_count_impact": "low", - "depth_impact": "low", - "resolution_complexity": "simple" - } - } - } + "depth_impact": "low", + "resolution_complexity": "simple", + }, + }, + }, } # Usage examples for different complexity levels @@ -174,44 +161,44 @@ complexity_examples = { "simple_package": { "package": "six", "expected_packages": 1, # No dependencies - "complexity": "low" + "complexity": "low", }, "moderate_package": { "package": "requests", "expected_packages": 5, # Few dependencies - "complexity": "low" + "complexity": "low", }, "complex_package": { "package": "django", "expected_packages": 15, # Moderate dependencies - "complexity": "moderate" + "complexity": "moderate", }, "very_complex_package": { "package": "tensorflow", "expected_packages": 50, # Many dependencies - "complexity": "high" - } + "complexity": "high", + }, } # Test cases for edge cases edge_case_examples = { "circular_dependencies": { "description": "Package with circular dependency references", - "expected_behavior": "Detected and reported in circular_dependencies array" + "expected_behavior": "Detected and reported in circular_dependencies array", }, "deep_nesting": { - "description": "Package with very deep dependency chains", + "description": "Package with very deep dependency chains", "max_depth": 2, - "expected_behavior": "Truncated at max_depth with depth tracking" + "expected_behavior": "Truncated at max_depth with depth tracking", }, "version_conflicts": { "description": "Dependencies with conflicting version requirements", - "expected_behavior": "Reported in potential_conflicts array" + "expected_behavior": "Reported in potential_conflicts array", }, "missing_packages": { "description": "Dependencies that don't exist on PyPI", - "expected_behavior": "Graceful handling with warnings in logs" - } + "expected_behavior": "Graceful handling with warnings in logs", + }, } print("Enhanced get_package_dependencies Tool") @@ -231,4 +218,4 @@ print("โœ“ Detailed dependency tree structure") print("โœ“ Version conflict detection") print("โœ“ Python version filtering") print() -print("See TRANSITIVE_DEPS_DOCUMENTATION.md for full details.") \ No newline at end of file +print("See TRANSITIVE_DEPS_DOCUMENTATION.md for full details.") diff --git a/examples/extras_usage_demo.py b/examples/extras_usage_demo.py index b749a5c..a05a7e5 100644 --- a/examples/extras_usage_demo.py +++ b/examples/extras_usage_demo.py @@ -7,39 +7,38 @@ to resolve optional dependencies for Python packages. """ import asyncio -import json -from pathlib import Path -from pypi_query_mcp.tools.dependency_resolver import resolve_package_dependencies from pypi_query_mcp.core.pypi_client import PyPIClient +from pypi_query_mcp.tools.dependency_resolver import resolve_package_dependencies async def show_available_extras(package_name: str): """Show what extras are available for a package.""" print(f"\n๐Ÿ“ฆ Available extras for {package_name}:") - + async with PyPIClient() as client: package_data = await client.get_package_info(package_name) - + info = package_data.get("info", {}) provides_extra = info.get("provides_extra", []) requires_dist = info.get("requires_dist", []) or [] - + if provides_extra: print(f" Provides extras: {', '.join(provides_extra)}") else: print(" No provides_extra field found") - + # Find extras from requires_dist extras_in_deps = set() for req in requires_dist: if "extra ==" in req: # Extract extra name from requirement like: pytest>=6.0.0; extra=='test' import re + match = re.search(r'extra\s*==\s*["\']([^"\']+)["\']', req) if match: extras_in_deps.add(match.group(1)) - + if extras_in_deps: print(f" Extras with dependencies: {', '.join(sorted(extras_in_deps))}") else: @@ -48,44 +47,44 @@ async def show_available_extras(package_name: str): async def demo_extras_resolution(): """Demonstrate extras resolution with various packages.""" - + # Examples of packages with well-known extras examples = [ { "package": "requests", "extras": ["socks"], - "description": "HTTP library with SOCKS proxy support" + "description": "HTTP library with SOCKS proxy support", }, { - "package": "django", + "package": "django", "extras": ["argon2", "bcrypt"], - "description": "Web framework with password hashing extras" + "description": "Web framework with password hashing extras", }, { "package": "setuptools", "extras": ["test"], - "description": "Package development tools with testing extras" + "description": "Package development tools with testing extras", }, { "package": "flask", "extras": ["async", "dotenv"], - "description": "Web framework with async and dotenv support" - } + "description": "Web framework with async and dotenv support", + }, ] - + for example in examples: package_name = example["package"] extras = example["extras"] description = example["description"] - - print(f"\n{'='*60}") + + print(f"\n{'=' * 60}") print(f"๐Ÿ” Example: {package_name}") print(f"๐Ÿ“‹ Description: {description}") print(f"๐ŸŽฏ Testing extras: {extras}") - + # Show available extras await show_available_extras(package_name) - + try: # Resolve without extras print(f"\n๐Ÿ“Š Resolving {package_name} WITHOUT extras...") @@ -93,29 +92,33 @@ async def demo_extras_resolution(): package_name=package_name, python_version="3.10", include_extras=[], - max_depth=1 # Limit depth for demo + max_depth=1, # Limit depth for demo ) - + # Resolve with extras print(f"๐Ÿ“Š Resolving {package_name} WITH extras {extras}...") result_with_extras = await resolve_package_dependencies( package_name=package_name, python_version="3.10", include_extras=extras, - max_depth=1 + max_depth=1, ) - + # Compare results - print(f"\n๐Ÿ“ˆ Results comparison:") - print(f" Without extras: {result_no_extras['summary']['total_extra_dependencies']} extra deps") - print(f" With extras: {result_with_extras['summary']['total_extra_dependencies']} extra deps") - + print("\n๐Ÿ“ˆ Results comparison:") + print( + f" Without extras: {result_no_extras['summary']['total_extra_dependencies']} extra deps" + ) + print( + f" With extras: {result_with_extras['summary']['total_extra_dependencies']} extra deps" + ) + # Show actual extras resolved - main_pkg = next(iter(result_with_extras['dependency_tree'].values()), {}) - extras_resolved = main_pkg.get('dependencies', {}).get('extras', {}) - + main_pkg = next(iter(result_with_extras["dependency_tree"].values()), {}) + extras_resolved = main_pkg.get("dependencies", {}).get("extras", {}) + if extras_resolved: - print(f" โœ… Extras resolved successfully:") + print(" โœ… Extras resolved successfully:") for extra_name, deps in extras_resolved.items(): print(f" - {extra_name}: {len(deps)} dependencies") for dep in deps[:2]: # Show first 2 @@ -123,53 +126,55 @@ async def demo_extras_resolution(): if len(deps) > 2: print(f" * ... and {len(deps) - 2} more") else: - print(f" โš ๏ธ No extras resolved (may not exist or have no dependencies)") - + print( + " โš ๏ธ No extras resolved (may not exist or have no dependencies)" + ) + except Exception as e: print(f" โŒ Error: {e}") async def demo_incorrect_usage(): """Demonstrate common mistakes with extras usage.""" - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("โŒ Common Mistakes with Extras") print("='*60") - + mistakes = [ { - "package": "requests", + "package": "requests", "extras": ["dev", "test"], # These don't exist for requests - "error": "Using generic extra names instead of package-specific ones" + "error": "Using generic extra names instead of package-specific ones", }, { "package": "setuptools", "extras": ["testing"], # Should be "test" not "testing" - "error": "Using similar but incorrect extra names" - } + "error": "Using similar but incorrect extra names", + }, ] - + for mistake in mistakes: package_name = mistake["package"] extras = mistake["extras"] error_desc = mistake["error"] - + print(f"\n๐Ÿšซ Mistake: {error_desc}") print(f" Package: {package_name}") print(f" Incorrect extras: {extras}") - + try: result = await resolve_package_dependencies( package_name=package_name, - python_version="3.10", + python_version="3.10", include_extras=extras, - max_depth=1 + max_depth=1, ) - - total_extras = result['summary']['total_extra_dependencies'] + + total_extras = result["summary"]["total_extra_dependencies"] print(f" Result: {total_extras} extra dependencies resolved") if total_extras == 0: - print(f" โš ๏ธ No extras resolved - these extras likely don't exist") - + print(" โš ๏ธ No extras resolved - these extras likely don't exist") + except Exception as e: print(f" โŒ Error: {e}") @@ -181,18 +186,20 @@ async def main(): print() print("This demo shows how to properly use the include_extras parameter") print("to resolve optional dependencies for Python packages.") - + await demo_extras_resolution() await demo_incorrect_usage() - - print(f"\n{'='*60}") + + print(f"\n{'=' * 60}") print("โœจ Demo completed!") print() print("๐Ÿ’ก Key takeaways:") print(" 1. Always check what extras are available for a package first") print(" 2. Use the exact extra names defined by the package") print(" 3. Check package documentation or PyPI page for available extras") - print(" 4. Not all packages have extras, and some extras may have no dependencies") + print( + " 4. Not all packages have extras, and some extras may have no dependencies" + ) print() print("๐Ÿ“š To find available extras:") print(" - Check the package's PyPI page") @@ -202,4 +209,4 @@ async def main(): if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/fallback_test.py b/fallback_test.py index 8b4254d..18bfb9c 100644 --- a/fallback_test.py +++ b/fallback_test.py @@ -2,8 +2,9 @@ """Direct test of fallback mechanisms.""" import asyncio -import sys import os +import sys + sys.path.insert(0, os.path.abspath(".")) from pypi_query_mcp.core.stats_client import PyPIStatsClient @@ -12,29 +13,31 @@ from pypi_query_mcp.core.stats_client import PyPIStatsClient async def test_fallback(): """Test fallback data generation directly.""" print("Testing fallback data generation...") - + async with PyPIStatsClient() as client: # Force API failure tracking to trigger fallback client._api_health["consecutive_failures"] = 5 # Force fallback mode - + # Test recent downloads fallback - fallback_recent = client._generate_fallback_recent_downloads("requests", "month") - print(f"โœ… Fallback recent downloads generated for requests:") + fallback_recent = client._generate_fallback_recent_downloads( + "requests", "month" + ) + print("โœ… Fallback recent downloads generated for requests:") print(f" Source: {fallback_recent.get('source')}") print(f" Downloads: {fallback_recent['data']['last_month']:,}") print(f" Note: {fallback_recent.get('note')}") - - # Test overall downloads fallback + + # Test overall downloads fallback fallback_overall = client._generate_fallback_overall_downloads("numpy", False) - print(f"\nโœ… Fallback time series generated for numpy:") + print("\nโœ… Fallback time series generated for numpy:") print(f" Source: {fallback_overall.get('source')}") print(f" Data points: {len(fallback_overall['data'])}") print(f" Note: {fallback_overall.get('note')}") - + # Test the should_use_fallback logic should_fallback = client._should_use_fallback() print(f"\nโœ… Fallback logic working: {should_fallback}") if __name__ == "__main__": - asyncio.run(test_fallback()) \ No newline at end of file + asyncio.run(test_fallback()) diff --git a/poetry.lock b/poetry.lock index 8209c01..ebd88d3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -14,14 +14,14 @@ files = [ [[package]] name = "anyio" -version = "4.9.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" +version = "4.10.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, + {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, + {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, ] [package.dependencies] @@ -31,8 +31,6 @@ sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -72,14 +70,14 @@ tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" a [[package]] name = "authlib" -version = "1.6.0" +version = "1.6.1" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d"}, - {file = "authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210"}, + {file = "authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e"}, + {file = "authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd"}, ] [package.dependencies] @@ -87,14 +85,14 @@ cryptography = "*" [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, - {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ] [[package]] @@ -192,116 +190,103 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.2" +version = "3.4.3" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, ] [[package]] name = "click" -version = "8.2.2" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.10" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "click-8.2.2-py3-none-any.whl", hash = "sha256:52e1e9f5d3db8c85aa76968c7c67ed41ddbacb167f43201511c8fd61eb5ba2ca"}, - {file = "click-8.2.2.tar.gz", hash = "sha256:068616e6ef9705a07b6db727cb9c248f4eb9dae437a30239f56fa94b18b852ef"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -364,79 +349,100 @@ tomlkit = ">=0.5.3,<1.0.0" [[package]] name = "coverage" -version = "7.8.2" +version = "7.10.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"}, - {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"}, - {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"}, - {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"}, - {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"}, - {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"}, - {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"}, - {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"}, - {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"}, - {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, - {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, - {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, - {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, - {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, - {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"}, - {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"}, - {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"}, - {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"}, - {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"}, - {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"}, - {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"}, - {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"}, - {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"}, - {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"}, - {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"}, - {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"}, - {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"}, - {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"}, - {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"}, - {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, - {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, + {file = "coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe"}, + {file = "coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00"}, + {file = "coverage-7.10.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2a90dd4505d3cc68b847ab10c5ee81822a968b5191664e8a0801778fa60459fa"}, + {file = "coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596"}, + {file = "coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5"}, + {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4"}, + {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:992f48bf35b720e174e7fae916d943599f1a66501a2710d06c5f8104e0756ee1"}, + {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb"}, + {file = "coverage-7.10.3-cp310-cp310-win32.whl", hash = "sha256:9e92fa1f2bd5a57df9d00cf9ce1eb4ef6fccca4ceabec1c984837de55329db34"}, + {file = "coverage-7.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b96524d6e4a3ce6a75c56bb15dbd08023b0ae2289c254e15b9fbdddf0c577416"}, + {file = "coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397"}, + {file = "coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85"}, + {file = "coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157"}, + {file = "coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54"}, + {file = "coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a"}, + {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84"}, + {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160"}, + {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124"}, + {file = "coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8"}, + {file = "coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117"}, + {file = "coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770"}, + {file = "coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42"}, + {file = "coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294"}, + {file = "coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7"}, + {file = "coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437"}, + {file = "coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587"}, + {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea"}, + {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613"}, + {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb"}, + {file = "coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a"}, + {file = "coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5"}, + {file = "coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571"}, + {file = "coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a"}, + {file = "coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a"}, + {file = "coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46"}, + {file = "coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4"}, + {file = "coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a"}, + {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3"}, + {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c"}, + {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21"}, + {file = "coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0"}, + {file = "coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c"}, + {file = "coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87"}, + {file = "coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84"}, + {file = "coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e"}, + {file = "coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f"}, + {file = "coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5"}, + {file = "coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8"}, + {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1"}, + {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256"}, + {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b"}, + {file = "coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e"}, + {file = "coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c"}, + {file = "coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098"}, + {file = "coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de"}, + {file = "coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8"}, + {file = "coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667"}, + {file = "coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4"}, + {file = "coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26"}, + {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a"}, + {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd"}, + {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec"}, + {file = "coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5"}, + {file = "coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833"}, + {file = "coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4"}, + {file = "coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6"}, + {file = "coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241"}, + {file = "coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e"}, + {file = "coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5"}, + {file = "coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b"}, + {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0"}, + {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1"}, + {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c"}, + {file = "coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869"}, + {file = "coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64"}, + {file = "coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35"}, + {file = "coverage-7.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da749daa7e141985487e1ff90a68315b0845930ed53dc397f4ae8f8bab25b551"}, + {file = "coverage-7.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3126fb6a47d287f461d9b1aa5d1a8c97034d1dffb4f452f2cf211289dae74ef"}, + {file = "coverage-7.10.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3da794db13cc27ca40e1ec8127945b97fab78ba548040047d54e7bfa6d442dca"}, + {file = "coverage-7.10.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4e27bebbd184ef8d1c1e092b74a2b7109dcbe2618dce6e96b1776d53b14b3fe8"}, + {file = "coverage-7.10.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fd4ee2580b9fefbd301b4f8f85b62ac90d1e848bea54f89a5748cf132782118"}, + {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6999920bdd73259ce11cabfc1307484f071ecc6abdb2ca58d98facbcefc70f16"}, + {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3623f929db885fab100cb88220a5b193321ed37e03af719efdbaf5d10b6e227"}, + {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:25b902c5e15dea056485d782e420bb84621cc08ee75d5131ecb3dbef8bd1365f"}, + {file = "coverage-7.10.3-cp39-cp39-win32.whl", hash = "sha256:f930a4d92b004b643183451fe9c8fe398ccf866ed37d172ebaccfd443a097f61"}, + {file = "coverage-7.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:08e638a93c8acba13c7842953f92a33d52d73e410329acd472280d2a21a6c0e1"}, + {file = "coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1"}, + {file = "coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619"}, ] [package.dependencies] @@ -447,49 +453,49 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "45.0.4" +version = "45.0.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main"] files = [ - {file = "cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999"}, - {file = "cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750"}, - {file = "cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2"}, - {file = "cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257"}, - {file = "cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8"}, - {file = "cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6"}, - {file = "cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872"}, - {file = "cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4"}, - {file = "cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97"}, - {file = "cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a77c6fb8d76e9c9f99f2f3437c1a4ac287b34eaf40997cfab1e9bd2be175ac39"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b97737a3ffbea79eebb062eb0d67d72307195035332501722a9ca86bab9e3ab2"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4828190fb6c4bcb6ebc6331f01fe66ae838bb3bd58e753b59d4b22eb444b996c"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bbc505d1dc469ac12a0a064214879eac6294038d6b24ae9f71faae1448a9608d"}, - {file = "cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57"}, + {file = "cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42"}, + {file = "cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05"}, + {file = "cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453"}, + {file = "cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159"}, + {file = "cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec"}, + {file = "cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016"}, + {file = "cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3"}, + {file = "cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9"}, + {file = "cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02"}, + {file = "cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043"}, + {file = "cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719"}, ] [package.dependencies] @@ -502,7 +508,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8 pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==45.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==45.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -531,14 +537,14 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "decli" -version = "0.6.2" +version = "0.6.3" description = "Minimal, easy-to-use, declarative cli tool" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed"}, - {file = "decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f"}, + {file = "decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3"}, + {file = "decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656"}, ] [[package]] @@ -562,14 +568,14 @@ cli = ["tomli ; python_version < \"3.11\""] [[package]] name = "distlib" -version = "0.3.9" +version = "0.4.0" description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, - {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] [[package]] @@ -649,7 +655,7 @@ files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] -markers = {dev = "python_version < \"3.11\""} +markers = {dev = "python_version == \"3.10\""} [package.dependencies] typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} @@ -659,14 +665,14 @@ test = ["pytest (>=6)"] [[package]] name = "fastmcp" -version = "2.11.0" +version = "2.11.3" description = "The fast, Pythonic way to build MCP servers and clients." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fastmcp-2.11.0-py3-none-any.whl", hash = "sha256:8709a04522e66fda407b469fbe4d3290651aa7b06097b91c097e9a973c9b9bb3"}, - {file = "fastmcp-2.11.0.tar.gz", hash = "sha256:af0c52988607d8e9197df300e91880169e8fe24fd6ca177dca6a9eb6b245ce3c"}, + {file = "fastmcp-2.11.3-py3-none-any.whl", hash = "sha256:28f22126c90fd36e5de9cc68b9c271b6d832dcf322256f23d220b68afb3352cc"}, + {file = "fastmcp-2.11.3.tar.gz", hash = "sha256:e8e3834a3e0b513712b8e63a6f0d4cbe19093459a1da3f7fbf8ef2810cfd34e3"}, ] [package.dependencies] @@ -674,7 +680,7 @@ authlib = ">=1.5.2" cyclopts = ">=3.0.0" exceptiongroup = ">=1.2.2" httpx = ">=0.28.1" -mcp = ">=1.10.0" +mcp = ">=1.12.4,<2.0.0" openapi-core = ">=0.19.5" openapi-pydantic = ">=0.5.1" pydantic = {version = ">=2.11.7", extras = ["email"]} @@ -687,21 +693,16 @@ websockets = ["websockets (>=15.0.1)"] [[package]] name = "filelock" -version = "3.18.0" +version = "3.19.1" description = "A platform independent file lock." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, - {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, + {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, + {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, ] -[package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] - [[package]] name = "h11" version = "0.16.0" @@ -763,26 +764,26 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "httpx-sse" -version = "0.4.0" +version = "0.4.1" description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, + {file = "httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37"}, + {file = "httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e"}, ] [[package]] name = "identify" -version = "2.6.12" +version = "2.6.13" description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}, - {file = "identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}, + {file = "identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b"}, + {file = "identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32"}, ] [package.extras] @@ -847,14 +848,14 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" -version = "4.24.0" +version = "4.25.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d"}, - {file = "jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196"}, + {file = "jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716"}, + {file = "jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f"}, ] [package.dependencies] @@ -865,7 +866,7 @@ rpds-py = ">=0.7.1" [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] [[package]] name = "jsonschema-path" @@ -926,14 +927,14 @@ files = [ [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, ] [package.dependencies] @@ -941,13 +942,12 @@ mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] +plugins = ["mdit-py-plugins (>=0.5.0)"] profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] [[package]] name = "markupsafe" @@ -1022,30 +1022,31 @@ files = [ [[package]] name = "mcp" -version = "1.10.0" +version = "1.13.0" description = "Model Context Protocol SDK" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "mcp-1.10.0-py3-none-any.whl", hash = "sha256:925c45482d75b1b6f11febddf9736d55edf7739c7ea39b583309f6651cbc9e5c"}, - {file = "mcp-1.10.0.tar.gz", hash = "sha256:91fb1623c3faf14577623d14755d3213db837c5da5dae85069e1b59124cbe0e9"}, + {file = "mcp-1.13.0-py3-none-any.whl", hash = "sha256:8b1a002ebe6e17e894ec74d1943cc09aa9d23cb931bf58d49ab2e9fa6bb17e4b"}, + {file = "mcp-1.13.0.tar.gz", hash = "sha256:70452f56f74662a94eb72ac5feb93997b35995e389b3a3a574e078bed2aa9ab3"}, ] [package.dependencies] anyio = ">=4.5" -httpx = ">=0.27" +httpx = ">=0.27.1" httpx-sse = ">=0.4" jsonschema = ">=4.20.0" -pydantic = ">=2.7.2,<3.0.0" +pydantic = ">=2.11.0,<3.0.0" pydantic-settings = ">=2.5.2" python-multipart = ">=0.0.9" +pywin32 = {version = ">=310", markers = "sys_platform == \"win32\""} sse-starlette = ">=1.6.1" starlette = ">=0.27" -uvicorn = {version = ">=0.23.1", markers = "sys_platform != \"emscripten\""} +uvicorn = {version = ">=0.31.1", markers = "sys_platform != \"emscripten\""} [package.extras] -cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] +cli = ["python-dotenv (>=1.0.0)", "typer (>=0.16.0)"] rich = ["rich (>=13.9.4)"] ws = ["websockets (>=15.0.1)"] @@ -1263,14 +1264,14 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "4.2.0" +version = "4.3.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, - {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, + {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, + {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, ] [package.dependencies] @@ -1469,14 +1470,14 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -1495,26 +1496,27 @@ files = [ [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" +pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" @@ -1537,19 +1539,20 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "6.1.1" +version = "6.2.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, - {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [package.dependencies] coverage = {version = ">=7.5", extras = ["toml"]} -pytest = ">=4.6" +pluggy = ">=1.2" +pytest = ">=6.2.5" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] @@ -1574,14 +1577,14 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "python-dotenv" -version = "1.1.0" +version = "1.1.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, - {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, ] [package.extras] @@ -1599,6 +1602,37 @@ files = [ {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, ] +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1733,20 +1767,19 @@ six = "*" [[package]] name = "rich" -version = "14.0.0" +version = "14.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -1769,184 +1802,196 @@ rich = ">=12.0.0" [[package]] name = "rpds-py" -version = "0.26.0" +version = "0.27.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37"}, - {file = "rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc"}, - {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19"}, - {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11"}, - {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f"}, - {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323"}, - {file = "rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45"}, - {file = "rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84"}, - {file = "rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed"}, - {file = "rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d"}, - {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3"}, - {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107"}, - {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a"}, - {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318"}, - {file = "rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a"}, - {file = "rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03"}, - {file = "rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41"}, - {file = "rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d"}, - {file = "rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a"}, - {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323"}, - {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158"}, - {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3"}, - {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2"}, - {file = "rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44"}, - {file = "rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c"}, - {file = "rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8"}, - {file = "rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d"}, - {file = "rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04"}, - {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1"}, - {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9"}, - {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9"}, - {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba"}, - {file = "rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b"}, - {file = "rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5"}, - {file = "rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256"}, - {file = "rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618"}, - {file = "rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f"}, - {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed"}, - {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632"}, - {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c"}, - {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0"}, - {file = "rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9"}, - {file = "rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9"}, - {file = "rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a"}, - {file = "rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246"}, - {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387"}, - {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af"}, - {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33"}, - {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953"}, - {file = "rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9"}, - {file = "rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37"}, - {file = "rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867"}, - {file = "rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da"}, - {file = "rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8"}, - {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b"}, - {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a"}, - {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170"}, - {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e"}, - {file = "rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f"}, - {file = "rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7"}, - {file = "rpds_py-0.26.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7a48af25d9b3c15684059d0d1fc0bc30e8eee5ca521030e2bffddcab5be40226"}, - {file = "rpds_py-0.26.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c71c2f6bf36e61ee5c47b2b9b5d47e4d1baad6426bfed9eea3e858fc6ee8806"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d815d48b1804ed7867b539236b6dd62997850ca1c91cad187f2ddb1b7bbef19"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84cfbd4d4d2cdeb2be61a057a258d26b22877266dd905809e94172dff01a42ae"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbaa70553ca116c77717f513e08815aec458e6b69a028d4028d403b3bc84ff37"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39bfea47c375f379d8e87ab4bb9eb2c836e4f2069f0f65731d85e55d74666387"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1533b7eb683fb5f38c1d68a3c78f5fdd8f1412fa6b9bf03b40f450785a0ab915"}, - {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5ab0ee51f560d179b057555b4f601b7df909ed31312d301b99f8b9fc6028284"}, - {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e5162afc9e0d1f9cae3b577d9c29ddbab3505ab39012cb794d94a005825bde21"}, - {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:43f10b007033f359bc3fa9cd5e6c1e76723f056ffa9a6b5c117cc35720a80292"}, - {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3730a48e5622e598293eee0762b09cff34dd3f271530f47b0894891281f051d"}, - {file = "rpds_py-0.26.0-cp39-cp39-win32.whl", hash = "sha256:4b1f66eb81eab2e0ff5775a3a312e5e2e16bf758f7b06be82fb0d04078c7ac51"}, - {file = "rpds_py-0.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:519067e29f67b5c90e64fb1a6b6e9d2ec0ba28705c51956637bac23a2f4ddae1"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b"}, - {file = "rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0"}, - {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a90a13408a7a856b87be8a9f008fff53c5080eea4e4180f6c2e546e4a972fb5d"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ac51b65e8dc76cf4949419c54c5528adb24fc721df722fd452e5fbc236f5c40"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b2093224a18c6508d95cfdeba8db9cbfd6f3494e94793b58972933fcee4c6d"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f01a5d6444a3258b00dc07b6ea4733e26f8072b788bef750baa37b370266137"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6e2c12160c72aeda9d1283e612f68804621f448145a210f1bf1d79151c47090"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb28c1f569f8d33b2b5dcd05d0e6ef7005d8639c54c2f0be824f05aedf715255"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1766b5724c3f779317d5321664a343c07773c8c5fd1532e4039e6cc7d1a815be"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b6d9e5a2ed9c4988c8f9b28b3bc0e3e5b1aaa10c28d210a594ff3a8c02742daf"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b5f7a446ddaf6ca0fad9a5535b56fbfc29998bf0e0b450d174bbec0d600e1d72"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:eed5ac260dd545fbc20da5f4f15e7efe36a55e0e7cf706e4ec005b491a9546a0"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:582462833ba7cee52e968b0341b85e392ae53d44c0f9af6a5927c80e539a8b67"}, - {file = "rpds_py-0.26.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69a607203441e07e9a8a529cff1d5b73f6a160f22db1097211e6212a68567d11"}, - {file = "rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0"}, + {file = "rpds_py-0.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:130c1ffa5039a333f5926b09e346ab335f0d4ec393b030a18549a7c7e7c2cea4"}, + {file = "rpds_py-0.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a4cf32a26fa744101b67bfd28c55d992cd19438aff611a46cac7f066afca8fd4"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64a0fe3f334a40b989812de70160de6b0ec7e3c9e4a04c0bbc48d97c5d3600ae"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a0ff7ee28583ab30a52f371b40f54e7138c52ca67f8ca17ccb7ccf0b383cb5f"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15ea4d2e182345dd1b4286593601d766411b43f868924afe297570658c31a62b"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36184b44bf60a480863e51021c26aca3dfe8dd2f5eeabb33622b132b9d8b8b54"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b78430703cfcf5f5e86eb74027a1ed03a93509273d7c705babb547f03e60016"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:dbd749cff1defbde270ca346b69b3baf5f1297213ef322254bf2a28537f0b046"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bde37765564cd22a676dd8101b657839a1854cfaa9c382c5abf6ff7accfd4ae"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1d66f45b9399036e890fb9c04e9f70c33857fd8f58ac8db9f3278cfa835440c3"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d85d784c619370d9329bbd670f41ff5f2ae62ea4519761b679d0f57f0f0ee267"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5df559e9e7644d9042f626f2c3997b555f347d7a855a15f170b253f6c5bfe358"}, + {file = "rpds_py-0.27.0-cp310-cp310-win32.whl", hash = "sha256:b8a4131698b6992b2a56015f51646711ec5d893a0b314a4b985477868e240c87"}, + {file = "rpds_py-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:cbc619e84a5e3ab2d452de831c88bdcad824414e9c2d28cd101f94dbdf26329c"}, + {file = "rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622"}, + {file = "rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e"}, + {file = "rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7"}, + {file = "rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261"}, + {file = "rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0"}, + {file = "rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4"}, + {file = "rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374"}, + {file = "rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97"}, + {file = "rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5"}, + {file = "rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9"}, + {file = "rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff"}, + {file = "rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d"}, + {file = "rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd"}, + {file = "rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2"}, + {file = "rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac"}, + {file = "rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774"}, + {file = "rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c"}, + {file = "rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23"}, + {file = "rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1"}, + {file = "rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb"}, + {file = "rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e"}, + {file = "rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6"}, + {file = "rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a"}, + {file = "rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d"}, + {file = "rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828"}, + {file = "rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae"}, + {file = "rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5"}, + {file = "rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391"}, + {file = "rpds_py-0.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e0d7151a1bd5d0a203a5008fc4ae51a159a610cb82ab0a9b2c4d80241745582e"}, + {file = "rpds_py-0.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42ccc57ff99166a55a59d8c7d14f1a357b7749f9ed3584df74053fd098243451"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e377e4cf8795cdbdff75b8f0223d7b6c68ff4fef36799d88ccf3a995a91c0112"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79af163a4b40bbd8cfd7ca86ec8b54b81121d3b213b4435ea27d6568bcba3e9d"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2eff8ee57c5996b0d2a07c3601fb4ce5fbc37547344a26945dd9e5cbd1ed27a"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7cf9bc4508efb18d8dff6934b602324eb9f8c6644749627ce001d6f38a490889"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05284439ebe7d9f5f5a668d4d8a0a1d851d16f7d47c78e1fab968c8ad30cab04"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:1321bce595ad70e80f97f998db37356b2e22cf98094eba6fe91782e626da2f71"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:737005088449ddd3b3df5a95476ee1c2c5c669f5c30eed909548a92939c0e12d"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b2a4e17bfd68536c3b801800941c95a1d4a06e3cada11c146093ba939d9638d"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dc6b0d5a1ea0318ef2def2b6a55dccf1dcaf77d605672347271ed7b829860765"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4c3f8a0d4802df34fcdbeb3dfe3a4d8c9a530baea8fafdf80816fcaac5379d83"}, + {file = "rpds_py-0.27.0-cp39-cp39-win32.whl", hash = "sha256:699c346abc73993962cac7bb4f02f58e438840fa5458a048d3a178a7a670ba86"}, + {file = "rpds_py-0.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:be806e2961cd390a89d6c3ce8c2ae34271cfcd05660f716257838bb560f1c3b6"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:46f48482c1a4748ab2773f75fffbdd1951eb59794e32788834b945da857c47a8"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:419dd9c98bcc9fb0242be89e0c6e922df333b975d4268faa90d58499fd9c9ebe"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d42a0ef2bdf6bc81e1cc2d49d12460f63c6ae1423c4f4851b828e454ccf6f1"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e39169ac6aae06dd79c07c8a69d9da867cef6a6d7883a0186b46bb46ccfb0c3"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:935afcdea4751b0ac918047a2df3f720212892347767aea28f5b3bf7be4f27c0"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de567dec6d451649a781633d36f5c7501711adee329d76c095be2178855b042"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:555ed147cbe8c8f76e72a4c6cd3b7b761cbf9987891b9448808148204aed74a5"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:d2cc2b34f9e1d31ce255174da82902ad75bd7c0d88a33df54a77a22f2ef421ee"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb0702c12983be3b2fab98ead349ac63a98216d28dda6f518f52da5498a27a1b"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ba783541be46f27c8faea5a6645e193943c17ea2f0ffe593639d906a327a9bcc"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:2406d034635d1497c596c40c85f86ecf2bf9611c1df73d14078af8444fe48031"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dea0808153f1fbbad772669d906cddd92100277533a03845de6893cadeffc8be"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d2a81bdcfde4245468f7030a75a37d50400ac2455c3a4819d9d550c937f90ab5"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9ad08547995a57e74fea6abaf5940d399447935faebbd2612b3b0ca6f987946b"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:61490d57e82e23b45c66f96184237994bfafa914433b8cd1a9bb57fecfced59d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7cf5e726b6fa977e428a61880fb108a62f28b6d0c7ef675b117eaff7076df49"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc662bc9375a6a394b62dfd331874c434819f10ee3902123200dbcf116963f89"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:299a245537e697f28a7511d01038c310ac74e8ea213c0019e1fc65f52c0dcb23"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be3964f7312ea05ed283b20f87cb533fdc555b2e428cc7be64612c0b2124f08c"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ba649a6e55ae3808e4c39e01580dc9a9b0d5b02e77b66bb86ef117922b1264"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:81f81bbd7cdb4bdc418c09a73809abeda8f263a6bf8f9c7f93ed98b5597af39d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11e8e28c0ba0373d052818b600474cfee2fafa6c9f36c8587d217b13ee28ca7d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e3acb9c16530362aeaef4e84d57db357002dc5cbfac9a23414c3e73c08301ab2"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2e307cb5f66c59ede95c00e93cd84190a5b7f3533d7953690b2036780622ba81"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f09c9d4c26fa79c1bad927efb05aca2391350b8e61c38cbc0d7d3c814e463124"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af22763a0a1eff106426a6e1f13c4582e0d0ad89c1493ab6c058236174cd6c6a"}, + {file = "rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f"}, ] [[package]] name = "ruff" -version = "0.12.7" +version = "0.12.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303"}, - {file = "ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb"}, - {file = "ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3"}, - {file = "ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860"}, - {file = "ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c"}, - {file = "ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423"}, - {file = "ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb"}, - {file = "ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd"}, - {file = "ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e"}, - {file = "ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606"}, - {file = "ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8"}, - {file = "ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa"}, - {file = "ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5"}, - {file = "ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4"}, - {file = "ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77"}, - {file = "ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f"}, - {file = "ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69"}, - {file = "ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71"}, + {file = "ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e"}, + {file = "ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f"}, + {file = "ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7"}, + {file = "ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93"}, + {file = "ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908"}, + {file = "ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089"}, + {file = "ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a"}, ] [[package]] @@ -1975,38 +2020,40 @@ files = [ [[package]] name = "sse-starlette" -version = "2.3.5" +version = "3.0.2" description = "SSE plugin for Starlette" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8"}, - {file = "sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2"}, + {file = "sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a"}, + {file = "sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a"}, ] [package.dependencies] anyio = ">=4.7.0" -starlette = ">=0.41.3" [package.extras] -examples = ["fastapi"] +daphne = ["daphne (>=4.2.0)"] +examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"] +granian = ["granian (>=2.3.1)"] uvicorn = ["uvicorn (>=0.34.0)"] [[package]] name = "starlette" -version = "0.46.2" +version = "0.47.2" description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, - {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, + {file = "starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b"}, + {file = "starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8"}, ] [package.dependencies] anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] @@ -2071,28 +2118,28 @@ files = [ [[package]] name = "tomlkit" -version = "0.13.2" +version = "0.13.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, + {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, + {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] [[package]] name = "typing-extensions" -version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] -markers = {dev = "python_version < \"3.11\""} +markers = {dev = "python_version == \"3.10\""} [[package]] name = "typing-inspection" @@ -2129,15 +2176,15 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.34.2" +version = "0.35.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" groups = ["main"] markers = "sys_platform != \"emscripten\"" files = [ - {file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"}, - {file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"}, + {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, + {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, ] [package.dependencies] @@ -2146,24 +2193,25 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "virtualenv" -version = "20.31.2" +version = "20.34.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, - {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, + {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, + {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] @@ -2202,4 +2250,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "4dad61d6dbc2480229a3f3db6d1ae0205fdae550958771403dcd33f9c2bc52c9" +content-hash = "9785e18d2d996f5e58e1b06c722f6de31c445a1a83528f39227d1c373b91f989" diff --git a/pypi_query_mcp/core/dependency_parser.py b/pypi_query_mcp/core/dependency_parser.py index 33a5a1b..aff6d3d 100644 --- a/pypi_query_mcp/core/dependency_parser.py +++ b/pypi_query_mcp/core/dependency_parser.py @@ -117,12 +117,29 @@ class DependencyParser: Dictionary with categorized dependencies """ categories = {"runtime": [], "development": [], "optional": {}, "extras": {}} - + # Define development-related extra names dev_extra_names = { - 'dev', 'development', 'test', 'testing', 'tests', 'lint', 'linting', - 'doc', 'docs', 'documentation', 'build', 'check', 'cover', 'coverage', - 'type', 'typing', 'mypy', 'style', 'format', 'quality' + "dev", + "development", + "test", + "testing", + "tests", + "lint", + "linting", + "doc", + "docs", + "documentation", + "build", + "check", + "cover", + "coverage", + "type", + "typing", + "mypy", + "style", + "format", + "quality", } for req in requirements: @@ -141,7 +158,7 @@ class DependencyParser: if extra_name not in categories["extras"]: categories["extras"][extra_name] = [] categories["extras"][extra_name].append(req) - + # Check if this extra is development-related if extra_name.lower() in dev_extra_names: categories["development"].append(req) diff --git a/pypi_query_mcp/core/github_client.py b/pypi_query_mcp/core/github_client.py index b7f0f88..ff60f77 100644 --- a/pypi_query_mcp/core/github_client.py +++ b/pypi_query_mcp/core/github_client.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Any, Dict, Optional +from typing import Any import httpx @@ -17,7 +17,7 @@ class GitHubAPIClient: timeout: float = 10.0, max_retries: int = 2, retry_delay: float = 1.0, - github_token: Optional[str] = None, + github_token: str | None = None, ): """Initialize GitHub API client. @@ -33,7 +33,7 @@ class GitHubAPIClient: self.retry_delay = retry_delay # Simple in-memory cache for repository data - self._cache: Dict[str, Dict[str, Any]] = {} + self._cache: dict[str, dict[str, Any]] = {} self._cache_ttl = 3600 # 1 hour cache # HTTP client configuration @@ -41,7 +41,7 @@ class GitHubAPIClient: "Accept": "application/vnd.github.v3+json", "User-Agent": "pypi-query-mcp-server/0.1.0", } - + if github_token: headers["Authorization"] = f"token {github_token}" @@ -67,12 +67,13 @@ class GitHubAPIClient: """Generate cache key for repository data.""" return f"repo:{repo}" - def _is_cache_valid(self, cache_entry: Dict[str, Any]) -> bool: + def _is_cache_valid(self, cache_entry: dict[str, Any]) -> bool: """Check if cache entry is still valid.""" import time + return time.time() - cache_entry.get("timestamp", 0) < self._cache_ttl - async def _make_request(self, url: str) -> Optional[Dict[str, Any]]: + async def _make_request(self, url: str) -> dict[str, Any] | None: """Make HTTP request with retry logic and error handling. Args: @@ -85,7 +86,9 @@ class GitHubAPIClient: for attempt in range(self.max_retries + 1): try: - logger.debug(f"Making GitHub API request to {url} (attempt {attempt + 1})") + logger.debug( + f"Making GitHub API request to {url} (attempt {attempt + 1})" + ) response = await self._client.get(url) @@ -100,12 +103,16 @@ class GitHubAPIClient: logger.warning(f"GitHub API rate limit or permission denied: {url}") return None elif response.status_code >= 500: - logger.warning(f"GitHub API server error {response.status_code}: {url}") + logger.warning( + f"GitHub API server error {response.status_code}: {url}" + ) if attempt < self.max_retries: continue return None else: - logger.warning(f"Unexpected GitHub API status {response.status_code}: {url}") + logger.warning( + f"Unexpected GitHub API status {response.status_code}: {url}" + ) return None except httpx.TimeoutException: @@ -120,13 +127,17 @@ class GitHubAPIClient: # Wait before retry (except on last attempt) if attempt < self.max_retries: - await asyncio.sleep(self.retry_delay * (2 ** attempt)) + await asyncio.sleep(self.retry_delay * (2**attempt)) # If we get here, all retries failed - logger.error(f"Failed to fetch GitHub data after {self.max_retries + 1} attempts: {last_exception}") + logger.error( + f"Failed to fetch GitHub data after {self.max_retries + 1} attempts: {last_exception}" + ) return None - async def get_repository_stats(self, repo_path: str, use_cache: bool = True) -> Optional[Dict[str, Any]]: + async def get_repository_stats( + self, repo_path: str, use_cache: bool = True + ) -> dict[str, Any] | None: """Get repository statistics from GitHub API. Args: @@ -147,10 +158,10 @@ class GitHubAPIClient: # Make API request url = f"{self.base_url}/repos/{repo_path}" - + try: data = await self._make_request(url) - + if data: # Extract relevant statistics stats = { @@ -171,14 +182,19 @@ class GitHubAPIClient: "has_wiki": data.get("has_wiki", False), "archived": data.get("archived", False), "disabled": data.get("disabled", False), - "license": data.get("license", {}).get("name") if data.get("license") else None, + "license": data.get("license", {}).get("name") + if data.get("license") + else None, } # Cache the result import time + self._cache[cache_key] = {"data": stats, "timestamp": time.time()} - logger.debug(f"Fetched GitHub stats for {repo_path}: {stats['stars']} stars") + logger.debug( + f"Fetched GitHub stats for {repo_path}: {stats['stars']} stars" + ) return stats else: return None @@ -188,11 +204,8 @@ class GitHubAPIClient: return None async def get_multiple_repo_stats( - self, - repo_paths: list[str], - use_cache: bool = True, - max_concurrent: int = 5 - ) -> Dict[str, Optional[Dict[str, Any]]]: + self, repo_paths: list[str], use_cache: bool = True, max_concurrent: int = 5 + ) -> dict[str, dict[str, Any] | None]: """Get statistics for multiple repositories concurrently. Args: @@ -205,7 +218,7 @@ class GitHubAPIClient: """ semaphore = asyncio.Semaphore(max_concurrent) - async def fetch_repo_stats(repo_path: str) -> tuple[str, Optional[Dict[str, Any]]]: + async def fetch_repo_stats(repo_path: str) -> tuple[str, dict[str, Any] | None]: async with semaphore: stats = await self.get_repository_stats(repo_path, use_cache) return repo_path, stats @@ -220,7 +233,7 @@ class GitHubAPIClient: if isinstance(result, Exception): logger.error(f"Error in concurrent GitHub fetch: {result}") continue - + repo_path, stats = result repo_stats[repo_path] = stats @@ -231,14 +244,14 @@ class GitHubAPIClient: self._cache.clear() logger.debug("GitHub cache cleared") - async def get_rate_limit(self) -> Optional[Dict[str, Any]]: + async def get_rate_limit(self) -> dict[str, Any] | None: """Get current GitHub API rate limit status. Returns: Dictionary containing rate limit information """ url = f"{self.base_url}/rate_limit" - + try: data = await self._make_request(url) if data: @@ -246,4 +259,4 @@ class GitHubAPIClient: return None except Exception as e: logger.error(f"Error fetching GitHub rate limit: {e}") - return None \ No newline at end of file + return None diff --git a/pypi_query_mcp/core/pypi_client.py b/pypi_query_mcp/core/pypi_client.py index fdda270..9315c39 100644 --- a/pypi_query_mcp/core/pypi_client.py +++ b/pypi_query_mcp/core/pypi_client.py @@ -182,7 +182,7 @@ class PyPIClient: NetworkError: For network-related errors """ normalized_name = self._validate_package_name(package_name) - + # Create cache key that includes version info cache_suffix = f"v{version}" if version else "latest" cache_key = self._get_cache_key(normalized_name, f"info_{cache_suffix}") @@ -191,13 +191,17 @@ class PyPIClient: if use_cache and cache_key in self._cache: cache_entry = self._cache[cache_key] if self._is_cache_valid(cache_entry): - logger.debug(f"Using cached data for package: {normalized_name} version: {version or 'latest'}") + logger.debug( + f"Using cached data for package: {normalized_name} version: {version or 'latest'}" + ) return cache_entry["data"] # Build URL - include version if specified if version: url = f"{self.base_url}/{quote(normalized_name)}/{quote(version)}/json" - logger.info(f"Fetching package info for: {normalized_name} version {version}") + logger.info( + f"Fetching package info for: {normalized_name} version {version}" + ) else: url = f"{self.base_url}/{quote(normalized_name)}/json" logger.info(f"Fetching package info for: {normalized_name} (latest)") @@ -215,13 +219,19 @@ class PyPIClient: except PackageNotFoundError as e: if version: # More specific error message for version not found - logger.error(f"Version {version} not found for package {normalized_name}") - raise PackageNotFoundError(f"Version {version} not found for package {normalized_name}") + logger.error( + f"Version {version} not found for package {normalized_name}" + ) + raise PackageNotFoundError( + f"Version {version} not found for package {normalized_name}" + ) else: logger.error(f"Failed to fetch package info for {normalized_name}: {e}") raise except Exception as e: - logger.error(f"Failed to fetch package info for {normalized_name} version {version or 'latest'}: {e}") + logger.error( + f"Failed to fetch package info for {normalized_name} version {version or 'latest'}: {e}" + ) raise async def get_package_versions( @@ -236,7 +246,9 @@ class PyPIClient: Returns: List of version strings """ - package_info = await self.get_package_info(package_name, version=None, use_cache=use_cache) + package_info = await self.get_package_info( + package_name, version=None, use_cache=use_cache + ) releases = package_info.get("releases", {}) return list(releases.keys()) @@ -252,7 +264,9 @@ class PyPIClient: Returns: Latest version string """ - package_info = await self.get_package_info(package_name, version=None, use_cache=use_cache) + package_info = await self.get_package_info( + package_name, version=None, use_cache=use_cache + ) return package_info.get("info", {}).get("version", "") def clear_cache(self): diff --git a/pypi_query_mcp/core/stats_client.py b/pypi_query_mcp/core/stats_client.py index 7213267..2d706dc 100644 --- a/pypi_query_mcp/core/stats_client.py +++ b/pypi_query_mcp/core/stats_client.py @@ -5,7 +5,7 @@ import logging import random import time from datetime import datetime, timedelta -from typing import Any, Dict, List, Optional +from typing import Any import httpx @@ -50,7 +50,7 @@ class PyPIStatsClient: self._cache: dict[str, dict[str, Any]] = {} self._cache_ttl = 86400 # 24 hours (increased for resilience) self._fallback_cache_ttl = 604800 # 7 days for fallback data - + # Track API health for smart fallback decisions self._api_health = { "last_success": None, @@ -106,31 +106,33 @@ class PyPIStatsClient: ) return f"{endpoint}:{package_name}:{param_str}" - def _is_cache_valid(self, cache_entry: dict[str, Any], fallback: bool = False) -> bool: + def _is_cache_valid( + self, cache_entry: dict[str, Any], fallback: bool = False + ) -> bool: """Check if cache entry is still valid. - + Args: cache_entry: Cache entry to validate fallback: Whether to use fallback cache TTL (longer for resilience) """ ttl = self._fallback_cache_ttl if fallback else self._cache_ttl return time.time() - cache_entry.get("timestamp", 0) < ttl - + def _should_use_fallback(self) -> bool: """Determine if fallback mechanisms should be used based on API health.""" if not self.fallback_enabled: return False - + # Use fallback if we've had multiple consecutive failures if self._api_health["consecutive_failures"] >= 3: return True - + # Use fallback if last success was more than 1 hour ago if self._api_health["last_success"]: time_since_success = time.time() - self._api_health["last_success"] if time_since_success > 3600: # 1 hour return True - + return False async def _make_request(self, url: str) -> dict[str, Any]: @@ -152,7 +154,9 @@ class PyPIStatsClient: for attempt in range(self.max_retries + 1): try: - logger.debug(f"Making request to {url} (attempt {attempt + 1}/{self.max_retries + 1})") + logger.debug( + f"Making request to {url} (attempt {attempt + 1}/{self.max_retries + 1})" + ) response = await self._client.get(url) @@ -171,16 +175,25 @@ class PyPIStatsClient: elif response.status_code == 429: retry_after = response.headers.get("Retry-After") retry_after_int = int(retry_after) if retry_after else None - self._update_api_failure(f"Rate limit exceeded (retry after {retry_after_int}s)") + self._update_api_failure( + f"Rate limit exceeded (retry after {retry_after_int}s)" + ) raise RateLimitError(retry_after_int) elif response.status_code >= 500: error_msg = f"Server error: HTTP {response.status_code}" self._update_api_failure(error_msg) - + # For 502/503/504 errors, continue retrying - if response.status_code in [502, 503, 504] and attempt < self.max_retries: - last_exception = PyPIServerError(response.status_code, error_msg) - logger.warning(f"Retryable server error {response.status_code}, attempt {attempt + 1}") + if ( + response.status_code in [502, 503, 504] + and attempt < self.max_retries + ): + last_exception = PyPIServerError( + response.status_code, error_msg + ) + logger.warning( + f"Retryable server error {response.status_code}, attempt {attempt + 1}" + ) else: raise PyPIServerError(response.status_code, error_msg) else: @@ -205,7 +218,9 @@ class PyPIStatsClient: # Only retry certain server errors if e.status_code in [502, 503, 504] and attempt < self.max_retries: last_exception = e - logger.warning(f"Retrying server error {e.status_code}, attempt {attempt + 1}") + logger.warning( + f"Retrying server error {e.status_code}, attempt {attempt + 1}" + ) else: raise except Exception as e: @@ -216,7 +231,7 @@ class PyPIStatsClient: # Calculate exponential backoff with jitter if attempt < self.max_retries: - base_delay = self.retry_delay * (2 ** attempt) + base_delay = self.retry_delay * (2**attempt) jitter = random.uniform(0.1, 0.3) * base_delay # Add 10-30% jitter delay = base_delay + jitter logger.debug(f"Waiting {delay:.2f}s before retry...") @@ -227,21 +242,25 @@ class PyPIStatsClient: raise last_exception else: raise NetworkError("All retry attempts failed with unknown error") - + def _update_api_failure(self, error_msg: str) -> None: """Update API health tracking on failure.""" self._api_health["consecutive_failures"] += 1 self._api_health["last_error"] = error_msg - logger.debug(f"API failure count: {self._api_health['consecutive_failures']}, error: {error_msg}") - - def _generate_fallback_recent_downloads(self, package_name: str, period: str = "month") -> dict[str, Any]: + logger.debug( + f"API failure count: {self._api_health['consecutive_failures']}, error: {error_msg}" + ) + + def _generate_fallback_recent_downloads( + self, package_name: str, period: str = "month" + ) -> dict[str, Any]: """Generate fallback download statistics when API is unavailable. - + This provides estimated download counts based on package popularity patterns to ensure the system remains functional during API outages. """ logger.warning(f"Generating fallback download data for {package_name}") - + # Base estimates for popular packages (these are conservative estimates) popular_packages = { "requests": {"day": 1500000, "week": 10500000, "month": 45000000}, @@ -270,39 +289,50 @@ class PyPIStatsClient: "pandas": {"day": 200000, "week": 1400000, "month": 6000000}, "sqlalchemy": {"day": 90000, "week": 630000, "month": 2700000}, } - + # Get estimates for known packages or generate based on package name characteristics if package_name.lower() in popular_packages: estimates = popular_packages[package_name.lower()] else: # Generate estimates based on common package patterns - if any(keyword in package_name.lower() for keyword in ["test", "dev", "debug"]): + if any( + keyword in package_name.lower() for keyword in ["test", "dev", "debug"] + ): # Development/testing packages - lower usage base_daily = random.randint(100, 1000) - elif any(keyword in package_name.lower() for keyword in ["aws", "google", "microsoft", "azure"]): + elif any( + keyword in package_name.lower() + for keyword in ["aws", "google", "microsoft", "azure"] + ): # Cloud provider packages - higher usage base_daily = random.randint(10000, 50000) - elif any(keyword in package_name.lower() for keyword in ["http", "request", "client", "api"]): + elif any( + keyword in package_name.lower() + for keyword in ["http", "request", "client", "api"] + ): # HTTP/API packages - moderate to high usage base_daily = random.randint(5000, 25000) - elif any(keyword in package_name.lower() for keyword in ["data", "pandas", "numpy", "scipy"]): + elif any( + keyword in package_name.lower() + for keyword in ["data", "pandas", "numpy", "scipy"] + ): # Data science packages - high usage base_daily = random.randint(15000, 75000) else: # Generic packages - moderate usage base_daily = random.randint(1000, 10000) - + estimates = { "day": base_daily, "week": base_daily * 7, "month": base_daily * 30, } - + # Add some realistic variation (ยฑ20%) variation = random.uniform(0.8, 1.2) for key in estimates: estimates[key] = int(estimates[key] * variation) - + return { "data": { "last_day": estimates["day"], @@ -314,42 +344,48 @@ class PyPIStatsClient: "source": "fallback_estimates", "note": "Estimated data due to API unavailability. Actual values may differ.", } - - def _generate_fallback_overall_downloads(self, package_name: str, mirrors: bool = False) -> dict[str, Any]: + + def _generate_fallback_overall_downloads( + self, package_name: str, mirrors: bool = False + ) -> dict[str, Any]: """Generate fallback time series data when API is unavailable.""" logger.warning(f"Generating fallback time series data for {package_name}") - + # Generate 180 days of synthetic time series data time_series = [] base_date = datetime.now() - timedelta(days=180) - + # Get base daily estimate from recent downloads fallback recent_fallback = self._generate_fallback_recent_downloads(package_name) base_daily = recent_fallback["data"]["last_day"] - + for i in range(180): current_date = base_date + timedelta(days=i) - + # Add weekly and seasonal patterns day_of_week = current_date.weekday() # Lower downloads on weekends week_factor = 0.7 if day_of_week >= 5 else 1.0 - + # Add some growth trend (packages generally grow over time) growth_factor = 1.0 + (i / 180) * 0.3 # 30% growth over 180 days - + # Add random daily variation daily_variation = random.uniform(0.7, 1.3) - - daily_downloads = int(base_daily * week_factor * growth_factor * daily_variation) - + + daily_downloads = int( + base_daily * week_factor * growth_factor * daily_variation + ) + category = "with_mirrors" if mirrors else "without_mirrors" - time_series.append({ - "category": category, - "date": current_date.strftime("%Y-%m-%d"), - "downloads": daily_downloads, - }) - + time_series.append( + { + "category": category, + "date": current_date.strftime("%Y-%m-%d"), + "downloads": daily_downloads, + } + ) + return { "data": time_series, "package": package_name, @@ -385,16 +421,24 @@ class PyPIStatsClient: if self._is_cache_valid(cache_entry): logger.debug(f"Using cached recent downloads for: {normalized_name}") return cache_entry["data"] - elif self._should_use_fallback() and self._is_cache_valid(cache_entry, fallback=True): - logger.info(f"Using extended cache (fallback mode) for: {normalized_name}") + elif self._should_use_fallback() and self._is_cache_valid( + cache_entry, fallback=True + ): + logger.info( + f"Using extended cache (fallback mode) for: {normalized_name}" + ) cache_entry["data"]["note"] = "Extended cache data due to API issues" return cache_entry["data"] # Check if we should use fallback immediately if self._should_use_fallback(): - logger.warning(f"API health poor, using fallback data for: {normalized_name}") - fallback_data = self._generate_fallback_recent_downloads(normalized_name, period) - + logger.warning( + f"API health poor, using fallback data for: {normalized_name}" + ) + fallback_data = self._generate_fallback_recent_downloads( + normalized_name, period + ) + # Cache fallback data with extended TTL self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()} return fallback_data @@ -418,28 +462,39 @@ class PyPIStatsClient: except (PyPIServerError, NetworkError) as e: logger.error(f"API request failed for {normalized_name}: {e}") - + # Try to use stale cache data if available if use_cache and cache_key in self._cache: cache_entry = self._cache[cache_key] - logger.warning(f"Using stale cache data for {normalized_name} due to API failure") + logger.warning( + f"Using stale cache data for {normalized_name} due to API failure" + ) cache_entry["data"]["note"] = f"Stale cache data due to API error: {e}" return cache_entry["data"] - + # Last resort: generate fallback data if self.fallback_enabled: - logger.warning(f"Generating fallback data for {normalized_name} due to API failure") - fallback_data = self._generate_fallback_recent_downloads(normalized_name, period) - + logger.warning( + f"Generating fallback data for {normalized_name} due to API failure" + ) + fallback_data = self._generate_fallback_recent_downloads( + normalized_name, period + ) + # Cache fallback data - self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()} + self._cache[cache_key] = { + "data": fallback_data, + "timestamp": time.time(), + } return fallback_data - + # If fallback is disabled, re-raise the original exception raise except Exception as e: - logger.error(f"Unexpected error fetching recent downloads for {normalized_name}: {e}") + logger.error( + f"Unexpected error fetching recent downloads for {normalized_name}: {e}" + ) raise async def get_overall_downloads( @@ -469,16 +524,24 @@ class PyPIStatsClient: if self._is_cache_valid(cache_entry): logger.debug(f"Using cached overall downloads for: {normalized_name}") return cache_entry["data"] - elif self._should_use_fallback() and self._is_cache_valid(cache_entry, fallback=True): - logger.info(f"Using extended cache (fallback mode) for: {normalized_name}") + elif self._should_use_fallback() and self._is_cache_valid( + cache_entry, fallback=True + ): + logger.info( + f"Using extended cache (fallback mode) for: {normalized_name}" + ) cache_entry["data"]["note"] = "Extended cache data due to API issues" return cache_entry["data"] # Check if we should use fallback immediately if self._should_use_fallback(): - logger.warning(f"API health poor, using fallback data for: {normalized_name}") - fallback_data = self._generate_fallback_overall_downloads(normalized_name, mirrors) - + logger.warning( + f"API health poor, using fallback data for: {normalized_name}" + ) + fallback_data = self._generate_fallback_overall_downloads( + normalized_name, mirrors + ) + # Cache fallback data with extended TTL self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()} return fallback_data @@ -502,28 +565,39 @@ class PyPIStatsClient: except (PyPIServerError, NetworkError) as e: logger.error(f"API request failed for {normalized_name}: {e}") - + # Try to use stale cache data if available if use_cache and cache_key in self._cache: cache_entry = self._cache[cache_key] - logger.warning(f"Using stale cache data for {normalized_name} due to API failure") + logger.warning( + f"Using stale cache data for {normalized_name} due to API failure" + ) cache_entry["data"]["note"] = f"Stale cache data due to API error: {e}" return cache_entry["data"] - + # Last resort: generate fallback data if self.fallback_enabled: - logger.warning(f"Generating fallback data for {normalized_name} due to API failure") - fallback_data = self._generate_fallback_overall_downloads(normalized_name, mirrors) - + logger.warning( + f"Generating fallback data for {normalized_name} due to API failure" + ) + fallback_data = self._generate_fallback_overall_downloads( + normalized_name, mirrors + ) + # Cache fallback data - self._cache[cache_key] = {"data": fallback_data, "timestamp": time.time()} + self._cache[cache_key] = { + "data": fallback_data, + "timestamp": time.time(), + } return fallback_data - + # If fallback is disabled, re-raise the original exception raise except Exception as e: - logger.error(f"Unexpected error fetching overall downloads for {normalized_name}: {e}") + logger.error( + f"Unexpected error fetching overall downloads for {normalized_name}: {e}" + ) raise def clear_cache(self): diff --git a/pypi_query_mcp/core/version_utils.py b/pypi_query_mcp/core/version_utils.py index b232386..9003971 100644 --- a/pypi_query_mcp/core/version_utils.py +++ b/pypi_query_mcp/core/version_utils.py @@ -288,31 +288,31 @@ class VersionCompatibility: def sort_versions_semantically(versions: list[str], reverse: bool = True) -> list[str]: """Sort package versions using semantic version ordering. - + This function properly sorts versions by parsing them as semantic versions, ensuring that pre-release versions (alpha, beta, rc) are ordered correctly relative to stable releases. - + Args: versions: List of version strings to sort reverse: If True, sort in descending order (newest first). Default True. - + Returns: List of version strings sorted semantically - + Examples: >>> sort_versions_semantically(['1.0.0', '2.0.0a1', '1.5.0', '2.0.0']) ['2.0.0', '2.0.0a1', '1.5.0', '1.0.0'] - + >>> sort_versions_semantically(['5.2rc1', '5.2.5', '5.2.0']) ['5.2.5', '5.2.0', '5.2rc1'] """ if not versions: return [] - + def parse_version_safe(version_str: str) -> tuple[Version | None, str]: """Safely parse a version string, returning (parsed_version, original_string). - + Returns (None, original_string) if parsing fails. """ try: @@ -320,26 +320,26 @@ def sort_versions_semantically(versions: list[str], reverse: bool = True) -> lis except InvalidVersion: logger.debug(f"Failed to parse version '{version_str}' as semantic version") return (None, version_str) - + # Parse all versions, keeping track of originals parsed_versions = [parse_version_safe(v) for v in versions] - + # Separate valid and invalid versions valid_versions = [(v, orig) for v, orig in parsed_versions if v is not None] invalid_versions = [orig for v, orig in parsed_versions if v is None] - + # Sort valid versions semantically valid_versions.sort(key=lambda x: x[0], reverse=reverse) - + # Sort invalid versions lexicographically as fallback invalid_versions.sort(reverse=reverse) - + # Combine results: valid versions first, then invalid ones result = [orig for _, orig in valid_versions] + invalid_versions - + logger.debug( f"Sorted {len(versions)} versions: {len(valid_versions)} valid, " f"{len(invalid_versions)} invalid" ) - + return result diff --git a/pypi_query_mcp/data/__init__.py b/pypi_query_mcp/data/__init__.py index 91347a5..258acd0 100644 --- a/pypi_query_mcp/data/__init__.py +++ b/pypi_query_mcp/data/__init__.py @@ -1 +1 @@ -"""Data module for PyPI package information.""" \ No newline at end of file +"""Data module for PyPI package information.""" diff --git a/pypi_query_mcp/data/popular_packages.py b/pypi_query_mcp/data/popular_packages.py index f0b0b9d..9fb754b 100644 --- a/pypi_query_mcp/data/popular_packages.py +++ b/pypi_query_mcp/data/popular_packages.py @@ -10,10 +10,12 @@ The rankings and download estimates are based on: Data is organized by categories and includes estimated relative popularity. """ -from typing import Dict, List, NamedTuple +from typing import NamedTuple + class PackageInfo(NamedTuple): """Information about a popular package.""" + name: str category: str estimated_monthly_downloads: int @@ -21,60 +23,226 @@ class PackageInfo(NamedTuple): description: str primary_use_case: str + # Core packages that are dependencies for many other packages INFRASTRUCTURE_PACKAGES = [ - PackageInfo("setuptools", "packaging", 800_000_000, 2100, "Package development tools", "packaging"), - PackageInfo("wheel", "packaging", 700_000_000, 400, "Binary package format", "packaging"), - PackageInfo("pip", "packaging", 600_000_000, 9500, "Package installer", "packaging"), - PackageInfo("certifi", "security", 500_000_000, 800, "Certificate bundle", "security"), - PackageInfo("urllib3", "networking", 450_000_000, 3600, "HTTP client library", "networking"), - PackageInfo("charset-normalizer", "text", 400_000_000, 400, "Character encoding detection", "text-processing"), - PackageInfo("idna", "networking", 380_000_000, 200, "Internationalized domain names", "networking"), - PackageInfo("six", "compatibility", 350_000_000, 900, "Python 2 and 3 compatibility", "compatibility"), - PackageInfo("python-dateutil", "datetime", 320_000_000, 2200, "Date and time utilities", "datetime"), - PackageInfo("requests", "networking", 300_000_000, 51000, "HTTP library", "networking"), + PackageInfo( + "setuptools", + "packaging", + 800_000_000, + 2100, + "Package development tools", + "packaging", + ), + PackageInfo( + "wheel", "packaging", 700_000_000, 400, "Binary package format", "packaging" + ), + PackageInfo( + "pip", "packaging", 600_000_000, 9500, "Package installer", "packaging" + ), + PackageInfo( + "certifi", "security", 500_000_000, 800, "Certificate bundle", "security" + ), + PackageInfo( + "urllib3", "networking", 450_000_000, 3600, "HTTP client library", "networking" + ), + PackageInfo( + "charset-normalizer", + "text", + 400_000_000, + 400, + "Character encoding detection", + "text-processing", + ), + PackageInfo( + "idna", + "networking", + 380_000_000, + 200, + "Internationalized domain names", + "networking", + ), + PackageInfo( + "six", + "compatibility", + 350_000_000, + 900, + "Python 2 and 3 compatibility", + "compatibility", + ), + PackageInfo( + "python-dateutil", + "datetime", + 320_000_000, + 2200, + "Date and time utilities", + "datetime", + ), + PackageInfo( + "requests", "networking", 300_000_000, 51000, "HTTP library", "networking" + ), ] # AWS and cloud packages CLOUD_PACKAGES = [ PackageInfo("boto3", "cloud", 280_000_000, 8900, "AWS SDK", "cloud"), PackageInfo("botocore", "cloud", 275_000_000, 1400, "AWS SDK core", "cloud"), - PackageInfo("s3transfer", "cloud", 250_000_000, 200, "S3 transfer utilities", "cloud"), + PackageInfo( + "s3transfer", "cloud", 250_000_000, 200, "S3 transfer utilities", "cloud" + ), PackageInfo("awscli", "cloud", 80_000_000, 15000, "AWS command line", "cloud"), PackageInfo("azure-core", "cloud", 45_000_000, 400, "Azure SDK core", "cloud"), - PackageInfo("google-cloud-storage", "cloud", 35_000_000, 300, "Google Cloud Storage", "cloud"), - PackageInfo("azure-storage-blob", "cloud", 30_000_000, 200, "Azure Blob Storage", "cloud"), + PackageInfo( + "google-cloud-storage", + "cloud", + 35_000_000, + 300, + "Google Cloud Storage", + "cloud", + ), + PackageInfo( + "azure-storage-blob", "cloud", 30_000_000, 200, "Azure Blob Storage", "cloud" + ), ] # Data science and ML packages DATA_SCIENCE_PACKAGES = [ - PackageInfo("numpy", "data-science", 200_000_000, 26000, "Numerical computing", "data-science"), - PackageInfo("pandas", "data-science", 150_000_000, 42000, "Data manipulation", "data-science"), - PackageInfo("scikit-learn", "machine-learning", 80_000_000, 58000, "Machine learning", "machine-learning"), - PackageInfo("matplotlib", "visualization", 75_000_000, 19000, "Plotting library", "visualization"), - PackageInfo("scipy", "data-science", 70_000_000, 12000, "Scientific computing", "data-science"), - PackageInfo("seaborn", "visualization", 45_000_000, 11000, "Statistical visualization", "visualization"), - PackageInfo("plotly", "visualization", 40_000_000, 15000, "Interactive plots", "visualization"), - PackageInfo("jupyter", "development", 35_000_000, 7000, "Interactive notebooks", "development"), - PackageInfo("ipython", "development", 50_000_000, 8000, "Interactive Python", "development"), - PackageInfo("tensorflow", "machine-learning", 25_000_000, 185000, "Deep learning", "machine-learning"), - PackageInfo("torch", "machine-learning", 20_000_000, 81000, "PyTorch deep learning", "machine-learning"), - PackageInfo("transformers", "machine-learning", 15_000_000, 130000, "NLP transformers", "machine-learning"), + PackageInfo( + "numpy", + "data-science", + 200_000_000, + 26000, + "Numerical computing", + "data-science", + ), + PackageInfo( + "pandas", + "data-science", + 150_000_000, + 42000, + "Data manipulation", + "data-science", + ), + PackageInfo( + "scikit-learn", + "machine-learning", + 80_000_000, + 58000, + "Machine learning", + "machine-learning", + ), + PackageInfo( + "matplotlib", + "visualization", + 75_000_000, + 19000, + "Plotting library", + "visualization", + ), + PackageInfo( + "scipy", + "data-science", + 70_000_000, + 12000, + "Scientific computing", + "data-science", + ), + PackageInfo( + "seaborn", + "visualization", + 45_000_000, + 11000, + "Statistical visualization", + "visualization", + ), + PackageInfo( + "plotly", + "visualization", + 40_000_000, + 15000, + "Interactive plots", + "visualization", + ), + PackageInfo( + "jupyter", + "development", + 35_000_000, + 7000, + "Interactive notebooks", + "development", + ), + PackageInfo( + "ipython", "development", 50_000_000, 8000, "Interactive Python", "development" + ), + PackageInfo( + "tensorflow", + "machine-learning", + 25_000_000, + 185000, + "Deep learning", + "machine-learning", + ), + PackageInfo( + "torch", + "machine-learning", + 20_000_000, + 81000, + "PyTorch deep learning", + "machine-learning", + ), + PackageInfo( + "transformers", + "machine-learning", + 15_000_000, + 130000, + "NLP transformers", + "machine-learning", + ), ] # Development and testing DEVELOPMENT_PACKAGES = [ - PackageInfo("typing-extensions", "development", 180_000_000, 3000, "Typing extensions", "development"), - PackageInfo("packaging", "development", 160_000_000, 600, "Package utilities", "development"), - PackageInfo("pytest", "testing", 100_000_000, 11000, "Testing framework", "testing"), + PackageInfo( + "typing-extensions", + "development", + 180_000_000, + 3000, + "Typing extensions", + "development", + ), + PackageInfo( + "packaging", "development", 160_000_000, 600, "Package utilities", "development" + ), + PackageInfo( + "pytest", "testing", 100_000_000, 11000, "Testing framework", "testing" + ), PackageInfo("click", "cli", 90_000_000, 15000, "Command line interface", "cli"), - PackageInfo("pyyaml", "serialization", 85_000_000, 2200, "YAML parser", "serialization"), - PackageInfo("jinja2", "templating", 80_000_000, 10000, "Template engine", "templating"), - PackageInfo("markupsafe", "templating", 75_000_000, 600, "Safe markup", "templating"), - PackageInfo("attrs", "development", 60_000_000, 5000, "Classes without boilerplate", "development"), - PackageInfo("black", "development", 40_000_000, 38000, "Code formatter", "development"), - PackageInfo("flake8", "development", 35_000_000, 3000, "Code linting", "development"), - PackageInfo("mypy", "development", 30_000_000, 17000, "Static type checker", "development"), + PackageInfo( + "pyyaml", "serialization", 85_000_000, 2200, "YAML parser", "serialization" + ), + PackageInfo( + "jinja2", "templating", 80_000_000, 10000, "Template engine", "templating" + ), + PackageInfo( + "markupsafe", "templating", 75_000_000, 600, "Safe markup", "templating" + ), + PackageInfo( + "attrs", + "development", + 60_000_000, + 5000, + "Classes without boilerplate", + "development", + ), + PackageInfo( + "black", "development", 40_000_000, 38000, "Code formatter", "development" + ), + PackageInfo( + "flake8", "development", 35_000_000, 3000, "Code linting", "development" + ), + PackageInfo( + "mypy", "development", 30_000_000, 17000, "Static type checker", "development" + ), ] # Web development @@ -83,49 +251,87 @@ WEB_PACKAGES = [ PackageInfo("flask", "web", 55_000_000, 66000, "Micro web framework", "web"), PackageInfo("fastapi", "web", 35_000_000, 74000, "Modern web API framework", "web"), PackageInfo("sqlalchemy", "database", 50_000_000, 8000, "SQL toolkit", "database"), - PackageInfo("psycopg2", "database", 25_000_000, 3000, "PostgreSQL adapter", "database"), + PackageInfo( + "psycopg2", "database", 25_000_000, 3000, "PostgreSQL adapter", "database" + ), PackageInfo("redis", "database", 30_000_000, 12000, "Redis client", "database"), - PackageInfo("celery", "async", 25_000_000, 23000, "Distributed task queue", "async"), + PackageInfo( + "celery", "async", 25_000_000, 23000, "Distributed task queue", "async" + ), PackageInfo("gunicorn", "web", 20_000_000, 9000, "WSGI server", "web"), PackageInfo("uvicorn", "web", 15_000_000, 8000, "ASGI server", "web"), ] # Security and cryptography SECURITY_PACKAGES = [ - PackageInfo("cryptography", "security", 120_000_000, 6000, "Cryptographic library", "security"), - PackageInfo("pyopenssl", "security", 60_000_000, 800, "OpenSSL wrapper", "security"), + PackageInfo( + "cryptography", + "security", + 120_000_000, + 6000, + "Cryptographic library", + "security", + ), + PackageInfo( + "pyopenssl", "security", 60_000_000, 800, "OpenSSL wrapper", "security" + ), PackageInfo("pyjwt", "security", 40_000_000, 5000, "JSON Web Tokens", "security"), PackageInfo("bcrypt", "security", 35_000_000, 1200, "Password hashing", "security"), - PackageInfo("pycryptodome", "security", 30_000_000, 2700, "Cryptographic library", "security"), + PackageInfo( + "pycryptodome", + "security", + 30_000_000, + 2700, + "Cryptographic library", + "security", + ), ] # Networking and API NETWORKING_PACKAGES = [ PackageInfo("httpx", "networking", 25_000_000, 12000, "HTTP client", "networking"), PackageInfo("aiohttp", "networking", 35_000_000, 14000, "Async HTTP", "networking"), - PackageInfo("websockets", "networking", 20_000_000, 5000, "WebSocket implementation", "networking"), + PackageInfo( + "websockets", + "networking", + 20_000_000, + 5000, + "WebSocket implementation", + "networking", + ), PackageInfo("paramiko", "networking", 25_000_000, 8000, "SSH client", "networking"), ] # Text processing and parsing TEXT_PACKAGES = [ - PackageInfo("beautifulsoup4", "parsing", 40_000_000, 13000, "HTML/XML parser", "parsing"), + PackageInfo( + "beautifulsoup4", "parsing", 40_000_000, 13000, "HTML/XML parser", "parsing" + ), PackageInfo("lxml", "parsing", 35_000_000, 2600, "XML/HTML parser", "parsing"), - PackageInfo("regex", "text", 30_000_000, 700, "Regular expressions", "text-processing"), - PackageInfo("python-docx", "text", 15_000_000, 4000, "Word document processing", "text-processing"), + PackageInfo( + "regex", "text", 30_000_000, 700, "Regular expressions", "text-processing" + ), + PackageInfo( + "python-docx", + "text", + 15_000_000, + 4000, + "Word document processing", + "text-processing", + ), PackageInfo("pillow", "imaging", 60_000_000, 11000, "Image processing", "imaging"), ] # All packages combined for easy access ALL_POPULAR_PACKAGES = ( - INFRASTRUCTURE_PACKAGES + - CLOUD_PACKAGES + - DATA_SCIENCE_PACKAGES + - DEVELOPMENT_PACKAGES + - WEB_PACKAGES + - SECURITY_PACKAGES + - NETWORKING_PACKAGES + - TEXT_PACKAGES + INFRASTRUCTURE_PACKAGES + + CLOUD_PACKAGES + + DATA_SCIENCE_PACKAGES + + DEVELOPMENT_PACKAGES + + WEB_PACKAGES + + SECURITY_PACKAGES + + NETWORKING_PACKAGES + + TEXT_PACKAGES ) # Create lookup dictionaries @@ -136,41 +342,45 @@ for pkg in ALL_POPULAR_PACKAGES: PACKAGES_BY_CATEGORY[pkg.category] = [] PACKAGES_BY_CATEGORY[pkg.category].append(pkg) + def get_popular_packages( - category: str = None, - limit: int = 50, - min_downloads: int = 0 -) -> List[PackageInfo]: + category: str = None, limit: int = 50, min_downloads: int = 0 +) -> list[PackageInfo]: """Get popular packages filtered by criteria. - + Args: category: Filter by category (e.g., 'web', 'data-science', 'cloud') limit: Maximum number of packages to return min_downloads: Minimum estimated monthly downloads - + Returns: List of PackageInfo objects sorted by estimated downloads """ packages = ALL_POPULAR_PACKAGES - + if category: packages = [pkg for pkg in packages if pkg.category == category] - + if min_downloads: - packages = [pkg for pkg in packages if pkg.estimated_monthly_downloads >= min_downloads] - + packages = [ + pkg for pkg in packages if pkg.estimated_monthly_downloads >= min_downloads + ] + # Sort by estimated downloads (descending) - packages = sorted(packages, key=lambda x: x.estimated_monthly_downloads, reverse=True) - + packages = sorted( + packages, key=lambda x: x.estimated_monthly_downloads, reverse=True + ) + return packages[:limit] + def estimate_downloads_for_period(monthly_downloads: int, period: str) -> int: """Estimate downloads for different time periods. - + Args: monthly_downloads: Estimated monthly downloads period: Time period ('day', 'week', 'month') - + Returns: Estimated downloads for the period """ @@ -183,16 +393,20 @@ def estimate_downloads_for_period(monthly_downloads: int, period: str) -> int: else: return monthly_downloads + def get_package_info(package_name: str) -> PackageInfo: """Get information about a specific package. - + Args: package_name: Name of the package - + Returns: PackageInfo object or None if not found """ - return PACKAGES_BY_NAME.get(package_name.lower().replace("-", "_").replace("_", "-")) + return PACKAGES_BY_NAME.get( + package_name.lower().replace("-", "_").replace("_", "-") + ) + # GitHub repository URL patterns for fetching real-time data GITHUB_REPO_PATTERNS = { @@ -211,4 +425,4 @@ GITHUB_REPO_PATTERNS = { "boto3": "boto/boto3", "sqlalchemy": "sqlalchemy/sqlalchemy", # Add more mappings as needed -} \ No newline at end of file +} diff --git a/pypi_query_mcp/server.py b/pypi_query_mcp/server.py index 80c0632..60d2504 100644 --- a/pypi_query_mcp/server.py +++ b/pypi_query_mcp/server.py @@ -136,11 +136,11 @@ async def get_package_versions(package_name: str) -> dict[str, Any]: @mcp.tool() async def get_package_dependencies( - package_name: str, + package_name: str, version: str | None = None, include_transitive: bool = False, max_depth: int = 5, - python_version: str | None = None + python_version: str | None = None, ) -> dict[str, Any]: """Get dependency information for a PyPI package. @@ -175,7 +175,11 @@ async def get_package_dependencies( logger.info( f"MCP tool: Querying dependencies for {package_name}" + (f" version {version}" if version else " (latest)") - + (f" with transitive dependencies (max depth: {max_depth})" if include_transitive else " (direct only)") + + ( + f" with transitive dependencies (max depth: {max_depth})" + if include_transitive + else " (direct only)" + ) ) result = await query_package_dependencies( package_name, version, include_transitive, max_depth, python_version @@ -326,9 +330,9 @@ async def resolve_dependencies( Args: package_name: The name of the PyPI package to analyze (e.g., 'pyside2', 'django') python_version: Target Python version for dependency filtering (e.g., '3.10', '3.11') - include_extras: List of extra dependency groups to include. These are optional - dependency groups defined by the package (e.g., ['socks'] for requests, - ['argon2', 'bcrypt'] for django, ['test', 'doc'] for setuptools). Check the + include_extras: List of extra dependency groups to include. These are optional + dependency groups defined by the package (e.g., ['socks'] for requests, + ['argon2', 'bcrypt'] for django, ['test', 'doc'] for setuptools). Check the package's PyPI page or use the provides_extra field to see available extras. include_dev: Whether to include development dependencies (default: False) max_depth: Maximum recursion depth for dependency resolution (default: 5) @@ -397,8 +401,8 @@ async def download_package( package_name: The name of the PyPI package to download (e.g., 'pyside2', 'requests') download_dir: Local directory to download packages to (default: './downloads') python_version: Target Python version for compatibility (e.g., '3.10', '3.11') - include_extras: List of extra dependency groups to include. These are optional - dependency groups defined by the package (e.g., ['socks'] for requests, + include_extras: List of extra dependency groups to include. These are optional + dependency groups defined by the package (e.g., ['socks'] for requests, ['argon2', 'bcrypt'] for django). Check the package's PyPI page to see available extras. include_dev: Whether to include development dependencies (default: False) prefer_wheel: Whether to prefer wheel files over source distributions (default: True) diff --git a/pypi_query_mcp/tools/dependency_resolver.py b/pypi_query_mcp/tools/dependency_resolver.py index 0047aff..8132142 100644 --- a/pypi_query_mcp/tools/dependency_resolver.py +++ b/pypi_query_mcp/tools/dependency_resolver.py @@ -35,7 +35,7 @@ class DependencyResolver: Args: package_name: Name of the package to resolve python_version: Target Python version (e.g., "3.10") - include_extras: List of extra dependency groups to include (e.g., ['socks'] for requests, + include_extras: List of extra dependency groups to include (e.g., ['socks'] for requests, ['test', 'doc'] for setuptools). These are optional dependencies defined by the package. include_dev: Whether to include development dependencies max_depth: Maximum recursion depth (overrides instance default) @@ -243,7 +243,7 @@ async def resolve_package_dependencies( Args: package_name: Name of the package to resolve python_version: Target Python version (e.g., "3.10") - include_extras: List of extra dependency groups to include (e.g., ['socks'] for requests, + include_extras: List of extra dependency groups to include (e.g., ['socks'] for requests, ['test', 'doc'] for setuptools). These are optional dependencies defined by the package. include_dev: Whether to include development dependencies max_depth: Maximum recursion depth diff --git a/pypi_query_mcp/tools/download_stats.py b/pypi_query_mcp/tools/download_stats.py index dbc516c..6f99154 100644 --- a/pypi_query_mcp/tools/download_stats.py +++ b/pypi_query_mcp/tools/download_stats.py @@ -3,14 +3,13 @@ import logging import os from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any from ..core.github_client import GitHubAPIClient from ..core.pypi_client import PyPIClient from ..core.stats_client import PyPIStatsClient from ..data.popular_packages import ( GITHUB_REPO_PATTERNS, - PACKAGES_BY_NAME, estimate_downloads_for_period, get_popular_packages, ) @@ -73,11 +72,11 @@ async def get_package_download_stats( # Calculate trends and analysis analysis = _analyze_download_stats(download_data) - + # Determine data source and add warnings if needed data_source = recent_stats.get("source", "pypistats.org") warning_note = recent_stats.get("note") - + result = { "package": package_name, "metadata": package_metadata, @@ -87,15 +86,17 @@ async def get_package_download_stats( "data_source": data_source, "timestamp": datetime.now().isoformat(), } - + # Add warning/note about data quality if present if warning_note: result["data_quality_note"] = warning_note - + # Add reliability indicator if data_source == "fallback_estimates": result["reliability"] = "estimated" - result["warning"] = "Data is estimated due to API unavailability. Actual download counts may differ significantly." + result["warning"] = ( + "Data is estimated due to API unavailability. Actual download counts may differ significantly." + ) elif "stale" in warning_note.lower() if warning_note else False: result["reliability"] = "cached" result["warning"] = "Data may be outdated due to current API issues." @@ -142,7 +143,7 @@ async def get_package_download_trends( # Analyze trends trend_analysis = _analyze_download_trends(time_series_data, include_mirrors) - + # Determine data source and add warnings if needed data_source = overall_stats.get("source", "pypistats.org") warning_note = overall_stats.get("note") @@ -155,15 +156,17 @@ async def get_package_download_trends( "data_source": data_source, "timestamp": datetime.now().isoformat(), } - + # Add warning/note about data quality if present if warning_note: result["data_quality_note"] = warning_note - + # Add reliability indicator if data_source == "fallback_estimates": result["reliability"] = "estimated" - result["warning"] = "Data is estimated due to API unavailability. Actual download trends may differ significantly." + result["warning"] = ( + "Data is estimated due to API unavailability. Actual download trends may differ significantly." + ) elif "stale" in warning_note.lower() if warning_note else False: result["reliability"] = "cached" result["warning"] = "Data may be outdated due to current API issues." @@ -201,56 +204,54 @@ async def get_top_packages_by_downloads( """ # Get curated popular packages as base data curated_packages = get_popular_packages(limit=max(limit * 2, 100)) - + # Try to enhance with real PyPI stats - enhanced_packages = await _enhance_with_real_stats( - curated_packages, period, limit - ) - + enhanced_packages = await _enhance_with_real_stats(curated_packages, period, limit) + # Try to enhance with GitHub metrics - final_packages = await _enhance_with_github_stats( - enhanced_packages, limit - ) - + final_packages = await _enhance_with_github_stats(enhanced_packages, limit) + # Ensure we have the requested number of packages if len(final_packages) < limit: # Add more from curated list if needed additional_needed = limit - len(final_packages) existing_names = {pkg["package"] for pkg in final_packages} - + for pkg_info in curated_packages: if pkg_info.name not in existing_names and additional_needed > 0: - final_packages.append({ - "package": pkg_info.name, - "downloads": estimate_downloads_for_period( - pkg_info.estimated_monthly_downloads, period - ), - "period": period, - "data_source": "curated", - "category": pkg_info.category, - "description": pkg_info.description, - "estimated": True, - }) + final_packages.append( + { + "package": pkg_info.name, + "downloads": estimate_downloads_for_period( + pkg_info.estimated_monthly_downloads, period + ), + "period": period, + "data_source": "curated", + "category": pkg_info.category, + "description": pkg_info.description, + "estimated": True, + } + ) additional_needed -= 1 - + # Sort by download count and assign ranks final_packages.sort(key=lambda x: x.get("downloads", 0), reverse=True) final_packages = final_packages[:limit] - + for i, package in enumerate(final_packages): package["rank"] = i + 1 - + # Determine primary data source real_stats_count = len([p for p in final_packages if not p.get("estimated", False)]) github_enhanced_count = len([p for p in final_packages if "github_stars" in p]) - + if real_stats_count > limit // 2: primary_source = "pypistats.org with curated fallback" elif github_enhanced_count > 0: primary_source = "curated data enhanced with GitHub metrics" else: primary_source = "curated popular packages database" - + return { "top_packages": final_packages, "period": period, @@ -386,50 +387,73 @@ def _analyze_download_trends( async def _enhance_with_real_stats( - curated_packages: List, period: str, limit: int -) -> List[Dict[str, Any]]: + curated_packages: list, period: str, limit: int +) -> list[dict[str, Any]]: """Try to enhance curated packages with real PyPI download statistics. - + Args: curated_packages: List of PackageInfo objects from curated data period: Time period for stats limit: Maximum number of packages to process - + Returns: List of enhanced package dictionaries """ enhanced_packages = [] - + try: async with PyPIStatsClient() as stats_client: # Try to get real stats for top packages - for pkg_info in curated_packages[:limit * 2]: # Try more than needed + for pkg_info in curated_packages[: limit * 2]: # Try more than needed try: stats = await stats_client.get_recent_downloads( pkg_info.name, period, use_cache=True ) - + download_data = stats.get("data", {}) real_download_count = _extract_download_count(download_data, period) - + if real_download_count > 0: # Use real stats - enhanced_packages.append({ - "package": pkg_info.name, - "downloads": real_download_count, - "period": period, - "data_source": "pypistats.org", - "category": pkg_info.category, - "description": pkg_info.description, - "estimated": False, - }) - logger.debug(f"Got real stats for {pkg_info.name}: {real_download_count}") + enhanced_packages.append( + { + "package": pkg_info.name, + "downloads": real_download_count, + "period": period, + "data_source": "pypistats.org", + "category": pkg_info.category, + "description": pkg_info.description, + "estimated": False, + } + ) + logger.debug( + f"Got real stats for {pkg_info.name}: {real_download_count}" + ) else: # Fall back to estimated downloads estimated_downloads = estimate_downloads_for_period( pkg_info.estimated_monthly_downloads, period ) - enhanced_packages.append({ + enhanced_packages.append( + { + "package": pkg_info.name, + "downloads": estimated_downloads, + "period": period, + "data_source": "estimated", + "category": pkg_info.category, + "description": pkg_info.description, + "estimated": True, + } + ) + + except Exception as e: + logger.debug(f"Failed to get real stats for {pkg_info.name}: {e}") + # Fall back to estimated downloads + estimated_downloads = estimate_downloads_for_period( + pkg_info.estimated_monthly_downloads, period + ) + enhanced_packages.append( + { "package": pkg_info.name, "downloads": estimated_downloads, "period": period, @@ -437,28 +461,13 @@ async def _enhance_with_real_stats( "category": pkg_info.category, "description": pkg_info.description, "estimated": True, - }) - - except Exception as e: - logger.debug(f"Failed to get real stats for {pkg_info.name}: {e}") - # Fall back to estimated downloads - estimated_downloads = estimate_downloads_for_period( - pkg_info.estimated_monthly_downloads, period + } ) - enhanced_packages.append({ - "package": pkg_info.name, - "downloads": estimated_downloads, - "period": period, - "data_source": "estimated", - "category": pkg_info.category, - "description": pkg_info.description, - "estimated": True, - }) - + # Stop if we have enough packages if len(enhanced_packages) >= limit: break - + except Exception as e: logger.warning(f"PyPI stats client failed entirely: {e}") # Fall back to all estimated data @@ -466,52 +475,56 @@ async def _enhance_with_real_stats( estimated_downloads = estimate_downloads_for_period( pkg_info.estimated_monthly_downloads, period ) - enhanced_packages.append({ - "package": pkg_info.name, - "downloads": estimated_downloads, - "period": period, - "data_source": "estimated", - "category": pkg_info.category, - "description": pkg_info.description, - "estimated": True, - }) - + enhanced_packages.append( + { + "package": pkg_info.name, + "downloads": estimated_downloads, + "period": period, + "data_source": "estimated", + "category": pkg_info.category, + "description": pkg_info.description, + "estimated": True, + } + ) + return enhanced_packages async def _enhance_with_github_stats( - packages: List[Dict[str, Any]], limit: int -) -> List[Dict[str, Any]]: + packages: list[dict[str, Any]], limit: int +) -> list[dict[str, Any]]: """Try to enhance packages with GitHub repository statistics. - + Args: packages: List of package dictionaries to enhance limit: Maximum number of packages to process - + Returns: List of enhanced package dictionaries """ github_token = os.getenv("GITHUB_TOKEN") # Optional GitHub token - + try: async with GitHubAPIClient(github_token=github_token) as github_client: # Get GitHub repo paths for packages that have them repo_paths = [] package_to_repo = {} - + for pkg in packages[:limit]: repo_path = GITHUB_REPO_PATTERNS.get(pkg["package"]) if repo_path: repo_paths.append(repo_path) package_to_repo[pkg["package"]] = repo_path - + if repo_paths: # Fetch GitHub stats for all repositories concurrently - logger.debug(f"Fetching GitHub stats for {len(repo_paths)} repositories") + logger.debug( + f"Fetching GitHub stats for {len(repo_paths)} repositories" + ) repo_stats = await github_client.get_multiple_repo_stats( repo_paths, use_cache=True, max_concurrent=3 ) - + # Enhance packages with GitHub data for pkg in packages: repo_path = package_to_repo.get(pkg["package"]) @@ -523,38 +536,42 @@ async def _enhance_with_github_stats( pkg["github_updated_at"] = stats["updated_at"] pkg["github_language"] = stats["language"] pkg["github_topics"] = stats.get("topics", []) - + # Adjust download estimates based on GitHub popularity if pkg.get("estimated", False): popularity_boost = _calculate_popularity_boost(stats) - pkg["downloads"] = int(pkg["downloads"] * popularity_boost) + pkg["downloads"] = int( + pkg["downloads"] * popularity_boost + ) pkg["github_enhanced"] = True - - logger.info(f"Enhanced {len([p for p in packages if 'github_stars' in p])} packages with GitHub data") - + + logger.info( + f"Enhanced {len([p for p in packages if 'github_stars' in p])} packages with GitHub data" + ) + except Exception as e: logger.debug(f"GitHub enhancement failed: {e}") # Continue without GitHub enhancement pass - + return packages -def _calculate_popularity_boost(github_stats: Dict[str, Any]) -> float: +def _calculate_popularity_boost(github_stats: dict[str, Any]) -> float: """Calculate a popularity boost multiplier based on GitHub metrics. - + Args: github_stats: GitHub repository statistics - + Returns: Multiplier between 0.5 and 2.0 based on popularity """ stars = github_stats.get("stars", 0) forks = github_stats.get("forks", 0) - + # Base multiplier multiplier = 1.0 - + # Adjust based on stars (logarithmic scale) if stars > 50000: multiplier *= 1.5 @@ -568,7 +585,7 @@ def _calculate_popularity_boost(github_stats: Dict[str, Any]) -> float: multiplier *= 0.9 elif stars < 500: multiplier *= 0.8 - + # Adjust based on forks (indicates active usage) if forks > 10000: multiplier *= 1.2 @@ -576,7 +593,7 @@ def _calculate_popularity_boost(github_stats: Dict[str, Any]) -> float: multiplier *= 1.1 elif forks < 100: multiplier *= 0.9 - + # Ensure multiplier stays within reasonable bounds return max(0.5, min(2.0, multiplier)) diff --git a/pypi_query_mcp/tools/package_query.py b/pypi_query_mcp/tools/package_query.py index 0c8d8c1..a6bd23e 100644 --- a/pypi_query_mcp/tools/package_query.py +++ b/pypi_query_mcp/tools/package_query.py @@ -68,8 +68,12 @@ def format_package_info(package_data: dict[str, Any]) -> dict[str, Any]: formatted["total_versions"] = len(releases) # Sort versions semantically and get the most recent 10 if releases: - sorted_versions = sort_versions_semantically(list(releases.keys()), reverse=True) - formatted["available_versions"] = sorted_versions[:10] # Most recent 10 versions + sorted_versions = sort_versions_semantically( + list(releases.keys()), reverse=True + ) + formatted["available_versions"] = sorted_versions[ + :10 + ] # Most recent 10 versions else: formatted["available_versions"] = [] @@ -139,7 +143,7 @@ def format_dependency_info(package_data: dict[str, Any]) -> dict[str, Any]: Formatted dependency information """ from ..core.dependency_parser import DependencyParser - + info = package_data.get("info", {}) requires_dist = info.get("requires_dist", []) or [] provides_extra = info.get("provides_extra", []) or [] @@ -152,7 +156,7 @@ def format_dependency_info(package_data: dict[str, Any]) -> dict[str, Any]: # Convert Requirements back to strings for JSON serialization runtime_deps = [str(req) for req in categories["runtime"]] dev_deps = [str(req) for req in categories["development"]] - + # Convert optional dependencies (extras) to string format optional_deps = {} for extra_name, reqs in categories["extras"].items(): @@ -161,14 +165,31 @@ def format_dependency_info(package_data: dict[str, Any]) -> dict[str, Any]: # Separate development and non-development optional dependencies dev_optional_deps = {} non_dev_optional_deps = {} - + # Define development-related extra names (same as in DependencyParser) dev_extra_names = { - 'dev', 'development', 'test', 'testing', 'tests', 'lint', 'linting', - 'doc', 'docs', 'documentation', 'build', 'check', 'cover', 'coverage', - 'type', 'typing', 'mypy', 'style', 'format', 'quality' + "dev", + "development", + "test", + "testing", + "tests", + "lint", + "linting", + "doc", + "docs", + "documentation", + "build", + "check", + "cover", + "coverage", + "type", + "typing", + "mypy", + "style", + "format", + "quality", } - + for extra_name, deps in optional_deps.items(): if extra_name.lower() in dev_extra_names: dev_optional_deps[extra_name] = deps @@ -260,11 +281,11 @@ async def query_package_versions(package_name: str) -> dict[str, Any]: async def query_package_dependencies( - package_name: str, - version: str | None = None, + package_name: str, + version: str | None = None, include_transitive: bool = False, max_depth: int = 5, - python_version: str | None = None + python_version: str | None = None, ) -> dict[str, Any]: """Query package dependency information from PyPI. @@ -293,29 +314,35 @@ async def query_package_dependencies( logger.info( f"Querying dependencies for package: {package_name}" + (f" version {version}" if version else " (latest)") - + (f" with transitive dependencies (max depth: {max_depth})" if include_transitive else " (direct only)") + + ( + f" with transitive dependencies (max depth: {max_depth})" + if include_transitive + else " (direct only)" + ) ) try: if include_transitive: # Use the comprehensive dependency resolver for transitive dependencies from .dependency_resolver import resolve_package_dependencies - + result = await resolve_package_dependencies( package_name=package_name, python_version=python_version, include_extras=[], include_dev=False, - max_depth=max_depth + max_depth=max_depth, ) - + # Format the transitive dependency result to match expected structure return format_transitive_dependency_info(result, package_name, version) else: # Use direct dependency logic with version support async with PyPIClient() as client: # Pass the version parameter to get_package_info - package_data = await client.get_package_info(package_name, version=version) + package_data = await client.get_package_info( + package_name, version=version + ) return format_dependency_info(package_data) except PyPIError: # Re-raise PyPI-specific errors @@ -342,9 +369,9 @@ def format_transitive_dependency_info( normalized_name = package_name.lower().replace("_", "-") dependency_tree = resolver_result.get("dependency_tree", {}) summary = resolver_result.get("summary", {}) - + main_package = dependency_tree.get(normalized_name, {}) - + # Build the response in the same format as direct dependencies but with tree structure result = { "package_name": package_name, @@ -353,42 +380,51 @@ def format_transitive_dependency_info( "include_transitive": True, "max_depth": summary.get("max_depth", 0), "python_version": resolver_result.get("python_version"), - # Direct dependencies (same as before) "runtime_dependencies": main_package.get("dependencies", {}).get("runtime", []), - "development_dependencies": main_package.get("dependencies", {}).get("development", []), + "development_dependencies": main_package.get("dependencies", {}).get( + "development", [] + ), "optional_dependencies": main_package.get("dependencies", {}).get("extras", {}), - # Transitive dependency information "transitive_dependencies": { - "dependency_tree": _build_dependency_tree_structure(dependency_tree, normalized_name), + "dependency_tree": _build_dependency_tree_structure( + dependency_tree, normalized_name + ), "all_packages": _extract_all_packages_info(dependency_tree), "circular_dependencies": _detect_circular_dependencies(dependency_tree), "depth_analysis": _analyze_dependency_depths(dependency_tree), }, - # Enhanced summary statistics "dependency_summary": { - "direct_runtime_count": len(main_package.get("dependencies", {}).get("runtime", [])), - "direct_dev_count": len(main_package.get("dependencies", {}).get("development", [])), - "direct_optional_groups": len(main_package.get("dependencies", {}).get("extras", {})), - "total_transitive_packages": summary.get("total_packages", 0) - 1, # Exclude main package + "direct_runtime_count": len( + main_package.get("dependencies", {}).get("runtime", []) + ), + "direct_dev_count": len( + main_package.get("dependencies", {}).get("development", []) + ), + "direct_optional_groups": len( + main_package.get("dependencies", {}).get("extras", {}) + ), + "total_transitive_packages": summary.get("total_packages", 0) + - 1, # Exclude main package "total_runtime_dependencies": summary.get("total_runtime_dependencies", 0), - "total_development_dependencies": summary.get("total_development_dependencies", 0), + "total_development_dependencies": summary.get( + "total_development_dependencies", 0 + ), "total_extra_dependencies": summary.get("total_extra_dependencies", 0), "max_dependency_depth": summary.get("max_depth", 0), "complexity_score": _calculate_complexity_score(summary), }, - # Performance and health metrics "analysis": { "resolution_stats": summary, "potential_conflicts": _analyze_potential_conflicts(dependency_tree), "maintenance_concerns": _analyze_maintenance_concerns(dependency_tree), "performance_impact": _assess_performance_impact(summary), - } + }, } - + return result @@ -398,27 +434,27 @@ def _build_dependency_tree_structure( """Build a hierarchical dependency tree structure.""" if visited is None: visited = set() - + if root_package in visited: return {"circular_reference": True, "package_name": root_package} - + visited.add(root_package) - + if root_package not in dependency_tree: return {} - + package_info = dependency_tree[root_package] children = package_info.get("children", {}) - + tree_node = { "package_name": package_info.get("name", root_package), "version": package_info.get("version", "unknown"), "depth": package_info.get("depth", 0), "requires_python": package_info.get("requires_python", ""), "dependencies": package_info.get("dependencies", {}), - "children": {} + "children": {}, } - + # Recursively build children (with visited tracking to prevent infinite loops) for child_name in children: if child_name not in visited: @@ -427,17 +463,19 @@ def _build_dependency_tree_structure( ) else: tree_node["children"][child_name] = { - "circular_reference": True, - "package_name": child_name + "circular_reference": True, + "package_name": child_name, } - + return tree_node -def _extract_all_packages_info(dependency_tree: dict[str, Any]) -> dict[str, dict[str, Any]]: +def _extract_all_packages_info( + dependency_tree: dict[str, Any], +) -> dict[str, dict[str, Any]]: """Extract comprehensive information about all packages in the dependency tree.""" all_packages = {} - + for package_name, package_info in dependency_tree.items(): all_packages[package_name] = { "name": package_info.get("name", package_name), @@ -446,60 +484,73 @@ def _extract_all_packages_info(dependency_tree: dict[str, Any]) -> dict[str, dic "requires_python": package_info.get("requires_python", ""), "direct_dependencies": { "runtime": package_info.get("dependencies", {}).get("runtime", []), - "development": package_info.get("dependencies", {}).get("development", []), + "development": package_info.get("dependencies", {}).get( + "development", [] + ), "extras": package_info.get("dependencies", {}).get("extras", {}), }, "dependency_count": { "runtime": len(package_info.get("dependencies", {}).get("runtime", [])), - "development": len(package_info.get("dependencies", {}).get("development", [])), - "total_extras": sum(len(deps) for deps in package_info.get("dependencies", {}).get("extras", {}).values()), - } + "development": len( + package_info.get("dependencies", {}).get("development", []) + ), + "total_extras": sum( + len(deps) + for deps in package_info.get("dependencies", {}) + .get("extras", {}) + .values() + ), + }, } - + return all_packages -def _detect_circular_dependencies(dependency_tree: dict[str, Any]) -> list[dict[str, Any]]: +def _detect_circular_dependencies( + dependency_tree: dict[str, Any], +) -> list[dict[str, Any]]: """Detect circular dependencies in the dependency tree.""" circular_deps = [] - + def dfs(package_name: str, path: list[str], visited: set[str]) -> None: if package_name in path: # Found a circular dependency cycle_start = path.index(package_name) cycle = path[cycle_start:] + [package_name] - circular_deps.append({ - "cycle": cycle, - "length": len(cycle) - 1, - "packages_involved": list(set(cycle)) - }) + circular_deps.append( + { + "cycle": cycle, + "length": len(cycle) - 1, + "packages_involved": list(set(cycle)), + } + ) return - + if package_name in visited or package_name not in dependency_tree: return - + visited.add(package_name) path.append(package_name) - + # Check children children = dependency_tree[package_name].get("children", {}) for child_name in children: dfs(child_name, path.copy(), visited) - + # Start DFS from each package for package_name in dependency_tree: dfs(package_name, [], set()) - + # Remove duplicates unique_cycles = [] seen_cycles = set() - + for cycle_info in circular_deps: cycle_set = frozenset(cycle_info["packages_involved"]) if cycle_set not in seen_cycles: seen_cycles.add(cycle_set) unique_cycles.append(cycle_info) - + return unique_cycles @@ -507,29 +558,36 @@ def _analyze_dependency_depths(dependency_tree: dict[str, Any]) -> dict[str, Any """Analyze the depth distribution of dependencies.""" depth_counts = {} depth_packages = {} - + for package_name, package_info in dependency_tree.items(): depth = package_info.get("depth", 0) - + if depth not in depth_counts: depth_counts[depth] = 0 depth_packages[depth] = [] - + depth_counts[depth] += 1 depth_packages[depth].append(package_name) - + max_depth = max(depth_counts.keys()) if depth_counts else 0 - + return { "max_depth": max_depth, "depth_distribution": depth_counts, "packages_by_depth": depth_packages, - "average_depth": sum(d * c for d, c in depth_counts.items()) / sum(depth_counts.values()) if depth_counts else 0, + "average_depth": sum(d * c for d, c in depth_counts.items()) + / sum(depth_counts.values()) + if depth_counts + else 0, "depth_analysis": { "shallow_deps": depth_counts.get(1, 0), # Direct dependencies - "deep_deps": sum(count for depth, count in depth_counts.items() if depth > 2), - "leaf_packages": [pkg for pkg, info in dependency_tree.items() if not info.get("children")] - } + "deep_deps": sum( + count for depth, count in depth_counts.items() if depth > 2 + ), + "leaf_packages": [ + pkg for pkg, info in dependency_tree.items() if not info.get("children") + ], + }, } @@ -538,14 +596,14 @@ def _calculate_complexity_score(summary: dict[str, Any]) -> dict[str, Any]: total_packages = summary.get("total_packages", 0) max_depth = summary.get("max_depth", 0) total_deps = summary.get("total_runtime_dependencies", 0) - + # Simple complexity scoring (can be enhanced) base_score = total_packages * 0.3 depth_penalty = max_depth * 1.5 dependency_penalty = total_deps * 0.1 - + complexity_score = base_score + depth_penalty + dependency_penalty - + # Classify complexity if complexity_score < 10: complexity_level = "low" @@ -558,8 +616,10 @@ def _calculate_complexity_score(summary: dict[str, Any]) -> dict[str, Any]: recommendation = "High complexity, consider dependency management strategies" else: complexity_level = "very_high" - recommendation = "Very high complexity, significant maintenance overhead expected" - + recommendation = ( + "Very high complexity, significant maintenance overhead expected" + ) + return { "score": round(complexity_score, 2), "level": complexity_level, @@ -568,42 +628,50 @@ def _calculate_complexity_score(summary: dict[str, Any]) -> dict[str, Any]: "total_packages": total_packages, "max_depth": max_depth, "total_dependencies": total_deps, - } + }, } -def _analyze_potential_conflicts(dependency_tree: dict[str, Any]) -> list[dict[str, Any]]: +def _analyze_potential_conflicts( + dependency_tree: dict[str, Any], +) -> list[dict[str, Any]]: """Analyze potential version conflicts in dependencies.""" - # This is a simplified analysis - in a real implementation, + # This is a simplified analysis - in a real implementation, # you'd parse version constraints and check for conflicts package_versions = {} potential_conflicts = [] - + for package_name, package_info in dependency_tree.items(): runtime_deps = package_info.get("dependencies", {}).get("runtime", []) - + for dep_str in runtime_deps: # Basic parsing of "package>=version" format if ">=" in dep_str or "==" in dep_str or "<" in dep_str or ">" in dep_str: - parts = dep_str.replace(">=", "@").replace("==", "@").replace("<", "@").replace(">", "@") + parts = ( + dep_str.replace(">=", "@") + .replace("==", "@") + .replace("<", "@") + .replace(">", "@") + ) dep_name = parts.split("@")[0].strip() - + if dep_name not in package_versions: package_versions[dep_name] = [] - package_versions[dep_name].append({ - "constraint": dep_str, - "required_by": package_name - }) - + package_versions[dep_name].append( + {"constraint": dep_str, "required_by": package_name} + ) + # Look for packages with multiple version constraints for dep_name, constraints in package_versions.items(): if len(constraints) > 1: - potential_conflicts.append({ - "package": dep_name, - "conflicting_constraints": constraints, - "severity": "potential" if len(constraints) == 2 else "high" - }) - + potential_conflicts.append( + { + "package": dep_name, + "conflicting_constraints": constraints, + "severity": "potential" if len(constraints) == 2 else "high", + } + ) + return potential_conflicts @@ -611,25 +679,25 @@ def _analyze_maintenance_concerns(dependency_tree: dict[str, Any]) -> dict[str, """Analyze maintenance concerns in the dependency tree.""" total_packages = len(dependency_tree) packages_without_version = sum( - 1 for info in dependency_tree.values() + 1 + for info in dependency_tree.values() if info.get("version") in ["unknown", "", None] ) - + packages_without_python_req = sum( - 1 for info in dependency_tree.values() - if not info.get("requires_python") + 1 for info in dependency_tree.values() if not info.get("requires_python") ) - + # Calculate dependency concentration (packages with many dependencies) high_dep_packages = [ { "name": name, - "dependency_count": len(info.get("dependencies", {}).get("runtime", [])) + "dependency_count": len(info.get("dependencies", {}).get("runtime", [])), } for name, info in dependency_tree.items() if len(info.get("dependencies", {}).get("runtime", [])) > 5 ] - + return { "total_packages": total_packages, "packages_without_version_info": packages_without_version, @@ -637,11 +705,18 @@ def _analyze_maintenance_concerns(dependency_tree: dict[str, Any]) -> dict[str, "high_dependency_packages": high_dep_packages, "maintenance_risk_score": { "score": round( - (packages_without_version / total_packages * 100) + - (len(high_dep_packages) / total_packages * 50), 2 - ) if total_packages > 0 else 0, - "level": "low" if total_packages < 10 else "moderate" if total_packages < 30 else "high" - } + (packages_without_version / total_packages * 100) + + (len(high_dep_packages) / total_packages * 50), + 2, + ) + if total_packages > 0 + else 0, + "level": "low" + if total_packages < 10 + else "moderate" + if total_packages < 30 + else "high", + }, } @@ -649,34 +724,40 @@ def _assess_performance_impact(summary: dict[str, Any]) -> dict[str, Any]: """Assess the performance impact of the dependency tree.""" total_packages = summary.get("total_packages", 0) max_depth = summary.get("max_depth", 0) - + # Estimate installation time (rough approximation) estimated_install_time = total_packages * 2 + max_depth * 5 # seconds - + # Estimate memory footprint (very rough) estimated_memory_mb = total_packages * 10 + max_depth * 5 - + # Performance recommendations recommendations = [] if total_packages > 50: - recommendations.append("Consider using virtual environments to isolate dependencies") + recommendations.append( + "Consider using virtual environments to isolate dependencies" + ) if max_depth > 5: - recommendations.append("Deep dependency chains may slow resolution and installation") + recommendations.append( + "Deep dependency chains may slow resolution and installation" + ) if total_packages > 100: recommendations.append("Consider dependency analysis tools for large projects") - + return { "estimated_install_time_seconds": estimated_install_time, "estimated_memory_footprint_mb": estimated_memory_mb, "performance_level": ( - "good" if total_packages < 20 - else "moderate" if total_packages < 50 + "good" + if total_packages < 20 + else "moderate" + if total_packages < 50 else "concerning" ), "recommendations": recommendations, "metrics": { "package_count_impact": "low" if total_packages < 20 else "high", "depth_impact": "low" if max_depth < 4 else "high", - "resolution_complexity": "simple" if total_packages < 10 else "complex" - } + "resolution_complexity": "simple" if total_packages < 10 else "complex", + }, } diff --git a/pyproject.toml b/pyproject.toml index bda48e6..411dc9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ httpx = "^0.28.0" packaging = "^24.0" pydantic = "^2.0.0" pydantic-settings = "^2.0.0" -click = "^8.1.0" +click = "8.1.7" [tool.poetry.group.dev.dependencies] pytest = "^8.0.0" diff --git a/quick_test.py b/quick_test.py index 179c7eb..20b100e 100644 --- a/quick_test.py +++ b/quick_test.py @@ -2,8 +2,9 @@ """Quick test to verify fallback mechanism works.""" import asyncio -import sys import os +import sys + sys.path.insert(0, os.path.abspath(".")) from pypi_query_mcp.tools.download_stats import get_package_download_stats @@ -12,23 +13,23 @@ from pypi_query_mcp.tools.download_stats import get_package_download_stats async def quick_test(): """Quick test with a single package.""" print("Testing fallback mechanism with requests package...") - + try: stats = await get_package_download_stats("requests", period="month") - - print(f"โœ… Success!") + + print("โœ… Success!") print(f"Package: {stats.get('package')}") print(f"Data Source: {stats.get('data_source')}") print(f"Reliability: {stats.get('reliability')}") - - if stats.get('warning'): + + if stats.get("warning"): print(f"โš ๏ธ Warning: {stats['warning']}") - + downloads = stats.get("downloads", {}) print(f"Downloads - Month: {downloads.get('last_month', 0):,}") - + return True - + except Exception as e: print(f"โŒ Error: {e}") return False @@ -36,4 +37,4 @@ async def quick_test(): if __name__ == "__main__": success = asyncio.run(quick_test()) - sys.exit(0 if success else 1) \ No newline at end of file + sys.exit(0 if success else 1) diff --git a/simple_test.py b/simple_test.py index 3199f0b..a294a14 100644 --- a/simple_test.py +++ b/simple_test.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 """Simple test for the transitive dependency formatting functions.""" -import sys import os +import sys # Add the current directory to Python path sys.path.insert(0, os.path.dirname(__file__)) + def test_formatting_functions(): """Test the formatting functions directly.""" print("Testing transitive dependency formatting functions...") - + # Sample data that mimics what the dependency resolver would return sample_resolver_result = { "package_name": "requests", @@ -21,9 +22,13 @@ def test_formatting_functions(): "version": "2.31.0", "requires_python": ">=3.7", "dependencies": { - "runtime": ["urllib3>=1.21.1", "certifi>=2017.4.17", "charset-normalizer>=2.0"], + "runtime": [ + "urllib3>=1.21.1", + "certifi>=2017.4.17", + "charset-normalizer>=2.0", + ], "development": [], - "extras": {} + "extras": {}, }, "depth": 0, "children": { @@ -34,10 +39,10 @@ def test_formatting_functions(): "dependencies": { "runtime": [], "development": [], - "extras": {} + "extras": {}, }, "depth": 1, - "children": {} + "children": {}, }, "certifi": { "name": "certifi", @@ -46,37 +51,29 @@ def test_formatting_functions(): "dependencies": { "runtime": [], "development": [], - "extras": {} + "extras": {}, }, "depth": 1, - "children": {} - } - } + "children": {}, + }, + }, }, "urllib3": { "name": "urllib3", "version": "2.0.4", "requires_python": ">=3.7", - "dependencies": { - "runtime": [], - "development": [], - "extras": {} - }, + "dependencies": {"runtime": [], "development": [], "extras": {}}, "depth": 1, - "children": {} + "children": {}, }, "certifi": { "name": "certifi", "version": "2023.7.22", "requires_python": ">=3.6", - "dependencies": { - "runtime": [], - "development": [], - "extras": {} - }, + "dependencies": {"runtime": [], "development": [], "extras": {}}, "depth": 1, - "children": {} - } + "children": {}, + }, }, "summary": { "total_packages": 3, @@ -84,55 +81,62 @@ def test_formatting_functions(): "total_development_dependencies": 0, "total_extra_dependencies": 0, "max_depth": 1, - "package_list": ["requests", "urllib3", "certifi"] - } + "package_list": ["requests", "urllib3", "certifi"], + }, } - + # Import the formatting function try: from pypi_query_mcp.tools.package_query import ( - format_transitive_dependency_info, - _build_dependency_tree_structure, - _extract_all_packages_info, - _detect_circular_dependencies, _analyze_dependency_depths, - _calculate_complexity_score + _build_dependency_tree_structure, + _calculate_complexity_score, + _detect_circular_dependencies, + _extract_all_packages_info, + format_transitive_dependency_info, ) - + # Test format_transitive_dependency_info print("โœ“ Successfully imported formatting functions") - + result = format_transitive_dependency_info(sample_resolver_result, "requests") - + print(f"โœ“ Formatted result for package: {result.get('package_name')}") print(f" Include transitive: {result.get('include_transitive')}") print(f" Version: {result.get('version')}") print(f" Max depth: {result.get('max_depth')}") - + # Test transitive dependencies section - transitive = result.get('transitive_dependencies', {}) + transitive = result.get("transitive_dependencies", {}) print(f" All packages count: {len(transitive.get('all_packages', {}))}") - print(f" Circular dependencies: {len(transitive.get('circular_dependencies', []))}") - + print( + f" Circular dependencies: {len(transitive.get('circular_dependencies', []))}" + ) + # Test dependency summary - summary = result.get('dependency_summary', {}) + summary = result.get("dependency_summary", {}) print(f" Direct runtime count: {summary.get('direct_runtime_count')}") - print(f" Total transitive packages: {summary.get('total_transitive_packages')}") + print( + f" Total transitive packages: {summary.get('total_transitive_packages')}" + ) print(f" Complexity level: {summary.get('complexity_score', {}).get('level')}") - + # Test analysis section - analysis = result.get('analysis', {}) - print(f" Performance level: {analysis.get('performance_impact', {}).get('performance_level')}") - + analysis = result.get("analysis", {}) + print( + f" Performance level: {analysis.get('performance_impact', {}).get('performance_level')}" + ) + print("โœ“ All formatting functions working correctly") return True - + except ImportError as e: print(f"โœ— Import error: {e}") return False except Exception as e: print(f"โœ— Error testing formatting functions: {e}") import traceback + traceback.print_exc() return False @@ -140,50 +144,51 @@ def test_formatting_functions(): def test_helper_functions(): """Test individual helper functions.""" print("\nTesting helper functions...") - + sample_tree = { "pkg-a": { "name": "pkg-a", "version": "1.0.0", "depth": 0, - "children": {"pkg-b": {}, "pkg-c": {}} - }, - "pkg-b": { - "name": "pkg-b", - "version": "2.0.0", - "depth": 1, - "children": {} + "children": {"pkg-b": {}, "pkg-c": {}}, }, + "pkg-b": {"name": "pkg-b", "version": "2.0.0", "depth": 1, "children": {}}, "pkg-c": { "name": "pkg-c", - "version": "3.0.0", + "version": "3.0.0", "depth": 1, - "children": {"pkg-b": {}} # Creates potential circular reference - } + "children": {"pkg-b": {}}, # Creates potential circular reference + }, } - + try: from pypi_query_mcp.tools.package_query import ( - _extract_all_packages_info, _analyze_dependency_depths, - _calculate_complexity_score + _calculate_complexity_score, + _extract_all_packages_info, ) - + # Test _extract_all_packages_info all_packages = _extract_all_packages_info(sample_tree) print(f"โœ“ Extracted {len(all_packages)} packages") - + # Test _analyze_dependency_depths depth_analysis = _analyze_dependency_depths(sample_tree) print(f"โœ“ Depth analysis - max depth: {depth_analysis.get('max_depth')}") - + # Test _calculate_complexity_score - sample_summary = {"total_packages": 3, "max_depth": 1, "total_runtime_dependencies": 2} + sample_summary = { + "total_packages": 3, + "max_depth": 1, + "total_runtime_dependencies": 2, + } complexity = _calculate_complexity_score(sample_summary) - print(f"โœ“ Complexity score: {complexity.get('score')} ({complexity.get('level')})") - + print( + f"โœ“ Complexity score: {complexity.get('score')} ({complexity.get('level')})" + ) + return True - + except Exception as e: print(f"โœ— Error testing helper functions: {e}") return False @@ -193,14 +198,14 @@ def main(): """Run tests.""" print("Simple Test for Transitive Dependencies") print("=" * 50) - + results = [] results.append(test_formatting_functions()) results.append(test_helper_functions()) - + print("\n" + "=" * 50) print(f"Test Results: {sum(results)}/{len(results)} passed") - + if all(results): print("โœ“ All formatting tests passed!") return 0 @@ -210,4 +215,4 @@ def main(): if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/test_core_only.py b/test_core_only.py index b536e46..d1c6d44 100644 --- a/test_core_only.py +++ b/test_core_only.py @@ -3,23 +3,25 @@ import asyncio import logging -import sys import os +import sys # Add the project root to the Python path sys.path.insert(0, os.path.dirname(__file__)) # Set up logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) async def test_pypi_client(): """Test the PyPIClient with version-specific queries.""" # Import only the core modules we need - from pypi_query_mcp.core.pypi_client import PyPIClient from pypi_query_mcp.core.exceptions import PackageNotFoundError - + from pypi_query_mcp.core.pypi_client import PyPIClient + async with PyPIClient() as client: # Test 1: Django 4.2.0 (specific version) logger.info("Testing Django 4.2.0...") @@ -27,19 +29,21 @@ async def test_pypi_client(): data = await client.get_package_info("django", version="4.2.0") actual_version = data.get("info", {}).get("version", "") if actual_version in ["4.2", "4.2.0"]: # PyPI may normalize version numbers - logger.info(f"โœ… Django 4.2.0 test passed (got version: {actual_version})") + logger.info( + f"โœ… Django 4.2.0 test passed (got version: {actual_version})" + ) else: logger.error(f"โŒ Expected version 4.2.0, got {actual_version}") return False - + # Check dependencies deps = data.get("info", {}).get("requires_dist", []) logger.info(f" Dependencies found: {len(deps) if deps else 0}") - + except Exception as e: logger.error(f"โŒ Django 4.2.0 test failed: {e}") return False - + # Test 2: Latest Django (no version) logger.info("Testing Django latest...") try: @@ -49,7 +53,7 @@ async def test_pypi_client(): except Exception as e: logger.error(f"โŒ Django latest test failed: {e}") return False - + # Test 3: Non-existent version (should fail) logger.info("Testing Django 999.999.999 (should fail)...") try: @@ -61,7 +65,7 @@ async def test_pypi_client(): except Exception as e: logger.error(f"โŒ Unexpected error type: {e}") return False - + # Test 4: FastAPI 0.100.0 logger.info("Testing FastAPI 0.100.0...") try: @@ -75,7 +79,7 @@ async def test_pypi_client(): except Exception as e: logger.error(f"โŒ FastAPI 0.100.0 test failed: {e}") return False - + # Test 5: NumPy 1.20.0 logger.info("Testing NumPy 1.20.0...") try: @@ -89,14 +93,17 @@ async def test_pypi_client(): except Exception as e: logger.error(f"โŒ NumPy 1.20.0 test failed: {e}") return False - + return True async def test_dependency_formatting(): """Test the dependency formatting functions.""" - from pypi_query_mcp.tools.package_query import format_dependency_info, validate_version_format - + from pypi_query_mcp.tools.package_query import ( + format_dependency_info, + validate_version_format, + ) + # Test version validation logger.info("Testing version validation...") test_versions = [ @@ -110,15 +117,17 @@ async def test_dependency_formatting(): ("", False), (None, True), ] - + for version, expected in test_versions: result = validate_version_format(version) if result == expected: logger.info(f"โœ… Version validation for '{version}': {result}") else: - logger.error(f"โŒ Version validation for '{version}': expected {expected}, got {result}") + logger.error( + f"โŒ Version validation for '{version}': expected {expected}, got {result}" + ) return False - + # Test dependency formatting with mock data logger.info("Testing dependency formatting...") mock_data = { @@ -130,84 +139,97 @@ async def test_dependency_formatting(): "requests>=2.25.0", "click>=8.0.0", "pytest>=6.0.0; extra=='test'", - "black>=21.0.0; extra=='dev'" - ] + "black>=21.0.0; extra=='dev'", + ], } } - + result = format_dependency_info(mock_data) - expected_fields = ["package_name", "version", "runtime_dependencies", "dependency_summary"] + expected_fields = [ + "package_name", + "version", + "runtime_dependencies", + "dependency_summary", + ] for field in expected_fields: if field not in result: logger.error(f"โŒ Missing field '{field}' in dependency formatting result") return False - + if len(result["runtime_dependencies"]) >= 2: # Should have requests and click logger.info("โœ… Dependency formatting test passed") else: - logger.error(f"โŒ Expected at least 2 runtime dependencies, got {len(result['runtime_dependencies'])}") + logger.error( + f"โŒ Expected at least 2 runtime dependencies, got {len(result['runtime_dependencies'])}" + ) return False - + return True async def test_comparison(): """Test that version-specific queries return different results than latest.""" from pypi_query_mcp.core.pypi_client import PyPIClient - + logger.info("Testing that version-specific queries work differently than latest...") - + async with PyPIClient() as client: # Get Django latest latest_data = await client.get_package_info("django", version=None) latest_version = latest_data.get("info", {}).get("version", "") - + # Get Django 4.2.0 specifically specific_data = await client.get_package_info("django", version="4.2.0") specific_version = specific_data.get("info", {}).get("version", "") - + logger.info(f"Latest Django version: {latest_version}") logger.info(f"Specific Django version: {specific_version}") - + # They should be different (unless 4.2.0 happens to be latest, which is unlikely) if specific_version in ["4.2", "4.2.0"] and latest_version != specific_version: - logger.info("โœ… Version-specific query returns different version than latest") + logger.info( + "โœ… Version-specific query returns different version than latest" + ) return True elif specific_version in ["4.2", "4.2.0"]: - logger.info("โš ๏ธ Specific version matches latest (this is fine, but less conclusive)") + logger.info( + "โš ๏ธ Specific version matches latest (this is fine, but less conclusive)" + ) return True else: - logger.error(f"โŒ Specific version query failed: expected 4.2.0, got {specific_version}") + logger.error( + f"โŒ Specific version query failed: expected 4.2.0, got {specific_version}" + ) return False async def main(): """Run all tests.""" logger.info("Starting PyPI client and dependency query tests...") - + success = True - + # Test PyPI client if await test_pypi_client(): logger.info("โœ… PyPI client tests passed") else: logger.error("โŒ PyPI client tests failed") success = False - + # Test dependency formatting if await test_dependency_formatting(): logger.info("โœ… Dependency formatting tests passed") else: logger.error("โŒ Dependency formatting tests failed") success = False - + # Test comparison if await test_comparison(): logger.info("โœ… Version comparison test passed") else: logger.error("โŒ Version comparison test failed") success = False - + if success: logger.info("๐ŸŽ‰ All tests passed!") return 0 @@ -218,4 +240,4 @@ async def main(): if __name__ == "__main__": exit_code = asyncio.run(main()) - sys.exit(exit_code) \ No newline at end of file + sys.exit(exit_code) diff --git a/test_direct.py b/test_direct.py index 04e5305..d3c1094 100644 --- a/test_direct.py +++ b/test_direct.py @@ -3,28 +3,32 @@ import asyncio import logging -import sys import os import re -import httpx +import sys from urllib.parse import quote +import httpx + # Add the project root to the Python path sys.path.insert(0, os.path.dirname(__file__)) # Set up logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) class SimplePackageNotFoundError(Exception): """Simple exception for package not found.""" + pass class SimplePyPIClient: """Simplified PyPI client for testing.""" - + def __init__(self): self.base_url = "https://pypi.org/pypi" self.client = httpx.AsyncClient( @@ -35,28 +39,30 @@ class SimplePyPIClient: }, follow_redirects=True, ) - + async def __aenter__(self): return self - + async def __aexit__(self, exc_type, exc_val, exc_tb): await self.client.aclose() - + async def get_package_info(self, package_name: str, version: str = None): """Get package info with optional version.""" if version: url = f"{self.base_url}/{quote(package_name)}/{quote(version)}/json" else: url = f"{self.base_url}/{quote(package_name)}/json" - + response = await self.client.get(url) - + if response.status_code == 404: if version: - raise SimplePackageNotFoundError(f"Version {version} not found for package {package_name}") + raise SimplePackageNotFoundError( + f"Version {version} not found for package {package_name}" + ) else: raise SimplePackageNotFoundError(f"Package {package_name} not found") - + response.raise_for_status() return response.json() @@ -65,7 +71,7 @@ def validate_version_format(version: str | None) -> bool: """Validate version format.""" if version is None: return True - + version_pattern = r"^[0-9]+(?:\.[0-9]+)*(?:[\.\-]?(?:a|b|rc|alpha|beta|dev|pre|post|final)[0-9]*)*$" return bool(re.match(version_pattern, version.strip(), re.IGNORECASE)) @@ -73,49 +79,53 @@ def validate_version_format(version: str | None) -> bool: async def test_version_parameter_fix(): """Test the version parameter functionality.""" logger.info("Testing version parameter fix...") - + async with SimplePyPIClient() as client: # Test 1: Django 4.2.0 (specific version) logger.info("Testing Django 4.2.0...") try: data = await client.get_package_info("django", "4.2.0") actual_version = data.get("info", {}).get("version", "") - + if actual_version in ["4.2", "4.2.0"]: - logger.info(f"โœ… Django 4.2.0 test passed (got version: {actual_version})") - + logger.info( + f"โœ… Django 4.2.0 test passed (got version: {actual_version})" + ) + # Check dependencies deps = data.get("info", {}).get("requires_dist", []) logger.info(f" Dependencies found: {len(deps) if deps else 0}") - + # Print a few dependencies to show they're different from latest if deps: logger.info(f" Sample dependencies: {deps[:3]}") else: logger.error(f"โŒ Expected version 4.2.0, got {actual_version}") return False - + except Exception as e: logger.error(f"โŒ Django 4.2.0 test failed: {e}") return False - + # Test 2: Django latest (no version) logger.info("Testing Django latest...") try: data = await client.get_package_info("django") latest_version = data.get("info", {}).get("version", "") logger.info(f"โœ… Django latest test passed - version: {latest_version}") - + # Verify that latest != 4.2.0 (to prove we're getting different results) if latest_version not in ["4.2", "4.2.0"]: logger.info("โœ… Confirmed: latest version is different from 4.2.0") else: - logger.info("โ„น๏ธ Latest version happens to be 4.2.0 (unlikely but possible)") - + logger.info( + "โ„น๏ธ Latest version happens to be 4.2.0 (unlikely but possible)" + ) + except Exception as e: logger.error(f"โŒ Django latest test failed: {e}") return False - + # Test 3: FastAPI 0.100.0 logger.info("Testing FastAPI 0.100.0...") try: @@ -123,7 +133,7 @@ async def test_version_parameter_fix(): actual_version = data.get("info", {}).get("version", "") if actual_version == "0.100.0": logger.info("โœ… FastAPI 0.100.0 test passed") - + # Check dependencies deps = data.get("info", {}).get("requires_dist", []) logger.info(f" Dependencies found: {len(deps) if deps else 0}") @@ -133,7 +143,7 @@ async def test_version_parameter_fix(): except Exception as e: logger.error(f"โŒ FastAPI 0.100.0 test failed: {e}") return False - + # Test 4: NumPy 1.20.0 logger.info("Testing NumPy 1.20.0...") try: @@ -141,7 +151,7 @@ async def test_version_parameter_fix(): actual_version = data.get("info", {}).get("version", "") if actual_version == "1.20.0": logger.info("โœ… NumPy 1.20.0 test passed") - + # Check dependencies deps = data.get("info", {}).get("requires_dist", []) logger.info(f" Dependencies found: {len(deps) if deps else 0}") @@ -151,7 +161,7 @@ async def test_version_parameter_fix(): except Exception as e: logger.error(f"โŒ NumPy 1.20.0 test failed: {e}") return False - + # Test 5: Non-existent version (should fail) logger.info("Testing Django 999.999.999 (should fail)...") try: @@ -163,7 +173,7 @@ async def test_version_parameter_fix(): except Exception as e: logger.error(f"โŒ Unexpected error type: {e}") return False - + # Test 6: Pre-release version logger.info("Testing Django 5.0a1 (pre-release)...") try: @@ -171,18 +181,20 @@ async def test_version_parameter_fix(): actual_version = data.get("info", {}).get("version", "") logger.info(f"โœ… Django 5.0a1 test passed - got version: {actual_version}") except SimplePackageNotFoundError: - logger.info("โ„น๏ธ Django 5.0a1 not found (this is expected for some pre-release versions)") + logger.info( + "โ„น๏ธ Django 5.0a1 not found (this is expected for some pre-release versions)" + ) except Exception as e: logger.error(f"โŒ Django 5.0a1 test failed: {e}") return False - + return True def test_version_validation(): """Test version validation.""" logger.info("Testing version validation...") - + test_cases = [ ("1.0.0", True), ("2.1", True), @@ -195,41 +207,45 @@ def test_version_validation(): ("", False), (None, True), ] - + all_passed = True for version, expected in test_cases: result = validate_version_format(version) if result == expected: logger.info(f"โœ… Version validation for '{version}': {result}") else: - logger.error(f"โŒ Version validation for '{version}': expected {expected}, got {result}") + logger.error( + f"โŒ Version validation for '{version}': expected {expected}, got {result}" + ) all_passed = False - + return all_passed async def compare_dependencies(): """Compare dependencies between different versions.""" logger.info("Comparing dependencies between Django versions...") - + async with SimplePyPIClient() as client: # Get Django 4.2.0 dependencies data_420 = await client.get_package_info("django", "4.2.0") deps_420 = data_420.get("info", {}).get("requires_dist", []) - + # Get Django latest dependencies data_latest = await client.get_package_info("django") deps_latest = data_latest.get("info", {}).get("requires_dist", []) - + logger.info(f"Django 4.2.0 dependencies: {len(deps_420) if deps_420 else 0}") - logger.info(f"Django latest dependencies: {len(deps_latest) if deps_latest else 0}") - + logger.info( + f"Django latest dependencies: {len(deps_latest) if deps_latest else 0}" + ) + # Show some dependencies for comparison if deps_420: logger.info(f"Django 4.2.0 sample deps: {deps_420[:2]}") if deps_latest: logger.info(f"Django latest sample deps: {deps_latest[:2]}") - + # They might be the same if 4.2.0 is latest, but structure should be correct return True @@ -237,32 +253,34 @@ async def compare_dependencies(): async def main(): """Run all tests.""" logger.info("๐Ÿงช Starting version parameter fix verification tests...") - + success = True - + # Test version validation if test_version_validation(): logger.info("โœ… Version validation tests passed") else: logger.error("โŒ Version validation tests failed") success = False - + # Test version parameter functionality if await test_version_parameter_fix(): logger.info("โœ… Version parameter fix tests passed") else: logger.error("โŒ Version parameter fix tests failed") success = False - + # Compare dependencies if await compare_dependencies(): logger.info("โœ… Dependency comparison test passed") else: logger.error("โŒ Dependency comparison test failed") success = False - + if success: - logger.info("๐ŸŽ‰ All tests passed! The version parameter fix is working correctly.") + logger.info( + "๐ŸŽ‰ All tests passed! The version parameter fix is working correctly." + ) logger.info("") logger.info("Summary of what was fixed:") logger.info("- PyPIClient now supports version-specific queries") @@ -277,4 +295,4 @@ async def main(): if __name__ == "__main__": exit_code = asyncio.run(main()) - sys.exit(exit_code) \ No newline at end of file + sys.exit(exit_code) diff --git a/test_enhanced_stats.py b/test_enhanced_stats.py index ee91532..f653269 100644 --- a/test_enhanced_stats.py +++ b/test_enhanced_stats.py @@ -4,8 +4,8 @@ Test script for the enhanced PyPI download statistics with fallback mechanisms. """ import asyncio -import sys import os +import sys # Add the package to Python path sys.path.insert(0, os.path.abspath(".")) @@ -33,62 +33,66 @@ async def test_download_stats(): try: # Test recent downloads stats = await get_package_download_stats(package_name, period="month") - + print(f"Package: {stats.get('package')}") print(f"Data Source: {stats.get('data_source')}") print(f"Reliability: {stats.get('reliability', 'unknown')}") - - if stats.get('warning'): + + if stats.get("warning"): print(f"โš ๏ธ Warning: {stats['warning']}") - + downloads = stats.get("downloads", {}) - print(f"Downloads - Day: {downloads.get('last_day', 0):,}, " + - f"Week: {downloads.get('last_week', 0):,}, " + - f"Month: {downloads.get('last_month', 0):,}") - - if stats.get('data_quality_note'): + print( + f"Downloads - Day: {downloads.get('last_day', 0):,}, " + + f"Week: {downloads.get('last_week', 0):,}, " + + f"Month: {downloads.get('last_month', 0):,}" + ) + + if stats.get("data_quality_note"): print(f"Note: {stats['data_quality_note']}") except Exception as e: print(f"โŒ Error: {e}") - print(f"\n๐Ÿ“ˆ Testing download trends for 'requests':") + print("\n๐Ÿ“ˆ Testing download trends for 'requests':") print("-" * 50) try: trends = await get_package_download_trends("requests", include_mirrors=False) - + print(f"Package: {trends.get('package')}") print(f"Data Source: {trends.get('data_source')}") print(f"Reliability: {trends.get('reliability', 'unknown')}") - - if trends.get('warning'): + + if trends.get("warning"): print(f"โš ๏ธ Warning: {trends['warning']}") - + trend_analysis = trends.get("trend_analysis", {}) print(f"Data Points: {trend_analysis.get('data_points', 0)}") print(f"Total Downloads: {trend_analysis.get('total_downloads', 0):,}") print(f"Trend Direction: {trend_analysis.get('trend_direction', 'unknown')}") - - if trends.get('data_quality_note'): + + if trends.get("data_quality_note"): print(f"Note: {trends['data_quality_note']}") except Exception as e: print(f"โŒ Error: {e}") - print(f"\n๐Ÿ† Testing top packages:") + print("\n๐Ÿ† Testing top packages:") print("-" * 50) try: top_packages = await get_top_packages_by_downloads(period="month", limit=5) - + print(f"Data Source: {top_packages.get('data_source')}") print(f"Reliability: {top_packages.get('reliability', 'unknown')}") - print(f"Success Rate: {top_packages.get('data_collection_success_rate', 'unknown')}") - - if top_packages.get('warning'): + print( + f"Success Rate: {top_packages.get('data_collection_success_rate', 'unknown')}" + ) + + if top_packages.get("warning"): print(f"โš ๏ธ Warning: {top_packages['warning']}") - + packages_list = top_packages.get("top_packages", []) print(f"\nTop {len(packages_list)} packages:") for package in packages_list[:5]: @@ -107,4 +111,4 @@ async def test_download_stats(): if __name__ == "__main__": - asyncio.run(test_download_stats()) \ No newline at end of file + asyncio.run(test_download_stats()) diff --git a/test_improved.py b/test_improved.py index 087a6c3..d54d02b 100644 --- a/test_improved.py +++ b/test_improved.py @@ -2,44 +2,56 @@ """Test script for the improved get_top_packages_by_downloads function.""" import asyncio + from pypi_query_mcp.tools.download_stats import get_top_packages_by_downloads + async def test_improved(): try: - result = await get_top_packages_by_downloads('month', 10) - print('โœ… Success! Result keys:', list(result.keys())) - print(f'Number of packages returned: {len(result.get("top_packages", []))}') - print(f'Data source: {result.get("data_source")}') - print(f'Methodology: {result.get("methodology")}') - - print('\nTop 5 packages:') - for i, pkg in enumerate(result.get('top_packages', [])[:5]): - downloads = pkg.get('downloads', 0) - stars = pkg.get('github_stars', 'N/A') - estimated = '(estimated)' if pkg.get('estimated', False) else '(real)' - github_enhanced = ' ๐ŸŒŸ' if pkg.get('github_enhanced', False) else '' - print(f'{i+1}. {pkg.get("package", "N/A")} - {downloads:,} downloads {estimated}{github_enhanced}') - if stars != 'N/A': - print(f' GitHub: {stars:,} stars, {pkg.get("category", "N/A")} category') - + result = await get_top_packages_by_downloads("month", 10) + print("โœ… Success! Result keys:", list(result.keys())) + print(f"Number of packages returned: {len(result.get('top_packages', []))}") + print(f"Data source: {result.get('data_source')}") + print(f"Methodology: {result.get('methodology')}") + + print("\nTop 5 packages:") + for i, pkg in enumerate(result.get("top_packages", [])[:5]): + downloads = pkg.get("downloads", 0) + stars = pkg.get("github_stars", "N/A") + estimated = "(estimated)" if pkg.get("estimated", False) else "(real)" + github_enhanced = " ๐ŸŒŸ" if pkg.get("github_enhanced", False) else "" + print( + f"{i + 1}. {pkg.get('package', 'N/A')} - {downloads:,} downloads {estimated}{github_enhanced}" + ) + if stars != "N/A": + print( + f" GitHub: {stars:,} stars, {pkg.get('category', 'N/A')} category" + ) + # Test different periods - print('\n--- Testing different periods ---') - for period in ['day', 'week', 'month']: + print("\n--- Testing different periods ---") + for period in ["day", "week", "month"]: result = await get_top_packages_by_downloads(period, 3) - top_3 = result.get('top_packages', []) - print(f'{period}: {len(top_3)} packages, avg downloads: {sum(p.get("downloads", 0) for p in top_3) // max(len(top_3), 1):,}') - - print('\n--- Testing different limits ---') + top_3 = result.get("top_packages", []) + print( + f"{period}: {len(top_3)} packages, avg downloads: {sum(p.get('downloads', 0) for p in top_3) // max(len(top_3), 1):,}" + ) + + print("\n--- Testing different limits ---") for limit in [5, 20, 50]: - result = await get_top_packages_by_downloads('month', limit) - packages = result.get('top_packages', []) - real_count = len([p for p in packages if not p.get('estimated', False)]) - print(f'Limit {limit}: {len(packages)} packages returned, {real_count} with real stats') - + result = await get_top_packages_by_downloads("month", limit) + packages = result.get("top_packages", []) + real_count = len([p for p in packages if not p.get("estimated", False)]) + print( + f"Limit {limit}: {len(packages)} packages returned, {real_count} with real stats" + ) + except Exception as e: - print(f'โŒ Error: {e}') + print(f"โŒ Error: {e}") import traceback + traceback.print_exc() -if __name__ == '__main__': - asyncio.run(test_improved()) \ No newline at end of file + +if __name__ == "__main__": + asyncio.run(test_improved()) diff --git a/test_our_implementation.py b/test_our_implementation.py index c419e04..23f4272 100644 --- a/test_our_implementation.py +++ b/test_our_implementation.py @@ -3,38 +3,43 @@ import asyncio import logging -import sys import os +import sys # Add the project root to the Python path sys.path.insert(0, os.path.dirname(__file__)) # Set up logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) async def test_our_implementation(): """Test our actual implementation directly.""" - + # Import just the core pieces we need + from pypi_query_mcp.core.exceptions import ( + InvalidPackageNameError, + PackageNotFoundError, + ) from pypi_query_mcp.core.pypi_client import PyPIClient from pypi_query_mcp.tools.package_query import ( - query_package_dependencies, + format_dependency_info, + query_package_dependencies, validate_version_format, - format_dependency_info ) - from pypi_query_mcp.core.exceptions import PackageNotFoundError, InvalidPackageNameError - + logger.info("Testing our actual implementation...") - + # Test 1: Version validation logger.info("Testing version validation...") assert validate_version_format("1.0.0") == True assert validate_version_format("invalid!") == False assert validate_version_format(None) == True logger.info("โœ… Version validation works correctly") - + # Test 2: PyPI Client with version logger.info("Testing PyPIClient with version parameter...") async with PyPIClient() as client: @@ -42,82 +47,94 @@ async def test_our_implementation(): data = await client.get_package_info("django", version="4.2.0") assert data["info"]["version"] in ["4.2", "4.2.0"] logger.info(f"โœ… Got Django 4.2.0: {data['info']['version']}") - + # Test latest version data = await client.get_package_info("django", version=None) latest_version = data["info"]["version"] logger.info(f"โœ… Got Django latest: {latest_version}") - + # Verify they're different (unless 4.2 is latest, which is unlikely) if latest_version not in ["4.2", "4.2.0"]: - logger.info("โœ… Confirmed version-specific queries work differently than latest") - + logger.info( + "โœ… Confirmed version-specific queries work differently than latest" + ) + # Test 3: Dependency formatting logger.info("Testing dependency formatting...") async with PyPIClient() as client: data = await client.get_package_info("django", version="4.2.0") formatted = format_dependency_info(data) - + assert "package_name" in formatted assert "version" in formatted assert "runtime_dependencies" in formatted assert "dependency_summary" in formatted assert formatted["version"] in ["4.2", "4.2.0"] - logger.info(f"โœ… Dependency formatting works: {len(formatted['runtime_dependencies'])} runtime deps") - + logger.info( + f"โœ… Dependency formatting works: {len(formatted['runtime_dependencies'])} runtime deps" + ) + # Test 4: Full query_package_dependencies function logger.info("Testing query_package_dependencies function...") - + # Test with Django 4.2.0 result = await query_package_dependencies("django", "4.2.0") assert result["package_name"].lower() == "django" assert result["version"] in ["4.2", "4.2.0"] - logger.info(f"โœ… Django 4.2.0 dependencies: {len(result['runtime_dependencies'])} runtime deps") - + logger.info( + f"โœ… Django 4.2.0 dependencies: {len(result['runtime_dependencies'])} runtime deps" + ) + # Test with Django latest result_latest = await query_package_dependencies("django", None) assert result_latest["package_name"].lower() == "django" - logger.info(f"โœ… Django latest dependencies: {len(result_latest['runtime_dependencies'])} runtime deps") - + logger.info( + f"โœ… Django latest dependencies: {len(result_latest['runtime_dependencies'])} runtime deps" + ) + # Verify they might be different if result["version"] != result_latest["version"]: - logger.info("โœ… Confirmed: version-specific query returns different version than latest") - + logger.info( + "โœ… Confirmed: version-specific query returns different version than latest" + ) + # Test 5: Error cases logger.info("Testing error cases...") - + # Invalid version format try: await query_package_dependencies("django", "invalid!") assert False, "Should have raised InvalidPackageNameError" except InvalidPackageNameError: logger.info("โœ… Invalid version format correctly rejected") - + # Non-existent version try: await query_package_dependencies("django", "999.999.999") assert False, "Should have raised PackageNotFoundError" except PackageNotFoundError: logger.info("โœ… Non-existent version correctly rejected") - + # Test 6: Multiple packages logger.info("Testing multiple packages...") - + packages_and_versions = [ ("fastapi", "0.100.0"), ("numpy", "1.20.0"), ("requests", "2.25.1"), ] - + for package, version in packages_and_versions: try: result = await query_package_dependencies(package, version) assert result["package_name"].lower() == package.lower() assert result["version"] == version - logger.info(f"โœ… {package} {version}: {len(result['runtime_dependencies'])} runtime deps") + logger.info( + f"โœ… {package} {version}: {len(result['runtime_dependencies'])} runtime deps" + ) except Exception as e: logger.warning(f"โš ๏ธ {package} {version} failed (may not exist): {e}") - + return True @@ -134,10 +151,11 @@ async def main(): except Exception as e: logger.error(f"โŒ Test failed with exception: {e}") import traceback + traceback.print_exc() return 1 if __name__ == "__main__": exit_code = asyncio.run(main()) - sys.exit(exit_code) \ No newline at end of file + sys.exit(exit_code) diff --git a/test_real_packages.py b/test_real_packages.py index 6d1cf52..38604e9 100644 --- a/test_real_packages.py +++ b/test_real_packages.py @@ -3,6 +3,7 @@ import asyncio import logging + from pypi_query_mcp.tools.package_query import query_package_versions # Configure logging @@ -15,39 +16,39 @@ async def test_real_package_versions(): print("=" * 60) print("Testing Real Package Version Sorting") print("=" * 60) - + # Test packages known to have complex version histories test_packages = [ - "django", # Known for alpha, beta, rc versions - "numpy", # Long history with various formats - "requests" # Simple but well-known package + "django", # Known for alpha, beta, rc versions + "numpy", # Long history with various formats + "requests", # Simple but well-known package ] - + for package_name in test_packages: try: print(f"\nTesting {package_name}:") result = await query_package_versions(package_name) - + recent_versions = result.get("recent_versions", [])[:10] print(f" Recent versions (first 10): {recent_versions}") - + # Show older-style string sorting for comparison all_versions = result.get("versions", []) if all_versions: # Use basic string sorting (the old way) string_sorted = sorted(all_versions[:20], reverse=True) print(f" String-sorted (first 10): {string_sorted[:10]}") - - print(f" Semantic vs String comparison:") + + print(" Semantic vs String comparison:") for i in range(min(5, len(recent_versions))): semantic = recent_versions[i] if i < len(recent_versions) else "N/A" string_sort = string_sorted[i] if i < len(string_sorted) else "N/A" match = "โœ“" if semantic == string_sort else "โœ—" - print(f" {i+1}: {semantic} vs {string_sort} {match}") - + print(f" {i + 1}: {semantic} vs {string_sort} {match}") + except Exception as e: print(f" Error querying {package_name}: {e}") - + print() @@ -56,50 +57,50 @@ async def test_specific_version_ordering(): print("=" * 60) print("Specific Version Ordering Tests") print("=" * 60) - + # Let's test django which is known to have alpha, beta, rc versions try: print("Testing Django version ordering:") result = await query_package_versions("django") - + all_versions = result.get("versions", []) - + # Find versions around a specific release to verify ordering django_4_versions = [v for v in all_versions if v.startswith("4.2")][:15] print(f" Django 4.2.x versions: {django_4_versions}") - + # Check if pre-release versions are properly ordered pre_release_pattern = ["4.2a1", "4.2b1", "4.2rc1", "4.2.0"] found_versions = [v for v in django_4_versions if v in pre_release_pattern] print(f" Found pre-release sequence: {found_versions}") - + if len(found_versions) > 1: print(" Checking pre-release ordering:") for i in range(len(found_versions) - 1): current = found_versions[i] next_ver = found_versions[i + 1] print(f" {current} comes before {next_ver}") - + except Exception as e: print(f" Error testing Django versions: {e}") - + print() async def main(): """Main test function.""" print("Real Package Version Sorting Test") - print("="*60) - + print("=" * 60) + # Test with real packages await test_real_package_versions() - + # Test specific version ordering scenarios await test_specific_version_ordering() - + print("=" * 60) print("Real package test completed!") if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/test_simple.py b/test_simple.py index 495c975..fc2e6cd 100644 --- a/test_simple.py +++ b/test_simple.py @@ -3,22 +3,24 @@ import asyncio import logging -import sys import os +import sys # Add the project root to the Python path sys.path.insert(0, os.path.dirname(__file__)) # Set up logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) async def test_pypi_client(): """Test the PyPIClient with version-specific queries.""" - from pypi_query_mcp.core.pypi_client import PyPIClient from pypi_query_mcp.core.exceptions import PackageNotFoundError - + from pypi_query_mcp.core.pypi_client import PyPIClient + async with PyPIClient() as client: # Test 1: Django 4.2.0 (specific version) logger.info("Testing Django 4.2.0...") @@ -32,15 +34,15 @@ async def test_pypi_client(): else: logger.error(f"โŒ Expected version 4.2.0, got {actual_version}") return False - + # Check dependencies deps = data.get("info", {}).get("requires_dist", []) logger.info(f" Dependencies found: {len(deps)}") - + except Exception as e: logger.error(f"โŒ Django 4.2.0 test failed: {e}") return False - + # Test 2: Latest Django (no version) logger.info("Testing Django latest...") try: @@ -50,7 +52,7 @@ async def test_pypi_client(): except Exception as e: logger.error(f"โŒ Django latest test failed: {e}") return False - + # Test 3: Non-existent version (should fail) logger.info("Testing Django 999.999.999 (should fail)...") try: @@ -62,7 +64,7 @@ async def test_pypi_client(): except Exception as e: logger.error(f"โŒ Unexpected error type: {e}") return False - + # Test 4: FastAPI 0.100.0 logger.info("Testing FastAPI 0.100.0...") try: @@ -76,7 +78,7 @@ async def test_pypi_client(): except Exception as e: logger.error(f"โŒ FastAPI 0.100.0 test failed: {e}") return False - + # Test 5: NumPy 1.20.0 logger.info("Testing NumPy 1.20.0...") try: @@ -90,15 +92,20 @@ async def test_pypi_client(): except Exception as e: logger.error(f"โŒ NumPy 1.20.0 test failed: {e}") return False - + return True async def test_dependency_query(): """Test the query_package_dependencies function.""" - from pypi_query_mcp.tools.package_query import query_package_dependencies, validate_version_format - from pypi_query_mcp.core.exceptions import InvalidPackageNameError, PackageNotFoundError - + from pypi_query_mcp.core.exceptions import ( + InvalidPackageNameError, + ) + from pypi_query_mcp.tools.package_query import ( + query_package_dependencies, + validate_version_format, + ) + # Test version validation logger.info("Testing version validation...") test_versions = [ @@ -112,30 +119,39 @@ async def test_dependency_query(): ("", False), (None, True), ] - + for version, expected in test_versions: result = validate_version_format(version) if result == expected: logger.info(f"โœ… Version validation for '{version}': {result}") else: - logger.error(f"โŒ Version validation for '{version}': expected {expected}, got {result}") + logger.error( + f"โŒ Version validation for '{version}': expected {expected}, got {result}" + ) return False - + # Test dependency queries logger.info("Testing dependency queries...") - + # Test Django 4.2.0 dependencies try: result = await query_package_dependencies("django", "4.2.0") - if result["package_name"].lower() == "django" and result["version"] in ["4.2", "4.2.0"]: - logger.info(f"โœ… Django 4.2.0 dependencies query passed - {len(result['runtime_dependencies'])} runtime deps") + if result["package_name"].lower() == "django" and result["version"] in [ + "4.2", + "4.2.0", + ]: + logger.info( + f"โœ… Django 4.2.0 dependencies query passed - {len(result['runtime_dependencies'])} runtime deps" + ) else: - logger.error(f"โŒ Django dependencies query failed - got {result['package_name']} v{result['version']}") + logger.error( + f"โŒ Django dependencies query failed - got {result['package_name']} v{result['version']}" + ) return False except Exception as e: logger.error(f"โŒ Django dependencies query failed: {e}") return False - + # Test invalid version format try: result = await query_package_dependencies("django", "invalid.version!") @@ -146,30 +162,30 @@ async def test_dependency_query(): except Exception as e: logger.error(f"โŒ Unexpected error for invalid version: {e}") return False - + return True async def main(): """Run all tests.""" logger.info("Starting PyPI client and dependency query tests...") - + success = True - + # Test PyPI client if await test_pypi_client(): logger.info("โœ… PyPI client tests passed") else: logger.error("โŒ PyPI client tests failed") success = False - + # Test dependency queries if await test_dependency_query(): logger.info("โœ… Dependency query tests passed") else: logger.error("โŒ Dependency query tests failed") success = False - + if success: logger.info("๐ŸŽ‰ All tests passed!") return 0 @@ -180,4 +196,4 @@ async def main(): if __name__ == "__main__": exit_code = asyncio.run(main()) - sys.exit(exit_code) \ No newline at end of file + sys.exit(exit_code) diff --git a/test_specific_case.py b/test_specific_case.py index f9d9aaf..33b6d44 100644 --- a/test_specific_case.py +++ b/test_specific_case.py @@ -3,58 +3,65 @@ from pypi_query_mcp.core.version_utils import sort_versions_semantically + def test_specific_case(): """Test the exact case mentioned in the task requirements.""" print("=" * 60) print("Testing Specific Task Requirement") print("=" * 60) - + # The exact problem mentioned in the task versions = ["5.2rc1", "5.2.5"] - + # Old way (string sorting) old_sorted = sorted(versions, reverse=True) - + # New way (semantic sorting) new_sorted = sort_versions_semantically(versions, reverse=True) - + print(f"Original versions: {versions}") print(f"Old string sorting: {old_sorted}") print(f"New semantic sorting: {new_sorted}") print() - + print("Analysis:") - print(f" Problem: '5.2rc1' was appearing before '5.2.5' in string sorting") + print(" Problem: '5.2rc1' was appearing before '5.2.5' in string sorting") print(f" String sorting result: {old_sorted[0]} comes first") print(f" Semantic sorting result: {new_sorted[0]} comes first") print() - + if new_sorted == ["5.2.5", "5.2rc1"]: print(" โœ… SUCCESS: Semantic sorting correctly places 5.2.5 before 5.2rc1") print(" โœ… This fixes the issue described in the task!") else: print(" โŒ FAILED: The issue is not resolved") - + print() - + # Test a more comprehensive example comprehensive_test = [ - "5.2.5", "5.2rc1", "5.2.0", "5.2a1", "5.2b1", - "5.1.0", "5.3.0", "5.2.1" + "5.2.5", + "5.2rc1", + "5.2.0", + "5.2a1", + "5.2b1", + "5.1.0", + "5.3.0", + "5.2.1", ] - + old_comprehensive = sorted(comprehensive_test, reverse=True) new_comprehensive = sort_versions_semantically(comprehensive_test, reverse=True) - + print("Comprehensive version sorting test:") print(f" Input: {comprehensive_test}") print(f" String sorted: {old_comprehensive}") print(f" Semantic sorted: {new_comprehensive}") print() - + print("Expected semantic order (newest to oldest):") print(" 5.3.0 > 5.2.5 > 5.2.1 > 5.2.0 > 5.2rc1 > 5.2b1 > 5.2a1 > 5.1.0") if __name__ == "__main__": - test_specific_case() \ No newline at end of file + test_specific_case() diff --git a/test_transitive_deps.py b/test_transitive_deps.py index 6aa1010..e59c376 100644 --- a/test_transitive_deps.py +++ b/test_transitive_deps.py @@ -3,7 +3,7 @@ import asyncio import sys -import json + from pypi_query_mcp.tools.package_query import query_package_dependencies @@ -12,10 +12,14 @@ async def test_direct_dependencies(): print("Testing direct dependencies for 'requests'...") try: result = await query_package_dependencies("requests", include_transitive=False) - print(f"โœ“ Direct dependencies found: {len(result.get('runtime_dependencies', []))}") + print( + f"โœ“ Direct dependencies found: {len(result.get('runtime_dependencies', []))}" + ) print(f" Package: {result.get('package_name')}") print(f" Version: {result.get('version')}") - print(f" Runtime deps: {result.get('runtime_dependencies', [])[:3]}...") # Show first 3 + print( + f" Runtime deps: {result.get('runtime_dependencies', [])[:3]}..." + ) # Show first 3 return True except Exception as e: print(f"โœ— Error testing direct dependencies: {e}") @@ -27,44 +31,44 @@ async def test_transitive_dependencies(): print("\nTesting transitive dependencies for 'requests'...") try: result = await query_package_dependencies( - "requests", - include_transitive=True, - max_depth=3, - python_version="3.10" + "requests", include_transitive=True, max_depth=3, python_version="3.10" ) - - print(f"โœ“ Transitive analysis completed") + + print("โœ“ Transitive analysis completed") print(f" Include transitive: {result.get('include_transitive')}") print(f" Package: {result.get('package_name')}") print(f" Version: {result.get('version')}") - + # Check transitive dependency structure - transitive = result.get('transitive_dependencies', {}) - all_packages = transitive.get('all_packages', {}) + transitive = result.get("transitive_dependencies", {}) + all_packages = transitive.get("all_packages", {}) print(f" Total packages in tree: {len(all_packages)}") - + # Check summary - summary = result.get('dependency_summary', {}) + summary = result.get("dependency_summary", {}) print(f" Direct runtime deps: {summary.get('direct_runtime_count', 0)}") - print(f" Total transitive packages: {summary.get('total_transitive_packages', 0)}") + print( + f" Total transitive packages: {summary.get('total_transitive_packages', 0)}" + ) print(f" Max depth: {summary.get('max_dependency_depth', 0)}") - + # Check analysis - analysis = result.get('analysis', {}) - performance = analysis.get('performance_impact', {}) + analysis = result.get("analysis", {}) + performance = analysis.get("performance_impact", {}) print(f" Performance level: {performance.get('performance_level', 'unknown')}") - - complexity = summary.get('complexity_score', {}) + + complexity = summary.get("complexity_score", {}) print(f" Complexity level: {complexity.get('level', 'unknown')}") - + # Check circular dependencies - circular = transitive.get('circular_dependencies', []) + circular = transitive.get("circular_dependencies", []) print(f" Circular dependencies found: {len(circular)}") - + return True except Exception as e: print(f"โœ— Error testing transitive dependencies: {e}") import traceback + traceback.print_exc() return False @@ -74,19 +78,17 @@ async def test_small_package(): print("\nTesting transitive dependencies for 'six' (smaller package)...") try: result = await query_package_dependencies( - "six", - include_transitive=True, - max_depth=2 + "six", include_transitive=True, max_depth=2 ) - - transitive = result.get('transitive_dependencies', {}) - all_packages = transitive.get('all_packages', {}) - print(f"โœ“ Analysis completed for 'six'") + + transitive = result.get("transitive_dependencies", {}) + all_packages = transitive.get("all_packages", {}) + print("โœ“ Analysis completed for 'six'") print(f" Total packages: {len(all_packages)}") - - summary = result.get('dependency_summary', {}) + + summary = result.get("dependency_summary", {}) print(f" Direct runtime deps: {summary.get('direct_runtime_count', 0)}") - + return True except Exception as e: print(f"โœ— Error testing 'six': {e}") @@ -97,21 +99,21 @@ async def main(): """Run all tests.""" print("Testing PyPI Query MCP Server - Transitive Dependencies") print("=" * 60) - + results = [] - + # Test 1: Direct dependencies (existing functionality) results.append(await test_direct_dependencies()) - + # Test 2: Transitive dependencies (new functionality) results.append(await test_transitive_dependencies()) - + # Test 3: Small package test results.append(await test_small_package()) - + print("\n" + "=" * 60) print(f"Test Results: {sum(results)}/{len(results)} passed") - + if all(results): print("โœ“ All tests passed! Transitive dependency functionality is working.") return 0 @@ -121,4 +123,4 @@ async def main(): if __name__ == "__main__": - sys.exit(asyncio.run(main())) \ No newline at end of file + sys.exit(asyncio.run(main())) diff --git a/test_version_fix.py b/test_version_fix.py index 2b04985..8421d3e 100644 --- a/test_version_fix.py +++ b/test_version_fix.py @@ -3,95 +3,114 @@ import asyncio import logging -import sys import os +import sys # Add the project root to the Python path sys.path.insert(0, os.path.dirname(__file__)) from pypi_query_mcp.tools.package_query import query_package_dependencies -from pypi_query_mcp.core.exceptions import PackageNotFoundError, InvalidPackageNameError # Set up logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) -async def test_package_version(package_name: str, version: str = None, expect_error: bool = False): +async def test_package_version( + package_name: str, version: str = None, expect_error: bool = False +): """Test a specific package and version combination.""" version_str = f" version {version}" if version else " (latest)" logger.info(f"Testing {package_name}{version_str}") - + try: result = await query_package_dependencies(package_name, version) - + if expect_error: - logger.error(f"Expected error for {package_name}{version_str}, but got result") + logger.error( + f"Expected error for {package_name}{version_str}, but got result" + ) return False - + # Verify the result contains expected fields - required_fields = ["package_name", "version", "runtime_dependencies", "dependency_summary"] + required_fields = [ + "package_name", + "version", + "runtime_dependencies", + "dependency_summary", + ] for field in required_fields: if field not in result: - logger.error(f"Missing field '{field}' in result for {package_name}{version_str}") + logger.error( + f"Missing field '{field}' in result for {package_name}{version_str}" + ) return False - + # Check if we got the correct version actual_version = result.get("version", "") if version and actual_version != version: - logger.error(f"Expected version {version}, got {actual_version} for {package_name}") + logger.error( + f"Expected version {version}, got {actual_version} for {package_name}" + ) return False - - logger.info(f"โœ… Success: {package_name}{version_str} - Got version {actual_version}") + + logger.info( + f"โœ… Success: {package_name}{version_str} - Got version {actual_version}" + ) logger.info(f" Runtime dependencies: {len(result['runtime_dependencies'])}") - logger.info(f" Total dependencies: {result['dependency_summary']['runtime_count']}") - + logger.info( + f" Total dependencies: {result['dependency_summary']['runtime_count']}" + ) + return True - + except Exception as e: if expect_error: - logger.info(f"โœ… Expected error for {package_name}{version_str}: {type(e).__name__}: {e}") + logger.info( + f"โœ… Expected error for {package_name}{version_str}: {type(e).__name__}: {e}" + ) return True else: - logger.error(f"โŒ Unexpected error for {package_name}{version_str}: {type(e).__name__}: {e}") + logger.error( + f"โŒ Unexpected error for {package_name}{version_str}: {type(e).__name__}: {e}" + ) return False async def main(): """Run all tests.""" logger.info("Starting version parameter fix tests...") - + tests = [ # Test with valid package versions ("django", "4.2.0", False), ("fastapi", "0.100.0", False), ("numpy", "1.20.0", False), - # Test latest versions (no version specified) ("requests", None, False), ("click", None, False), - # Test edge cases - should fail ("django", "999.999.999", True), # Non-existent version ("nonexistent-package-12345", None, True), # Non-existent package ("django", "invalid.version.format!", True), # Invalid version format - # Test pre-release versions ("django", "5.0a1", False), # Pre-release (may or may not exist) ] - + passed = 0 total = len(tests) - + for package, version, expect_error in tests: try: if await test_package_version(package, version, expect_error): passed += 1 except Exception as e: logger.error(f"Test framework error: {e}") - + logger.info(f"\nTest Results: {passed}/{total} tests passed") - + if passed == total: logger.info("๐ŸŽ‰ All tests passed!") return 0 @@ -102,4 +121,4 @@ async def main(): if __name__ == "__main__": exit_code = asyncio.run(main()) - sys.exit(exit_code) \ No newline at end of file + sys.exit(exit_code) diff --git a/test_version_sorting.py b/test_version_sorting.py index 8cf6b20..fed829c 100644 --- a/test_version_sorting.py +++ b/test_version_sorting.py @@ -3,6 +3,7 @@ import asyncio import logging + from pypi_query_mcp.core.version_utils import sort_versions_semantically from pypi_query_mcp.tools.package_query import query_package_versions @@ -16,55 +17,75 @@ def test_semantic_version_sorting(): print("=" * 60) print("Testing Semantic Version Sorting Function") print("=" * 60) - + # Test case 1: Basic pre-release ordering test1_versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"] sorted1 = sort_versions_semantically(test1_versions) - print(f"Test 1 - Pre-release ordering:") + print("Test 1 - Pre-release ordering:") print(f" Input: {test1_versions}") print(f" Output: {sorted1}") - print(f" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']") + print(" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']") print() - + # Test case 2: Complex Django-like versions test2_versions = [ - "4.2.0", "4.2a1", "4.2b1", "4.2rc1", "4.1.0", "4.1.7", - "4.0.0", "3.2.18", "4.2.1", "4.2.2" + "4.2.0", + "4.2a1", + "4.2b1", + "4.2rc1", + "4.1.0", + "4.1.7", + "4.0.0", + "3.2.18", + "4.2.1", + "4.2.2", ] sorted2 = sort_versions_semantically(test2_versions) - print(f"Test 2 - Django-like versions:") + print("Test 2 - Django-like versions:") print(f" Input: {test2_versions}") print(f" Output: {sorted2}") print() - + # Test case 3: TensorFlow-like versions with dev builds test3_versions = [ - "2.13.0", "2.13.0rc1", "2.13.0rc0", "2.12.0", "2.12.1", - "2.14.0dev20230517", "2.13.0rc2" # This might not parse correctly + "2.13.0", + "2.13.0rc1", + "2.13.0rc0", + "2.12.0", + "2.12.1", + "2.14.0dev20230517", + "2.13.0rc2", # This might not parse correctly ] sorted3 = sort_versions_semantically(test3_versions) - print(f"Test 3 - TensorFlow-like versions:") + print("Test 3 - TensorFlow-like versions:") print(f" Input: {test3_versions}") print(f" Output: {sorted3}") print() - + # Test case 4: Edge cases and malformed versions test4_versions = [ - "1.0.0", "1.0.0.post1", "1.0.0.dev0", "1.0.0a1", "1.0.0b1", - "1.0.0rc1", "1.0.1", "invalid-version", "1.0" + "1.0.0", + "1.0.0.post1", + "1.0.0.dev0", + "1.0.0a1", + "1.0.0b1", + "1.0.0rc1", + "1.0.1", + "invalid-version", + "1.0", ] sorted4 = sort_versions_semantically(test4_versions) - print(f"Test 4 - Edge cases and malformed versions:") + print("Test 4 - Edge cases and malformed versions:") print(f" Input: {test4_versions}") print(f" Output: {sorted4}") print() - + # Test case 5: Empty and single item lists test5_empty = [] test5_single = ["1.0.0"] sorted5_empty = sort_versions_semantically(test5_empty) sorted5_single = sort_versions_semantically(test5_single) - print(f"Test 5 - Edge cases:") + print("Test 5 - Edge cases:") print(f" Empty list: {sorted5_empty}") print(f" Single item: {sorted5_single}") print() @@ -75,30 +96,30 @@ async def test_real_package_versions(): print("=" * 60) print("Testing Real Package Version Sorting") print("=" * 60) - + # Test packages known to have complex version histories test_packages = [ - "django", # Known for alpha, beta, rc versions + "django", # Known for alpha, beta, rc versions "tensorflow", # Complex versioning with dev builds - "numpy", # Long history with various formats - "requests" # Simple but well-known package + "numpy", # Long history with various formats + "requests", # Simple but well-known package ] - + for package_name in test_packages: try: print(f"\nTesting {package_name}:") result = await query_package_versions(package_name) - + recent_versions = result.get("recent_versions", [])[:10] print(f" Recent versions (first 10): {recent_versions}") - + # Check if versions seem to be properly sorted if len(recent_versions) >= 3: print(f" First three versions: {recent_versions[:3]}") - + except Exception as e: print(f" Error querying {package_name}: {e}") - + print() @@ -107,58 +128,58 @@ def validate_sorting_correctness(): print("=" * 60) print("Validation Tests") print("=" * 60) - + # The specific example from the task: "5.2rc1" should come after "5.2.5" task_example = ["5.2rc1", "5.2.5"] sorted_task = sort_versions_semantically(task_example) - + print("Task requirement validation:") print(f" Input: {task_example}") print(f" Output: {sorted_task}") - print(f" Requirement: '5.2rc1' should come after '5.2.5'") - + print(" Requirement: '5.2rc1' should come after '5.2.5'") + if sorted_task == ["5.2.5", "5.2rc1"]: print(" โœ… PASS: Requirement met!") else: print(" โŒ FAIL: Requirement not met!") - + print() - + # Test pre-release ordering: alpha < beta < rc < stable pre_release_test = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"] sorted_pre = sort_versions_semantically(pre_release_test) - + print("Pre-release ordering validation:") print(f" Input: {pre_release_test}") print(f" Output: {sorted_pre}") - print(f" Expected order: stable > rc > beta > alpha") - + print(" Expected order: stable > rc > beta > alpha") + expected_order = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"] if sorted_pre == expected_order: print(" โœ… PASS: Pre-release ordering correct!") else: print(" โŒ FAIL: Pre-release ordering incorrect!") - + print() async def main(): """Main test function.""" print("Semantic Version Sorting Test Suite") - print("="*60) - + print("=" * 60) + # Run unit tests test_semantic_version_sorting() - + # Validate specific requirements validate_sorting_correctness() - + # Test with real packages await test_real_package_versions() - + print("=" * 60) print("Test suite completed!") if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/test_version_sorting_standalone.py b/test_version_sorting_standalone.py index 7a2acb1..ffb3203 100644 --- a/test_version_sorting_standalone.py +++ b/test_version_sorting_standalone.py @@ -2,7 +2,8 @@ """Standalone test script to verify semantic version sorting functionality.""" import logging -from packaging.version import Version, InvalidVersion + +from packaging.version import InvalidVersion, Version # Configure logging logging.basicConfig(level=logging.INFO) @@ -11,31 +12,31 @@ logger = logging.getLogger(__name__) def sort_versions_semantically(versions: list[str], reverse: bool = True) -> list[str]: """Sort package versions using semantic version ordering. - + This function properly sorts versions by parsing them as semantic versions, ensuring that pre-release versions (alpha, beta, rc) are ordered correctly relative to stable releases. - + Args: versions: List of version strings to sort reverse: If True, sort in descending order (newest first). Default True. - + Returns: List of version strings sorted semantically - + Examples: >>> sort_versions_semantically(['1.0.0', '2.0.0a1', '1.5.0', '2.0.0']) ['2.0.0', '2.0.0a1', '1.5.0', '1.0.0'] - + >>> sort_versions_semantically(['5.2rc1', '5.2.5', '5.2.0']) ['5.2.5', '5.2.0', '5.2rc1'] """ if not versions: return [] - + def parse_version_safe(version_str: str) -> tuple[Version | None, str]: """Safely parse a version string, returning (parsed_version, original_string). - + Returns (None, original_string) if parsing fails. """ try: @@ -43,28 +44,28 @@ def sort_versions_semantically(versions: list[str], reverse: bool = True) -> lis except InvalidVersion: logger.debug(f"Failed to parse version '{version_str}' as semantic version") return (None, version_str) - + # Parse all versions, keeping track of originals parsed_versions = [parse_version_safe(v) for v in versions] - + # Separate valid and invalid versions valid_versions = [(v, orig) for v, orig in parsed_versions if v is not None] invalid_versions = [orig for v, orig in parsed_versions if v is None] - + # Sort valid versions semantically valid_versions.sort(key=lambda x: x[0], reverse=reverse) - + # Sort invalid versions lexicographically as fallback invalid_versions.sort(reverse=reverse) - + # Combine results: valid versions first, then invalid ones result = [orig for _, orig in valid_versions] + invalid_versions - + logger.debug( f"Sorted {len(versions)} versions: {len(valid_versions)} valid, " f"{len(invalid_versions)} invalid" ) - + return result @@ -73,55 +74,75 @@ def test_semantic_version_sorting(): print("=" * 60) print("Testing Semantic Version Sorting Function") print("=" * 60) - + # Test case 1: Basic pre-release ordering test1_versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"] sorted1 = sort_versions_semantically(test1_versions) - print(f"Test 1 - Pre-release ordering:") + print("Test 1 - Pre-release ordering:") print(f" Input: {test1_versions}") print(f" Output: {sorted1}") - print(f" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']") + print(" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']") print() - + # Test case 2: Complex Django-like versions test2_versions = [ - "4.2.0", "4.2a1", "4.2b1", "4.2rc1", "4.1.0", "4.1.7", - "4.0.0", "3.2.18", "4.2.1", "4.2.2" + "4.2.0", + "4.2a1", + "4.2b1", + "4.2rc1", + "4.1.0", + "4.1.7", + "4.0.0", + "3.2.18", + "4.2.1", + "4.2.2", ] sorted2 = sort_versions_semantically(test2_versions) - print(f"Test 2 - Django-like versions:") + print("Test 2 - Django-like versions:") print(f" Input: {test2_versions}") print(f" Output: {sorted2}") print() - + # Test case 3: TensorFlow-like versions with dev builds test3_versions = [ - "2.13.0", "2.13.0rc1", "2.13.0rc0", "2.12.0", "2.12.1", - "2.14.0dev20230517", "2.13.0rc2" # This might not parse correctly + "2.13.0", + "2.13.0rc1", + "2.13.0rc0", + "2.12.0", + "2.12.1", + "2.14.0dev20230517", + "2.13.0rc2", # This might not parse correctly ] sorted3 = sort_versions_semantically(test3_versions) - print(f"Test 3 - TensorFlow-like versions:") + print("Test 3 - TensorFlow-like versions:") print(f" Input: {test3_versions}") print(f" Output: {sorted3}") print() - + # Test case 4: Edge cases and malformed versions test4_versions = [ - "1.0.0", "1.0.0.post1", "1.0.0.dev0", "1.0.0a1", "1.0.0b1", - "1.0.0rc1", "1.0.1", "invalid-version", "1.0" + "1.0.0", + "1.0.0.post1", + "1.0.0.dev0", + "1.0.0a1", + "1.0.0b1", + "1.0.0rc1", + "1.0.1", + "invalid-version", + "1.0", ] sorted4 = sort_versions_semantically(test4_versions) - print(f"Test 4 - Edge cases and malformed versions:") + print("Test 4 - Edge cases and malformed versions:") print(f" Input: {test4_versions}") print(f" Output: {sorted4}") print() - + # Test case 5: Empty and single item lists test5_empty = [] test5_single = ["1.0.0"] sorted5_empty = sort_versions_semantically(test5_empty) sorted5_single = sort_versions_semantically(test5_single) - print(f"Test 5 - Edge cases:") + print("Test 5 - Edge cases:") print(f" Empty list: {sorted5_empty}") print(f" Single item: {sorted5_single}") print() @@ -132,38 +153,38 @@ def validate_sorting_correctness(): print("=" * 60) print("Validation Tests") print("=" * 60) - + # The specific example from the task: "5.2rc1" should come after "5.2.5" task_example = ["5.2rc1", "5.2.5"] sorted_task = sort_versions_semantically(task_example) - + print("Task requirement validation:") print(f" Input: {task_example}") print(f" Output: {sorted_task}") - print(f" Requirement: '5.2rc1' should come after '5.2.5'") - + print(" Requirement: '5.2rc1' should come after '5.2.5'") + if sorted_task == ["5.2.5", "5.2rc1"]: print(" โœ… PASS: Requirement met!") else: print(" โŒ FAIL: Requirement not met!") - + print() - + # Test pre-release ordering: alpha < beta < rc < stable pre_release_test = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"] sorted_pre = sort_versions_semantically(pre_release_test) - + print("Pre-release ordering validation:") print(f" Input: {pre_release_test}") print(f" Output: {sorted_pre}") - print(f" Expected order: stable > rc > beta > alpha") - + print(" Expected order: stable > rc > beta > alpha") + expected_order = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"] if sorted_pre == expected_order: print(" โœ… PASS: Pre-release ordering correct!") else: print(" โŒ FAIL: Pre-release ordering incorrect!") - + print() @@ -172,7 +193,7 @@ def test_version_comparison_details(): print("=" * 60) print("Version Comparison Details") print("=" * 60) - + test_versions = [ ("1.0.0", "1.0.0a1"), ("1.0.0", "1.0.0b1"), @@ -184,7 +205,7 @@ def test_version_comparison_details(): ("1.0.0.post1", "1.0.0"), ("1.0.0.dev0", "1.0.0"), ] - + for v1, v2 in test_versions: try: ver1 = Version(v1) @@ -193,27 +214,27 @@ def test_version_comparison_details(): print(f" {v1} {comparison} {v2}") except Exception as e: print(f" Error comparing {v1} and {v2}: {e}") - + print() def main(): """Main test function.""" print("Semantic Version Sorting Test Suite") - print("="*60) - + print("=" * 60) + # Run unit tests test_semantic_version_sorting() - + # Validate specific requirements validate_sorting_correctness() - + # Show detailed version comparisons test_version_comparison_details() - + print("=" * 60) print("Test suite completed!") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tests/test_dependency_resolver.py b/tests/test_dependency_resolver.py index b0b128f..042513e 100644 --- a/tests/test_dependency_resolver.py +++ b/tests/test_dependency_resolver.py @@ -99,7 +99,7 @@ class TestDependencyResolver: "requires_dist": [], } } - + mock_pytest_data = { "info": { "name": "pytest", @@ -112,7 +112,7 @@ class TestDependencyResolver: with patch("pypi_query_mcp.core.PyPIClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value.__aenter__.return_value = mock_client - + # Setup mock to return different data based on package name def mock_get_package_info(package_name): if package_name.lower() == "mock-test-package-12345": @@ -122,8 +122,14 @@ class TestDependencyResolver: elif package_name.lower() == "pytest": return mock_pytest_data else: - return {"info": {"name": package_name, "version": "1.0.0", "requires_dist": []}} - + return { + "info": { + "name": package_name, + "version": "1.0.0", + "requires_dist": [], + } + } + mock_client.get_package_info.side_effect = mock_get_package_info result = await resolver.resolve_dependencies( @@ -132,7 +138,7 @@ class TestDependencyResolver: assert result["include_extras"] == ["test"] assert "dependency_tree" in result - + # Verify that extras are properly resolved and included assert result["summary"]["total_extra_dependencies"] == 1 main_pkg = result["dependency_tree"]["mock-test-package-12345"] @@ -166,7 +172,7 @@ class TestDependencyResolver: "requires_dist": [], } } - + mock_pytest_data = { "info": { "name": "pytest", @@ -175,7 +181,7 @@ class TestDependencyResolver: "requires_dist": [], } } - + mock_coverage_data = { "info": { "name": "coverage", @@ -188,7 +194,7 @@ class TestDependencyResolver: with patch("pypi_query_mcp.core.PyPIClient") as mock_client_class: mock_client = AsyncMock() mock_client_class.return_value.__aenter__.return_value = mock_client - + # Setup mock to return different data based on package name def mock_get_package_info(package_name): if package_name.lower() == "test-package": @@ -200,24 +206,33 @@ class TestDependencyResolver: elif package_name.lower() == "coverage": return mock_coverage_data else: - return {"info": {"name": package_name, "version": "1.0.0", "requires_dist": []}} - + return { + "info": { + "name": package_name, + "version": "1.0.0", + "requires_dist": [], + } + } + mock_client.get_package_info.side_effect = mock_get_package_info # Test with Python 3.11 - should not include typing-extensions but should include extras result = await resolver.resolve_dependencies( - "test-package", python_version="3.11", include_extras=["test"], max_depth=2 + "test-package", + python_version="3.11", + include_extras=["test"], + max_depth=2, ) assert result["include_extras"] == ["test"] assert result["python_version"] == "3.11" - + # Verify that extras are properly resolved assert result["summary"]["total_extra_dependencies"] == 2 main_pkg = result["dependency_tree"]["test-package"] assert "test" in main_pkg["dependencies"]["extras"] assert len(main_pkg["dependencies"]["extras"]["test"]) == 2 - + # Verify Python version filtering worked for runtime deps but not extras runtime_deps = main_pkg["dependencies"]["runtime"] assert len(runtime_deps) == 1 # Only requests, not typing-extensions diff --git a/tests/test_download_stats.py b/tests/test_download_stats.py index 3dbe346..35d3c85 100644 --- a/tests/test_download_stats.py +++ b/tests/test_download_stats.py @@ -159,7 +159,7 @@ class TestDownloadStats: async def test_get_top_packages_by_downloads_fallback(self): """Test top packages retrieval when PyPI API fails (fallback mode).""" from pypi_query_mcp.core.exceptions import PyPIServerError - + with patch( "pypi_query_mcp.tools.download_stats.PyPIStatsClient" ) as mock_stats_client: @@ -180,7 +180,7 @@ class TestDownloadStats: assert all("category" in pkg for pkg in result["top_packages"]) assert all("description" in pkg for pkg in result["top_packages"]) assert "curated" in result["data_source"] - + # Check that all packages have estimated downloads assert all(pkg.get("estimated", False) for pkg in result["top_packages"]) @@ -188,47 +188,56 @@ class TestDownloadStats: async def test_get_top_packages_github_enhancement(self): """Test GitHub enhancement functionality.""" from pypi_query_mcp.core.exceptions import PyPIServerError - + mock_github_stats = { "stars": 50000, "forks": 5000, "updated_at": "2024-01-01T00:00:00Z", "language": "Python", - "topics": ["http", "requests"] + "topics": ["http", "requests"], } - + with ( - patch("pypi_query_mcp.tools.download_stats.PyPIStatsClient") as mock_stats_client, - patch("pypi_query_mcp.tools.download_stats.GitHubAPIClient") as mock_github_client + patch( + "pypi_query_mcp.tools.download_stats.PyPIStatsClient" + ) as mock_stats_client, + patch( + "pypi_query_mcp.tools.download_stats.GitHubAPIClient" + ) as mock_github_client, ): # Mock PyPI failure mock_stats_instance = AsyncMock() mock_stats_instance.get_recent_downloads.side_effect = PyPIServerError(502) mock_stats_client.return_value.__aenter__.return_value = mock_stats_instance - - # Mock GitHub success + + # Mock GitHub success mock_github_instance = AsyncMock() mock_github_instance.get_multiple_repo_stats.return_value = { "psf/requests": mock_github_stats } - mock_github_client.return_value.__aenter__.return_value = mock_github_instance + mock_github_client.return_value.__aenter__.return_value = ( + mock_github_instance + ) result = await get_top_packages_by_downloads("month", 10) - + # Find requests package (should be enhanced with GitHub data) - requests_pkg = next((pkg for pkg in result["top_packages"] if pkg["package"] == "requests"), None) - + requests_pkg = next( + (pkg for pkg in result["top_packages"] if pkg["package"] == "requests"), + None, + ) + if requests_pkg: assert "github_stars" in requests_pkg assert "github_forks" in requests_pkg assert requests_pkg["github_stars"] == 50000 assert requests_pkg.get("github_enhanced", False) == True - @pytest.mark.asyncio + @pytest.mark.asyncio async def test_get_top_packages_different_periods(self): """Test top packages with different time periods.""" from pypi_query_mcp.core.exceptions import PyPIServerError - + with patch( "pypi_query_mcp.tools.download_stats.PyPIStatsClient" ) as mock_stats_client: @@ -238,16 +247,20 @@ class TestDownloadStats: for period in ["day", "week", "month"]: result = await get_top_packages_by_downloads(period, 3) - + assert result["period"] == period assert len(result["top_packages"]) == 3 - + # Check that downloads are scaled appropriately for the period # Day should have much smaller numbers than month if period == "day": - assert all(pkg["downloads"] < 50_000_000 for pkg in result["top_packages"]) + assert all( + pkg["downloads"] < 50_000_000 for pkg in result["top_packages"] + ) elif period == "month": - assert any(pkg["downloads"] > 100_000_000 for pkg in result["top_packages"]) + assert any( + pkg["downloads"] > 100_000_000 for pkg in result["top_packages"] + ) def test_analyze_download_stats(self): """Test download statistics analysis.""" diff --git a/tests/test_version_sorting.py b/tests/test_version_sorting.py index 3c805d9..17fc5a1 100644 --- a/tests/test_version_sorting.py +++ b/tests/test_version_sorting.py @@ -1,6 +1,5 @@ """Tests for semantic version sorting functionality.""" -import pytest from pypi_query_mcp.core.version_utils import sort_versions_semantically @@ -39,7 +38,7 @@ class TestSemanticVersionSorting: """Test development and post-release versions.""" versions = ["1.0.0", "1.0.0.post1", "1.0.0.dev0", "1.0.1"] result = sort_versions_semantically(versions, reverse=True) - + # 1.0.1 should be first, then 1.0.0.post1, then 1.0.0, then 1.0.0.dev0 assert result[0] == "1.0.1" assert result[1] == "1.0.0.post1" @@ -50,7 +49,7 @@ class TestSemanticVersionSorting: """Test that invalid versions fall back to string sorting.""" versions = ["1.0.0", "invalid-version", "another-invalid", "2.0.0"] result = sort_versions_semantically(versions, reverse=True) - + # Valid versions should come first assert result[0] == "2.0.0" assert result[1] == "1.0.0" @@ -79,9 +78,9 @@ class TestSemanticVersionSorting: """Test sorting with mixed version formats.""" versions = ["1.0", "1.0.0", "1.0.1", "v1.0.2"] # v1.0.2 might be invalid result = sort_versions_semantically(versions, reverse=True) - + # Should handle mixed formats gracefully assert len(result) == 4 assert "1.0.1" in result assert "1.0.0" in result - assert "1.0" in result \ No newline at end of file + assert "1.0" in result