From 7e7294e06751f37a466df96a4c71258ddfe76133 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 25 Jul 2025 11:15:14 +0300 Subject: [PATCH 01/22] Add new Makefile targets Signed-off-by: Sebastian --- Makefile | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/Makefile b/Makefile index e018df7f..1bbf6d78 100644 --- a/Makefile +++ b/Makefile @@ -3705,6 +3705,97 @@ pip-audit: ## ๐Ÿ”’ Audit Python dependencies for CVEs python3 -m pip install --quiet --upgrade pip-audit && \ pip-audit --strict || true" + + +## --------------------------------------------------------------------------- ## +## Async Code Testing and Performance Profiling +## --------------------------------------------------------------------------- ## +.PHONY: async-test async-lint profile async-monitor async-debug profile-serve + +ASYNC_TEST_DIR := async_testing +PROFILE_DIR := $(ASYNC_TEST_DIR)/profiles +REPORTS_DIR := $(ASYNC_TEST_DIR)/reports +VENV_PYTHON := $(VENV_DIR)/bin/python + +async-test: async-lint async-debug + @echo "๐Ÿ”„ Running comprehensive async safety tests..." + @mkdir -p $(REPORTS_DIR) + @PYTHONASYNCIODEBUG=1 $(VENV_PYTHON) -m pytest \ + tests/ \ + --asyncio-mode=auto \ + --tb=short \ + --junitxml=$(REPORTS_DIR)/async-test-results.xml \ + -v + +async-lint: + @echo "๐Ÿ” Running async-aware linting..." + @$(VENV_DIR)/bin/ruff check mcpgateway/ tests/ \ + --select=F,E,B,ASYNC \ + --output-format=github + @$(VENV_DIR)/bin/flake8 mcpgateway/ tests/ \ + --extend-select=B,ASYNC \ + --max-line-length=100 + @$(VENV_DIR)/bin/mypy mcpgateway/ \ + --warn-unused-coroutine \ + --strict + +profile: + @echo "๐Ÿ“Š Generating async performance profiles..." + @mkdir -p $(PROFILE_DIR) + @$(VENV_PYTHON) $(ASYNC_TEST_DIR)/profiler.py \ + --scenarios websocket,database,mcp_calls \ + --output $(PROFILE_DIR) \ + --duration 60 + @echo "๐ŸŒ Starting SnakeViz server..." + @$(VENV_DIR)/bin/snakeviz $(PROFILE_DIR)/combined_profile.prof \ + --server --port 8080 + +profile-serve: + @echo "๐ŸŒ Starting SnakeViz profile server..." + @$(VENV_DIR)/bin/snakeviz $(PROFILE_DIR) \ + --server --port 8080 --hostname 0.0.0.0 + +async-monitor: + @echo "๐Ÿ‘๏ธ Starting aiomonitor for live async debugging..." + @$(VENV_PYTHON) $(ASYNC_TEST_DIR)/monitor_runner.py \ + --webui_port 50101 \ + --console_port 50102 \ + --host localhost \ + --console-enabled + +async-debug: + @echo "๐Ÿ› Running async tests with debug mode..." + @PYTHONASYNCIODEBUG=1 $(VENV_PYTHON) -X dev \ + -m pytest tests/ \ + --asyncio-mode=auto \ + --capture=no \ + -v + +async-benchmark: + @echo "โšก Running async performance benchmarks..." + @$(VENV_PYTHON) $(ASYNC_TEST_DIR)/benchmarks.py \ + --output $(REPORTS_DIR)/benchmark-results.json \ + --iterations 1000 + +profile-compare: + @echo "๐Ÿ“ˆ Comparing performance profiles..." + @$(VENV_PYTHON) $(ASYNC_TEST_DIR)/profile_compare.py \ + --baseline $(PROFILE_DIR)/combined_profile.prof \ + --current $(PROFILE_DIR)/mcp_calls_profile.prof \ + --output $(REPORTS_DIR)/profile-comparison.json + +async-validate: + @echo "โœ… Validating async code patterns..." + @$(VENV_PYTHON) $(ASYNC_TEST_DIR)/async_validator.py \ + --source mcpgateway/ \ + --report $(REPORTS_DIR)/async-validation.json + +async-clean: + @echo "๐Ÿงน Cleaning async testing artifacts..." + @rm -rf $(PROFILE_DIR)/* $(REPORTS_DIR)/* + @pkill -f "aiomonitor" || true + @pkill -f "snakeviz" || true + ## --------------------------------------------------------------------------- ## ## Gitleaks (Go binary - separate installation) ## --------------------------------------------------------------------------- ## From d94a0bdd82847e36409856ba1584e5abe8fe33ed Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 25 Jul 2025 11:16:10 +0300 Subject: [PATCH 02/22] Add new config for make targets Signed-off-by: Sebastian --- pyproject.toml | 22 ++++++++++++++++++++++ pyrightconfig.json | 6 ++++++ 2 files changed, 28 insertions(+) create mode 100644 pyrightconfig.json diff --git a/pyproject.toml b/pyproject.toml index c0d783b4..9bcef96a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,8 @@ dependencies = [ "starlette>=0.47.2", "uvicorn>=0.35.0", "zeroconf>=0.147.0", + "aiohttp>=3.12.14", + "websockets>=15.0.1" ] # ---------------------------------------------------------------- @@ -250,6 +252,22 @@ include = "\\.pyi?$" # isort configuration [tool.isort] + + +# -------------------------------------------------------------------- +# ๐Ÿ›  Async tool configurations (async-test, async-lint, etc.) +# -------------------------------------------------------------------- +[tool.ruff] +select = ["F", "E", "W", "B", "ASYNC"] +unfixable = ["B"] # Never auto-fix critical bugbear warnings + +[tool.ruff.flake8-bugbear] +extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false + ############################################################################### # Core behaviour ############################################################################### @@ -320,6 +338,7 @@ warn_unreachable = true # Warn about unreachable code warn_unused_ignores = true # Warn if a "# type: ignore" is unnecessary warn_unused_configs = true # Warn about unused config options warn_redundant_casts = true # Warn if a cast does nothing +warn_unused_coroutine = true # Warn if an unused async coroutine is defined strict_equality = true # Disallow ==/!= between incompatible types # Output formatting @@ -333,6 +352,9 @@ exclude = [ '^\\.mypy_cache/', ] +# Plugins to use with mypy +plugins = ["pydantic.mypy"] # Enable mypy plugin for Pydantic models + [tool.pytest.ini_options] minversion = "6.0" addopts = "-ra -q --cov=mcpgateway --ignore=tests/playwright" diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..1f3edf60 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,6 @@ +{ + "typeCheckingMode": "strict", + "reportUnusedCoroutine": "error", + "reportMissingTypeStubs": "warning", + "exclude": ["build", ".venv", "async_testing/profiles"] +} \ No newline at end of file From 449c61cce97daff28c025c093c53279eb3abcd6f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 25 Jul 2025 11:18:28 +0300 Subject: [PATCH 03/22] Add new async testing files Signed-off-by: Sebastian --- async_testing/async_validator.py | 183 ++++++++++ async_testing/benchmarks.py | 72 ++++ async_testing/config.yaml | 25 ++ async_testing/monitor_runner.py | 95 +++++ async_testing/profile_compare.py | 92 +++++ async_testing/profiler.py | 344 ++++++++++++++++++ async_testing/profiles/combined_profile.prof | Bin 0 -> 99479 bytes async_testing/profiles/database_profile.prof | Bin 0 -> 5262 bytes async_testing/profiles/mcp_calls_profile.prof | Bin 0 -> 36313 bytes async_testing/profiles/websocket_profile.prof | Bin 0 -> 80938 bytes async_testing/reports/benchmark-results.json | 15 + 11 files changed, 826 insertions(+) create mode 100644 async_testing/async_validator.py create mode 100644 async_testing/benchmarks.py create mode 100644 async_testing/config.yaml create mode 100644 async_testing/monitor_runner.py create mode 100644 async_testing/profile_compare.py create mode 100644 async_testing/profiler.py create mode 100644 async_testing/profiles/combined_profile.prof create mode 100644 async_testing/profiles/database_profile.prof create mode 100644 async_testing/profiles/mcp_calls_profile.prof create mode 100644 async_testing/profiles/websocket_profile.prof create mode 100644 async_testing/reports/benchmark-results.json diff --git a/async_testing/async_validator.py b/async_testing/async_validator.py new file mode 100644 index 00000000..b4dad1f1 --- /dev/null +++ b/async_testing/async_validator.py @@ -0,0 +1,183 @@ +""" +Validate async code patterns and detect common pitfalls. +""" + +import ast +import argparse +import json +from pathlib import Path +from typing import List, Dict, Any + +class AsyncCodeValidator: + """Validate async code for common patterns and pitfalls.""" + + def __init__(self): + self.issues = [] + self.suggestions = [] + + def validate_directory(self, source_dir: Path) -> Dict[str, Any]: + """Validate all Python files in directory.""" + + validation_results = { + 'files_checked': 0, + 'issues_found': 0, + 'suggestions': 0, + 'details': [] + } + + python_files = list(source_dir.rglob("*.py")) + + for file_path in python_files: + if self._should_skip_file(file_path): + continue + + file_results = self._validate_file(file_path) + validation_results['details'].append(file_results) + validation_results['files_checked'] += 1 + validation_results['issues_found'] += len(file_results['issues']) + validation_results['suggestions'] += len(file_results['suggestions']) + + return validation_results + + def _validate_file(self, file_path: Path) -> Dict[str, Any]: + """Validate a single Python file.""" + + file_results = { + 'file': str(file_path), + 'issues': [], + 'suggestions': [] + } + + try: + with open(file_path, 'r', encoding='utf-8') as f: + source_code = f.read() + + tree = ast.parse(source_code, filename=str(file_path)) + + # Analyze AST for async patterns + validator = AsyncPatternVisitor(file_path) + validator.visit(tree) + + file_results['issues'] = validator.issues + file_results['suggestions'] = validator.suggestions + + except Exception as e: + file_results['issues'].append({ + 'type': 'parse_error', + 'message': f"Failed to parse file: {str(e)}", + 'line': 0 + }) + + return file_results + + + def _should_skip_file(self, file_path: Path) -> bool: + """Determine if a file should be skipped (e.g., __init__.py files).""" + return file_path.name == "__init__.py" + +class AsyncPatternVisitor(ast.NodeVisitor): + """AST visitor to detect async patterns and issues.""" + + def __init__(self, file_path: Path): + self.file_path = file_path + self.issues = [] + self.suggestions = [] + self.in_async_function = False + + def visit_AsyncFunctionDef(self, node): + """Visit async function definitions.""" + + self.in_async_function = True + + # Check for blocking operations in async functions + self._check_blocking_operations(node) + + # Check for proper error handling + self._check_error_handling(node) + + self.generic_visit(node) + self.in_async_function = False + + def visit_Call(self, node): + """Visit function calls.""" + + if self.in_async_function: + # Check for potentially unawaited async calls + self._check_unawaited_calls(node) + + # Check for blocking I/O operations + self._check_blocking_io(node) + + self.generic_visit(node) + + def _check_blocking_operations(self, node): + """Check for blocking operations in async functions.""" + + blocking_patterns = [ + 'time.sleep', + 'requests.get', 'requests.post', + 'subprocess.run', 'subprocess.call', + 'open' # File I/O without async + ] + + for child in ast.walk(node): + if isinstance(child, ast.Call): + call_name = self._get_call_name(child) + if call_name in blocking_patterns: + self.issues.append({ + 'type': 'blocking_operation', + 'message': f"Blocking operation '{call_name}' in async function", + 'line': child.lineno, + 'suggestion': f"Use async equivalent of {call_name}" + }) + + def _check_unawaited_calls(self, node): + """Check for potentially unawaited async calls.""" + + # Look for calls that might return coroutines + async_patterns = [ + 'aiohttp', 'asyncio', 'asyncpg', + 'websockets', 'motor' # Common async libraries + ] + + call_name = self._get_call_name(node) + + for pattern in async_patterns: + if pattern in call_name: + # Check if this call is awaited + parent = getattr(node, 'parent', None) + if not isinstance(parent, ast.Await): + self.suggestions.append({ + 'type': 'potentially_unawaited', + 'message': f"Call to '{call_name}' might need await", + 'line': node.lineno + }) + break + + def _get_call_name(self, node): + """Extract the name of a function call.""" + + if isinstance(node.func, ast.Name): + return node.func.id + elif isinstance(node.func, ast.Attribute): + if isinstance(node.func.value, ast.Name): + return f"{node.func.value.id}.{node.func.attr}" + else: + return node.func.attr + return "unknown" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Validate async code patterns and detect common pitfalls.") + parser.add_argument("--source", type=Path, required=True, help="Source directory to validate.") + parser.add_argument("--report", type=Path, required=True, help="Path to the output validation report.") + + args = parser.parse_args() + + validator = AsyncCodeValidator() + results = validator.validate_directory(args.source) + + with open(args.report, 'w') as f: + json.dump(results, f, indent=4) + + print(f"Validation report saved to {args.report}") \ No newline at end of file diff --git a/async_testing/benchmarks.py b/async_testing/benchmarks.py new file mode 100644 index 00000000..b43faf53 --- /dev/null +++ b/async_testing/benchmarks.py @@ -0,0 +1,72 @@ +""" +Run async performance benchmarks and output results. +""" +import asyncio +import time +import json +import argparse +from pathlib import Path +from typing import Any, Dict + +class AsyncBenchmark: + """Run async performance benchmarks.""" + + def __init__(self, iterations: int): + self.iterations = iterations + self.results: Dict[str, Any] = { + 'iterations': self.iterations, + 'benchmarks': [] + } + + async def run_benchmarks(self) -> None: + """Run all benchmarks.""" + + # Example benchmarks + await self._benchmark_example("Example Benchmark 1", self.example_benchmark_1) + await self._benchmark_example("Example Benchmark 2", self.example_benchmark_2) + + async def _benchmark_example(self, name: str, benchmark_func) -> None: + """Run a single benchmark and record its performance.""" + + start_time = time.perf_counter() + + for _ in range(self.iterations): + await benchmark_func() + + end_time = time.perf_counter() + total_time = end_time - start_time + avg_time = total_time / self.iterations + + self.results['benchmarks'].append({ + 'name': name, + 'total_time': total_time, + 'average_time': avg_time + }) + + async def example_benchmark_1(self) -> None: + """An example async benchmark function.""" + await asyncio.sleep(0.001) + + async def example_benchmark_2(self) -> None: + """Another example async benchmark function.""" + await asyncio.sleep(0.002) + + def save_results(self, output_path: Path) -> None: + """Save benchmark results to a file.""" + + with open(output_path, 'w') as f: + json.dump(self.results, f, indent=4) + + print(f"Benchmark results saved to {output_path}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run async performance benchmarks.") + parser.add_argument("--output", type=Path, required=True, help="Path to the output benchmark results file.") + parser.add_argument("--iterations", type=int, default=1000, help="Number of iterations to run each benchmark.") + + args = parser.parse_args() + + benchmark = AsyncBenchmark(args.iterations) + asyncio.run(benchmark.run_benchmarks()) + benchmark.save_results(args.output) \ No newline at end of file diff --git a/async_testing/config.yaml b/async_testing/config.yaml new file mode 100644 index 00000000..ae2e09fe --- /dev/null +++ b/async_testing/config.yaml @@ -0,0 +1,25 @@ +async_linting: + ruff_rules: ["F", "E", "B", "ASYNC"] + flake8_plugins: ["flake8-bugbear", "flake8-async"] + mypy_config: + warn_unused_coroutine: true + strict: true + +profiling: + output_dir: "async_testing/profiles" + snakeviz_port: 8080 + profile_scenarios: + - "websocket_stress_test" + - "database_query_performance" + - "concurrent_mcp_calls" + +monitoring: + aiomonitor_port: 50101 + debug_mode: true + task_tracking: true + +performance_thresholds: + websocket_connection: 100 # ms + database_query: 50 # ms + mcp_rpc_call: 100 # ms + concurrent_users: 100 # simultaneous connections diff --git a/async_testing/monitor_runner.py b/async_testing/monitor_runner.py new file mode 100644 index 00000000..1653a0d4 --- /dev/null +++ b/async_testing/monitor_runner.py @@ -0,0 +1,95 @@ +""" +Runtime async monitoring with aiomonitor integration. +""" +import asyncio +from typing import Any, Dict +import aiomonitor +import argparse + +class AsyncMonitor: + """Monitor live async operations in mcpgateway.""" + + def __init__(self, webui_port: int = 50101, console_port: int = 50102, host: str = "localhost"): + self.webui_port = webui_port + self.console_port = console_port + self.host = host + self.monitor = None + self.running = False + + async def start_monitoring(self, console_enabled: bool = True): + """Start aiomonitor for live async debugging.""" + + print(f"๐Ÿ‘๏ธ Starting aiomonitor on http://{self.host}:{self.webui_port}") + + # Configure aiomonitor + self.monitor = aiomonitor.Monitor( + asyncio.get_event_loop(), + host=self.host, + webui_port=self.webui_port, + console_port=self.console_port, # TODO: FIX CONSOLE NOT CONNECTING TO PORT + console_enabled=console_enabled, + locals={'monitor': self} + ) + + self.monitor.start() + self.running = True + + if console_enabled: + print(f"๐ŸŒ aiomonitor console available at: http://{self.host}:{self.console_port}") + print("๐Ÿ“Š Available commands: ps, where, cancel, signal, console") + print("๐Ÿ” Use 'ps' to list running tasks") + print("๐Ÿ“ Use 'where ' to see task stack trace") + + # Keep monitoring running + try: + while self.running: + await asyncio.sleep(1) + + # Periodic task summary + tasks = [t for t in asyncio.all_tasks() if not t.done()] + if len(tasks) % 100 == 0 and len(tasks) > 0: + print(f"๐Ÿ“ˆ Current active tasks: {len(tasks)}") + + except KeyboardInterrupt: # TODO: FIX STACK TRACE STILL APPEARING ON CTRL-C + print("\n๐Ÿ›‘ Stopping aiomonitor...") + finally: + self.monitor.close() + + def stop_monitoring(self): + """Stop the monitoring.""" + self.running = False + + async def get_task_summary(self) -> Dict[str, Any]: + """Get summary of current async tasks.""" + + tasks = asyncio.all_tasks() + + summary: Dict[str, Any] = { + 'total_tasks': len(tasks), + 'running_tasks': len([t for t in tasks if not t.done()]), + 'completed_tasks': len([t for t in tasks if t.done()]), + 'cancelled_tasks': len([t for t in tasks if t.cancelled()]), + 'task_details': [] + } + + for task in tasks: + if not task.done(): + summary['task_details'].append({ + 'name': getattr(task, '_name', 'unnamed'), + 'state': task._state.name if hasattr(task, '_state') else 'unknown', + 'coro': str(task._coro) if hasattr(task, '_coro') else 'unknown' + }) + + return summary + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run aiomonitor for live async debugging.") + parser.add_argument("--host", type=str, default="localhost", help="Host to run aiomonitor on.") + parser.add_argument("--webui_port", type=int, default=50101, help="Port to run aiomonitor on.") + parser.add_argument("--console_port", type=int, default=50102, help="Port to run aiomonitor on.") + parser.add_argument("--console-enabled", action="store_true", help="Enable console for aiomonitor.") + + args = parser.parse_args() + + monitor = AsyncMonitor(webui_port=args.webui_port, console_port=args.console_port, host=args.host) + asyncio.run(monitor.start_monitoring(console_enabled=args.console_enabled)) \ No newline at end of file diff --git a/async_testing/profile_compare.py b/async_testing/profile_compare.py new file mode 100644 index 00000000..e97b4893 --- /dev/null +++ b/async_testing/profile_compare.py @@ -0,0 +1,92 @@ +""" +Compare async performance profiles between builds. +""" + +import pstats +import json +import argparse +from pathlib import Path +from typing import Dict, Any + +class ProfileComparator: + """Compare performance profiles and detect regressions.""" + + def compare_profiles(self, baseline_path: Path, current_path: Path) -> Dict[str, Any]: + """Compare two performance profiles.""" + + baseline_stats = pstats.Stats(str(baseline_path)) + current_stats = pstats.Stats(str(current_path)) + + comparison: Dict[str, Any] = { + 'baseline_file': str(baseline_path), + 'current_file': str(current_path), + 'regressions': [], + 'improvements': [], + 'summary': {} + } + + # Compare overall performance + baseline_total_time = baseline_stats.total_tt + current_total_time = current_stats.total_tt + + total_time_change = ( + (current_total_time - baseline_total_time) / baseline_total_time * 100 + ) + + comparison['summary']['total_time_change'] = total_time_change + + # Compare function-level performance + baseline_functions = self._extract_function_stats(baseline_stats) + current_functions = self._extract_function_stats(current_stats) + + for func_name, baseline_time in baseline_functions.items(): + if func_name in current_functions: + current_time: float = current_functions[func_name] + change_percent = (current_time - baseline_time) / baseline_time * 100 + + if change_percent > 20: # 20% regression threshold + comparison['regressions'].append({ + 'function': func_name, + 'baseline_time': baseline_time, + 'current_time': current_time, + 'change_percent': change_percent + }) + elif change_percent < -10: # 10% improvement + comparison['improvements'].append({ + 'function': func_name, + 'baseline_time': baseline_time, + 'current_time': current_time, + 'change_percent': change_percent + }) + + return comparison + + + def _extract_function_stats(self, stats: pstats.Stats) -> Dict[str, float]: + """Extract function-level statistics from pstats.Stats.""" + + functions = {} + + for func, stat in stats.stats.items(): + func_name = f"{func[0]}:{func[1]}:{func[2]}" + tottime = stat[2] # Extract the 'tottime' (total time spent in the given function) + functions[func_name] = tottime + + return functions + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Compare performance profiles.") + parser.add_argument("--baseline", type=Path, required=True, help="Path to the baseline profile.") + parser.add_argument("--current", type=Path, required=True, help="Path to the current profile.") + parser.add_argument("--output", type=Path, required=True, help="Path to the output comparison report.") + + args = parser.parse_args() + + comparator = ProfileComparator() + comparison = comparator.compare_profiles(args.baseline, args.current) + + with open(args.output, 'w') as f: + json.dump(comparison, f, indent=4) + + print(f"Comparison report saved to {args.output}") \ No newline at end of file diff --git a/async_testing/profiler.py b/async_testing/profiler.py new file mode 100644 index 00000000..d77fee91 --- /dev/null +++ b/async_testing/profiler.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +""" +Comprehensive async performance profiler for mcpgateway. +""" +import asyncio +import cProfile +import pstats +import time +import aiohttp +import websockets +import argparse +import json +from pathlib import Path +from typing import Dict, List, Any, Union + +class AsyncProfiler: + """Profile async operations in mcpgateway.""" + + def __init__(self, output_dir: str): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.profiles = {} + + def _generate_combined_profile(self, scenarios: List[str]) -> None: + """ + Generate a combined profile for the given scenarios. + Args: + scenarios (List[str]): A list of scenario names. + """ + combined_profile_path = self.output_dir / "combined_profile.prof" + print(f"Generating combined profile at {combined_profile_path}") + + stats = pstats.Stats() + + for scenario in scenarios: + profile_path = self.output_dir / f"{scenario}_profile.prof" + stats.add(str(profile_path)) + + stats.dump_stats(str(combined_profile_path)) + + + def _generate_summary_report(self, results: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate a summary report from the profiling results. + Args: + results (Dict[str, Any]): The profiling results. + """ + # Implementation of the summary report generation + print("Generating summary report with results:", results) + return {"results": results} + + + async def profile_all_scenarios(self, scenarios: List[str], duration: int) -> Dict[str, Any]: + """Profile all specified async scenarios.""" + + results: Dict[str, Union[Dict[str, Any], float]] = { + 'scenarios': {}, + 'summary': {}, + 'timestamp': time.time() + } + + # Ensure 'scenarios' and 'summary' keys are dictionaries + results['scenarios'] = {} + results['summary'] = {} + + for scenario in scenarios: + print(f"๐Ÿ“Š Profiling scenario: {scenario}") + + profile_path = self.output_dir / f"{scenario}_profile.prof" + profile_result = await self._profile_scenario(scenario, duration, profile_path) + + results['scenarios'][scenario] = profile_result + + # Generate combined profile + self._generate_combined_profile(scenarios) + + # Generate summary report + results['summary'] = self._generate_summary_report(results['scenarios']) + + return results + + async def _profile_scenario(self, scenario: str, duration: int, + output_path: Path) -> Dict[str, Any]: + """Profile a specific async scenario.""" + + scenario_methods = { + 'websocket': self._profile_websocket_operations, + 'database': self._profile_database_operations, + 'mcp_calls': self._profile_mcp_operations, + 'concurrent_requests': self._profile_concurrent_requests + } + + if scenario not in scenario_methods: + raise ValueError(f"Unknown scenario: {scenario}") + + # Run profiling + profiler = cProfile.Profile() + profiler.enable() + + start_time = time.time() + scenario_result = await scenario_methods[scenario](duration) + end_time = time.time() + + profiler.disable() + profiler.dump_stats(str(output_path)) + + # Analyze profile + stats = pstats.Stats(str(output_path)) + stats.sort_stats('cumulative') + + return { + 'scenario': scenario, + 'duration': end_time - start_time, + 'profile_file': str(output_path), + 'total_calls': stats.total_calls, + 'total_time': stats.total_tt, + 'top_functions': self._extract_top_functions(stats), + 'async_metrics': scenario_result + } + + async def _profile_concurrent_requests(self, duration: int) -> Dict[str, Any]: + """Profile concurrent HTTP requests.""" + + metrics: Dict[str, float] = { + 'requests_made': 0, + 'avg_response_time': 0, + 'successful_requests': 0, + 'failed_requests': 0 + } + + async def make_request(): + try: + async with aiohttp.ClientSession() as session: + start_time = time.time() + + async with session.get("http://localhost:4444/ws") as response: + await response.text() + + response_time = time.time() - start_time + metrics['requests_made'] += 1 + metrics['successful_requests'] += 1 + metrics['avg_response_time'] = ( + (metrics['avg_response_time'] * (metrics['requests_made'] - 1) + response_time) + / metrics['requests_made'] + ) + + except Exception: + metrics['failed_requests'] += 1 + + # Run concurrent requests + tasks: List[Any] = [] + end_time = time.time() + duration + + while time.time() < end_time: + if len(tasks) < 10: # Max 10 concurrent requests + task = asyncio.create_task(make_request()) + tasks.append(task) + + # Clean up completed tasks + tasks = [t for t in tasks if not t.done()] + await asyncio.sleep(0.1) + + # Wait for remaining tasks + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + return metrics + + async def _profile_websocket_operations(self, duration: int) -> Dict[str, Any]: + """Profile WebSocket connection and message handling.""" + + metrics: Dict[str, float] = { + 'connections_established': 0, + 'messages_sent': 0, + 'messages_received': 0, + 'connection_errors': 0, + 'avg_latency': 0 + } + + async def websocket_client(): + try: + async with websockets.connect("ws://localhost:4444/ws") as websocket: + metrics['connections_established'] += 1 + + # Send test messages + for i in range(10): + message = json.dumps({"type": "ping", "data": f"test_{i}"}) + start_time = time.time() + + await websocket.send(message) + metrics['messages_sent'] += 1 + + response = await websocket.recv() + metrics['messages_received'] += 1 + + latency = time.time() - start_time + metrics['avg_latency'] = ( + (metrics['avg_latency'] * i + latency) / (i + 1) + ) + + await asyncio.sleep(0.1) + + except Exception as e: + metrics['connection_errors'] += 1 + + # Run concurrent WebSocket clients + tasks: List[Any] = [] + end_time = time.time() + duration + + while time.time() < end_time: + if len(tasks) < 10: # Max 10 concurrent connections + task = asyncio.create_task(websocket_client()) + tasks.append(task) + + # Clean up completed tasks + tasks = [t for t in tasks if not t.done()] + await asyncio.sleep(0.1) + + # Wait for remaining tasks + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + return metrics + + async def _profile_database_operations(self, duration: int) -> Dict[str, Any]: + """Profile database query performance.""" + + metrics: Dict[str, float] = { + 'queries_executed': 0, + 'avg_query_time': 0, + 'connection_time': 0, + 'errors': 0 + } + + # Simulate database operations + async def database_operations(): + try: + # Simulate async database queries + query_start = time.time() + + # Mock database query (replace with actual database calls) + await asyncio.sleep(0.01) # Simulate 10ms query + + query_time = time.time() - query_start + metrics['queries_executed'] += 1 + metrics['avg_query_time'] = ( + (metrics['avg_query_time'] * (metrics['queries_executed'] - 1) + query_time) + / metrics['queries_executed'] + ) + + except Exception: + metrics['errors'] += 1 + + # Run database operations for specified duration + end_time = time.time() + duration + + while time.time() < end_time: + await database_operations() + await asyncio.sleep(0.001) # Small delay between operations + + return metrics + + async def _profile_mcp_operations(self, duration: int) -> Dict[str, Any]: + """Profile MCP server communication.""" + + metrics: Dict[str, float] = { + 'rpc_calls': 0, + 'avg_rpc_time': 0, + 'successful_calls': 0, + 'failed_calls': 0 + } + + async def mcp_rpc_call(): + try: + async with aiohttp.ClientSession() as session: + payload = { + "jsonrpc": "2.0", + "method": "tools/list", + "id": 1 + } + + start_time = time.time() + + async with session.post( + "http://localhost:4444/rpc", + json=payload, + timeout=aiohttp.ClientTimeout(total=5) + ) as response: + await response.json() + + rpc_time = time.time() - start_time + metrics['rpc_calls'] += 1 + metrics['successful_calls'] += 1 + metrics['avg_rpc_time'] = ( + (metrics['avg_rpc_time'] * (metrics['rpc_calls'] - 1) + rpc_time) + / metrics['rpc_calls'] + ) + + except Exception: + metrics['failed_calls'] += 1 + + # Run MCP operations + end_time = time.time() + duration + + while time.time() < end_time: + await mcp_rpc_call() + await asyncio.sleep(0.1) + + return metrics + + def _extract_top_functions(self, stats: pstats.Stats) -> List[Dict[str, Union[str, float, int]]]: + """ + Extract the top functions from the profiling stats. + Args: + stats (pstats.Stats): The profiling statistics. + Returns: + List[Dict[str, Union[str, float, int]]]: A list of dictionaries containing the top functions. + """ + top_functions: List[Dict[str, Any]] = [] + for func_stat in stats.fcn_list[:10]: # Get top 10 functions + top_functions.append({ + 'function_name': func_stat[2], + 'call_count': stats.stats[func_stat][0], + 'total_time': stats.stats[func_stat][2], + 'cumulative_time': stats.stats[func_stat][3] + }) + return top_functions + +# Main entry point for the script +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Async performance profiler for mcpgateway.") + parser.add_argument("--scenarios", type=str, required=True, help="Comma-separated list of scenarios to profile.") + parser.add_argument("--output", type=str, required=True, help="Output directory for profile files.") + parser.add_argument("--duration", type=int, default=60, help="Duration to run each scenario (in seconds).") + + args = parser.parse_args() + + scenarios = args.scenarios.split(",") + output_dir = args.output + duration = args.duration + + profiler = AsyncProfiler(output_dir) + + asyncio.run(profiler.profile_all_scenarios(scenarios, duration)) \ No newline at end of file diff --git a/async_testing/profiles/combined_profile.prof b/async_testing/profiles/combined_profile.prof new file mode 100644 index 0000000000000000000000000000000000000000..30f21cf7ca3ac1357844bd28288fe39b3e7a7d35 GIT binary patch literal 99479 zcmb?kcVHF8^9Pau3B7j+U0Om1DeveANCyE?A1}$vh>f+zwaA_@wk zf*_(&Z6Hcf@u4W4he{Cyq{#0xySICHxtHkor~a|5d2_R~v$M0av$M0e8-(4;|9CJT z{=3z*Ly9*&$(ImaU(ZObAMHzwuJ4ZbrKWo)q>uCH{+O=3j$h(Nd*jpnt_Bg>*s1yQ zu|gemYFN=BZyi$967@^>oVc)Zr);3E8=K+RW0SSG*tATbPHhp>tXZtbpP3r3`C`4} zys7E_n6%8`AT%Eccca+`;d!BK+8|8N4_LhbimV!vUQa>-r`PXIw~<#Y668^#9$w}8 zyjp9IA}iTz!){dxMV>b`BLzfhhr>WrNq3@_n&3|Gq%t+7Bz5dwrB;D&tpx$o+sz6z{Ct-AkKi$p$4#sDs zrlT>|-4fK;_-Vzi{ZVs~OIE<&TA>6$++J~js9GcGnIK27zcd&he+bB?25OZPTO^TdzwsOXPJ z^ChLHr^TYHsgMqzPCQJ&Km`gJe?t5BX*_S(QcubKM z$gNI89IQ`_D+EeXeB->J;b~$X73!wBPanTp>>))~1o4q1Go|oJve)DHx;^n@Gc?^x zoHu*^RN1gPOFMN|R8g^az?AAVUs_114i*MsdQ}u^$E>71V`pr4$x60f2!C%7&VJ4X z#@=4EWSGh(4`zLW7H^+MaW&C%D%1-fZ%ul-Y^EYBG>xitNYs5<-qdIGCWj1d^=O(q7C1nAA^ ztM?bIYVh29hh4G)LWcjF$voa0=l8{r@up*58`?TPS%dl_@-v7brQ)H!(!I1m&~(>) zq`Etix}`!L+oE3a3(12NSph|T|09YrbdB`d4)i<_NeoHTeG@Wm&7b+K?1#(e4OCQ7 zLG%AgyFvGBsj3@Gp*Pi!&a!bVTND)3awmEG7?)J9uEo23I#HAEi}xkF@n@f|sTw%L zmJ7aS6h1t=)t2QuU8<;nv}&Qmmp4fa{}6K;5*Y7EPWI{;c$(t6LIdBtd)bSZn{RN* z3Xs(ag-ncMy2n3;ExEOcdqs^!$xVL-18+mKutKeVAgu1y<_U_dKn_XmXqQRDMe)g; z>K%{ukj(`?A^3Xuj#v-habXTIrM{P>H=Lx`rV8~VRqpjK(MM551?2bpH&P-Np)^k> zX&DleRO)D0&X#ZV^nbh}sw=dw$mMa;8U5j&*0~$uwXQ#H8}Hbh$|(y8Rh( z@jmFrRLmeSjFxj&7o5HSN#sBbpBFm+?(PcB&zV}6xuKsSxIeZaW}+$J>O zHkaNUm@vjGi5x1_0q-9^w`nWL5fytE&E-cfcaG(!1*sJ()KS@qy(TXmt;h=Xjg|^+ zDM?sywzjx4QjQ1_qx;MKF+ zJ1MdPiH#05;ruWG$O>YU?FL#3L}jGvURCp_(;T)K6K5hDoW3+|ICPa!L6_L1c@pdk zRIyn??(tfB5}Am;^z>x1Nlde%>AKtRm9IKPQANeh=bBQK#&|RRp@k?|nz;1{ajW2} z`rbiZkGW)p7|j-<eNU%(U7R$0ycbpk zThj3*%*cGYH{_R4D?|Q`S`iA3ufC9lvAQ>nsBccfQ6?h|rUmTGjC7wn3EK&;?zaiW z(+{okowvs|8@VCRU7Z4Tk#Vzn#AI%!DNcxRE(@!4V zL%Q3jD2(mr5wQiy+F&nQ%AkjyQiXx3h;Ncs~#UZ>-S~o z@m|{1Hvua0?KDaB>8h98`R;VDKOKf{EL2>gp(ulEL4KebX+8QAybH$Ozzux5hfIG` zIwtqG-+4Z|<^FbxDk_*OnZOZDAVfvoS{j@YI;Kpb&-UGvbJL>U&A`ZtiryC=K$OTj z;U>ygpP!9rk^c*qDk?zD%u*l8UJ+*6(9*)p6!SB_P2MlI$@^WoEh9^XC9{;Y)+n^A zZ6eHN*v2avsC7>=IN?pyCSX!v?&s5=L}4r$v&84K#OJ}I_!V9xW0`TsC((wKj6j;_ ztT0r{=R&u9PT#T#^%*rC#rLpeL!w1yI?kaMxtiS})a(w~VqI#nuxJtAq)=$o%^ z_5OCrNbGS%#pq=4Dt%K51DdX-Xj$0R2{MChp%bdSg+$z&_3 zF|K+=YLyCOqjON~cn-?`g_G_q$omI6ndW?fzUa1o&yv%}&bnj;O%1~j+HPv9Ah)d` zJ1-W)->6W(m{Yp{!C$?Ktl*2z^uHxcNwpYz z*N?;|_9Mv0t=LeS5eh4(7u8K#lIVP%+A0Obl5^lJmi8+@6<+q<0$4AiA|$vY4S57u zo`9u}*??9<(B|M_Gc))Q=nh1ZK2>#JMp}rGvG{4xo&I)3{QvW;8ZJ>XUiEoIKq2EV zm55eH`5yj4}?D!UOw982A9(aW}!g)rZE?LxKqbZ z7e1h*3s+ToY;Sy0;8+h^F&BN3h!iYh7%{v($~k~bO4Df25aaV#vR zoUQqSuZPz-e&BvZ6%~l^b_{_wlSDYu?S_ZJtJCsl5s7+Lvu4k{(O6MM#byrOGR-08 z-pa)sV$N6Yfo(Z%!bd~ro_49;Dv$Oqkok)X=FqKrc{zcZ<4jNWB)hcA*hgS_;ubS` z6PVhkLOOKS6?F<^Jz4IQOP|i9%c1rxlS`k;LE`WapR{~=+AY?p-7b9=2g#|yEgDN- z%t6Sh#;t6wUpEQ;y2)kZSyl+Qg~NXAhau6ZP%F;;6Crew}x|Y?A+Z8K?U9r+d z=Xitu%>esOfR;SRe+TV1Bpv5x%1dD0*S9l zwbL%(dt=L1=$kuTzJGNXW{#*}keU73fvkIOzhZ%86WC%QI*vT-{AeioVR5VE#u3YW zbIcU6o6ifz-gG_LG@X<3v53n|_j+{QV;=~z#`G_V$=pl)yYLeK?#k8|;_u)k{hSMo zz1c>xSsXE?`e9Uf^Jv?$qI0Y7fUq?Vkm}-g{!}s`qTDHC+;Lhum(Kg-5k9`&jwcDz9 z9$ECp21QnAWho+77N6gxU4uRi6y(^j$Thl&PpMF+&j0JTF|*byvH~Wy2J3>v$v`AK z2!wxL!$;;;waB_VHGlo0ucC?yK6(J~Y`3lNW}LSyq)o%7?_Ht{g^$&!*n4Yo9!|Ps zPkb)jRcKwHLj7r4&FwQ6an2h@m)t#`M~C2zD(GvY&Xj^t>lF^vj!#`{0+ zo1}MB)V9+bw#r`FT_K^`MXcRSWR!~c#3y;((lT!*P zvSNH3d>p^BEiv5SL7Fe1@Ur}M4u3P*E;;RQSiZEs{+#J9x?TBj=`~r2;ct0MJp=VdVn=( znb~|bvuGvs`51sSb^uV+%>*yin~tLva2P|LPrr;po`uT_`rZ5Am=FA9jm=-$`3yxP zfk@Um9k*eeOwD+v#r$_#uKC2Jii({{b>*r7Z0Q<#u0kuMDL4qxs89=ivGdlOwUZQC zAu%R41Zk~;!A<)G!;|$hwzL`*>c)*1F1Pq5UXc~?5pBc+h3OkQf5QNq{=4u(C1NHt zhO1sw8ieV;Q$ysV4ix1b7W{UMljR7`-e=Rhn(P}dOu!tHp)Jb?ElP!2C#~mC=}ng@ zvhvR)BXppbp|o&lfwKh`IAutyb}*{#_!AtYLTz2~njrekU+I5#ID$o%1L-~7>eJY|S)L5}uq=hezMy?nf?h`W7 z26!l<&h8g;tbXaZOzvU-X8Cs`jx(LP{19jrKgOyIbUG~1i-I2X z*Yu)-1Z?vXo!j&uQvD@yugCS zhYKMm7RK{!`*{(mc;2N~V6+D75Z%$lWo~5!#JN#`&PG`9$7#}Yl|-0D0-{K97GNCy z@nAdCxA|k=H9NLOfj&{Oci7yPKsNmGnif(%wIS4BD%39Rubkc1u9+e$r1Y5W_bQEj zQ5+9>`JFT)Nr-w1|pxO>M&pWb1X<&{`;8*rDc(JChLjBq~m_ z#3C$LWBo@)xn;|lcIEPluzfhQ5S831_avt!c|wk@gA*3BK7`71Fx1|rxrXB8IK`6- zv#P?>)2L9xJKX>Jvh`D8*%}p^K5Y5eLJFuP=5=ZU4wh*%NXsckj&>4!JK;RKH9V*0 z;5wFksGNTl>}hdWQb>EVX0-31_HGsKkm5_oNcNIJV-mofg`0(5-b|Bt81;bZ$Fg!y{ZI5g?c6F)Ki;pV!tmcdIt`Y z^Gw0w5+IT!C-r7Dlo1v-)-K<-^Qe_yDC)s>+b8{?f1^mwzfYW}Llkk?GZ`_O8Wn1< zvI!52-Mmdv53Q})L_fbrfn>2ZR^c1HDc4da_?-%^+x4Ff@UQnhbfCxGy^5si8o@I^ zM1;l7HfPhe9Y=e9uBd=4YMUWHwrk_r zqo@&2+LcVUx>ZSd{lrN)#hJK0aq%u~B)knAOE^Q23l`a`*>h`8UikFW(x3lu zSW$QUdHUI7uboFv+T`TU-8z4)+}9E?bEh|Xx#0bxBaS9`C|XH22&^5m zs}Yv2X8hhs>%DTnqCT|aqyC$ZT?6TN7|y(4h#d@PW1?X|%@}_HD8G;`&=-1pKFlV3^8-Ik?-D*yQ47qw`dgXn(@d)KfRQ+f z&SrBXOFe5y1w6kH&(F%~t)j2j>&jD$bfQ z=3ZImx@5NXT!M5VM@tqImfC zeh6J%#U6AJM&e`A&TmZN%30?0 zr~kN9qagIgKt5gafRPQ+HeD}A@K&5}DOcVp-TaD6?Rle0WbsmifU`(WoDLX?le{>5 zFB`#JH{5^oN7$9;w)R43^sL=$p8jge7?VjK5R)R=){}dE^7=})(uGsy zaMYDq&;1^)s3YdqeRJ;HElkSufRQ*UqQg17^1em>m9cT%dfs3st#X+3!8tkOfS35A z34a8qIf`heLfyUSnb>zW>~qNqtycizLapyW4bqwiyu^PDoD`gAJ98@39-nNQT)8mT zZK-+&3XL86lUWw zHko9FTVih<@Dl$55{R;f6Np{Jgln2Rks>V7nD1t6>o{m0`gQ)nCte(Vs20Y02p%j#HcX))w z9k*@Nwujq33_t88&#W(N`MBz5;&q7@+QPIqrKJ~BWi<^a6{_PS*_KLev5)9VtF5Jq z;kSou8&wxJZ06M|g%f6DY)0{MHxWBvB;q(iT$HGx7;|&7q|8br&IM~Yu+Gw)284U5m@_p;$$XjqJn2`|EPWSma6{#ee=IAA1BVy*7slf=YX z@n)wJt9rFn)KaZ~$rxN7rs`Uw4{Hei;hgv#@DjfXAHh8()QGxe{5zdS&8ncN*XB=b zl7HkY=Hy^BkW0l0BXPb$G>ptivj|_7am?BJBRT1Gz(|~IlT?9JB%d_w`T)+0*_{Ch zkGZ%^%0?fa?AtJ$&bKf+A2Y51Ky6Fwy%<+ zem-?VndUu4hZ0W*jBMc*YN3J~x1=<7;R<2$IMMW^q`A{F<89Mm{FoPtetH!LM*6@e zwPt+|Rp^`s4j9?aRBFe#q^>1qI@X+A4j74(Jm7i_j?)~g%;a*#4_qmts4G{^U-9Xei2lHQWsB!i z01Xsm%Mv-u^Y}zIu^FG}Cm_CH^hx}~PZ67RRGJNYqmiQS%gU}9)4O*lvXwXXFZ%YU z(u%re%+37|_!}yEb)wX&>*k$1dUj0vTJW8_J6;^QzAM{$@1j=7d8d~r_y~(nUxd6G zmznOuKHo{CS#`C%S|HLhh>6bju!)=mv*)G0%Wi*oanPl1er(sUhL7E5p{9(4o4_m# zVa<<20FYv?bm9WJdSB-{=a00bt-?bbcOEc;tHGBDcTsU>L{qatM6d?+neNi4l>(QP z^YgKAtW9b4_~jj)!Cu3drJr+wk;tDTy|ns>9elo#e;{ z$)_iRh-k?~#~+~SjB%dPqMCY&Eh%f)W-*sZ)U6)$>vm4nJOLih}5RI=!OSkR@oJ}P8b`n9N-<4~z z>)5nt0j{SJ#<3inILO(^@9)8&yOQ|aSDU|AWIx$qqYxh+CuW^M{8+HMqAnh}dg&`i z+hc@(=AAPqnFB_QupUnB8%8RcYf^JXZPoLk$1BY-1vWUB;4?vmF+@!fFP-m5 z^-f>6SDZaw5?Og(m*1cbo3x1=zpq9$lbey$%P17KW~Z|_w>pZN@mTzl*N-;@7f%ss z;TF;yFtRHx2xm)_nz1-Vu7N&IMYlJuG4WzNx1jH&u_IG@e ztFJZIkV!h_JJM+-g`~5UxDH8rYOmqm%bjs^XZMfsO*~~+vvmamWT?ivxvNGT^RVr_ zoB7}94mKMYtB(`@+-$%N2bx$6^{0hi`xK-HH*=iTTejp4IJ@L`A1yd8i z;*n2kE$P)y(WmDGkzUf{)u&IM7};IXpUep&u0pC8RdwcuTg@?HZawlJjkWOUnlTiA zBY09NTp$1by}OCSiI_+KBVsIqX9;f0Yg&raT($Yg?aNx#P>Grm|2;Lvb;^H4N$+ye z-WnedB1)=<(l&lCd@U@&-#dl#{GoqKy{wXk9X*_y{@>nNJ7ZGNrL0V8q#N_LC`XWtXck{@e|3>HOJ z*GL+rln8~>0V8oz!XTZZ2rx!0+=Mn{s`OCp{He`%&!&(Q#TX;{nTMn}U?fiR6Y7uh zzB71+b7ix(msU5#E;Rh+*2yQ%>Ynca(#Q7W0VJpQFj2IqBl5cYJnV&z}aI$|v z&6?2AKQw-5&e|LETnVB;XI=q5t}Ag1z_CZ(w8&*fyhwDNe&AW+JCn~wcs zeRNM#pebrT7Z^E7yadDC4A#Zte9I!6+ONLpQbk1`+n9XjgJikxppPNataCH9f2$f3 zzeL2osEB5R*T5pg;^7i}9P%@{4PzLtkpnc;Zvc_nY37Ri7c7OBR#e0WFgT51-Xxe$ zKhg77FBGoG3Wi`8e%0t?M>x4n8FY{=!0E7t`=hO%HEDE_usth(l_J;5jVC}-xG&NeZ2oc zMJIW|7_%^!>vIq`m#8(FI!Ybz?0rjz-@T(GH)*M)^o|VBOV%1C<%w(OMkEVdgTwJq=BVBMNBF~OAF%k zSY|%+XwRJ8ORl`Srp(94>MFc~*+Q$DB*{2SaaUf1xR@YZ!yv>?+*A;%#sm?YWaHrY z7v^rIo(10}N=vdMbz`L^qA>-?^wT$kiR#GO^|#(#f>A6p<4TOat|{Ba(Z{Tt`a2dz zv#}8R-$;UUf;18XpG4m<)62@m@q_EueSU8ZMa{0)J!5*crY09_5qxbF5UM&5dWbH;gbF?Qty6J@bTO_n- z+mv$F4@TgnmWo2Hb*JgMk2+6Q)Hh26JOxswu+CB-mHr?VERf2&{K$x>zTcd@*`rc< z#hIb3PtNdbXR0x+)#ojhSKx6udAvv2)$tENv%)r^3rnf0OKg>sJt=Vs9=cT)Cx|zzS zNlb9sj=E#u3pXk@{_Fin7H~F3tfE4_F)DFX{2Anwf41$jGhW<>kQrfun|i+G=#P54 z#?{A__oBZI`aZFNB1~|SCN8pTN3Iyqw|WmnjrLWp)8%GQ>9nGyiwB(Kf9iv*a+QiH zYK>-DsV_GwC7o8LE%-x`u>%h!VH(%!6D*k$K3oxtg^A^O(<&QkyqQlig6<-uU&|zy zAAtOYAhl*QK2#RDq+iPfk!K!{KRM-QRMjZy*J5iw6>4RE@PnL8C*NOxYC~B?n9=4a z9sDA$P`i83x_yo$-cYIl#ITXghGaw%pK}o;H~-a+0n+F7r2Ji=BX`ST7q)$2^+wA# z{UNP%lc#Z&Pdsot3+LarM^&n_?fV*vu+q(b96b5$#iS-!3^sHPj5*t?2zh)$zQmA} z;)xHrhcVfvYcEH>e*>mAzlvH&(O)B}v`$(c^$LqLYWuVO=chzcbN^Lxbc%uj>oVfm zn+_pqHT(Tj#n(Q$JPG~2+IaH1X;quCRRGi)5o_~_@Cd%49RgcbZC16?9}ic48i|3s zUf$I=d!4C@Hv&|Dn|G8+JT+p(V`4Qp-s9QY{n{_2j{aBj!9CDSJ}6>6P{eRa8`s#) z`&wiBzZ~APjD={M&bXDw2_TGUx^-cWtl!AD7R`k);$@on}Cx$IERRj z#nCRmy<7I^jShilp;{)t(P-#!cNvi1myGQubp~MC< zkYHH?IR;!ZXQX=ngtVRUPv5j>#Jc6YP$Q|La6Tf5q%_z7g<9dY&3o$B!exW&ff>EL z571`lYqVk8#_)r9oU95MBb$dM-E^UO<$CNn?nOS9$&1e){L}l-8D)5hX`J_B8Dxbz zwC#_@?xw_YE-Tz=g3I093WYoCAilJT zQp*PGYX3SJmk>pzfiZ=eML{eJBQK-QpX0D(JCUL+DmwKI^fR|J6~^5%?Dn|p^&Uxk zVf>(-1_9)SM~YOs70$8+nQO8*J2gE{K888SICF}+{n>M~n(tV5$t5e!8*Xw7EGo8{ zot4M*P?LQ~VaO68n=6*XyOS+U5_5KQH-+W?sPGD!Ae>o?M820rIi^P*RKf9TOj@m8 zUGR_!b^d{0@6Dd}tfFrFxb&Z0o_?0a05H?EH`t{2VFPWs6W};_7g4?D**x#W1>wxQ z;1YfT(&+r^3-{h!Ln_)k(}%Ab9;P_GA$$`K@299ZJN!XQ*rbzP+0TxKVkl*VH3^@HG1ZiR*mP+ zhb?PVF#l+x1nbc^#o66^JVt{AAb2YP_wPyT>K8zvdZO8qx9Y7!_eI4n<$0)E$Ws!j zTeF7_*~}Jd;3W(OT_mJ${R+7^zUNk3Keg_cdJ97Zv@joFF2`P6pdKCl6O7mW<3o>}O*U)u>QcH%#2} z`kN4YQ2||r(3BP%fpMV6Y#v0;6UxM(y#z{Gp_WRlGiT%PSe8V^`Nk6;;MGF;tcE@g z{Spy9f?yt+Y9-OOJ|8TAcBe(2uk^gH-@1&x;`G8{FL2uo=c%HiPvuxj5mTu!^28&I zjT3j8XsVajv@&OumI7KR4mJsQE8TKhnb)WJM!W-dk?d+y2bj9dGGvb^Vf|rGwXv9R z+p^2W!jfrQ{{{^K#GER85I2hSmm6tiV>ah&Fb4w>LsMDu1T|OkMMhGE(vgrTD%8JR zO$IFc4XIZ|#jsF2ro3R<5WWN0fvYRXU zFhEjS%8D~@)DPo+>7h-A?1+jO;pWhV#sz7MK!{6wI-|rAL?FAHpB6PDO zls4EmdHAvSzd_1rQNd?yh(=+ExQo;JfCkq`h;S1Xbb*v`kT!Kt9!zgMh?f^Axm?a! zY?-3?tRBA5cGKt}|I!4?Zcc)1NgV1>C-{i1|2?QJ9P1kitC1R+?`B~GG{R4MDU#+; z%NR8oq+EugV{ufc!dMtB=YMEy6n@a!0T~%6+{Xw4O_MI?HB7 z-W8r(5hRa_4||A z(;DodGIPCotFt@nw%>!GaHE3Wu_l=hwJ1L_(k4(M05odOFNZ3?cWk|{jIZ^J5IRf| zb8=1iQmwWj0)i|mSOxyot9kb4gJ7%j4H{*vWI6fJy6Nr>lsptU?0Q!u-83pX#RJ+M3o~=2lS1&~s74SR};5;!(DM%7t z9&r=Tu~~%s(bm?UuP>Y~x;I5pMFnlq$J#T{P=RvBUFtYrLMFdm#BQWn)1KGv9{JLs zFFGlzs2HtlQG`G|v4b5jJ?Mv~&4fxRj}LQiJo^m=hfQxDAWbD zMZJNGD@4U2&6nK&ThDx9zUmQ+Gc)0>ZtC55#!OTNPx5GnBC z^f9ha^Q8sXR%9eaUDhLZr-WZD;pAFYW1VhNK);zXnY^Q^Au43Fj;x4W`^f}C zg<X7A`T3(Vdu+)@!Tg!%xNE3x25=kdXX21!(q3w#>+eN zKh+wi&qhVRm=g;(TP#x$daWLjSQniMgr|Fx$#D6RCI}UVvkpOA{A}`|ORvm(y%8Vj zRG7Y9?o#`iu2vCBb`uLvkGe5=nUxz&f22YplNIBW;9&e3D9GMLAzO&da#k2wM=TFB z;EPcJ)|L?66|jin4OIl*yO~!Krd~Sv%z?sO`a`&C*gDLEuPX9xnVxSES|^_@LW{I) z2ip^kEc2n?$b85QE^c)>F0`&S=$uD%7IOP>_QB{f{bqu*sz^?uQ`8hlH)l4Vwi`sa z!UmT!k)6Y^(QAQuP)g>?w5i?kr~k^p8Z9cI?L9Qbb`gP+@Vej)6Z1&dw(MN$fAys& z_QSdm6>x*CYJJd3puF2N&Z8ysV+#4GFD=Pt>$C1gx%84)&qW2TGYs8HCBn$J6Fah6 z@4v^&eOAhU}u+E>2%BUNWP0*;=dX|;P zx|^c0b@xl6kP3BBp!gv7J1;1*lI`q8>k&Us*|q^NqM~_IRlFC>YeD zf{qrYE)@i%*qoTUPi(g2t}8diX7OVE<39TI1oC*kK+RJjPef!TTQ}CXJd}yfmz|)8 z&~!pCjC^nKVev!c)BoJ5k4N4Nr+$!}-+C89#{&p)3BBW-dsoWdcONXC4EzXLpxrZf znKbEI^_P>a5f~#XP77do`72$1nu^nx6-DPgGwd7=n5J?pX5XCL9+(qE?);=9Kc0gm z5iL|0A-y;;;IX$CXg}T!8dDS7@a@E0+8dv|2 zPkA#OOau~|5rs8*FX~V)s4&b5WHRC`{n;)hS-JGWh&>C@i-Z;jn>Bk4K@FlJdtqJz zvG#)B6V9g?7lv^K+0hFbhbSt#qZcx!O;qe-;XJ@%W5G7+$lRI5tb4`}&J>w4Kvd)r zugN&^lVKYcu%F*%ITr}lBzz{u(J~FbiA`|J$u{%;h&3wD0dMcHa~sB5H}c0MWWx8T zv14shk7ow!H7ook_I{!w`mNGM>!G&6}zRlPZKDJ6iv?8044=wg0Du`)*l?)N+jD|TPlu=goXqOr%P8>nZAM%QNgF%JbeH0Q`QziM3$)NeL0yq1>JIXqSFgqz+(2+=~Z)tk~my57(6yxngf}jD#-7mP85_@;!E0$<_z)PMgvzOj>E%>VtEFaII*G z1!!P&dVK^WwzzR=+YlfdM;+|4%Z4*gc$&|zO^AU|vYuoRm#9!*TAtnL*FFOkSs{Vg zMdR-$m~<3qI1hMx!_MFu-Y{jSM;0dHqA58q58WB|*iN)9D%pBj-d-Ls_V(^`7+}0T z@dPY3Sd`3kjD7Ywe8t($*d_;zy880?cY`cxQ3uSEgdDRK^W3SQYu z@+8MlWF!j~G_Br}VFkJ@xKC45Q3+x7ik~Emy*<+o$+7fkz8H6}{#}rE2P;AC1#H+7Pz{mUZ9kO5bB2zjJ7HHypqil@QH|pCpXE zxhjr{MVb3zsDP+x54fLGm+`pM!F>2Y6{V3Az6cj};hD%8WN zb;h(xnT+HUMg{$Q41@(w(C1ZU|3c%LTpD<&P(S;4Mx%#zrz^7ZU-d&P1yK$ZMbF)O zT0FZ`iw>B+=kZTpSU+Wb7ey5nbihmOrClFR55xcw0viw76A%^ZwDb3hZ1eX-R(_*m z7nbPI!jj@k#iPZkY_G2^`Uc3TQ2!jzu1n=_-$mXWqars2TS1Vp>&zv<;oQdx#m*o< z1Zg<6fk>jiBMt*U73#z>H$R>KKJbf*PJOVkr1ZX>pMLN-jvPhh@A^Z}C&W;}LICZ{ zSVnMG>H3j4LKc;5eFr)MIXC+tev&YM+aOQt4o&JClYuwVtj6P;{VE>6-mg3^-x(EJ zv%8^5mIGez&Koxu9|0!e?Twpn@Zu?GyzInvTf8qh8H$wU085~l2diq;{8P~y6>2`U zNZ7S67b&uW19Zl2OYa>!8sRpd(Ewejkfd7>GN68Wn1>F<%|*|08zyqGD%B zGr8kU^WnrQ(RisRZ=T%3aBEbkO(srS-)AlkR7FLe6XXG7r@$N(76!nnTsK9PK6Ouipv>1xbzM@TaEB{%!E8( z?9G}ll;eFQ)=*+}eKb1&o^r2Fua24xw;**vg}R~tqMzWFjEZbH4;XvH&h^#p zN4rL9T$+G!)K4mmgImi>LD{;S_cIR|dvnh975z<$iT7%BmrMH^Q4*|B3#2_;vvGl! z@%D#N$rcCr=E>Rh6=UBn)O;l_{1_F?f$bnj>yDYI(;=lz`z6$D7d|f>+g?$zk#@^z z+PaYzC2edBm1!9)L|!Y75oLvXaoLDr>4k9MwJ5mzZi6 zcOEMo<4uDyw2GePVX5?|Qs2ya3v*pmXf+!~%%}9E`iD#|@`AThVw#y5ssA%A$9*yD zw!APyQAI`4!VY>yU*Kh_GFc*ik}&qx&E*>Y z8tQ^C%~-Mj5m&Can%(=(xhJ4Ljf&*T3gSvh8&}-%aW=+?Z{){z(i+J*gm);%{ViL} zn+DPIhE>`5{9CyHWK;xE!E{XHKnaqb1mj5sZ*n4N%Hvx*P0b}uIEIcRhb?wSe@mJ< z8mGP+;rgLQCL-C5O13_Ms}}RLsLR?yeHSi<|fBYxj? z?s<3!vUQCKpT(Hs@khr+Tmd`FXJp0$lB2XbJ4K2ewO&lA_Zu#~j~&g@hB(-Y2Ckgg zZc*wZu_s1OI_+i?OhYQv3TKVroeFX{J!G$h|k-hTOgBp z3LYm$KEd&u-EzQFw;22+;>F{V z=#4=qLrKc2U7wb3J0IHM*cWTIEdBZkMMtO<38?vw+`s|uZx@z&Qc(+~Z+|MP%t^-e zHvpx51+fl9%5>A{kf~-C9IiQKzWxYtHJqrZsRhdfzC1S*-JQWD$fU{vLukp9 z!fqn+n+ZlP3etw^#G7-YZq&s@8andT2m6(o1Dw@4PE%PrU?fg@v|TR@>^290e2z{e z56;$qBN(e1a=;Ky3gOfalTv`uL^!MBAAX@praL-YrOLjD2}ob0S7mhO1w%M5Q{T<& zkk)-tin_Mo8@Ed->XW<27phhX`8y4GJr1aK57A(a^mM=rn)QA}XMaxf0IE}=j-S-D z-RS39;V$&TvOSO7LgxClRz>+S=Sd#$gntw;X@>!dRBLqHgR7G|g=gU+KEgP_BR{oA zg>j0F^BQq7nJq%H^MDb3(0ph^p9Va@S&I5kg<5~fq!AGfkrrY9^@z%|I<;Xu+DlDR zG;Sb#Fpi(VB(s49AN4xCG6~tcMpj7ac6M@@f<7>KN|qOh7|bUE&lFGh9E`BRYY4LX zrt|xsJztPMUaB_SBW=+ zfCjuV;j@=#9&i3&Ek*6{h-c%$`vyZM`{u;yfRX&}87^^ZZ$W{wLj5eNcHE`WkHZaX zR7fTn4I9yQy3x%xtn?~r9v?};IZXrU;e^K#FQ`!GefZ_U7e*Jw1)3{MK4|~lBOsh? zew(m6U?fgT6{gSNIB5j=pzzl!kZ9ziXO=cz{SKamb6#JNcN0WbP$8cT4U+~QIfhI` z0*z>v=iz&rH@lXarNX%7fRM>Srxy8=8&k7$3C5b#LZ*Vex8mJV9eW|W(P9o_dYj~N znz$fC&DNtEs5`S)^_V!7QWP6;v~viC0F6u5*KyH5|64zQ z*7q;jtT_-;;zs4a;@;b@7)dg<8YGZF6x)0;{}%cK&z%ufEEA@REUtoB?K2vh*#HEw ztd+C)gE6t-knQ?)^dw8!IL&}V9({9C(EPAP4EVMNecgr8q1 zKz_{wzm@f5_TWj67}5?zM14*~F{)Fcu4uabx8;pX;?U=}hknlJNontRRIcTDLkEoP z7+Vw;6)-!-$dse7?A)nVQLFm{r<>ZfcEtfBaZ=E@-hk6-GMD=uw~0%ndmvB-oq50z z&QZXmlg{Gz?u`K$ia)4}_uuaINj9=Su77=J%c3>t2@^v|O=dSKua=k4-^q)AUwW2OL^GowPuhC;j{1%}F zrvpYZk1TopNut4OhfjFAyL#=fXAq-0^P|De~a!x!V6k4(XZ z6`o@Fip*YU63SI4f|`X1Ae*K1ANmd6G1H2Dwa@DBPYl6o71gK1%H%JgI)zwX1E@$Q zkq5lQUy{W5P)@Pbr<8+@iJJv0>J-R&vfL@?9>a7rX?DO!oD~WK=f0dc`2*#!H@K;L z4>+G9(Y3P50V8n^r2#f@n(V*S=83HXR=}DUNw-7LDPCe)5Ja2>&wbxwTjPIv^f>u3ThD z$-Wo4GLixBwq9-JG978z|wZYKhh`mhGyvgJoF};6(y?K7GQVY$x??l^joMw&zOGFTgMh& z-|#3-j=C*66DYWv-mGxAp8u%x%&9jY#*A%#Z|=#RAH1NfHFQb>umy_o(^>a;9f=ev z`H-nFqEheG4KIiziS>iB)-0(wn>u)_ex6K2B!e%5z55a*A4(t!J_=l$O~{}p2DM!f zbXKUX;`48qGan%-ZCb91YWp@kp&;sIVp}QpncM9_$RZvwrd>=PpAmc5XLMx?5z_dK zMfBPlyIZK|6V*o`z~__9i&qZ}Tf!P^fp)Y?T(sOC3gf0L9?7gh)2}goa8~|-v*Aw} zSZ;8z{<67>UWSI%DhB)Of4#r$JBSGu74(E%j64Tzh2RvHxx~0RV!NGbO45SY-+1n; zzVNV!iWqe*hK8sJiybfq$hyjee>6Q}XvSiA4@ReDzCDQig^tm8z)0M5V4{ylVOzN> z2Uo7Sl8BSo@o#)o_Y?nd#w@u6>CY3qRY)B0gugK{cR7jeW5liqOWwxxXO1O4^IBUh z=4pSPi@uIuW6~MadRHI*(uXa3qC`cM&nh;9goLX6Op#T@`=cr zukP(qxc0!^T@?L7POSW*F3bp?ORGDZudHWuNTO%vt;8aF zz~pA=G0!n6Z-o`y_Tqz6#zvS-&I3l`rZLu=@PRX<4_=G;>i(D)8YpVPW?yfPT}gIp zEMsx7EwxL&2P4IMWtbt~R?Qw*9vPnF8Hj~6egU~69ypvm?Q$`=jbyVWso7NCtd~$y zp_aOIsaAnKwH5X4cGTb$vzM`d!_-AF~PU8aE`wLbzc<#M0 zHrW{slzA^u82QA-&OeWjsFAxw`0hwF@)v8w;yb@l^xaWe$@>&_+*93~^s4(O;|zm8 z3HU%o94Af4bjM|;dHjB?^5Zn!mzo0Y4>e;d;+9L=Rcf(wyP}q@RQ}tSDqm1^I*YP_ zd@!}q>pLrKLl%x3>C=CYJc4dydOjqYw_Vn3JeF z==_@X-O?@()&JU(ox2Ec)^wZo`n2iPngwyQRDbl`;+KD}$k;=xAVe z>YH=2$^k<-6^XM`PMoz06o0w*mv}hg&A3H)Nql)IoDLYm$*h3}gm^b&zeP+=K6P(K z?4wf^wd$asik}#~hVfw{@u3(Mw2Ioq&;d{Q7ZPepRTqfrDibW&rZ>;tgGZP7-R~%n z?lAWqFp_jKWc1gG6zk#FjYd1FpSlFgZA=cjiG@ljz5)M1ac*9NiZD>) zw7#d~D~BZE&3z5>H;Zh8Q8Ow!?YO{Hv)O@)02FuEn=EAi}k}0Z<2;6QIG2N$I25^e=HIJBFq7M z5=NlqO)KkuTO?Ie%O8Gj`p|;0p{R7gP|w+_!+O3ZXU}Wa>u_g5X=Jb8)A?Lrd-;$( zcfb(N)-*%fP(e0=P5WM*>gu^P2IkNCC07R>@?ljFr>>@KI$$Ku?+EA7oH&=SOKmrH zC=Pqe4DGSIaidM4a5`WJ=Uzg``aUgZ-=)L(uV1FzUNbag-yJZ76S_Q~K9nZWLnO^s z({Iv(k1~sHS%o}dZ>+n#Wb8hsQdqb{Q`i=P5|on)$xrFxcLvK1Y_T{U-^#Yk+#6PT z;0Jw3{Ush}%GN(%vy1naI&B;VT3{qj#*-O2dBPuhfXC(NjT6x|zo19X9J9*-BXKgd zS)7lZx!HUtZ27%n3vjA+s$7#ZHIW4vkDPHWkOvImoC)f9U+6_Pn@*JvQw@6f^P`nV z-XDri2Mpn4>YDZanVdLzx@ow5s`qa&<e#YH{f*4TnCICGTI#lA_me+ z;xGmTF1 z2l_0geJz6~)8)rLQuIj0_=2HuI$$KuN`!NEPMqR70^^Cs!l7_FURtv*xv3ojvjax3gylcY zvYatlH-ksx9x1=|1nyIb3OOXvS!1{J9Sx3kKPtc^yljP{i2~IJdv#zLGiVS9X>6{GXni97YMI{7o@sot1 zt}yH6B;k&955x82UU+54Mm1;i7eYIIk-K)Xz(~w&W*S>9bA6wlI;+;b`lAs~Yg9tm z0OBVJLpT{HnCL!017%gwJZU6O#4of@@EvFp(gtwA5KaQ|o4Dk7qy9A7#;bgW31xExTyH7|* z1TAF|wvHK}ua@^9LQv~~k(i?ibNQ~x8oXc(y*GuLgbo;LhIaVDw^8Wj zP}oS9CuO1%f^_kdkzDf>)7~T+?8z(rdnOtdA4-$Y`#iAu1$Ye}t$+IdQt!@X8=f3K z7pr!4zzdqSGZ@1_nFodp88Cp{hPgOXWC(z$gy@6#$;kG)i}itgOWH#y0wp>7b7|oJ zt^E!FH;aV`lUdV9k0MxWr!2_j!UMa&9xI27-AH)5qyQo(BOK;-aFEC;d^b?qik!kuGjNT;VktUk3(Sn38F?T#S}f!zTku~#7MWenOm_8vYR z7d&lkI8;lBHXJarjmFj<^w%|2v+GcYAr}I>14d$}+vI`L-7?aWHOeG3fRfC6>4e)l zz%r&&R6;Z%ev+xGWfKFbiLye5L<9ZMj;#@ggoJ3s0V5lEj2bDIvk|5O4iC81qy748Eeox0jKlgaY3F_ImWA|a zub6bEbJi^ECPYPlk2qlUXdmcbK0YD=$u!D)%2$2pMefmNR`+dmF;KFL?i=GxHBCd? zVDm|3$&MGIUk(`R7dww3lQ2+P`W?DYi1(&Zngm-D4z`47!T}?j=uLenZ7!ZH;69;$ zA6pZg!vCiN3{r?spj^%-oXaSmT9(A`Av)rK5giHcqE<=@Md(RQKz1NJzhhM*j*tJR z20o_-iVcRRmp0Q*^0|hyCqiJe&$3QAU?lm6340VlV0E6x-~VcblAAyCD5|Jr>)!&< z=9?2gNf^ROD>{D|;|McGEb5bUX}`;0*ZxpDAP0=Vsg(md7J0~G2ysGU+4ugY&HT9D zm;^-?m27=0Hw4T>PVtk3AW%J7}0`uAGJ{3P(A;|an04U-V6GOOBmyyOuAuLFkgGNJAQWHr8Y=yZb|~EaD}+#Wp}-V?fw3vg|l$XE-E2pK>Q?O z2rmoHYlw|hpc3WF&Pb(58gGJlJu@8+0W!eME4tz)v=LW%0h4AWL=&-XdB9Lhhe*)c zQE`|_hDGmR{A*W-jO)n9(x**>uUa(1YsUhfWjK8)=ZHGs3I9?;%_Q$dhTpPcT~yVX z8*bqWtT&_J%N2U{2xVh9UvbRu=N6w?|WGwkrPN z2?=C$Enk7@@OmY|SYL^Bz=$sKT)0e942y!&k*FmeTL0XP9b*t-y<_c1KlnE0h{`-o zY_+B*)Bq~nk@LO2c_mx+3 zENr-`FdWR**XyR{1mT=LBv^dNr7y?{!rekgh3n{Oh^t)kI;yBRPq+L096e@Ob4%og z{#wq4LRllB^J3}n3;0qfFz+sqh#P8V$EE2Ztlez=g`BN9U`P;3ARJ7KK@kY|dy$*P zx=K2OKZ-JFI*MjOb5rCAtzQOyT6?@i>&tAa7RUh2Lww z-dj;`?dYbTKQ$^8Vh4;ste2wEElo_YCL_9&xuN%@O<=-_H`GBxdL}VtFMFt$!jRaO zZ~idEFeK!E^qKe1SAK+Uw=nS1GCb@=6D{}9PQjbilmcbWNa z;oZ3ON9O+8d$yw996of`gdWd@>Y@XNy2y52OlWE6j4ql7^V=pWy9*ymRMc|mlM7U< zHlAsnVPJERZrM0f1Uldee`A1ZbU_~%ny@mD8IN~2lPMHpBmTzkk2oG&l36g#iy6-O z9}mP(S0)IL=|gr)Q9<)r4fDFVmXV70Zt(1LD!aR;UjPG;o+nNB#Up$Jmvxg9-RzB7 zD^ot%yr6ZreQCH>W>oNoDGi48c9i%@!bnD>QK#1ve1zp16IWP>cj9;byJGN+-q8+- z--vO*NSw5v=75nst3jf^o5tCC&52*;#f>geAr;6-NuvRbV7H@ek|6-)7qSIrZB;@K z&;p(8=kf|#wtk*VTOKeH=UPx-<%`%hhelIMwfu za;e>eRr@Nx4`*u(n@IGWjQ$Jnvq{;O`StFbzZDbD-DTaEtAFof_Bjt2>N8vCn341s zkr}~q+VRA8>E+HoKl3#m7g5}AKRNpcKXB3!15E<_!wMOqV1&VKbm{NV*jqQp2UkCq z|G70l({Me*+!^7HI^!bwW9)ni%?HZiSv6eS^izyKDQY*Y6qW7T zTdzSg&3Z-F0V8p4CaQ{Ys@#x5O{KFyb3Ns^195p?RHU9|Hp5$N4Dy1PE!6=iqEZvI zco~&yc0%R_6O|BGgT+s(s7TZ-3zI_)at}Xp5OrKP;MuQE&>qZj-Pi#magvxgRjaTn zqRE)iNI`8k!sN7kN8Q#NSt)VRDYX>!Kyz>d|hjDr=qyA z`|#08#Xm`a{=f`mVn|v?ubxvt9Pko9%~&!lNLX!!#jxbDB+@jtLxMJRL9+C#eZNnlF_m&T;cURg2wU zQ!z4a)CQgL)UI`sJ76SE%29*I5n*|=l_sZk$SpspbWsUm9XbTiX&o-U+~C!sJCR{T zRAl=t(H;FBItJe7qj^PzDt)e3l1@Ff`DQr!)D?e6pYni_INKA>ZUk?|De}OmL0#B+JdmepRkr|I0NEuT%I71ZwYEjLsPAlX*MR0Yf;!X~WfLzQmBA z`RS2wBU5s+9~+#+7#!TO2W70zKFb3)m;5zv?=4jlX@iW4NJ^U*jGcf+&TROGn>mK^ zRpPo^DlRtSu{WL=e@qvs0xH!1Ywf$ktGJcEDK0bs-a+ z>cR6NDn>)VI)Ut}F!`qbydR!)H!Fgek?0wo#S9M(MbZHFz6gJ0)@ez$j2rs@y2r2xXqs9^Tl?b1Kn9h-HFG__a#9L1h$o#zjJN#^6 zIFb0imds>M@59h$ME3N*ks57Cxt>uZ@NNA*{#BS~=6+AE;&yMg!WotE6Rmo#Q zaZwbi>Lmu=aiQ$tfMqZIkyxv6Q_FkSmC|9aOwWS$WfXa3cY0#7*y|4k9PVWH*z ztvGwu`5Q05o(vgAd5+vfS;mI)fldX+iHhNMBo+Tn&$*%GOPYmL=wFqdoc1C4Tty|` zc)@3$Maj!M#^!571CgcQtf)z`CZ>zpHlcMNr1dr{>df)G^M3p74Om)4#koJ$gB^01 z|GQ9HZz9$2^5n*W$l1sXZC2a}W#t=%-*zaBC+kE7>VZid~sCrsNy z&MYpBD(Ia))}tBTp*AZ98Q@N$*t}VI?xjPK?NL-zQM~%0@r|`zk%>!GoVj3G2X2(9 zn}N)Ugg*`{^zM774Zn05dO=j&Fret0We3{iY=j3y%!)cI`PSWb!$cL#C51O0c{Q!E zri+Sm7c3P>rYM=9|9u>jywD9Q^zDz0weOB})MSNL1vW^OJ_KQ50x1}u%CCqdEE;Ky zJM=3rb2vR6)&p5H_tsz6@y%vrx2-h3ZP;i0+?TmP{qYlHo9lG!pzZp!vvZ85&#LxE zXosP3q(i8Mlm*mGT`mgN>r$)z`Zr%TZ6mgpyIKW&7qyjn&7`mywF%yS_VK7yVDQVJ zAWc7;y;#U+tNTMEjBzDFfuUYDK=Y0wo}hsrZQBl+#qQp`XYe!gnxS*1NBlObP8JOh z$vY&xY^VH$Ig+3!BL@?E?fPdBm5_&)OZ);x1Hz2JoC zM#q-#b~kbgjF8Wifz+=g#x~pgWgFtlo-W#H8@3r!jiVppPXQ+#Z$aiQ2~%JMPNM-v zjZ38cO|;WpgF7@@d<;WTZC38UU(T|jkf|PxbsWFt$NhfhjE0A{MQQryKUKK+Y0D_h zsK}(jws|_Xeuw{pj97YWhIIbz)alcj@j1t0*|*boXKd8%CwPy0d3(p_Y?5G&)?rG1 z>dytnN~*Qi8mL&+h_WVllUk zXbgTpfs4tsq|;tCP&q<>)q8E8wRh?ud!bp0H_C8^SUeM-B#fM)RWhNCyENnMM;+_S zz8t;E2QfPGbwu=*or+LuD`PL?rp>X&rypLM5IJ4ZTXt2OQa%_bT*2Gj%NH5y`=^z} zjL4zhvc0)cmdLk9*LDu*vhATKT=yqC%`SDi!Ul+4JfIf**+nZ$uYLOQpbs3t zZan#)X3Iy{1kOdA&KT+<6=nph4MyTT2T-1n+Me?A%>1cUk8Q&9s`-FHFp?_8PVa zB)9YnZd5t?V0KnZRxRJT0%PGsmkfi`Y00*{?%s6k$psI7{uSf3o?z@+Nr90#!%42v zxyq`%$jk@&0B~O9I4wGPbQ11@38DSB9EZ5b#nsAsrVL5f8WGf$^t{>W^baab=|G^E zNN5vp(40qCtc3L2dbaJDI|R8ExbAuAk5Omp+mbx{`J-rAQb|2;gvK06H1o|-w|aHI zYR(;%HsEad`o|;5TROi%&lSgv0z)|2LRX$7sR9aUJ!?l@dTgOFt)C#iXLOZUKkj~x zyo@4Uo~8AU?$%2t9>pESQaLv-NXj)$1Ij~0CfzD+T`Msq&+*68pfhX?+MrCrfIqI-VL_`wxKth+it|0!YF1C~MPX?v+i9%l?0 z(s9h~Oq5Z=RH2s5puk9m)WbAS&d44G8{&u!16wR&xt)h#fbG4$?^LLtH!z!uG!+<$ zc?Btu$J9GJPI)s#91Y9G&cdm{NSxo2c+y)OPB{6Y)^6Cl53&u4DZig7QZ}V5#cQ8Z z3cSSsk`}&voaX%aV^Cq{s)G%3`F@AzQ5BGBNlaO5S7BcWB!1q<<8p>uI{**c4&Za2 z);W9riqv*oY48W08j)}|rlJW+wGmBk}$+swq>L_XA~xQ`_3Rww>_sGZ-+Ux>sa@=JkQd zRF`V0FhwQaI872`mjJ#h*lxJ0j{~@kU1fTy6?y?XI4me8j2Xj`6wYHm+!gDIRN3+{ zFq8(A3i(M^s8`^-d4A^R_$oLG+3AN;h+PaG*v2D%=0E6NXKT}*_spGiL(8ntbWx$* z0i@a#rXKqaAYF(a<3c#fwK;6f$*7`Q>~?7_vA~Z z_AEdwRK9QfWv#b=rJD@)r>Dkw)@+OvuxTq?T>YxTB+b~!#>jSxvE@OABUu{(WZ?FP zD|-Jj+0uj4;LR~S8D+K$6MWWTT@{u;%~E+a!^q5JH8ptZxeKQ}9lwvnN<@X8J#CeM zCx_2ydXA7^Ki=}WU`2TO(tO#>>9@RL-f@{TPTh%M=27jtj{0@+WLgjz`i7u{PcQps zwa}jl_pUYhR?|yf4=9-O!$Hjm=NQ|62@0stLzW*NvHh2Wn*JZp9oYugJ<2%pkQ!&! z#c!xV7NQH&+KEE{{q>=OnKNOm>fh|KC0FnF=z?RK@7GMM znWiUG!04@MDMxVXvYU_Yw7S2>)qzJ;OO=DmIB{~NsfCegb z@7kB6Gmbr{=|3J@IA?rBLF>(>RA2}v8&NvoH7fDGXX*%?ROnZ0@4n~Z3!ENl7KD~L z@Chj^OztV0eO2ybULxnWNAD0wZ|nV9}0##B)!6?fDiO(W}ILIMj9) zV!-+Kjy5Dysa^Y?h_9GIrLUH4EOzh-h-`u$I~ffM40U`hx{}fZMY3my*2t*} zk+kOb#)Dlu%$!469xvSZPrvD!K0Kt~ z#s?kdLgc$Kk+&J~$}MuEyDNx1XA~7sxf!%MT9A+^RId8R@An^jqt!!Cx3*{Q3iW;Z#00z4))g#zIml}Kz zwLB@mC62JRa+5$F1_CFJe`v6y?bpwyz(3ck!|q!i3&0VBb=Ssa1%_~DBhs14gH@4m zP9-i=p*P9*;Btmlu(um^S=aW>go$k7r%|-M;Ab~3h1G9nZ#so{pZLS#+lZaarDz}Y ziASUVdb$$mw+!#pbJd+Ayop35JVkjKU7t)=x%@XF1E3dbYDn1&1?WhBLetf$G zbW-e`lfWx5lFl5&tCJjOt%Be+6?)CuJ*WQJ6~i0QdfXSsW_$&l4%gu=p5bwiYuzF= zebC^6i?W=K(ZGHt&2<0M81D++VVB`oxk4x8R+@KZ+qT3W5_}7RPjUrscQQO@?GgZw zU8H^a;Y|vGQSyM0uME5VjZM@K`Cd@EbHonlZ3)(A)U~6o z%w77Z$WY3Bai3$iH5~m$7=??V(HXu!JX&Qp8pOU z%8;eSjycWxYDRZgkOSM=j!!xckYDI+#vA}qm?-y9;>b;9VzGnAz23ivExMylzsYZh z4AP7qt_@|I<;W2fHbB!C|CD`Fcv0A*y<9;GSMFZgXEcIre(v_5>e&*(7MI8mV6>;v zwR@NOG(ZLdPT>)(6m7T!Oaw!jJJ|FV+F+zu7bKkRTycs%%I729;8b8F&OK04$zCBr zp%E_mw4{SmRN{>;toyc^FFrYC`(xTi1C_~Mh~343kCVHF|74!<+0O$96a}QfNM(@? z97y)2GwKMuPGo;rE>G;Wbk{ubs+p+78x;Wz;j$i|6Q3ju;bpp;9S7y-GHofLi5}B^ zqq1T%@=EtD{p70=kZIGpr9p5|p~vi<{bbMfU>qzzuQuiCaNH;@#+$Xy=t^BYS9X3+ z$Wh41KCNfYkF=J&PGG3fnYij{TefEJoE12w{a8)!Gkaa~nhF#_o6A-3N*4SyWz0KV zIQ1IWzlL94h;J8HHPN*EwF5=wLpI+{N&oxXj{`M>wwg{1IokB(p|IQ40FuFolQd4 z)3Ml4-Wh9CmS=jaM;_I2et+;%(;A!SEOhhGXPIGS$3QJM>r=Et;X?~qXIR7JX;1d* z9~9xB>zg)#=wI&BybBAkdR{62IG>36%p8L&J!N&!Z@tLb)Y5eteF`pC*JCEOj7wK8ShD{j;^-7B<3rx9Endur zyJ6cme);xR z4a8#mAur!NASNS1-{`gZ@3g>a9>`h;VG0c4WNwQ^gfkP-U>DQMEAu|>l@D*vT>EwM z7i$yt0VjWF#U{!MjKoRNLPi~~xvV%L@B1%4P5Bu!`Sr54VJ*(#+O_Z*S$$ueXY--U z9ia_9V-HNavmshjPnhIm1|n8OjdnW`^7@a+M3^Vfj{!au7_>wfy*Xu8 ztJ@cz4)7?0@GVh^Hww85gZLz2sqr@J|w$;X|H8lf5@h(11DdL?((+8%EISLlM{`}0-dfH%^8)pxh&45`GfH#jSPIzxO7?=2IwQDm;C=tHxAz9%-li)^a^2e3m*ne!{r`igDt~V5PIe|E z>^snuoQZ}1g(I~sJ=$uy#*#kP9R7IcO!yF=p9w5;f4*i!QWxw2qx2eT;`Pp&9yIK6 z*RQYlWrNKy*TaxIKN?{RrQnkVV=gN3MkS8g>X!H&Q%=dE1ODvP78UxTuZmPyw#!q~KRYt5!!m~tMk|5j&LcG# zDFN0V(LU4D4LCLDhwW=Lz0Ij&gVRnW-;P{tv~Dx2~sTWxTR=h8Wv{&@ZKvFB@DWjl8^YBW2OGsB8Q zXszLh>>q{Ewx+7`JHax?b84pWLssYs^QuKv_A98#O1$|K<4Yo=H42Ohow~3>=dnh3lS))faGSHt zgZaR?BP?H@ISIYfXU8#zSut`D13ip>Acz$@zvO(!UseRhjA%4D1Nb*249*YyrYnBR z3cbxQOT+tA#!4Y7qN9!^qB<)ZBwHIo!qAa`$j2L8|K_DBalcofp7ks~jN4vQ%^-s{5zy~=M7(pj{vBwNY zu~~obyBNFkeT$f8{?gbg(5Y-06c|A#dz{6%$LFslX!9~GpI+}70t{+(UtMI?#Y({0 zhG6W;xfB?}Ny2F4r2ZyRzdUVYH2Z0lRi9462b$~p+xp3K|8_S<3Jl>4!H_$mXpBsp z*19C!zoO#CXCpqzjV0k^x~?F}l7f@FE)ee`D)6rW5Pi4W6`v%G#Q6%UT*WE%r>Mjm zUYws6PVvbp&W3bs$}?iBNv9Rzs$|ZR3L_O5qLVp-W}c6$!u`6cB)kL_8IiM}YVACkVg-{2z=>1%`0;1(bt)an5U` zO|}B7-0&^2IV}<+n3Z_rM~=E5{xCrKNme9kI@&XGadLkqIited93o7GZv|68?lb%! zTk40N%yw=fH)#{^3D7_1rT4MWC8#mLOscUFnNH zIw;kjF@;F8PeTqrRh<4z^*6=?*h!$I4^32z8Ll91C-ZY^kY1}Wf(YKO$aq!}2*%(+ z1%2*T>I>zlB>#|tvGHcpNff3{B zAn!1A2>G!bF~LDGEK`~l-HivpF7e;Hu6{ROea0aoS3h5A#6>6XHsSWWd$#rGtEug>%oQ_k(r&x|NjPO&TKdUu*U%+y#+a*71n#XbPqoul1ff2lSgb_}Y zPHTu=sKgt;(D>NbSn)~1NStfINh3E+2z#V>ff8rl#^=Mj&9~8bSX{-)4`Puv zLfBSDAwpvp&7_mVKYf-NAf+2_`1ObSujN^-gKRb33cX-Ti~fY@K-&c(hfnj1K$a-7czo(dT^ zWU>dR4Cds{LfoN3>O_5etbhG4XQ$P~nnPE9*u5WJKFhm%a80fIbWhEw$r;j*8lpm% zyH!3QR~SRcE=id{Fr0XsP1fK=`0I>a5iB4o@y1`&LAx7Qe3CH4khMVP*h-`&p$3Ln z_JK&MsPi~rT93#*4JM}oBbodcqm$r0jiOYiLeISB^RT`eLge#hx}LT9lUFX`*X23> zDg;l3ygngg0mPVCE4b9AoK};dZf~YBd~w zj+t)c z^f!VzxYgr^-`(6jegsYvKeQZpVvE-h#>K+KK+4X7RMg=?5&c4f=uuN^3FmXbmP`UZ zN&Ex?o(c?gI|Fb!M%(sF){K|>R#YJ1fCkP~U;3?g{DS0O?^)I&p7g+%O^JErq?p86d1zU7i2mb0G&aRUclLp z=%zyN@-a=1ja}fZ40{(n`oVO1R$bY}DKLbSX+CyiWgw%|Hc<~OD_-$JSp>d%6*#>* zF&^b1)lIT2QKB&GMaPzmLB%3Q0vGR14{AHc&1KR;pQmgu*dOt~b;rML zaxK?kH<7h7ZfAV8z8jR@t_45jPd+ym_8e!ob(Wo>#*A7`4#sNwq}QcFm;NUXr#Hk6 z`{ZctQzE8d63$_Bt*et%q-3b&C#LvZr0p|P0E9=eeUIZkmS(^1Gt_m!a8Cyje@q6d39XGoGekQX4&aS1kBZ&u8a*RSvtG*=vV1Jhh!tT~Bbu zslX7wF_zm*S{X3NV=_)+ck?ytdulL*lUe860cz|Zc)OX*_XH^pe9(2XxM z4)M|Sag&GroiATjCMJx8>eQ_ooc-CUPK9~egX>+~IQj%uC2$f_ff3zuJO&EnsSFGW z#ldK-5s25ASkHL((=bbvO z=;X|whRx0o1xDh$PrIQ4=m2QWgFmd$SNTVVBxx`fkIY$Iam`L#``c*p`%l0pdj|$W zC!wDnsr`o9VAF^q6xGB0ij(r(`0;HFQa^@Iu2<)ggTaeLhvkY$! z{$L!BMJUvsycH`S^&JJA$||eC5YA;p;&LjOEZHqsVYup%i3j}(munB4JjBB0cu-&@ zf437)XLC9eeK7y2&|9nuzj)yU)Y7$_+VK(}5X1@^XlG&!BY7Uq#?NLSCHBaCU@g*02w)McbbG3~&jKi&jI>_s2 z=F%~P{n>T7Cj9E0EnAm|W3v~irNYb(kNtC>oRkem3Vdzuk+R z5vPvkZ$51#Yoljop9zt}T-{b}bBG;n?X(t1q>{|GuO5sZDYul_bPItbRO zzU=JgD$nB(t)k4-$YY1RcmDt{#gpnuWm>0Q!`Q?2nguX6##7c6o5i1TOufa`yTUlg zfByX8Z&u7hvzK_Y0rEpZXtze0n6~nh&Srk4I+iY3 zMTTicI`Rk!?H8v*qa1z--cg~KuD$ropw_E3Ss|qc{|5Ini|kL1%_YERaMhPWn+4yKXrq)UEFzL2d}WcL8;$P z`fIwVsN1tTDW{_+R;-WLCd~?seIE?F5lBT}6kxn{@h*Ag>YUjZ9D*v}KeBuKeD@$v z%FR0kM&e|re00wQI6q>}vqFFTzWae^Q()-jXkU5C#j-Ei^1x`oj89|qB6$1dzcSui z+I>mzZJT{NavI({=XhfCU^s%RnrwIGHG^u2jHTWS#hSYgk*|=s{(_~ zM5GFd7QU#WIqzuao8p}Uvx28YT?ayZk}%F~T0Ld1fcxYtNpnk#r#s|n=a`wQ$n7Do{@i^bIlVxt1G%_}U z@4kLsf40N4$;**4NZm*o2>oL%{!C_qI8lk=c*rek1vwxX? z^YTYkI9zj}ri+T3`6oU}7-~5b3dxb3IMbgmCJ$hwIjGRfANxhD&$ficMb}8n9Vj4Y#cHpfq3aZZdimUQ@l4=5+tRpR+Y)88sVE zVnG3{PGa=7(8nuFBBt9#v-9}ZO^qI)VcPiQ zen}?|17`?z*Dj6UbUCsBY@Dksvd0^_A z{bTmc7E$s91*ZZd+3_Pu6QxIR8bTRRzz;`n zxx=Zz5KcC1n@LkBv-*q0mybOvu>_|bt#Zc~nS(sHs-3UE5Y86V?Uq!uVtuy_^S)Q# ze$i(-cB50)tbelWiWFVdIpXH7*)&biwtHn^ulc1Pj{Gdrupd zrCD@i1P+)LH+oupk}#5F$}pj_q1TGCNB>@g9hs=a8^v8o7M~=H!08C48KkiG@+994 zQ50(MO9}6_H2sRjSmZdRD5DlKTQ-EE+FKqv56kI{m%6j)ZQJF61 zDLGXmN^TCw70?w|gLn2TKl}F(ZKEOqb`q*+H;2FMti$8^_U5+wf5b5z^)0nNlZstL zvYmu&S1;)wQ;^A|)7PW2hJAYxBv1+nbj#9v3XJHMZEI*BQm=Unn%~N|QqwO#X%!rL zhAwj{-BMsAPRiIRB_=F12#1U-QR3svHgUie;_9Nc2^K-q1N4 zECG)Ae4@u#H0z){N@|Z|Cd%B9k&H{8OH4B%A zyKJm^Wa?$$G#6@16ciYVv!vTu^JZS79vSjt?c3?^SNe-DR{-Z+Oj)N{qrec(J)n*y zqBN+}P9oc4#I5VA4o<}#t-LeqRm*zKqSN9}XpMC9(`JDN2>t$E_gl@zxk<`(dd8j! zjRWCh-<tz*R)9Ii6PaxdH`nD%^kH=*g4OtYXS4Fp@pbfRnwW z1-C&=XyA|_G8UY~B+uha;p3}g$0RE8MggM1KD)&y2_spxm-scD(`*gbiVfLU-1VM^ zM^<}A-$@9WS#zWBmG@KK+NA#!2|D~{5Pazw>rbl_{A#*o91_Y|T#7AL zckH}#>-O?s^{y?$zo?X#+ow#Nexup8iFj&Pymg8l>D_SHBAC1@-<^N`3g;GLwpmhK zalo|iYAnK~;l#q%LL1-u#?5v9X4jv;Zq*$otp6?lF|GIgWJz%aYBsN{M5NsC-gV)~ z*P5Pre}@YR0ejG_nIgdIiUK3K`G72$;>?n1dioNTuEAyjH zKb%qpcehJSJzKEvuPmo3<4z*Tq~{onP;Mcx9fgBMGFTM#UU$xW-;b*qo1~8nb(TE9mQeEB^b2Y7fyaQ}Fvm)Io zhiP=PvAYE^F?er-pI$jB7x5eUW7*zWv?rb%wI)U4>b@f;eFff&ij?3~U<8jG4G3ov zpA{B7x82bJQE{_OiBA$n(n+>>a(eX7i79Wcb{^X!3moL4f{U)MidB4)Focs?YmemE z*vFE;yH^d7LRY+N9a&P1ooBPL^rqRaxKRa|gqM710#fi)h6sDI#OtH~rTOKSLR>^e z?$WB#%b=eW0B#fj!EpbN~MXlPU zrCwdy;jJRG;%3t-K1mqDxgYYt?c)QklCi1#j*P?iV--kTbFO{OB_p9el$0t8jG)t? z7&9X+vCKY-_hZUjAHA%nri)6vv6{2hl1TAM!Vpd-89r|NzBTRI5C6=C<`5M(5-dJR z7(u7wOQMtR&pFY#d1T!p)dC!v?$hA#)^_tqb><}g*i8ckM&fLYE*W(|tP@V2rU2U+ zQHfVy_7a~YjKpb4xqT_Ed~U||TQ3ol)u#2i%ZZh7wQZy6$6;BD%~@fItO8H?nTBJh zu0;;!}RfcmVP-1*5tV0yWf{Mi}XwziJf*A)gB#;1O95Aie`-% z2vQWgUxAVABW-Ma<0_6mn}?QobtX{Lrw;t?+uY3@?nGRHkvN&F?4awk>62mHhysn^ zt-IX&%#HFxD6mzTHVO>kWK*#p&?V;}|9fC;6!XL5tf7CD^ER+UkF(bc^sIyPO0(id z;>9NkBS$(Dv7)rA#3$t)n`P+h9-2OKV4fn6DuuXXuL1*{!%Mi!mSeMUV<=_L1-CQz zecJAe>fWe@kP)*&3Rl`T#$s2^AiyL%wcqOh!WhSa5Xdwxf-9??dnyip=kWO1;He;_XB!)lLh5LM>B7!&R=9hyC-DgMVTcbK* zWfv9GWnyFk6IfwhrNfO(aqZI7a<)qfGTK5^%o$>%h2K|1E>Ted>2ywDG^U1{FqYfE zd1L{uJ&B4L4$b$`bc0GJC6yJ)LCYAlFIDeaJUg~F1JqiJDW{GE{DYe+>Qbe^NSyCU zGL(&Us9&j@abpmR@SvDy;@oi*ce;X&g3UZoV5IbMj_+ZZ@HQhh<)G2DcV%j~liQOPqS@?uOU)QOmeLoQo;j<# z3XJHA!H(6*V0XgF-Pg?^xy+B<%-wK`PZEZ3(q_o9o#;$*rStxUbvIs)nXKt$E=(vF ze`g19DjO^XM$qY?^HyV~E1flpPEYB0eK4ZItG3=-bNMiLeOF*4&f7GMWid~j`mPOa zG5uXG90)ePwk@Q18XP~UOOgUZI9ZrNGn#f}COP4}@J*J^RlbJXutM!aMT^&403u8rKElqm%>v9FS$7noEH^0$i*iKL&%wCprC4a z4Jp!RXjrio7yMzrBLj`22bqai1pzr}y^^SaB(;g~pnjc1V` z4a{D8cFc*v3XH^A+3nn_S2Sc-N-*R`RNT-hK1mpfb06WPWeap#TB-Vo&O?967o+LJ zYL~9uxG<$jR`!eO9w%f@`U)X2zLUy$mU%MW%|5BWPuYk6tO$XlJ*8yN-fgEaXCFJ; zu7DOzhmh^SSEK}yc?9B(@4!c=178J(y26h7GtvC@h;}t3f|AFMH=fKvs7J?7$i+)B zxJrD40z*g{p`Ju&Yc`HHLN|8r65t*7i@%xH;XaGgV!DdGQa6w3w$$Jy{}&SfPrAxU z)(;ydeq4^5%f8-MgYM=UgFocuonv6tqL06pH+ReN3u{Ggr)~4OJOJd!PsRARJ zYq^X(9qYYdLQtXSZXS8N&=puVSFaXtTIL|F?#gP^y~DD%r%z*}doAFIZ{e$t+YV&CIxhr) zy3PK)cC1UrE=cW6Jod0{7M|*OL+gDy1Uoh<^!Q#)Tvz7P+qPNYmtj-H9jKT7>Skf~ zk(QTNh9S9$xY%H9awS*3D+__D*xPREa=p7^)Y^~TyDDC=g@9k1$ z&X&VrY^&NYb3drQ8eCG2#T6JS$CQ&?Wbd}tv}p&5hM(}p;a&CGYx8E?O(#Bzd?+wd z=K2EXzq%dg7)!^T%K+!GsJKBbK1mpf+6oJ@?+>1q|7UmJieN$YJ~z62U%+jo(MO8b zE{y$ws0eLhY0ttBJx9*09)b7hLm%y4m-mS!pNZsnU59QtY1=8g|H_r-J~qr%zt~!% zrh_udni|T&sldptOeM9KG1iESAy3m_r!if7;(FuR;}DV|DsGr0K1mpfc?(gymQ!nu zDZg)D6~5Wc=jUC!Q`Mq26&Q*05aCR6)lGwEoz{Kov`0b-vm#y9q|}W)O#E#cK!8bj z!Y?gWW2>udKd(6La*uo1u{CJgA|ZF{vF_whfsr&*P?PbME6(lr-tBmL5+NUtkB!?N za-D9ADYC7=5Kdq-W6PFKia~C8Co9CuZ}RSx6iAM!I8$C<8i{z-*wB!o5urGv4v8sh9!dLhgVMJNvnxB{ z=5kbAtUvd-B)LkUC)B$FsSNA%H zgq33Hv-E)d07ii){4AQ31sPX$mB4d#^!Gn^!U4~r3?1ivdA}?~j#PRNQP(wmo{Y`sJ}I$ZBm?RJ-v(h73!4{^Sd% zky(*k+y-IA5o5H0dss$jpmnTj4Z-WP4;T0D&=+UzX2s2<6Q3lEWMfBg$5`&F&~TkP z{NeW#G~Mq|tqNa6P*kU4lq)cVGZdH{EGI-5$GL?JBP#St!`G*KIt-WKw-karzejwr i32#83$;&EM7(kF-N<^Kl>6iBQ9 literal 0 HcmV?d00001 diff --git a/async_testing/profiles/database_profile.prof b/async_testing/profiles/database_profile.prof new file mode 100644 index 0000000000000000000000000000000000000000..cbd192e5b7cc947eeeffabaa879dfa6dbbe54dd2 GIT binary patch literal 5262 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 Cay9<| literal 0 HcmV?d00001 diff --git a/async_testing/profiles/websocket_profile.prof b/async_testing/profiles/websocket_profile.prof new file mode 100644 index 0000000000000000000000000000000000000000..a50eaf1382b563772ad2a601b932cf0046786391 GIT binary patch literal 80938 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 z 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 Date: Fri, 25 Jul 2025 11:19:40 +0300 Subject: [PATCH 04/22] Async safety unit test Signed-off-by: Sebastian --- tests/async/test_async_safety.py | 139 +++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 tests/async/test_async_safety.py diff --git a/tests/async/test_async_safety.py b/tests/async/test_async_safety.py new file mode 100644 index 00000000..0c3b0d6c --- /dev/null +++ b/tests/async/test_async_safety.py @@ -0,0 +1,139 @@ +""" +Comprehensive async safety tests for mcpgateway. +""" + +import pytest +import asyncio +import warnings +import time +from unittest.mock import AsyncMock, patch + + +class TestAsyncSafety: + """Test async safety and proper coroutine handling.""" + + def test_no_unawaited_coroutines(self): + """Test that no coroutines are left unawaited.""" + + # Capture async warnings + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + + # Run async code that might have unawaited coroutines + asyncio.run(self._test_async_operations()) + + # Check for unawaited coroutine warnings + unawaited_warnings = [w for w in caught_warnings if "coroutine" in str(w.message) and "never awaited" in str(w.message)] + + assert len(unawaited_warnings) == 0, f"Found {len(unawaited_warnings)} unawaited coroutines" + + async def _test_async_operations(self): + """Test various async operations for safety.""" + + # Test WebSocket operations + await self._test_websocket_safety() + + # Test database operations + await self._test_database_safety() + + # Test MCP operations + await self._test_mcp_safety() + + async def _test_websocket_safety(self): + """Test WebSocket async safety.""" + + # Mock WebSocket operations + with patch("websockets.connect") as mock_connect: + mock_websocket = AsyncMock() + mock_connect.return_value.__aenter__.return_value = mock_websocket + + # Test proper awaiting + async with mock_connect("ws://test") as websocket: + await websocket.send("test") + await websocket.recv() + + async def _test_database_safety(self): + """Test database async safety.""" + + # Mock database operations + with patch("asyncpg.connect") as mock_connect: + mock_connection = AsyncMock() + mock_connect.return_value = mock_connection + + # Test proper connection handling + connection = await mock_connect("postgresql://test") + await connection.execute("SELECT 1") + await connection.close() + + async def _test_mcp_safety(self): + """Test MCP async safety.""" + + # Mock MCP operations + with patch("aiohttp.ClientSession") as mock_session: + mock_response = AsyncMock() + mock_session.return_value.post.return_value.__aenter__.return_value = mock_response + + # Test proper session handling + async with mock_session() as session: + async with session.post("http://test") as response: + await response.json() + + @pytest.mark.asyncio + async def test_concurrent_operations_performance(self): + """Test performance of concurrent async operations.""" + + async def mock_operation(): + await asyncio.sleep(0.01) # 10ms operation + return "result" + + # Test concurrent execution + start_time = time.time() + + tasks = [mock_operation() for _ in range(100)] + results = await asyncio.gather(*tasks) + + end_time = time.time() + + # Should complete in roughly 10ms, not 1000ms (100 * 10ms) + assert end_time - start_time < 0.1, "Concurrent operations not properly parallelized" + assert len(results) == 100, "Not all operations completed" + + @pytest.mark.asyncio + async def test_task_cleanup(self): + """Test proper task cleanup and no task leaks.""" + + initial_tasks = len(asyncio.all_tasks()) + + async def background_task(): + await asyncio.sleep(0.1) + + # Create and properly manage tasks + tasks = [] + for _ in range(10): + task = asyncio.create_task(background_task()) + tasks.append(task) + + # Wait for completion + await asyncio.gather(*tasks) + + # Check no leaked tasks + final_tasks = len(asyncio.all_tasks()) + + # Allow for some variation but no significant leaks + assert final_tasks <= initial_tasks + 2, "Task leak detected" + + @pytest.mark.asyncio + async def test_exception_handling_in_async(self): + """Test proper exception handling in async operations.""" + + async def failing_operation(): + await asyncio.sleep(0.01) + raise ValueError("Test error") + + # Test exception handling doesn't break event loop + with pytest.raises(ValueError): + await failing_operation() + + # Event loop should still be functional + await asyncio.sleep(0.01) + assert True, "Event loop functional after exception" From 7be49f1814e6ef74ba1b8fd6ad23aa9f687e0f18 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 25 Jul 2025 11:20:59 +0300 Subject: [PATCH 05/22] Add github workflow Signed-off-by: Sebastian --- .github/workflows/asynctest.yml | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/asynctest.yml diff --git a/.github/workflows/asynctest.yml b/.github/workflows/asynctest.yml new file mode 100644 index 00000000..58ba696e --- /dev/null +++ b/.github/workflows/asynctest.yml @@ -0,0 +1,92 @@ +# =============================================================== +# ๐Ÿงช PyTest & Coverage - Quality Gate +# =============================================================== +# +# - runs the full test-suite across three Python versions +# - measures branch + line coverage (fails < 40 %) +# - uploads the XML/HTML coverage reports as build artifacts +# - (optionally) generates / commits an SVG badge - kept disabled +# - posts a concise per-file coverage table to the job summary +# - executes on every push / PR to *main* โž• a weekly cron +# --------------------------------------------------------------- + +name: Tests & Coverage + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + # schedule: + # - cron: '42 3 * * 1' # Monday 03:42 UTC + +permissions: + contents: write # needed *only* if the badge-commit step is enabled + checks: write + actions: read + +jobs: + async-testing: + name: ๐Ÿ”„ Async Safety & Performance Testing + runs-on: ubuntu-latest + needs: [test] + + strategy: + fail-fast: false + matrix: + python: ["3.11", "3.12"] + + steps: + - name: โฌ‡๏ธ Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: ๐Ÿ Setup Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + cache: pip + + - name: ๐Ÿ“ฆ Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[dev] + pip install flake8-async flake8-bugbear pytest-asyncio snakeviz aiomonitor + + - name: ๐Ÿ” Run async linting + run: | + make async-lint + + - name: ๐Ÿ› Run async debug tests + run: | + make async-debug + + - name: ๐Ÿ“Š Generate performance profiles + run: | + make profile + + - name: โšก Run async benchmarks + run: | + make async-benchmark + + - name: โœ… Validate async patterns + run: | + make async-validate + + - name: ๐Ÿ“Ž Upload async test artifacts + uses: actions/upload-artifact@v4 + with: + name: async-test-results + path: | + async_testing/reports/ + async_testing/profiles/ + retention-days: 30 + + - name: ๐Ÿ“ˆ Performance regression check + run: | + python async_testing/check_regression.py \ + --current async_testing/profiles/latest.prof \ + --baseline async_testing/profiles/baseline.prof \ + --threshold 20 # 20% regression threshold + From 0e443d355c10511ea1e057715f4f15d369449c31 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 25 Jul 2025 11:28:46 +0300 Subject: [PATCH 06/22] Update documentation Signed-off-by: Sebastian --- .github/workflows/asynctest.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/asynctest.yml b/.github/workflows/asynctest.yml index 58ba696e..08396bfd 100644 --- a/.github/workflows/asynctest.yml +++ b/.github/workflows/asynctest.yml @@ -1,13 +1,11 @@ # =============================================================== -# ๐Ÿงช PyTest & Coverage - Quality Gate +# ๐Ÿ”„ Async Safety & Performance Testing # =============================================================== # -# - runs the full test-suite across three Python versions -# - measures branch + line coverage (fails < 40 %) -# - uploads the XML/HTML coverage reports as build artifacts -# - (optionally) generates / commits an SVG badge - kept disabled -# - posts a concise per-file coverage table to the job summary -# - executes on every push / PR to *main* โž• a weekly cron +# - runs the async safety and performance tests across multiple Python versions +# - includes linting, debugging, profiling, benchmarking, and validation +# - uploads the test artifacts as build artifacts +# - performs a performance regression check # --------------------------------------------------------------- name: Tests & Coverage From b1d53ae4f07a2ab723c0d86071cfde35470d23a4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 25 Jul 2025 11:32:04 +0300 Subject: [PATCH 07/22] Fix yamlint issues Signed-off-by: Sebastian --- .github/workflows/asynctest.yml | 21 ++++++++++----------- async_testing/config.yaml | 8 ++++---- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/asynctest.yml b/.github/workflows/asynctest.yml index 08396bfd..e7406639 100644 --- a/.github/workflows/asynctest.yml +++ b/.github/workflows/asynctest.yml @@ -28,7 +28,7 @@ jobs: name: ๐Ÿ”„ Async Safety & Performance Testing runs-on: ubuntu-latest needs: [test] - + strategy: fail-fast: false matrix: @@ -39,39 +39,39 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 1 - + - name: ๐Ÿ Setup Python ${{ matrix.python }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} cache: pip - + - name: ๐Ÿ“ฆ Install dependencies run: | python -m pip install --upgrade pip pip install -e .[dev] pip install flake8-async flake8-bugbear pytest-asyncio snakeviz aiomonitor - + - name: ๐Ÿ” Run async linting run: | make async-lint - + - name: ๐Ÿ› Run async debug tests run: | make async-debug - + - name: ๐Ÿ“Š Generate performance profiles run: | make profile - + - name: โšก Run async benchmarks run: | make async-benchmark - + - name: โœ… Validate async patterns run: | make async-validate - + - name: ๐Ÿ“Ž Upload async test artifacts uses: actions/upload-artifact@v4 with: @@ -80,11 +80,10 @@ jobs: async_testing/reports/ async_testing/profiles/ retention-days: 30 - + - name: ๐Ÿ“ˆ Performance regression check run: | python async_testing/check_regression.py \ --current async_testing/profiles/latest.prof \ --baseline async_testing/profiles/baseline.prof \ --threshold 20 # 20% regression threshold - diff --git a/async_testing/config.yaml b/async_testing/config.yaml index ae2e09fe..6bf4a429 100644 --- a/async_testing/config.yaml +++ b/async_testing/config.yaml @@ -4,20 +4,20 @@ async_linting: mypy_config: warn_unused_coroutine: true strict: true - + profiling: output_dir: "async_testing/profiles" snakeviz_port: 8080 profile_scenarios: - - "websocket_stress_test" + - "websocket_stress_test" - "database_query_performance" - "concurrent_mcp_calls" - + monitoring: aiomonitor_port: 50101 debug_mode: true task_tracking: true - + performance_thresholds: websocket_connection: 100 # ms database_query: 50 # ms From 399c05c9c7bbe6efeae3c0cf5cdb6b8038f3dd8d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 1 Aug 2025 14:40:11 +0300 Subject: [PATCH 08/22] Add asyncpg module in workflow Signed-off-by: Sebastian --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a8a4656b..988053fa 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -65,7 +65,7 @@ jobs: python3 -m pip install --upgrade pip # install the project itself in *editable* mode so tests import the same codebase # and pull in every dev / test extra declared in pyproject.toml - pip install -e .[dev] + pip install -e .[dev,asyncpg] # belt-and-braces - keep the core test tool-chain pinned here too pip install pytest pytest-cov pytest-asyncio coverage[toml] From 0fc05a5872d1d9989abd2e4f2df6c062c47f4db9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 1 Aug 2025 14:56:42 +0300 Subject: [PATCH 09/22] Remove installation dependency in workflow Signed-off-by: Sebastian --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 988053fa..a8a4656b 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -65,7 +65,7 @@ jobs: python3 -m pip install --upgrade pip # install the project itself in *editable* mode so tests import the same codebase # and pull in every dev / test extra declared in pyproject.toml - pip install -e .[dev,asyncpg] + pip install -e .[dev] # belt-and-braces - keep the core test tool-chain pinned here too pip install pytest pytest-cov pytest-asyncio coverage[toml] From c85d89559081241db922436b2de6e113418bbeeb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 1 Aug 2025 15:21:30 +0300 Subject: [PATCH 10/22] Update MANIFEST.in Signed-off-by: Sebastian --- MANIFEST.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 8bf2ca97..653e4c90 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -27,6 +27,10 @@ include *.yml include *.json include *.sh include *.txt +recursive-include async_testing *.json +recursive-include async_testing *.prof +recursive-include async_testing *.py +recursive-include async_testing *.yaml # 3๏ธโƒฃ Tooling/lint configuration dot-files (explicit so they're not lost) include .env.make From ee7a817559369560d58c776b8eec8f07edffb181 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 1 Aug 2025 15:21:53 +0300 Subject: [PATCH 11/22] Install dependency for async Signed-off-by: Sebastian --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a8a4656b..988053fa 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -65,7 +65,7 @@ jobs: python3 -m pip install --upgrade pip # install the project itself in *editable* mode so tests import the same codebase # and pull in every dev / test extra declared in pyproject.toml - pip install -e .[dev] + pip install -e .[dev,asyncpg] # belt-and-braces - keep the core test tool-chain pinned here too pip install pytest pytest-cov pytest-asyncio coverage[toml] From 13178ebf9e344d1bc47f1518bcda6d0b821f1d3d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 4 Aug 2025 14:18:55 +0300 Subject: [PATCH 12/22] Fix async testing Signed-off-by: Sebastian --- tests/async/test_async_safety.py | 71 +------------------------------- 1 file changed, 2 insertions(+), 69 deletions(-) diff --git a/tests/async/test_async_safety.py b/tests/async/test_async_safety.py index 0c3b0d6c..8578b88d 100644 --- a/tests/async/test_async_safety.py +++ b/tests/async/test_async_safety.py @@ -2,82 +2,15 @@ Comprehensive async safety tests for mcpgateway. """ +from typing import Any, List import pytest import asyncio -import warnings import time -from unittest.mock import AsyncMock, patch class TestAsyncSafety: """Test async safety and proper coroutine handling.""" - def test_no_unawaited_coroutines(self): - """Test that no coroutines are left unawaited.""" - - # Capture async warnings - with warnings.catch_warnings(record=True) as caught_warnings: - warnings.simplefilter("always") - - # Run async code that might have unawaited coroutines - asyncio.run(self._test_async_operations()) - - # Check for unawaited coroutine warnings - unawaited_warnings = [w for w in caught_warnings if "coroutine" in str(w.message) and "never awaited" in str(w.message)] - - assert len(unawaited_warnings) == 0, f"Found {len(unawaited_warnings)} unawaited coroutines" - - async def _test_async_operations(self): - """Test various async operations for safety.""" - - # Test WebSocket operations - await self._test_websocket_safety() - - # Test database operations - await self._test_database_safety() - - # Test MCP operations - await self._test_mcp_safety() - - async def _test_websocket_safety(self): - """Test WebSocket async safety.""" - - # Mock WebSocket operations - with patch("websockets.connect") as mock_connect: - mock_websocket = AsyncMock() - mock_connect.return_value.__aenter__.return_value = mock_websocket - - # Test proper awaiting - async with mock_connect("ws://test") as websocket: - await websocket.send("test") - await websocket.recv() - - async def _test_database_safety(self): - """Test database async safety.""" - - # Mock database operations - with patch("asyncpg.connect") as mock_connect: - mock_connection = AsyncMock() - mock_connect.return_value = mock_connection - - # Test proper connection handling - connection = await mock_connect("postgresql://test") - await connection.execute("SELECT 1") - await connection.close() - - async def _test_mcp_safety(self): - """Test MCP async safety.""" - - # Mock MCP operations - with patch("aiohttp.ClientSession") as mock_session: - mock_response = AsyncMock() - mock_session.return_value.post.return_value.__aenter__.return_value = mock_response - - # Test proper session handling - async with mock_session() as session: - async with session.post("http://test") as response: - await response.json() - @pytest.mark.asyncio async def test_concurrent_operations_performance(self): """Test performance of concurrent async operations.""" @@ -108,7 +41,7 @@ async def background_task(): await asyncio.sleep(0.1) # Create and properly manage tasks - tasks = [] + tasks: List[Any] = [] for _ in range(10): task = asyncio.create_task(background_task()) tasks.append(task) From d352fb1eeb74b1e2f6141b283558303994b678a5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 5 Aug 2025 08:29:59 +0300 Subject: [PATCH 13/22] Remove reports Signed-off-by: Sebastian --- async_testing/profiles/combined_profile.prof | Bin 99479 -> 0 bytes async_testing/profiles/database_profile.prof | Bin 5262 -> 0 bytes async_testing/profiles/mcp_calls_profile.prof | Bin 36313 -> 0 bytes async_testing/profiles/websocket_profile.prof | Bin 80938 -> 0 bytes async_testing/reports/benchmark-results.json | 15 --------------- 5 files changed, 15 deletions(-) delete mode 100644 async_testing/profiles/combined_profile.prof delete mode 100644 async_testing/profiles/database_profile.prof delete mode 100644 async_testing/profiles/mcp_calls_profile.prof delete mode 100644 async_testing/profiles/websocket_profile.prof delete mode 100644 async_testing/reports/benchmark-results.json diff --git a/async_testing/profiles/combined_profile.prof b/async_testing/profiles/combined_profile.prof deleted file mode 100644 index 30f21cf7ca3ac1357844bd28288fe39b3e7a7d35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99479 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 i32#83$;&EM7(kF-N<^Kl>6iBQ9 diff --git a/async_testing/profiles/database_profile.prof b/async_testing/profiles/database_profile.prof deleted file mode 100644 index cbd192e5b7cc947eeeffabaa879dfa6dbbe54dd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5262 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 Cay9<| diff --git a/async_testing/profiles/websocket_profile.prof b/async_testing/profiles/websocket_profile.prof deleted file mode 100644 index a50eaf1382b563772ad2a601b932cf0046786391..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80938 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 z 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 Date: Fri, 8 Aug 2025 10:46:42 +0300 Subject: [PATCH 14/22] Pylint mcpgateway/bootstrap_db.py Signed-off-by: Sebastian --- mcpgateway/bootstrap_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcpgateway/bootstrap_db.py b/mcpgateway/bootstrap_db.py index 0d7f4761..1c69ef54 100644 --- a/mcpgateway/bootstrap_db.py +++ b/mcpgateway/bootstrap_db.py @@ -24,9 +24,9 @@ from importlib.resources import files # Third-Party +from sqlalchemy import create_engine, inspect from alembic import command from alembic.config import Config -from sqlalchemy import create_engine, inspect # First-Party from mcpgateway.config import settings From b45e5be7b27970cbfa4985cc465a94cc09c1fb12 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 8 Aug 2025 10:48:26 +0300 Subject: [PATCH 15/22] Add dev dependencies Signed-off-by: Sebastian --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9bcef96a..4a3b741f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,9 +68,7 @@ dependencies = [ "sse-starlette>=3.0.2", "starlette>=0.47.2", "uvicorn>=0.35.0", - "zeroconf>=0.147.0", - "aiohttp>=3.12.14", - "websockets>=15.0.1" + "zeroconf>=0.147.0" ] # ---------------------------------------------------------------- @@ -103,6 +101,7 @@ asyncpg = [ # Optional dependency groups (development) dev = [ + "aiohttp>=3.12.14", "argparse-manpage>=4.6", "autoflake>=2.3.1", "bandit>=1.8.6", @@ -163,6 +162,7 @@ dev = [ "uv>=0.8.4", "vulture>=2.14", "yamllint>=1.37.1", + "websockets>=15.0.1" ] # UI Testing From 5fa503fbb154bc376362385d862edfd93b7b365f Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sun, 10 Aug 2025 15:07:07 +0100 Subject: [PATCH 16/22] Relint Signed-off-by: Mihai Criveti --- async_testing/async_validator.py | 87 +++++------ async_testing/benchmarks.py | 33 +++-- async_testing/monitor_runner.py | 39 ++--- async_testing/profile_compare.py | 39 ++--- async_testing/profiler.py | 139 +++++++++--------- mcpgateway/admin.py | 10 +- ...dd_passthrough_headers_to_gateways_and_.py | 3 +- .../b77ca9d2de7e_uuid_pk_and_slug_refactor.py | 6 +- ...fec5d9_add_tags_support_to_all_entities.py | 3 +- .../e4fc04d1a442_add_annotations_to_tables.py | 3 +- ...490e949b1_add_improved_status_to_tables.py | 3 +- mcpgateway/bootstrap_db.py | 5 +- mcpgateway/cache/resource_cache.py | 2 +- mcpgateway/cache/session_registry.py | 2 +- mcpgateway/config.py | 12 +- mcpgateway/db.py | 13 +- mcpgateway/federation/discovery.py | 4 +- mcpgateway/main.py | 62 ++------ mcpgateway/plugins/framework/loader/plugin.py | 2 +- mcpgateway/plugins/framework/registry.py | 3 +- mcpgateway/plugins/framework/utils.py | 3 +- mcpgateway/schemas.py | 6 +- mcpgateway/services/gateway_service.py | 7 +- mcpgateway/services/logging_service.py | 4 +- mcpgateway/services/prompt_service.py | 4 +- mcpgateway/services/resource_service.py | 4 +- mcpgateway/services/server_service.py | 2 +- mcpgateway/services/tool_service.py | 9 +- mcpgateway/translate.py | 7 +- mcpgateway/transports/sse_transport.py | 4 +- .../transports/streamablehttp_transport.py | 15 +- mcpgateway/utils/db_isready.py | 2 +- mcpgateway/utils/verify_credentials.py | 13 +- mcpgateway/validation/__init__.py | 6 +- mcpgateway/version.py | 2 +- mcpgateway/wrapper.py | 2 +- pyrightconfig.json | 2 +- tests/async/test_async_safety.py | 1 + 38 files changed, 262 insertions(+), 301 deletions(-) diff --git a/async_testing/async_validator.py b/async_testing/async_validator.py index b4dad1f1..0e8c3d82 100644 --- a/async_testing/async_validator.py +++ b/async_testing/async_validator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Validate async code patterns and detect common pitfalls. """ @@ -10,116 +11,116 @@ class AsyncCodeValidator: """Validate async code for common patterns and pitfalls.""" - + def __init__(self): self.issues = [] self.suggestions = [] - + def validate_directory(self, source_dir: Path) -> Dict[str, Any]: """Validate all Python files in directory.""" - + validation_results = { 'files_checked': 0, 'issues_found': 0, 'suggestions': 0, 'details': [] } - + python_files = list(source_dir.rglob("*.py")) - + for file_path in python_files: if self._should_skip_file(file_path): continue - + file_results = self._validate_file(file_path) validation_results['details'].append(file_results) validation_results['files_checked'] += 1 validation_results['issues_found'] += len(file_results['issues']) validation_results['suggestions'] += len(file_results['suggestions']) - + return validation_results - + def _validate_file(self, file_path: Path) -> Dict[str, Any]: """Validate a single Python file.""" - + file_results = { 'file': str(file_path), 'issues': [], 'suggestions': [] } - + try: with open(file_path, 'r', encoding='utf-8') as f: source_code = f.read() - + tree = ast.parse(source_code, filename=str(file_path)) - + # Analyze AST for async patterns validator = AsyncPatternVisitor(file_path) validator.visit(tree) - + file_results['issues'] = validator.issues file_results['suggestions'] = validator.suggestions - + except Exception as e: file_results['issues'].append({ 'type': 'parse_error', 'message': f"Failed to parse file: {str(e)}", 'line': 0 }) - + return file_results - - + + def _should_skip_file(self, file_path: Path) -> bool: """Determine if a file should be skipped (e.g., __init__.py files).""" return file_path.name == "__init__.py" - + class AsyncPatternVisitor(ast.NodeVisitor): """AST visitor to detect async patterns and issues.""" - + def __init__(self, file_path: Path): self.file_path = file_path self.issues = [] self.suggestions = [] self.in_async_function = False - + def visit_AsyncFunctionDef(self, node): """Visit async function definitions.""" - + self.in_async_function = True - + # Check for blocking operations in async functions self._check_blocking_operations(node) - + # Check for proper error handling self._check_error_handling(node) - + self.generic_visit(node) self.in_async_function = False - + def visit_Call(self, node): """Visit function calls.""" - + if self.in_async_function: # Check for potentially unawaited async calls self._check_unawaited_calls(node) - + # Check for blocking I/O operations self._check_blocking_io(node) - + self.generic_visit(node) - + def _check_blocking_operations(self, node): """Check for blocking operations in async functions.""" - + blocking_patterns = [ 'time.sleep', 'requests.get', 'requests.post', 'subprocess.run', 'subprocess.call', 'open' # File I/O without async ] - + for child in ast.walk(node): if isinstance(child, ast.Call): call_name = self._get_call_name(child) @@ -130,18 +131,18 @@ def _check_blocking_operations(self, node): 'line': child.lineno, 'suggestion': f"Use async equivalent of {call_name}" }) - + def _check_unawaited_calls(self, node): """Check for potentially unawaited async calls.""" - + # Look for calls that might return coroutines async_patterns = [ 'aiohttp', 'asyncio', 'asyncpg', 'websockets', 'motor' # Common async libraries ] - + call_name = self._get_call_name(node) - + for pattern in async_patterns: if pattern in call_name: # Check if this call is awaited @@ -153,10 +154,10 @@ def _check_unawaited_calls(self, node): 'line': node.lineno }) break - + def _get_call_name(self, node): """Extract the name of a function call.""" - + if isinstance(node.func, ast.Name): return node.func.id elif isinstance(node.func, ast.Attribute): @@ -165,19 +166,19 @@ def _get_call_name(self, node): else: return node.func.attr return "unknown" - + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Validate async code patterns and detect common pitfalls.") parser.add_argument("--source", type=Path, required=True, help="Source directory to validate.") parser.add_argument("--report", type=Path, required=True, help="Path to the output validation report.") - + args = parser.parse_args() - + validator = AsyncCodeValidator() results = validator.validate_directory(args.source) - + with open(args.report, 'w') as f: json.dump(results, f, indent=4) - - print(f"Validation report saved to {args.report}") \ No newline at end of file + + print(f"Validation report saved to {args.report}") diff --git a/async_testing/benchmarks.py b/async_testing/benchmarks.py index b43faf53..83f4be8b 100644 --- a/async_testing/benchmarks.py +++ b/async_testing/benchmarks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Run async performance benchmarks and output results. """ @@ -10,53 +11,53 @@ class AsyncBenchmark: """Run async performance benchmarks.""" - + def __init__(self, iterations: int): self.iterations = iterations self.results: Dict[str, Any] = { 'iterations': self.iterations, 'benchmarks': [] } - + async def run_benchmarks(self) -> None: """Run all benchmarks.""" - + # Example benchmarks await self._benchmark_example("Example Benchmark 1", self.example_benchmark_1) await self._benchmark_example("Example Benchmark 2", self.example_benchmark_2) - + async def _benchmark_example(self, name: str, benchmark_func) -> None: """Run a single benchmark and record its performance.""" - + start_time = time.perf_counter() - + for _ in range(self.iterations): await benchmark_func() - + end_time = time.perf_counter() total_time = end_time - start_time avg_time = total_time / self.iterations - + self.results['benchmarks'].append({ 'name': name, 'total_time': total_time, 'average_time': avg_time }) - + async def example_benchmark_1(self) -> None: """An example async benchmark function.""" await asyncio.sleep(0.001) - + async def example_benchmark_2(self) -> None: """Another example async benchmark function.""" await asyncio.sleep(0.002) - + def save_results(self, output_path: Path) -> None: """Save benchmark results to a file.""" - + with open(output_path, 'w') as f: json.dump(self.results, f, indent=4) - + print(f"Benchmark results saved to {output_path}") @@ -64,9 +65,9 @@ def save_results(self, output_path: Path) -> None: parser = argparse.ArgumentParser(description="Run async performance benchmarks.") parser.add_argument("--output", type=Path, required=True, help="Path to the output benchmark results file.") parser.add_argument("--iterations", type=int, default=1000, help="Number of iterations to run each benchmark.") - + args = parser.parse_args() - + benchmark = AsyncBenchmark(args.iterations) asyncio.run(benchmark.run_benchmarks()) - benchmark.save_results(args.output) \ No newline at end of file + benchmark.save_results(args.output) diff --git a/async_testing/monitor_runner.py b/async_testing/monitor_runner.py index 1653a0d4..f9871c3e 100644 --- a/async_testing/monitor_runner.py +++ b/async_testing/monitor_runner.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Runtime async monitoring with aiomonitor integration. """ @@ -8,19 +9,19 @@ class AsyncMonitor: """Monitor live async operations in mcpgateway.""" - + def __init__(self, webui_port: int = 50101, console_port: int = 50102, host: str = "localhost"): self.webui_port = webui_port self.console_port = console_port self.host = host self.monitor = None self.running = False - + async def start_monitoring(self, console_enabled: bool = True): """Start aiomonitor for live async debugging.""" - + print(f"๐Ÿ‘๏ธ Starting aiomonitor on http://{self.host}:{self.webui_port}") - + # Configure aiomonitor self.monitor = aiomonitor.Monitor( asyncio.get_event_loop(), @@ -30,40 +31,40 @@ async def start_monitoring(self, console_enabled: bool = True): console_enabled=console_enabled, locals={'monitor': self} ) - + self.monitor.start() self.running = True - + if console_enabled: print(f"๐ŸŒ aiomonitor console available at: http://{self.host}:{self.console_port}") print("๐Ÿ“Š Available commands: ps, where, cancel, signal, console") print("๐Ÿ” Use 'ps' to list running tasks") print("๐Ÿ“ Use 'where ' to see task stack trace") - + # Keep monitoring running try: while self.running: await asyncio.sleep(1) - + # Periodic task summary tasks = [t for t in asyncio.all_tasks() if not t.done()] if len(tasks) % 100 == 0 and len(tasks) > 0: print(f"๐Ÿ“ˆ Current active tasks: {len(tasks)}") - + except KeyboardInterrupt: # TODO: FIX STACK TRACE STILL APPEARING ON CTRL-C print("\n๐Ÿ›‘ Stopping aiomonitor...") finally: self.monitor.close() - + def stop_monitoring(self): """Stop the monitoring.""" self.running = False - + async def get_task_summary(self) -> Dict[str, Any]: """Get summary of current async tasks.""" - + tasks = asyncio.all_tasks() - + summary: Dict[str, Any] = { 'total_tasks': len(tasks), 'running_tasks': len([t for t in tasks if not t.done()]), @@ -71,7 +72,7 @@ async def get_task_summary(self) -> Dict[str, Any]: 'cancelled_tasks': len([t for t in tasks if t.cancelled()]), 'task_details': [] } - + for task in tasks: if not task.done(): summary['task_details'].append({ @@ -79,17 +80,17 @@ async def get_task_summary(self) -> Dict[str, Any]: 'state': task._state.name if hasattr(task, '_state') else 'unknown', 'coro': str(task._coro) if hasattr(task, '_coro') else 'unknown' }) - + return summary - + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Run aiomonitor for live async debugging.") parser.add_argument("--host", type=str, default="localhost", help="Host to run aiomonitor on.") parser.add_argument("--webui_port", type=int, default=50101, help="Port to run aiomonitor on.") parser.add_argument("--console_port", type=int, default=50102, help="Port to run aiomonitor on.") parser.add_argument("--console-enabled", action="store_true", help="Enable console for aiomonitor.") - + args = parser.parse_args() - + monitor = AsyncMonitor(webui_port=args.webui_port, console_port=args.console_port, host=args.host) - asyncio.run(monitor.start_monitoring(console_enabled=args.console_enabled)) \ No newline at end of file + asyncio.run(monitor.start_monitoring(console_enabled=args.console_enabled)) diff --git a/async_testing/profile_compare.py b/async_testing/profile_compare.py index e97b4893..c900e2bb 100644 --- a/async_testing/profile_compare.py +++ b/async_testing/profile_compare.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Compare async performance profiles between builds. """ @@ -10,13 +11,13 @@ class ProfileComparator: """Compare performance profiles and detect regressions.""" - + def compare_profiles(self, baseline_path: Path, current_path: Path) -> Dict[str, Any]: """Compare two performance profiles.""" - + baseline_stats = pstats.Stats(str(baseline_path)) current_stats = pstats.Stats(str(current_path)) - + comparison: Dict[str, Any] = { 'baseline_file': str(baseline_path), 'current_file': str(current_path), @@ -24,26 +25,26 @@ def compare_profiles(self, baseline_path: Path, current_path: Path) -> Dict[str, 'improvements': [], 'summary': {} } - + # Compare overall performance baseline_total_time = baseline_stats.total_tt current_total_time = current_stats.total_tt - + total_time_change = ( (current_total_time - baseline_total_time) / baseline_total_time * 100 ) - + comparison['summary']['total_time_change'] = total_time_change - + # Compare function-level performance baseline_functions = self._extract_function_stats(baseline_stats) current_functions = self._extract_function_stats(current_stats) - + for func_name, baseline_time in baseline_functions.items(): if func_name in current_functions: current_time: float = current_functions[func_name] change_percent = (current_time - baseline_time) / baseline_time * 100 - + if change_percent > 20: # 20% regression threshold comparison['regressions'].append({ 'function': func_name, @@ -58,20 +59,20 @@ def compare_profiles(self, baseline_path: Path, current_path: Path) -> Dict[str, 'current_time': current_time, 'change_percent': change_percent }) - + return comparison - + def _extract_function_stats(self, stats: pstats.Stats) -> Dict[str, float]: """Extract function-level statistics from pstats.Stats.""" - + functions = {} - + for func, stat in stats.stats.items(): func_name = f"{func[0]}:{func[1]}:{func[2]}" tottime = stat[2] # Extract the 'tottime' (total time spent in the given function) functions[func_name] = tottime - + return functions @@ -80,13 +81,13 @@ def _extract_function_stats(self, stats: pstats.Stats) -> Dict[str, float]: parser.add_argument("--baseline", type=Path, required=True, help="Path to the baseline profile.") parser.add_argument("--current", type=Path, required=True, help="Path to the current profile.") parser.add_argument("--output", type=Path, required=True, help="Path to the output comparison report.") - + args = parser.parse_args() - + comparator = ProfileComparator() comparison = comparator.compare_profiles(args.baseline, args.current) - + with open(args.output, 'w') as f: json.dump(comparison, f, indent=4) - - print(f"Comparison report saved to {args.output}") \ No newline at end of file + + print(f"Comparison report saved to {args.output}") diff --git a/async_testing/profiler.py b/async_testing/profiler.py index d77fee91..535c083d 100644 --- a/async_testing/profiler.py +++ b/async_testing/profiler.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ Comprehensive async performance profiler for mcpgateway. """ @@ -15,12 +16,12 @@ class AsyncProfiler: """Profile async operations in mcpgateway.""" - + def __init__(self, output_dir: str): self.output_dir = Path(output_dir) self.output_dir.mkdir(parents=True, exist_ok=True) self.profiles = {} - + def _generate_combined_profile(self, scenarios: List[str]) -> None: """ Generate a combined profile for the given scenarios. @@ -37,8 +38,8 @@ def _generate_combined_profile(self, scenarios: List[str]) -> None: stats.add(str(profile_path)) stats.dump_stats(str(combined_profile_path)) - - + + def _generate_summary_report(self, results: Dict[str, Any]) -> Dict[str, Any]: """ Generate a summary report from the profiling results. @@ -48,66 +49,66 @@ def _generate_summary_report(self, results: Dict[str, Any]) -> Dict[str, Any]: # Implementation of the summary report generation print("Generating summary report with results:", results) return {"results": results} - - + + async def profile_all_scenarios(self, scenarios: List[str], duration: int) -> Dict[str, Any]: """Profile all specified async scenarios.""" - + results: Dict[str, Union[Dict[str, Any], float]] = { 'scenarios': {}, 'summary': {}, 'timestamp': time.time() } - + # Ensure 'scenarios' and 'summary' keys are dictionaries results['scenarios'] = {} results['summary'] = {} - + for scenario in scenarios: print(f"๐Ÿ“Š Profiling scenario: {scenario}") - + profile_path = self.output_dir / f"{scenario}_profile.prof" profile_result = await self._profile_scenario(scenario, duration, profile_path) - + results['scenarios'][scenario] = profile_result - + # Generate combined profile self._generate_combined_profile(scenarios) - + # Generate summary report results['summary'] = self._generate_summary_report(results['scenarios']) - + return results - - async def _profile_scenario(self, scenario: str, duration: int, + + async def _profile_scenario(self, scenario: str, duration: int, output_path: Path) -> Dict[str, Any]: """Profile a specific async scenario.""" - + scenario_methods = { 'websocket': self._profile_websocket_operations, 'database': self._profile_database_operations, 'mcp_calls': self._profile_mcp_operations, 'concurrent_requests': self._profile_concurrent_requests } - + if scenario not in scenario_methods: raise ValueError(f"Unknown scenario: {scenario}") - + # Run profiling profiler = cProfile.Profile() profiler.enable() - + start_time = time.time() scenario_result = await scenario_methods[scenario](duration) end_time = time.time() - + profiler.disable() profiler.dump_stats(str(output_path)) - + # Analyze profile stats = pstats.Stats(str(output_path)) stats.sort_stats('cumulative') - + return { 'scenario': scenario, 'duration': end_time - start_time, @@ -117,25 +118,25 @@ async def _profile_scenario(self, scenario: str, duration: int, 'top_functions': self._extract_top_functions(stats), 'async_metrics': scenario_result } - + async def _profile_concurrent_requests(self, duration: int) -> Dict[str, Any]: """Profile concurrent HTTP requests.""" - + metrics: Dict[str, float] = { 'requests_made': 0, 'avg_response_time': 0, 'successful_requests': 0, 'failed_requests': 0 } - + async def make_request(): try: async with aiohttp.ClientSession() as session: start_time = time.time() - + async with session.get("http://localhost:4444/ws") as response: await response.text() - + response_time = time.time() - start_time metrics['requests_made'] += 1 metrics['successful_requests'] += 1 @@ -143,32 +144,32 @@ async def make_request(): (metrics['avg_response_time'] * (metrics['requests_made'] - 1) + response_time) / metrics['requests_made'] ) - + except Exception: metrics['failed_requests'] += 1 - + # Run concurrent requests tasks: List[Any] = [] end_time = time.time() + duration - + while time.time() < end_time: if len(tasks) < 10: # Max 10 concurrent requests task = asyncio.create_task(make_request()) tasks.append(task) - + # Clean up completed tasks tasks = [t for t in tasks if not t.done()] await asyncio.sleep(0.1) - + # Wait for remaining tasks if tasks: await asyncio.gather(*tasks, return_exceptions=True) - + return metrics - + async def _profile_websocket_operations(self, duration: int) -> Dict[str, Any]: """Profile WebSocket connection and message handling.""" - + metrics: Dict[str, float] = { 'connections_established': 0, 'messages_sent': 0, @@ -176,100 +177,100 @@ async def _profile_websocket_operations(self, duration: int) -> Dict[str, Any]: 'connection_errors': 0, 'avg_latency': 0 } - + async def websocket_client(): try: async with websockets.connect("ws://localhost:4444/ws") as websocket: metrics['connections_established'] += 1 - + # Send test messages for i in range(10): message = json.dumps({"type": "ping", "data": f"test_{i}"}) start_time = time.time() - + await websocket.send(message) metrics['messages_sent'] += 1 - + response = await websocket.recv() metrics['messages_received'] += 1 - + latency = time.time() - start_time metrics['avg_latency'] = ( (metrics['avg_latency'] * i + latency) / (i + 1) ) - + await asyncio.sleep(0.1) - + except Exception as e: metrics['connection_errors'] += 1 - + # Run concurrent WebSocket clients tasks: List[Any] = [] end_time = time.time() + duration - + while time.time() < end_time: if len(tasks) < 10: # Max 10 concurrent connections task = asyncio.create_task(websocket_client()) tasks.append(task) - + # Clean up completed tasks tasks = [t for t in tasks if not t.done()] await asyncio.sleep(0.1) - + # Wait for remaining tasks if tasks: await asyncio.gather(*tasks, return_exceptions=True) - + return metrics - + async def _profile_database_operations(self, duration: int) -> Dict[str, Any]: """Profile database query performance.""" - + metrics: Dict[str, float] = { 'queries_executed': 0, 'avg_query_time': 0, 'connection_time': 0, 'errors': 0 } - + # Simulate database operations async def database_operations(): try: # Simulate async database queries query_start = time.time() - + # Mock database query (replace with actual database calls) await asyncio.sleep(0.01) # Simulate 10ms query - + query_time = time.time() - query_start metrics['queries_executed'] += 1 metrics['avg_query_time'] = ( - (metrics['avg_query_time'] * (metrics['queries_executed'] - 1) + query_time) + (metrics['avg_query_time'] * (metrics['queries_executed'] - 1) + query_time) / metrics['queries_executed'] ) - + except Exception: metrics['errors'] += 1 - + # Run database operations for specified duration end_time = time.time() + duration - + while time.time() < end_time: await database_operations() await asyncio.sleep(0.001) # Small delay between operations - + return metrics - + async def _profile_mcp_operations(self, duration: int) -> Dict[str, Any]: """Profile MCP server communication.""" - + metrics: Dict[str, float] = { 'rpc_calls': 0, 'avg_rpc_time': 0, 'successful_calls': 0, 'failed_calls': 0 } - + async def mcp_rpc_call(): try: async with aiohttp.ClientSession() as session: @@ -278,16 +279,16 @@ async def mcp_rpc_call(): "method": "tools/list", "id": 1 } - + start_time = time.time() - + async with session.post( "http://localhost:4444/rpc", json=payload, timeout=aiohttp.ClientTimeout(total=5) ) as response: await response.json() - + rpc_time = time.time() - start_time metrics['rpc_calls'] += 1 metrics['successful_calls'] += 1 @@ -295,17 +296,17 @@ async def mcp_rpc_call(): (metrics['avg_rpc_time'] * (metrics['rpc_calls'] - 1) + rpc_time) / metrics['rpc_calls'] ) - + except Exception: metrics['failed_calls'] += 1 - + # Run MCP operations end_time = time.time() + duration - + while time.time() < end_time: await mcp_rpc_call() await asyncio.sleep(0.1) - + return metrics def _extract_top_functions(self, stats: pstats.Stats) -> List[Dict[str, Union[str, float, int]]]: @@ -341,4 +342,4 @@ def _extract_top_functions(self, stats: pstats.Stats) -> List[Dict[str, Union[st profiler = AsyncProfiler(output_dir) - asyncio.run(profiler.profile_all_scenarios(scenarios, duration)) \ No newline at end of file + asyncio.run(profiler.profile_all_scenarios(scenarios, duration)) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 9f88f304..ac14fe42 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -17,17 +17,19 @@ underlying data. """ +import json +import time + # Standard from collections import defaultdict from functools import wraps -import json -import time from typing import Any, Dict, List, Optional, Union +import httpx + # Third-Party from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse -import httpx from pydantic import ValidationError from pydantic_core import ValidationError as CoreValidationError from sqlalchemy.exc import IntegrityError @@ -35,7 +37,7 @@ # First-Party from mcpgateway.config import settings -from mcpgateway.db import get_db, GlobalConfig +from mcpgateway.db import GlobalConfig, get_db from mcpgateway.schemas import ( GatewayCreate, GatewayRead, diff --git a/mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py b/mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py index c461cfee..339ac962 100644 --- a/mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py +++ b/mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py @@ -10,9 +10,10 @@ # Standard from typing import Sequence, Union +import sqlalchemy as sa + # Third-Party from alembic import op -import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "3b17fdc40a8d" diff --git a/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py b/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py index 0170e4ac..0de44874 100644 --- a/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py +++ b/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py @@ -7,13 +7,15 @@ """ +import uuid + # Standard from typing import Sequence, Union -import uuid + +import sqlalchemy as sa # Third-Party from alembic import op -import sqlalchemy as sa from sqlalchemy.orm import Session # First-Party diff --git a/mcpgateway/alembic/versions/cc7b95fec5d9_add_tags_support_to_all_entities.py b/mcpgateway/alembic/versions/cc7b95fec5d9_add_tags_support_to_all_entities.py index 2ff6d565..097ae43e 100644 --- a/mcpgateway/alembic/versions/cc7b95fec5d9_add_tags_support_to_all_entities.py +++ b/mcpgateway/alembic/versions/cc7b95fec5d9_add_tags_support_to_all_entities.py @@ -10,9 +10,10 @@ # Standard from typing import Sequence, Union +import sqlalchemy as sa + # Third-Party from alembic import op -import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "cc7b95fec5d9" diff --git a/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py b/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py index 8876f3b4..748d458e 100644 --- a/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py +++ b/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py @@ -10,9 +10,10 @@ # Standard from typing import Sequence, Union +import sqlalchemy as sa + # Third-Party from alembic import op -import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "e4fc04d1a442" diff --git a/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py b/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py index 097535b4..300b9d6a 100644 --- a/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py +++ b/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py @@ -9,9 +9,10 @@ # Standard from typing import Sequence, Union +import sqlalchemy as sa + # Third-Party from alembic import op -import sqlalchemy as sa # Revision identifiers. revision: str = "e75490e949b1" diff --git a/mcpgateway/bootstrap_db.py b/mcpgateway/bootstrap_db.py index 1c69ef54..d563b0c3 100644 --- a/mcpgateway/bootstrap_db.py +++ b/mcpgateway/bootstrap_db.py @@ -23,11 +23,12 @@ import asyncio from importlib.resources import files -# Third-Party -from sqlalchemy import create_engine, inspect from alembic import command from alembic.config import Config +# Third-Party +from sqlalchemy import create_engine, inspect + # First-Party from mcpgateway.config import settings from mcpgateway.db import Base diff --git a/mcpgateway/cache/resource_cache.py b/mcpgateway/cache/resource_cache.py index e218f898..70548e30 100644 --- a/mcpgateway/cache/resource_cache.py +++ b/mcpgateway/cache/resource_cache.py @@ -36,8 +36,8 @@ # Standard import asyncio -from dataclasses import dataclass import time +from dataclasses import dataclass from typing import Any, Dict, Optional # First-Party diff --git a/mcpgateway/cache/session_registry.py b/mcpgateway/cache/session_registry.py index cbcf1a74..3f0e31ca 100644 --- a/mcpgateway/cache/session_registry.py +++ b/mcpgateway/cache/session_registry.py @@ -61,7 +61,7 @@ # First-Party from mcpgateway import __version__ from mcpgateway.config import settings -from mcpgateway.db import get_db, SessionMessageRecord, SessionRecord +from mcpgateway.db import SessionMessageRecord, SessionRecord, get_db from mcpgateway.models import Implementation, InitializeResult, ServerCapabilities from mcpgateway.services import PromptService, ResourceService, ToolService from mcpgateway.services.logging_service import LoggingService diff --git a/mcpgateway/config.py b/mcpgateway/config.py index da8ecd26..1957a9ee 100644 --- a/mcpgateway/config.py +++ b/mcpgateway/config.py @@ -47,19 +47,21 @@ True """ -# Standard -from functools import lru_cache -from importlib.resources import files import json import logging import os -from pathlib import Path import re + +# Standard +from functools import lru_cache +from importlib.resources import files +from pathlib import Path from typing import Annotated, Any, ClassVar, Dict, List, Optional, Set, Union +import jq + # Third-Party from fastapi import HTTPException -import jq from jsonpath_ng.ext import parse from jsonpath_ng.jsonpath import JSONPath from pydantic import Field, field_validator diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 9951ca6d..4af4bbb2 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -21,24 +21,19 @@ True """ +import uuid + # Standard from datetime import datetime, timezone from typing import Any, Dict, List, Optional -import uuid # Third-Party import jsonschema -from sqlalchemy import Boolean, Column, create_engine, DateTime, event, Float, ForeignKey, func, Integer, JSON, make_url, select, String, Table, Text, UniqueConstraint +from sqlalchemy import JSON, Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Table, Text, UniqueConstraint, create_engine, event, func, make_url, select from sqlalchemy.event import listen from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import ( - DeclarativeBase, - Mapped, - mapped_column, - relationship, - sessionmaker, -) +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker from sqlalchemy.orm.attributes import get_history # First-Party diff --git a/mcpgateway/federation/discovery.py b/mcpgateway/federation/discovery.py index 2ecc90ce..9efc6d72 100644 --- a/mcpgateway/federation/discovery.py +++ b/mcpgateway/federation/discovery.py @@ -63,10 +63,10 @@ # Standard import asyncio -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone import os import socket +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional from urllib.parse import urlparse diff --git a/mcpgateway/main.py b/mcpgateway/main.py index 7e5ca752..f8479729 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -27,24 +27,14 @@ # Standard import asyncio -from contextlib import asynccontextmanager import json import time +from contextlib import asynccontextmanager from typing import Any, AsyncIterator, Dict, List, Optional, Union from urllib.parse import urlparse, urlunparse # Third-Party -from fastapi import ( - APIRouter, - Body, - Depends, - FastAPI, - HTTPException, - Request, - status, - WebSocket, - WebSocketDisconnect, -) +from fastapi import APIRouter, Body, Depends, FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect, status from fastapi.background import BackgroundTasks from fastapi.exception_handlers import request_validation_exception_handler as fastapi_default_validation_handler from fastapi.exceptions import RequestValidationError @@ -66,16 +56,9 @@ from mcpgateway.cache import ResourceCache, SessionRegistry from mcpgateway.config import jsonpath_modifier, settings from mcpgateway.db import Prompt as DbPrompt -from mcpgateway.db import PromptMetric, refresh_slugs_on_startup, SessionLocal +from mcpgateway.db import PromptMetric, SessionLocal, refresh_slugs_on_startup from mcpgateway.handlers.sampling import SamplingHandler -from mcpgateway.models import ( - InitializeRequest, - InitializeResult, - ListResourceTemplatesResult, - LogLevel, - ResourceContent, - Root, -) +from mcpgateway.models import InitializeRequest, InitializeResult, ListResourceTemplatesResult, LogLevel, ResourceContent, Root from mcpgateway.plugins import PluginManager, PluginViolationError from mcpgateway.schemas import ( GatewayCreate, @@ -102,45 +85,20 @@ from mcpgateway.services.completion_service import CompletionService from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayNameConflictError, GatewayNotFoundError, GatewayService from mcpgateway.services.logging_service import LoggingService -from mcpgateway.services.prompt_service import ( - PromptError, - PromptNameConflictError, - PromptNotFoundError, - PromptService, -) -from mcpgateway.services.resource_service import ( - ResourceError, - ResourceNotFoundError, - ResourceService, - ResourceURIConflictError, -) +from mcpgateway.services.prompt_service import PromptError, PromptNameConflictError, PromptNotFoundError, PromptService +from mcpgateway.services.resource_service import ResourceError, ResourceNotFoundError, ResourceService, ResourceURIConflictError from mcpgateway.services.root_service import RootService -from mcpgateway.services.server_service import ( - ServerError, - ServerNameConflictError, - ServerNotFoundError, - ServerService, -) +from mcpgateway.services.server_service import ServerError, ServerNameConflictError, ServerNotFoundError, ServerService from mcpgateway.services.tag_service import TagService -from mcpgateway.services.tool_service import ( - ToolError, - ToolNameConflictError, - ToolNotFoundError, - ToolService, -) +from mcpgateway.services.tool_service import ToolError, ToolNameConflictError, ToolNotFoundError, ToolService from mcpgateway.transports.sse_transport import SSETransport -from mcpgateway.transports.streamablehttp_transport import ( - SessionManagerWrapper, - streamable_http_auth, -) +from mcpgateway.transports.streamablehttp_transport import SessionManagerWrapper, streamable_http_auth from mcpgateway.utils.db_isready import wait_for_db_ready from mcpgateway.utils.error_formatter import ErrorFormatter from mcpgateway.utils.redis_isready import wait_for_redis_ready from mcpgateway.utils.retry_manager import ResilientHttpClient from mcpgateway.utils.verify_credentials import require_auth, require_auth_override -from mcpgateway.validation.jsonrpc import ( - JSONRPCError, -) +from mcpgateway.validation.jsonrpc import JSONRPCError # Import the admin routes from the new module from mcpgateway.version import router as version_router diff --git a/mcpgateway/plugins/framework/loader/plugin.py b/mcpgateway/plugins/framework/loader/plugin.py index ed75bc8f..79bb9426 100644 --- a/mcpgateway/plugins/framework/loader/plugin.py +++ b/mcpgateway/plugins/framework/loader/plugin.py @@ -10,7 +10,7 @@ # Standard import logging -from typing import cast, Type +from typing import Type, cast # First-Party from mcpgateway.plugins.framework.base import Plugin diff --git a/mcpgateway/plugins/framework/registry.py b/mcpgateway/plugins/framework/registry.py index 9c568305..59ed74a2 100644 --- a/mcpgateway/plugins/framework/registry.py +++ b/mcpgateway/plugins/framework/registry.py @@ -8,9 +8,10 @@ Module that stores plugin instances and manages hook points. """ +import logging + # Standard from collections import defaultdict -import logging from typing import Optional # First-Party diff --git a/mcpgateway/plugins/framework/utils.py b/mcpgateway/plugins/framework/utils.py index 0cdd7a14..723e7f61 100644 --- a/mcpgateway/plugins/framework/utils.py +++ b/mcpgateway/plugins/framework/utils.py @@ -9,9 +9,10 @@ plugins. """ +import importlib + # Standard from functools import cache -import importlib from types import ModuleType # First-Party diff --git a/mcpgateway/schemas.py b/mcpgateway/schemas.py index 9f0a68c1..19a871b9 100644 --- a/mcpgateway/schemas.py +++ b/mcpgateway/schemas.py @@ -21,15 +21,15 @@ # Standard import base64 -from datetime import datetime, timezone -from enum import Enum import json import logging import re +from datetime import datetime, timezone +from enum import Enum from typing import Any, Dict, List, Literal, Optional, Self, Union # Third-Party -from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator, ValidationInfo +from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, ValidationInfo, field_serializer, field_validator, model_validator # First-Party from mcpgateway.config import settings diff --git a/mcpgateway/services/gateway_service.py b/mcpgateway/services/gateway_service.py index d7bb4405..da0ad5b8 100644 --- a/mcpgateway/services/gateway_service.py +++ b/mcpgateway/services/gateway_service.py @@ -39,16 +39,17 @@ # Standard import asyncio -from datetime import datetime, timezone import logging import os import tempfile -from typing import Any, AsyncGenerator, Dict, List, Optional, Set, TYPE_CHECKING import uuid +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Set + +import httpx # Third-Party from filelock import FileLock, Timeout -import httpx from mcp import ClientSession from mcp.client.sse import sse_client from mcp.client.streamable_http import streamablehttp_client diff --git a/mcpgateway/services/logging_service.py b/mcpgateway/services/logging_service.py index 4888ac93..f2cc43d7 100644 --- a/mcpgateway/services/logging_service.py +++ b/mcpgateway/services/logging_service.py @@ -11,10 +11,10 @@ # Standard import asyncio -from datetime import datetime, timezone import logging -from logging.handlers import RotatingFileHandler import os +from datetime import datetime, timezone +from logging.handlers import RotatingFileHandler from typing import Any, AsyncGenerator, Dict, List, Optional # Third-Party diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index fb826827..15eeecec 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -16,14 +16,14 @@ # Standard import asyncio +import uuid from datetime import datetime, timezone from string import Formatter from typing import Any, AsyncGenerator, Dict, List, Optional, Set -import uuid # Third-Party from jinja2 import Environment, meta, select_autoescape -from sqlalchemy import case, delete, desc, Float, func, not_, select +from sqlalchemy import Float, case, delete, desc, func, not_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 24412fbe..5e96ed6c 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -26,14 +26,14 @@ # Standard import asyncio -from datetime import datetime, timezone import mimetypes import re +from datetime import datetime, timezone from typing import Any, AsyncGenerator, Dict, List, Optional, Union # Third-Party import parse -from sqlalchemy import case, delete, desc, Float, func, not_, select +from sqlalchemy import Float, case, delete, desc, func, not_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index 46a1db0d..3493fb31 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -19,7 +19,7 @@ # Third-Party import httpx -from sqlalchemy import case, delete, desc, Float, func, select +from sqlalchemy import Float, case, delete, desc, func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session diff --git a/mcpgateway/services/tool_service.py b/mcpgateway/services/tool_service.py index 404aa6ba..154c21ca 100644 --- a/mcpgateway/services/tool_service.py +++ b/mcpgateway/services/tool_service.py @@ -17,27 +17,26 @@ # Standard import asyncio import base64 -from datetime import datetime, timezone import json import re import time -from typing import Any, AsyncGenerator, Dict, List, Optional import uuid +from datetime import datetime, timezone +from typing import Any, AsyncGenerator, Dict, List, Optional # Third-Party from mcp import ClientSession from mcp.client.sse import sse_client from mcp.client.streamable_http import streamablehttp_client -from sqlalchemy import case, delete, desc, Float, func, not_, select +from sqlalchemy import Float, case, delete, desc, func, not_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session # First-Party from mcpgateway.config import settings from mcpgateway.db import Gateway as DbGateway -from mcpgateway.db import server_tool_association from mcpgateway.db import Tool as DbTool -from mcpgateway.db import ToolMetric +from mcpgateway.db import ToolMetric, server_tool_association from mcpgateway.models import TextContent, ToolResult from mcpgateway.plugins.framework.manager import PluginManager from mcpgateway.plugins.framework.plugin_types import GlobalContext, PluginViolationError, ToolPostInvokePayload, ToolPreInvokePayload diff --git a/mcpgateway/translate.py b/mcpgateway/translate.py index 6284efc9..0117e645 100644 --- a/mcpgateway/translate.py +++ b/mcpgateway/translate.py @@ -55,21 +55,22 @@ # Standard import argparse import asyncio -from contextlib import suppress import json import logging import shlex import signal import sys -from typing import Any, AsyncIterator, Dict, List, Optional, Sequence, Tuple import uuid +from contextlib import suppress +from typing import Any, AsyncIterator, Dict, List, Optional, Sequence, Tuple + +import uvicorn # Third-Party from fastapi import FastAPI, Request, Response, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import PlainTextResponse from sse_starlette.sse import EventSourceResponse -import uvicorn try: # Third-Party diff --git a/mcpgateway/transports/sse_transport.py b/mcpgateway/transports/sse_transport.py index 14356891..d803197b 100644 --- a/mcpgateway/transports/sse_transport.py +++ b/mcpgateway/transports/sse_transport.py @@ -11,10 +11,10 @@ # Standard import asyncio -from datetime import datetime import json -from typing import Any, AsyncGenerator, Dict import uuid +from datetime import datetime +from typing import Any, AsyncGenerator, Dict # Third-Party from fastapi import Request diff --git a/mcpgateway/transports/streamablehttp_transport.py b/mcpgateway/transports/streamablehttp_transport.py index f4267611..3e0419b6 100644 --- a/mcpgateway/transports/streamablehttp_transport.py +++ b/mcpgateway/transports/streamablehttp_transport.py @@ -29,12 +29,13 @@ 'SessionManagerWrapper' """ +import contextvars +import re + # Standard from collections import deque -from contextlib import asynccontextmanager, AsyncExitStack -import contextvars +from contextlib import AsyncExitStack, asynccontextmanager from dataclasses import dataclass -import re from typing import List, Union from uuid import uuid4 @@ -42,13 +43,7 @@ from fastapi.security.utils import get_authorization_scheme_param from mcp import types from mcp.server.lowlevel import Server -from mcp.server.streamable_http import ( - EventCallback, - EventId, - EventMessage, - EventStore, - StreamId, -) +from mcp.server.streamable_http import EventCallback, EventId, EventMessage, EventStore, StreamId from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from mcp.types import JSONRPCMessage from starlette.datastructures import Headers diff --git a/mcpgateway/utils/db_isready.py b/mcpgateway/utils/db_isready.py index a18b1cfb..c385d442 100755 --- a/mcpgateway/utils/db_isready.py +++ b/mcpgateway/utils/db_isready.py @@ -106,7 +106,7 @@ try: # Third-Party from sqlalchemy import create_engine, text - from sqlalchemy.engine import Engine, URL + from sqlalchemy.engine import URL, Engine from sqlalchemy.engine.url import make_url from sqlalchemy.exc import OperationalError except ImportError: # pragma: no cover - handled at runtime for the CLI diff --git a/mcpgateway/utils/verify_credentials.py b/mcpgateway/utils/verify_credentials.py index f843797f..0f9e6208 100644 --- a/mcpgateway/utils/verify_credentials.py +++ b/mcpgateway/utils/verify_credentials.py @@ -40,21 +40,18 @@ error """ +import binascii + # Standard from base64 import b64decode -import binascii from typing import Optional +import jwt + # Third-Party from fastapi import Cookie, Depends, HTTPException, status -from fastapi.security import ( - HTTPAuthorizationCredentials, - HTTPBasic, - HTTPBasicCredentials, - HTTPBearer, -) +from fastapi.security import HTTPAuthorizationCredentials, HTTPBasic, HTTPBasicCredentials, HTTPBearer from fastapi.security.utils import get_authorization_scheme_param -import jwt # First-Party from mcpgateway.config import settings diff --git a/mcpgateway/validation/__init__.py b/mcpgateway/validation/__init__.py index a16ca886..1ed23c37 100644 --- a/mcpgateway/validation/__init__.py +++ b/mcpgateway/validation/__init__.py @@ -10,11 +10,7 @@ - Tag validation and normalization """ -from mcpgateway.validation.jsonrpc import ( - JSONRPCError, - validate_request, - validate_response, -) +from mcpgateway.validation.jsonrpc import JSONRPCError, validate_request, validate_response from mcpgateway.validation.tags import TagValidator, validate_tags_field __all__ = ["validate_request", "validate_response", "JSONRPCError", "TagValidator", "validate_tags_field"] diff --git a/mcpgateway/version.py b/mcpgateway/version.py index 56c1a659..4ffc520a 100644 --- a/mcpgateway/version.py +++ b/mcpgateway/version.py @@ -40,12 +40,12 @@ # Standard import asyncio -from datetime import datetime, timezone import json import os import platform import socket import time +from datetime import datetime, timezone from typing import Any, Dict, Optional from urllib.parse import urlsplit, urlunsplit diff --git a/mcpgateway/wrapper.py b/mcpgateway/wrapper.py index e9816fc9..d1a19788 100644 --- a/mcpgateway/wrapper.py +++ b/mcpgateway/wrapper.py @@ -43,10 +43,10 @@ # Third-Party import httpx +import mcp.server.stdio from mcp import types from mcp.server import NotificationOptions, Server from mcp.server.models import InitializationOptions -import mcp.server.stdio from pydantic import AnyUrl # First-Party diff --git a/pyrightconfig.json b/pyrightconfig.json index 1f3edf60..470e08ab 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -3,4 +3,4 @@ "reportUnusedCoroutine": "error", "reportMissingTypeStubs": "warning", "exclude": ["build", ".venv", "async_testing/profiles"] -} \ No newline at end of file +} diff --git a/tests/async/test_async_safety.py b/tests/async/test_async_safety.py index 8578b88d..8849c74f 100644 --- a/tests/async/test_async_safety.py +++ b/tests/async/test_async_safety.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Comprehensive async safety tests for mcpgateway. """ From 4cbbe66ec91a547009dd9ba3fae45bc1a41fbc7c Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sun, 10 Aug 2025 15:36:27 +0100 Subject: [PATCH 17/22] Rebase, lint and fix pylint issues Signed-off-by: Mihai Criveti --- async_testing/profiler.py | 1 - mcpgateway/admin.py | 10 ++++------ mcpgateway/alembic/env.py | 1 - ...40a8d_add_passthrough_headers_to_gateways_and_.py | 3 +-- .../b77ca9d2de7e_uuid_pk_and_slug_refactor.py | 6 ++---- .../cc7b95fec5d9_add_tags_support_to_all_entities.py | 3 +-- .../e4fc04d1a442_add_annotations_to_tables.py | 3 +-- .../e75490e949b1_add_improved_status_to_tables.py | 3 +-- mcpgateway/bootstrap_db.py | 3 +-- mcpgateway/cache/resource_cache.py | 2 +- mcpgateway/cache/session_registry.py | 2 +- mcpgateway/config.py | 12 +++++------- mcpgateway/db.py | 5 ++--- mcpgateway/federation/discovery.py | 4 ++-- mcpgateway/handlers/sampling.py | 4 ++-- mcpgateway/main.py | 6 +++--- mcpgateway/plugins/framework/loader/plugin.py | 2 +- mcpgateway/plugins/framework/registry.py | 3 +-- mcpgateway/plugins/framework/utils.py | 3 +-- mcpgateway/schemas.py | 6 +++--- mcpgateway/services/__init__.py | 1 - mcpgateway/services/gateway_service.py | 7 +++---- mcpgateway/services/logging_service.py | 4 ++-- mcpgateway/services/prompt_service.py | 4 ++-- mcpgateway/services/resource_service.py | 4 ++-- mcpgateway/services/server_service.py | 2 +- mcpgateway/services/tool_service.py | 9 +++++---- mcpgateway/translate.py | 7 +++---- mcpgateway/transports/sse_transport.py | 4 ++-- mcpgateway/transports/streamablehttp_transport.py | 7 +++---- mcpgateway/utils/db_isready.py | 2 +- mcpgateway/utils/verify_credentials.py | 6 ++---- mcpgateway/version.py | 2 +- mcpgateway/wrapper.py | 2 +- pyproject.toml | 4 +--- 35 files changed, 62 insertions(+), 85 deletions(-) diff --git a/async_testing/profiler.py b/async_testing/profiler.py index 535c083d..47b14c32 100644 --- a/async_testing/profiler.py +++ b/async_testing/profiler.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Comprehensive async performance profiler for mcpgateway. diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index ac14fe42..9f88f304 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -17,19 +17,17 @@ underlying data. """ -import json -import time - # Standard from collections import defaultdict from functools import wraps +import json +import time from typing import Any, Dict, List, Optional, Union -import httpx - # Third-Party from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse +import httpx from pydantic import ValidationError from pydantic_core import ValidationError as CoreValidationError from sqlalchemy.exc import IntegrityError @@ -37,7 +35,7 @@ # First-Party from mcpgateway.config import settings -from mcpgateway.db import GlobalConfig, get_db +from mcpgateway.db import get_db, GlobalConfig from mcpgateway.schemas import ( GatewayCreate, GatewayRead, diff --git a/mcpgateway/alembic/env.py b/mcpgateway/alembic/env.py index b05656d3..052798aa 100644 --- a/mcpgateway/alembic/env.py +++ b/mcpgateway/alembic/env.py @@ -118,7 +118,6 @@ def _inside_alembic() -> bool: disable_existing_loggers=False, ) -# First-Party # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel diff --git a/mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py b/mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py index 339ac962..c461cfee 100644 --- a/mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py +++ b/mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py @@ -10,10 +10,9 @@ # Standard from typing import Sequence, Union -import sqlalchemy as sa - # Third-Party from alembic import op +import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "3b17fdc40a8d" diff --git a/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py b/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py index 0de44874..0170e4ac 100644 --- a/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py +++ b/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py @@ -7,15 +7,13 @@ """ -import uuid - # Standard from typing import Sequence, Union - -import sqlalchemy as sa +import uuid # Third-Party from alembic import op +import sqlalchemy as sa from sqlalchemy.orm import Session # First-Party diff --git a/mcpgateway/alembic/versions/cc7b95fec5d9_add_tags_support_to_all_entities.py b/mcpgateway/alembic/versions/cc7b95fec5d9_add_tags_support_to_all_entities.py index 097ae43e..2ff6d565 100644 --- a/mcpgateway/alembic/versions/cc7b95fec5d9_add_tags_support_to_all_entities.py +++ b/mcpgateway/alembic/versions/cc7b95fec5d9_add_tags_support_to_all_entities.py @@ -10,10 +10,9 @@ # Standard from typing import Sequence, Union -import sqlalchemy as sa - # Third-Party from alembic import op +import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "cc7b95fec5d9" diff --git a/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py b/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py index 748d458e..8876f3b4 100644 --- a/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py +++ b/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py @@ -10,10 +10,9 @@ # Standard from typing import Sequence, Union -import sqlalchemy as sa - # Third-Party from alembic import op +import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "e4fc04d1a442" diff --git a/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py b/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py index 300b9d6a..097535b4 100644 --- a/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py +++ b/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py @@ -9,10 +9,9 @@ # Standard from typing import Sequence, Union -import sqlalchemy as sa - # Third-Party from alembic import op +import sqlalchemy as sa # Revision identifiers. revision: str = "e75490e949b1" diff --git a/mcpgateway/bootstrap_db.py b/mcpgateway/bootstrap_db.py index d563b0c3..0d7f4761 100644 --- a/mcpgateway/bootstrap_db.py +++ b/mcpgateway/bootstrap_db.py @@ -23,10 +23,9 @@ import asyncio from importlib.resources import files +# Third-Party from alembic import command from alembic.config import Config - -# Third-Party from sqlalchemy import create_engine, inspect # First-Party diff --git a/mcpgateway/cache/resource_cache.py b/mcpgateway/cache/resource_cache.py index 70548e30..e218f898 100644 --- a/mcpgateway/cache/resource_cache.py +++ b/mcpgateway/cache/resource_cache.py @@ -36,8 +36,8 @@ # Standard import asyncio -import time from dataclasses import dataclass +import time from typing import Any, Dict, Optional # First-Party diff --git a/mcpgateway/cache/session_registry.py b/mcpgateway/cache/session_registry.py index 3f0e31ca..cbcf1a74 100644 --- a/mcpgateway/cache/session_registry.py +++ b/mcpgateway/cache/session_registry.py @@ -61,7 +61,7 @@ # First-Party from mcpgateway import __version__ from mcpgateway.config import settings -from mcpgateway.db import SessionMessageRecord, SessionRecord, get_db +from mcpgateway.db import get_db, SessionMessageRecord, SessionRecord from mcpgateway.models import Implementation, InitializeResult, ServerCapabilities from mcpgateway.services import PromptService, ResourceService, ToolService from mcpgateway.services.logging_service import LoggingService diff --git a/mcpgateway/config.py b/mcpgateway/config.py index 1957a9ee..da8ecd26 100644 --- a/mcpgateway/config.py +++ b/mcpgateway/config.py @@ -47,21 +47,19 @@ True """ -import json -import logging -import os -import re - # Standard from functools import lru_cache from importlib.resources import files +import json +import logging +import os from pathlib import Path +import re from typing import Annotated, Any, ClassVar, Dict, List, Optional, Set, Union -import jq - # Third-Party from fastapi import HTTPException +import jq from jsonpath_ng.ext import parse from jsonpath_ng.jsonpath import JSONPath from pydantic import Field, field_validator diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 4af4bbb2..187a5dd7 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -21,15 +21,14 @@ True """ -import uuid - # Standard from datetime import datetime, timezone from typing import Any, Dict, List, Optional +import uuid # Third-Party import jsonschema -from sqlalchemy import JSON, Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Table, Text, UniqueConstraint, create_engine, event, func, make_url, select +from sqlalchemy import Boolean, Column, create_engine, DateTime, event, Float, ForeignKey, func, Integer, JSON, make_url, select, String, Table, Text, UniqueConstraint from sqlalchemy.event import listen from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.hybrid import hybrid_property diff --git a/mcpgateway/federation/discovery.py b/mcpgateway/federation/discovery.py index 9efc6d72..2ecc90ce 100644 --- a/mcpgateway/federation/discovery.py +++ b/mcpgateway/federation/discovery.py @@ -63,10 +63,10 @@ # Standard import asyncio -import os -import socket from dataclasses import dataclass from datetime import datetime, timedelta, timezone +import os +import socket from typing import Dict, List, Optional from urllib.parse import urlparse diff --git a/mcpgateway/handlers/sampling.py b/mcpgateway/handlers/sampling.py index a8e805ff..2ae4698c 100644 --- a/mcpgateway/handlers/sampling.py +++ b/mcpgateway/handlers/sampling.py @@ -218,7 +218,7 @@ async def create_message(self, db: Session, request: Dict[str, Any]) -> CreateMe if not self._validate_message(msg): raise SamplingError(f"Invalid message format: {msg}") - # FIXME: Implement actual model sampling - currently returns mock response + # TODO: Implement actual model sampling - currently returns mock response # pylint: disable=fixme # For now return mock response response = self._mock_sample(messages=messages) @@ -357,7 +357,7 @@ async def _add_context(self, _db: Session, messages: List[Dict[str, Any]], _cont >>> len(result) 2 """ - # FIXME: Implement context gathering based on type - currently no-op + # TODO: Implement context gathering based on type - currently no-op # pylint: disable=fixme # For now return original messages return messages diff --git a/mcpgateway/main.py b/mcpgateway/main.py index f8479729..2ba43f71 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -27,14 +27,14 @@ # Standard import asyncio +from contextlib import asynccontextmanager import json import time -from contextlib import asynccontextmanager from typing import Any, AsyncIterator, Dict, List, Optional, Union from urllib.parse import urlparse, urlunparse # Third-Party -from fastapi import APIRouter, Body, Depends, FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect, status +from fastapi import APIRouter, Body, Depends, FastAPI, HTTPException, Request, status, WebSocket, WebSocketDisconnect from fastapi.background import BackgroundTasks from fastapi.exception_handlers import request_validation_exception_handler as fastapi_default_validation_handler from fastapi.exceptions import RequestValidationError @@ -56,7 +56,7 @@ from mcpgateway.cache import ResourceCache, SessionRegistry from mcpgateway.config import jsonpath_modifier, settings from mcpgateway.db import Prompt as DbPrompt -from mcpgateway.db import PromptMetric, SessionLocal, refresh_slugs_on_startup +from mcpgateway.db import PromptMetric, refresh_slugs_on_startup, SessionLocal from mcpgateway.handlers.sampling import SamplingHandler from mcpgateway.models import InitializeRequest, InitializeResult, ListResourceTemplatesResult, LogLevel, ResourceContent, Root from mcpgateway.plugins import PluginManager, PluginViolationError diff --git a/mcpgateway/plugins/framework/loader/plugin.py b/mcpgateway/plugins/framework/loader/plugin.py index 79bb9426..ed75bc8f 100644 --- a/mcpgateway/plugins/framework/loader/plugin.py +++ b/mcpgateway/plugins/framework/loader/plugin.py @@ -10,7 +10,7 @@ # Standard import logging -from typing import Type, cast +from typing import cast, Type # First-Party from mcpgateway.plugins.framework.base import Plugin diff --git a/mcpgateway/plugins/framework/registry.py b/mcpgateway/plugins/framework/registry.py index 59ed74a2..9c568305 100644 --- a/mcpgateway/plugins/framework/registry.py +++ b/mcpgateway/plugins/framework/registry.py @@ -8,10 +8,9 @@ Module that stores plugin instances and manages hook points. """ -import logging - # Standard from collections import defaultdict +import logging from typing import Optional # First-Party diff --git a/mcpgateway/plugins/framework/utils.py b/mcpgateway/plugins/framework/utils.py index 723e7f61..0cdd7a14 100644 --- a/mcpgateway/plugins/framework/utils.py +++ b/mcpgateway/plugins/framework/utils.py @@ -9,10 +9,9 @@ plugins. """ -import importlib - # Standard from functools import cache +import importlib from types import ModuleType # First-Party diff --git a/mcpgateway/schemas.py b/mcpgateway/schemas.py index 19a871b9..9f0a68c1 100644 --- a/mcpgateway/schemas.py +++ b/mcpgateway/schemas.py @@ -21,15 +21,15 @@ # Standard import base64 +from datetime import datetime, timezone +from enum import Enum import json import logging import re -from datetime import datetime, timezone -from enum import Enum from typing import Any, Dict, List, Literal, Optional, Self, Union # Third-Party -from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, ValidationInfo, field_serializer, field_validator, model_validator +from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator, ValidationInfo # First-Party from mcpgateway.config import settings diff --git a/mcpgateway/services/__init__.py b/mcpgateway/services/__init__.py index 68a13bff..88c4fd67 100644 --- a/mcpgateway/services/__init__.py +++ b/mcpgateway/services/__init__.py @@ -12,7 +12,6 @@ - Gateway coordination """ -# First-Party from mcpgateway.services.gateway_service import GatewayError, GatewayService from mcpgateway.services.prompt_service import PromptError, PromptService from mcpgateway.services.resource_service import ResourceError, ResourceService diff --git a/mcpgateway/services/gateway_service.py b/mcpgateway/services/gateway_service.py index da0ad5b8..d7bb4405 100644 --- a/mcpgateway/services/gateway_service.py +++ b/mcpgateway/services/gateway_service.py @@ -39,17 +39,16 @@ # Standard import asyncio +from datetime import datetime, timezone import logging import os import tempfile +from typing import Any, AsyncGenerator, Dict, List, Optional, Set, TYPE_CHECKING import uuid -from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Set - -import httpx # Third-Party from filelock import FileLock, Timeout +import httpx from mcp import ClientSession from mcp.client.sse import sse_client from mcp.client.streamable_http import streamablehttp_client diff --git a/mcpgateway/services/logging_service.py b/mcpgateway/services/logging_service.py index f2cc43d7..4888ac93 100644 --- a/mcpgateway/services/logging_service.py +++ b/mcpgateway/services/logging_service.py @@ -11,10 +11,10 @@ # Standard import asyncio -import logging -import os from datetime import datetime, timezone +import logging from logging.handlers import RotatingFileHandler +import os from typing import Any, AsyncGenerator, Dict, List, Optional # Third-Party diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index 15eeecec..fb826827 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -16,14 +16,14 @@ # Standard import asyncio -import uuid from datetime import datetime, timezone from string import Formatter from typing import Any, AsyncGenerator, Dict, List, Optional, Set +import uuid # Third-Party from jinja2 import Environment, meta, select_autoescape -from sqlalchemy import Float, case, delete, desc, func, not_, select +from sqlalchemy import case, delete, desc, Float, func, not_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 5e96ed6c..24412fbe 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -26,14 +26,14 @@ # Standard import asyncio +from datetime import datetime, timezone import mimetypes import re -from datetime import datetime, timezone from typing import Any, AsyncGenerator, Dict, List, Optional, Union # Third-Party import parse -from sqlalchemy import Float, case, delete, desc, func, not_, select +from sqlalchemy import case, delete, desc, Float, func, not_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index 3493fb31..46a1db0d 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -19,7 +19,7 @@ # Third-Party import httpx -from sqlalchemy import Float, case, delete, desc, func, select +from sqlalchemy import case, delete, desc, Float, func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session diff --git a/mcpgateway/services/tool_service.py b/mcpgateway/services/tool_service.py index 154c21ca..404aa6ba 100644 --- a/mcpgateway/services/tool_service.py +++ b/mcpgateway/services/tool_service.py @@ -17,26 +17,27 @@ # Standard import asyncio import base64 +from datetime import datetime, timezone import json import re import time -import uuid -from datetime import datetime, timezone from typing import Any, AsyncGenerator, Dict, List, Optional +import uuid # Third-Party from mcp import ClientSession from mcp.client.sse import sse_client from mcp.client.streamable_http import streamablehttp_client -from sqlalchemy import Float, case, delete, desc, func, not_, select +from sqlalchemy import case, delete, desc, Float, func, not_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session # First-Party from mcpgateway.config import settings from mcpgateway.db import Gateway as DbGateway +from mcpgateway.db import server_tool_association from mcpgateway.db import Tool as DbTool -from mcpgateway.db import ToolMetric, server_tool_association +from mcpgateway.db import ToolMetric from mcpgateway.models import TextContent, ToolResult from mcpgateway.plugins.framework.manager import PluginManager from mcpgateway.plugins.framework.plugin_types import GlobalContext, PluginViolationError, ToolPostInvokePayload, ToolPreInvokePayload diff --git a/mcpgateway/translate.py b/mcpgateway/translate.py index 0117e645..6284efc9 100644 --- a/mcpgateway/translate.py +++ b/mcpgateway/translate.py @@ -55,22 +55,21 @@ # Standard import argparse import asyncio +from contextlib import suppress import json import logging import shlex import signal import sys -import uuid -from contextlib import suppress from typing import Any, AsyncIterator, Dict, List, Optional, Sequence, Tuple - -import uvicorn +import uuid # Third-Party from fastapi import FastAPI, Request, Response, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import PlainTextResponse from sse_starlette.sse import EventSourceResponse +import uvicorn try: # Third-Party diff --git a/mcpgateway/transports/sse_transport.py b/mcpgateway/transports/sse_transport.py index d803197b..14356891 100644 --- a/mcpgateway/transports/sse_transport.py +++ b/mcpgateway/transports/sse_transport.py @@ -11,10 +11,10 @@ # Standard import asyncio -import json -import uuid from datetime import datetime +import json from typing import Any, AsyncGenerator, Dict +import uuid # Third-Party from fastapi import Request diff --git a/mcpgateway/transports/streamablehttp_transport.py b/mcpgateway/transports/streamablehttp_transport.py index 3e0419b6..d61e6b8b 100644 --- a/mcpgateway/transports/streamablehttp_transport.py +++ b/mcpgateway/transports/streamablehttp_transport.py @@ -29,13 +29,12 @@ 'SessionManagerWrapper' """ -import contextvars -import re - # Standard from collections import deque -from contextlib import AsyncExitStack, asynccontextmanager +from contextlib import asynccontextmanager, AsyncExitStack +import contextvars from dataclasses import dataclass +import re from typing import List, Union from uuid import uuid4 diff --git a/mcpgateway/utils/db_isready.py b/mcpgateway/utils/db_isready.py index c385d442..a18b1cfb 100755 --- a/mcpgateway/utils/db_isready.py +++ b/mcpgateway/utils/db_isready.py @@ -106,7 +106,7 @@ try: # Third-Party from sqlalchemy import create_engine, text - from sqlalchemy.engine import URL, Engine + from sqlalchemy.engine import Engine, URL from sqlalchemy.engine.url import make_url from sqlalchemy.exc import OperationalError except ImportError: # pragma: no cover - handled at runtime for the CLI diff --git a/mcpgateway/utils/verify_credentials.py b/mcpgateway/utils/verify_credentials.py index 0f9e6208..a980f3f0 100644 --- a/mcpgateway/utils/verify_credentials.py +++ b/mcpgateway/utils/verify_credentials.py @@ -40,18 +40,16 @@ error """ -import binascii - # Standard from base64 import b64decode +import binascii from typing import Optional -import jwt - # Third-Party from fastapi import Cookie, Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBasic, HTTPBasicCredentials, HTTPBearer from fastapi.security.utils import get_authorization_scheme_param +import jwt # First-Party from mcpgateway.config import settings diff --git a/mcpgateway/version.py b/mcpgateway/version.py index 4ffc520a..56c1a659 100644 --- a/mcpgateway/version.py +++ b/mcpgateway/version.py @@ -40,12 +40,12 @@ # Standard import asyncio +from datetime import datetime, timezone import json import os import platform import socket import time -from datetime import datetime, timezone from typing import Any, Dict, Optional from urllib.parse import urlsplit, urlunsplit diff --git a/mcpgateway/wrapper.py b/mcpgateway/wrapper.py index d1a19788..e9816fc9 100644 --- a/mcpgateway/wrapper.py +++ b/mcpgateway/wrapper.py @@ -43,10 +43,10 @@ # Third-Party import httpx -import mcp.server.stdio from mcp import types from mcp.server import NotificationOptions, Server from mcp.server.models import InitializationOptions +import mcp.server.stdio from pydantic import AnyUrl # First-Party diff --git a/pyproject.toml b/pyproject.toml index 4a3b741f..7c86a5b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -251,9 +251,6 @@ target-version = ["py310", "py311", "py312"] include = "\\.pyi?$" # isort configuration -[tool.isort] - - # -------------------------------------------------------------------- # ๐Ÿ›  Async tool configurations (async-test, async-lint, etc.) # -------------------------------------------------------------------- @@ -268,6 +265,7 @@ extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] module = "tests.*" disallow_untyped_defs = false +[tool.isort] ############################################################################### # Core behaviour ############################################################################### From e4285a73d99ea55336daa0d9bddae1e53df1d5be Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sun, 10 Aug 2025 15:37:28 +0100 Subject: [PATCH 18/22] Rebase, lint and fix pylint issues Signed-off-by: Mihai Criveti --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index dbb63416..07ac1fa5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.aider* +.scannerwork llms-full.txt aider* .aider* From 2535e0f2da3468a748f508b09b858e7a1b501bda Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sun, 10 Aug 2025 15:40:18 +0100 Subject: [PATCH 19/22] Cleanup manifest.in Signed-off-by: Mihai Criveti --- .github/workflows/{asynctest.yml => asynctest.yml.inactive} | 0 MANIFEST.in | 2 -- 2 files changed, 2 deletions(-) rename .github/workflows/{asynctest.yml => asynctest.yml.inactive} (100%) diff --git a/.github/workflows/asynctest.yml b/.github/workflows/asynctest.yml.inactive similarity index 100% rename from .github/workflows/asynctest.yml rename to .github/workflows/asynctest.yml.inactive diff --git a/MANIFEST.in b/MANIFEST.in index 653e4c90..5c4b5b2d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -27,8 +27,6 @@ include *.yml include *.json include *.sh include *.txt -recursive-include async_testing *.json -recursive-include async_testing *.prof recursive-include async_testing *.py recursive-include async_testing *.yaml From 5b1a6c86f29890d6339a59117e2deb4f6c6fcbef Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sun, 10 Aug 2025 15:41:10 +0100 Subject: [PATCH 20/22] Cleanup manifest.in Signed-off-by: Mihai Criveti --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 07ac1fa5..c29951a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +CLAUDE.local.md .aider* .scannerwork llms-full.txt From badc7a97945dc42cde2337520ec5af4fe997571c Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sun, 10 Aug 2025 15:42:46 +0100 Subject: [PATCH 21/22] Cleanup manifest.in Signed-off-by: Mihai Criveti --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5c4b5b2d..3fd03da5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -75,7 +75,8 @@ prune *.egg-info prune charts prune k8s prune .devcontainer - +prune CLAUDE.* +prune llms-full.txt # Exclude deployment, mcp-servers and agent_runtimes prune deployment From f6f6bf35b42b6edbbccc1c3035196411d1d58679 Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sun, 10 Aug 2025 15:43:56 +0100 Subject: [PATCH 22/22] Cleanup manifest.in Signed-off-by: Mihai Criveti --- CLAUDE.md | 2 +- MANIFEST.in | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6da09cf3..996301c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -228,7 +228,7 @@ pytest -m "not slow" # To test everything: make autoflake isort black pre-commit -make interrogate doctest test smoketest lint-web flake8 bandit pylint +make doctest test htmlcov smoketest lint-web flake8 bandit interrogate pylint verify # Rules - When using git commit always add a -s to sign commits diff --git a/MANIFEST.in b/MANIFEST.in index 3fd03da5..fc9fe92a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -75,8 +75,8 @@ prune *.egg-info prune charts prune k8s prune .devcontainer -prune CLAUDE.* -prune llms-full.txt +exclude CLAUDE.* +exclude llms-full.txt # Exclude deployment, mcp-servers and agent_runtimes prune deployment