From 282a1c0bd9a86aad75a7f48478fecaa2069efb3e Mon Sep 17 00:00:00 2001 From: Yijia Su <54164178+FreeOnePlus@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:29:37 +0800 Subject: [PATCH] [BUG]Further fix the at_eof problem caused by aiomysql (#9) * fix at_eof bug * update uv.lock --- doris_mcp_server/utils/db.py | 305 +++++++++++++++++++++++--------- uv.lock | 328 +++++++++++++++++------------------ 2 files changed, 390 insertions(+), 243 deletions(-) diff --git a/doris_mcp_server/utils/db.py b/doris_mcp_server/utils/db.py index 8c129ff..f6cc9f5 100644 --- a/doris_mcp_server/utils/db.py +++ b/doris_mcp_server/utils/db.py @@ -210,7 +210,8 @@ class DorisConnectionManager: if not self.config.database.password: self.logger.warning("Database password is empty, this may cause connection issues") - # Create connection pool with additional parameters for stability + # Create connection pool with improved stability parameters + # Key change: Set minsize=0 to avoid pre-creation issues that cause at_eof errors self.pool = await aiomysql.create_pool( host=self.config.database.host, port=self.config.database.port, @@ -218,22 +219,22 @@ class DorisConnectionManager: password=self.config.database.password, db=self.config.database.database, charset="utf8", - minsize=self.config.database.min_connections or 5, + minsize=0, # Avoid pre-creation issues - create connections on demand maxsize=self.config.database.max_connections or 20, autocommit=True, connect_timeout=self.connection_timeout, - # Additional parameters for stability - pool_recycle=3600, # Recycle connections every hour + # Enhanced stability parameters + pool_recycle=7200, # Recycle connections every 2 hours echo=False, # Don't echo SQL statements ) - # Test the connection pool - if not await self.test_connection(): - raise RuntimeError("Connection pool test failed") + # Test the connection pool with a more robust test + if not await self._robust_connection_test(): + raise RuntimeError("Connection pool robust test failed") self.logger.info( - f"Connection pool initialized successfully, min connections: {self.config.database.min_connections}, " - f"max connections: {self.config.database.max_connections}" + f"Connection pool initialized successfully with on-demand connection creation, " + f"max connections: {self.config.database.max_connections or 20}" ) # Start background monitoring tasks @@ -252,63 +253,178 @@ class DorisConnectionManager: self.pool = None raise + async def _robust_connection_test(self) -> bool: + """Perform a robust connection test that validates full connection health""" + max_retries = 3 + for attempt in range(max_retries): + try: + self.logger.debug(f"Testing connection pool (attempt {attempt + 1}/{max_retries})") + + # Test connection creation and validation + test_conn = await self._create_raw_connection_with_validation() + if test_conn: + # Test basic query execution + async with test_conn.cursor() as cursor: + await cursor.execute("SELECT 1") + result = await cursor.fetchone() + if result and result[0] == 1: + self.logger.debug("Connection pool test successful") + # Return connection to pool + if self.pool: + self.pool.release(test_conn) + return True + else: + self.logger.warning("Connection test query returned unexpected result") + + # Close test connection if we get here + await test_conn.ensure_closed() + + except Exception as e: + self.logger.warning(f"Connection test attempt {attempt + 1} failed: {e}") + if attempt == max_retries - 1: + self.logger.error("All connection test attempts failed") + return False + else: + # Wait before retry + await asyncio.sleep(1.0 * (attempt + 1)) + + return False + + async def _create_raw_connection_with_validation(self, max_retries: int = 3): + """Create a raw connection with comprehensive validation""" + for attempt in range(max_retries): + try: + if not self.pool: + raise RuntimeError("Connection pool not initialized") + + # Acquire connection from pool + raw_connection = await self.pool.acquire() + + # Basic connection validation + if not raw_connection: + self.logger.warning(f"Pool returned None connection (attempt {attempt + 1})") + continue + + if raw_connection.closed: + self.logger.warning(f"Pool returned closed connection (attempt {attempt + 1})") + continue + + # Perform a simple ping test instead of checking internal state + # Internal state (_reader, _transport) might not be fully initialized yet + try: + # Test basic connectivity with a simple query + async with raw_connection.cursor() as cursor: + await cursor.execute("SELECT 1") + result = await cursor.fetchone() + if result and result[0] == 1: + self.logger.debug(f"Successfully created and validated raw connection (attempt {attempt + 1})") + return raw_connection + else: + self.logger.warning(f"Connection test query failed (attempt {attempt + 1})") + await raw_connection.ensure_closed() + continue + + except Exception as e: + # Check if this is an at_eof error specifically + error_str = str(e).lower() + if 'at_eof' in error_str or 'nonetype' in error_str: + self.logger.warning(f"Connection has at_eof issue (attempt {attempt + 1}): {e}") + else: + self.logger.warning(f"Connection test failed (attempt {attempt + 1}): {e}") + + try: + await raw_connection.ensure_closed() + except Exception: + pass + continue + + except Exception as e: + self.logger.warning(f"Raw connection creation attempt {attempt + 1} failed: {e}") + if attempt == max_retries - 1: + raise RuntimeError(f"Failed to create valid connection after {max_retries} attempts: {e}") + else: + # Exponential backoff + await asyncio.sleep(0.5 * (2 ** attempt)) + + raise RuntimeError("Failed to create valid connection") + async def get_connection(self, session_id: str) -> DorisConnection: - """Get database connection + """Get database connection with enhanced reliability Supports session-level connection reuse to improve performance and consistency """ # Check if there's an existing session connection if session_id in self.session_connections: conn = self.session_connections[session_id] - # Check connection health - if await conn.ping(): + # Enhanced connection health check + if await self._comprehensive_connection_health_check(conn): return conn else: # Connection is unhealthy, clean up and create new one + self.logger.debug(f"Existing connection unhealthy for session {session_id}, creating new one") await self._cleanup_session_connection(session_id) - # Create new connection - return await self._create_new_connection(session_id) + # Create new connection with retry logic + return await self._create_new_connection_with_retry(session_id) - async def _create_new_connection(self, session_id: str) -> DorisConnection: - """Create new database connection""" + async def _comprehensive_connection_health_check(self, conn: DorisConnection) -> bool: + """Perform comprehensive connection health check""" try: - if not self.pool: - raise RuntimeError("Connection pool not initialized") - - # Get connection from pool - raw_connection = await self.pool.acquire() + # Check basic connection state + if not conn.connection or conn.connection.closed: + return False - # Validate the raw connection - if not raw_connection: - raise RuntimeError(f"Failed to acquire connection from pool for session {session_id}") + # Instead of checking internal state, perform a simple ping test + # This is more reliable and less dependent on aiomysql internals + if not await conn.ping(): + return False - # Verify the connection is not closed - if raw_connection.closed: - raise RuntimeError(f"Acquired connection is already closed for session {session_id}") + return True - # Create wrapped connection - doris_conn = DorisConnection(raw_connection, session_id, self.security_manager) - - # Test the connection before storing it - if not await doris_conn.ping(): - # If ping fails, release the connection and raise error - if self.pool and raw_connection and not raw_connection.closed: - self.pool.release(raw_connection) - raise RuntimeError(f"New connection failed ping test for session {session_id}") - - # Store in session connections - self.session_connections[session_id] = doris_conn - - self.metrics.total_connections += 1 - self.logger.debug(f"Created new connection for session: {session_id}") - - return doris_conn - except Exception as e: - self.metrics.connection_errors += 1 - self.logger.error(f"Failed to create connection for session {session_id}: {e}") - raise + # Check for at_eof errors specifically + error_str = str(e).lower() + if 'at_eof' in error_str: + self.logger.debug(f"Connection health check failed with at_eof error: {e}") + else: + self.logger.debug(f"Connection health check failed: {e}") + return False + + async def _create_new_connection_with_retry(self, session_id: str, max_retries: int = 3) -> DorisConnection: + """Create new database connection with retry logic""" + for attempt in range(max_retries): + try: + # Get validated raw connection + raw_connection = await self._create_raw_connection_with_validation() + + # Create wrapped connection + doris_conn = DorisConnection(raw_connection, session_id, self.security_manager) + + # Comprehensive connection test + if await self._comprehensive_connection_health_check(doris_conn): + # Store in session connections + self.session_connections[session_id] = doris_conn + self.metrics.total_connections += 1 + self.logger.debug(f"Successfully created new connection for session: {session_id}") + return doris_conn + else: + # Connection failed health check, clean up and retry + self.logger.warning(f"New connection failed health check for session {session_id} (attempt {attempt + 1})") + try: + await doris_conn.close() + except Exception: + pass + + except Exception as e: + self.logger.warning(f"Connection creation attempt {attempt + 1} failed for session {session_id}: {e}") + if attempt == max_retries - 1: + self.metrics.connection_errors += 1 + raise RuntimeError(f"Failed to create connection for session {session_id} after {max_retries} attempts: {e}") + else: + # Exponential backoff + await asyncio.sleep(0.5 * (2 ** attempt)) + + raise RuntimeError(f"Unexpected failure in connection creation for session {session_id}") async def release_connection(self, session_id: str): """Release session connection""" @@ -316,26 +432,47 @@ class DorisConnectionManager: await self._cleanup_session_connection(session_id) async def _cleanup_session_connection(self, session_id: str): - """Clean up session connection""" + """Clean up session connection with enhanced safety""" if session_id in self.session_connections: conn = self.session_connections[session_id] try: - # Return connection to pool only if it's valid and not closed + # Simplified connection validation before returning to pool + connection_healthy = False + if (self.pool and conn.connection and - not conn.connection.closed and - hasattr(conn.connection, '_reader') and - conn.connection._reader is not None): + not conn.connection.closed): + + # Test if connection is still healthy with a simple check + try: + # Quick ping test to see if connection is usable + async with conn.connection.cursor() as cursor: + await cursor.execute("SELECT 1") + await cursor.fetchone() + connection_healthy = True + except Exception as test_error: + self.logger.debug(f"Connection health test failed for session {session_id}: {test_error}") + connection_healthy = False + + if connection_healthy: + # Connection appears healthy, return to pool try: - # Try to gracefully return to pool self.pool.release(conn.connection) + self.logger.debug(f"Successfully returned connection to pool for session {session_id}") except Exception as pool_error: self.logger.debug(f"Failed to return connection to pool for session {session_id}: {pool_error}") - # If pool release fails, try to close the connection directly try: await conn.connection.ensure_closed() except Exception: - pass # Ignore errors during forced close + pass + else: + # Connection is unhealthy, force close + self.logger.debug(f"Connection unhealthy for session {session_id}, force closing") + try: + if conn.connection and not conn.connection.closed: + await conn.connection.ensure_closed() + except Exception: + pass # Close connection wrapper await conn.close() @@ -365,24 +502,24 @@ class DorisConnectionManager: self.logger.error(f"Health check error: {e}") async def _perform_health_check(self): - """Perform health check""" + """Perform enhanced health check""" try: unhealthy_sessions = [] - # First pass: check basic connectivity + # Enhanced health check with comprehensive validation for session_id, conn in self.session_connections.items(): - if not await conn.ping(): + if not await self._comprehensive_connection_health_check(conn): unhealthy_sessions.append(session_id) - # Second pass: check for stale connections (over 30 minutes old) + # Check for stale connections (over 30 minutes old) current_time = datetime.utcnow() stale_sessions = [] for session_id, conn in self.session_connections.items(): if session_id not in unhealthy_sessions: # Don't double-check last_used_delta = (current_time - conn.last_used).total_seconds() if last_used_delta > 1800: # 30 minutes - # Force a ping check for stale connections - if not await conn.ping(): + # Force a comprehensive health check for stale connections + if not await self._comprehensive_connection_health_check(conn): stale_sessions.append(session_id) all_problematic_sessions = list(set(unhealthy_sessions + stale_sessions)) @@ -453,9 +590,29 @@ class DorisConnectionManager: async def execute_query( self, session_id: str, sql: str, params: tuple | None = None, auth_context=None ) -> QueryResult: - """Execute query""" - conn = await self.get_connection(session_id) - return await conn.execute(sql, params, auth_context) + """Execute query with enhanced error handling and retry logic""" + max_retries = 2 + for attempt in range(max_retries): + try: + conn = await self.get_connection(session_id) + return await conn.execute(sql, params, auth_context) + except Exception as e: + error_msg = str(e).lower() + # Check for connection-related errors that warrant retry + is_connection_error = any(keyword in error_msg for keyword in [ + 'at_eof', 'connection', 'closed', 'nonetype', 'reader', 'transport' + ]) + + if is_connection_error and attempt < max_retries - 1: + self.logger.warning(f"Connection error during query execution (attempt {attempt + 1}): {e}") + # Clean up the problematic connection + await self.release_connection(session_id) + # Wait before retry + await asyncio.sleep(0.5 * (attempt + 1)) + continue + else: + # Not a connection error or final retry - re-raise + raise @asynccontextmanager async def get_connection_context(self, session_id: str): @@ -500,20 +657,8 @@ class DorisConnectionManager: self.logger.error(f"Error closing connection manager: {e}") async def test_connection(self) -> bool: - """Test database connection""" - try: - if not self.pool: - return False - - async with self.pool.acquire() as conn: - async with conn.cursor() as cursor: - await cursor.execute("SELECT 1") - result = await cursor.fetchone() - return result is not None - - except Exception as e: - self.logger.error(f"Connection test failed: {e}") - return False + """Test database connection using robust connection test""" + return await self._robust_connection_test() async def diagnose_connection_health(self) -> Dict[str, Any]: """Diagnose connection pool and session health""" @@ -680,3 +825,5 @@ class ConnectionPoolMonitor: report["recommendations"].append("Connection pool utilization is high, consider increasing pool size") return report + + diff --git a/uv.lock b/uv.lock index db4e9ea..518966a 100644 --- a/uv.lock +++ b/uv.lock @@ -518,6 +518,170 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, ] +[[package]] +name = "doris-mcp-server" +version = "0.4.2" +source = { editable = "." } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "aiomysql" }, + { name = "aioredis" }, + { name = "asyncio-mqtt" }, + { name = "bcrypt" }, + { name = "click" }, + { name = "cryptography" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "pandas" }, + { name = "passlib", extra = ["bcrypt"] }, + { name = "prometheus-client" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "pymysql" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "python-dateutil" }, + { name = "python-dotenv" }, + { name = "python-jose", extra = ["cryptography"] }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "sqlparse" }, + { name = "starlette" }, + { name = "structlog" }, + { name = "toml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "websockets" }, +] + +[package.optional-dependencies] +dev = [ + { name = "bandit" }, + { name = "black" }, + { name = "flake8" }, + { name = "isort" }, + { name = "mypy" }, + { name = "myst-parser" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "safety" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, + { name = "tox" }, +] +docs = [ + { name = "myst-parser" }, + { name = "sphinx" }, + { name = "sphinx-autoapi" }, + { name = "sphinx-rtd-theme" }, +] +monitoring = [ + { name = "grafana-client" }, + { name = "jaeger-client" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] +performance = [ + { name = "cchardet" }, + { name = "orjson" }, + { name = "uvloop" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = ">=23.0.0" }, + { name = "aiohttp", specifier = ">=3.9.0" }, + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aioredis", specifier = ">=2.0.0" }, + { name = "asyncio-mqtt", specifier = ">=0.16.0" }, + { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.0" }, + { name = "bcrypt", specifier = ">=4.1.0" }, + { name = "black", marker = "extra == 'dev'", specifier = ">=23.12.0" }, + { name = "cchardet", marker = "extra == 'performance'", specifier = ">=2.1.0" }, + { name = "click", specifier = ">=8.1.0" }, + { name = "cryptography", specifier = ">=41.0.0" }, + { name = "fastapi", specifier = ">=0.108.0" }, + { name = "flake8", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "grafana-client", marker = "extra == 'monitoring'", specifier = ">=3.5.0" }, + { name = "httpx", specifier = ">=0.26.0" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.0" }, + { name = "jaeger-client", marker = "extra == 'monitoring'", specifier = ">=4.8.0" }, + { name = "mcp", specifier = ">=1.8.0,<2.0.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, + { name = "myst-parser", marker = "extra == 'dev'", specifier = ">=2.0.0" }, + { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=2.0.0" }, + { name = "numpy", specifier = ">=1.24.0" }, + { name = "opentelemetry-api", marker = "extra == 'monitoring'", specifier = ">=1.21.0" }, + { name = "opentelemetry-sdk", marker = "extra == 'monitoring'", specifier = ">=1.21.0" }, + { name = "orjson", specifier = ">=3.9.0" }, + { name = "orjson", marker = "extra == 'performance'", specifier = ">=3.9.0" }, + { name = "pandas", specifier = ">=2.0.0" }, + { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.6.0" }, + { name = "prometheus-client", specifier = ">=0.19.0" }, + { name = "prometheus-client", marker = "extra == 'monitoring'", specifier = ">=0.19.0" }, + { name = "pydantic", specifier = ">=2.5.0" }, + { name = "pydantic-settings", specifier = ">=2.1.0" }, + { name = "pyjwt", specifier = ">=2.8.0" }, + { name = "pymysql", specifier = ">=1.1.0" }, + { name = "pytest", specifier = ">=8.4.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0" }, + { name = "python-dateutil", specifier = ">=2.8.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" }, + { name = "python-multipart", specifier = ">=0.0.6" }, + { name = "pyyaml", specifier = ">=6.0.0" }, + { name = "requests", specifier = ">=2.31.0" }, + { name = "rich", specifier = ">=13.7.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "safety", marker = "extra == 'dev'", specifier = ">=2.3.0" }, + { name = "sphinx", marker = "extra == 'dev'", specifier = ">=7.2.0" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.2.0" }, + { name = "sphinx-autoapi", marker = "extra == 'docs'", specifier = ">=3.0.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'dev'", specifier = ">=2.0.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=2.0.0" }, + { name = "sqlparse", specifier = ">=0.4.4" }, + { name = "starlette", specifier = ">=0.27.0" }, + { name = "structlog", specifier = ">=23.2.0" }, + { name = "toml", specifier = ">=0.10.0" }, + { name = "tox", marker = "extra == 'dev'", specifier = ">=4.11.0" }, + { name = "tqdm", specifier = ">=4.66.0" }, + { name = "typer", specifier = ">=0.9.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.25.0" }, + { name = "uvloop", marker = "extra == 'performance'", specifier = ">=0.19.0" }, + { name = "websockets", specifier = ">=12.0" }, +] +provides-extras = ["dev", "docs", "performance", "monitoring"] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.11.13" }] + [[package]] name = "dparse" version = "0.6.4" @@ -946,170 +1110,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/45/823ad05504bea55cb0feb7470387f151252127ad5c72f8882e8fe6cf5c0e/mcp-1.9.3-py3-none-any.whl", hash = "sha256:69b0136d1ac9927402ed4cf221d4b8ff875e7132b0b06edd446448766f34f9b9", size = 131063 }, ] -[[package]] -name = "mcp-doris-server" -version = "0.4.2" -source = { editable = "." } -dependencies = [ - { name = "aiofiles" }, - { name = "aiohttp" }, - { name = "aiomysql" }, - { name = "aioredis" }, - { name = "asyncio-mqtt" }, - { name = "bcrypt" }, - { name = "click" }, - { name = "cryptography" }, - { name = "fastapi" }, - { name = "httpx" }, - { name = "mcp" }, - { name = "numpy" }, - { name = "orjson" }, - { name = "pandas" }, - { name = "passlib", extra = ["bcrypt"] }, - { name = "prometheus-client" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt" }, - { name = "pymysql" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "python-dateutil" }, - { name = "python-dotenv" }, - { name = "python-jose", extra = ["cryptography"] }, - { name = "python-multipart" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "rich" }, - { name = "sqlparse" }, - { name = "starlette" }, - { name = "structlog" }, - { name = "toml" }, - { name = "tqdm" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, - { name = "websockets" }, -] - -[package.optional-dependencies] -dev = [ - { name = "bandit" }, - { name = "black" }, - { name = "flake8" }, - { name = "isort" }, - { name = "mypy" }, - { name = "myst-parser" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "pytest-xdist" }, - { name = "ruff" }, - { name = "safety" }, - { name = "sphinx" }, - { name = "sphinx-rtd-theme" }, - { name = "tox" }, -] -docs = [ - { name = "myst-parser" }, - { name = "sphinx" }, - { name = "sphinx-autoapi" }, - { name = "sphinx-rtd-theme" }, -] -monitoring = [ - { name = "grafana-client" }, - { name = "jaeger-client" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "prometheus-client" }, -] -performance = [ - { name = "cchardet" }, - { name = "orjson" }, - { name = "uvloop" }, -] - -[package.dev-dependencies] -dev = [ - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiofiles", specifier = ">=23.0.0" }, - { name = "aiohttp", specifier = ">=3.9.0" }, - { name = "aiomysql", specifier = ">=0.2.0" }, - { name = "aioredis", specifier = ">=2.0.0" }, - { name = "asyncio-mqtt", specifier = ">=0.16.0" }, - { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.0" }, - { name = "bcrypt", specifier = ">=4.1.0" }, - { name = "black", marker = "extra == 'dev'", specifier = ">=23.12.0" }, - { name = "cchardet", marker = "extra == 'performance'", specifier = ">=2.1.0" }, - { name = "click", specifier = ">=8.1.0" }, - { name = "cryptography", specifier = ">=41.0.0" }, - { name = "fastapi", specifier = ">=0.108.0" }, - { name = "flake8", marker = "extra == 'dev'", specifier = ">=7.0.0" }, - { name = "grafana-client", marker = "extra == 'monitoring'", specifier = ">=3.5.0" }, - { name = "httpx", specifier = ">=0.26.0" }, - { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.0" }, - { name = "jaeger-client", marker = "extra == 'monitoring'", specifier = ">=4.8.0" }, - { name = "mcp", specifier = ">=1.8.0,<2.0.0" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, - { name = "myst-parser", marker = "extra == 'dev'", specifier = ">=2.0.0" }, - { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=2.0.0" }, - { name = "numpy", specifier = ">=1.24.0" }, - { name = "opentelemetry-api", marker = "extra == 'monitoring'", specifier = ">=1.21.0" }, - { name = "opentelemetry-sdk", marker = "extra == 'monitoring'", specifier = ">=1.21.0" }, - { name = "orjson", specifier = ">=3.9.0" }, - { name = "orjson", marker = "extra == 'performance'", specifier = ">=3.9.0" }, - { name = "pandas", specifier = ">=2.0.0" }, - { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.0" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.6.0" }, - { name = "prometheus-client", specifier = ">=0.19.0" }, - { name = "prometheus-client", marker = "extra == 'monitoring'", specifier = ">=0.19.0" }, - { name = "pydantic", specifier = ">=2.5.0" }, - { name = "pydantic-settings", specifier = ">=2.1.0" }, - { name = "pyjwt", specifier = ">=2.8.0" }, - { name = "pymysql", specifier = ">=1.1.0" }, - { name = "pytest", specifier = ">=8.4.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, - { name = "pytest-asyncio", specifier = ">=1.0.0" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, - { name = "pytest-cov", specifier = ">=6.1.1" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, - { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" }, - { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0" }, - { name = "python-dateutil", specifier = ">=2.8.0" }, - { name = "python-dotenv", specifier = ">=1.0.0" }, - { name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" }, - { name = "python-multipart", specifier = ">=0.0.6" }, - { name = "pyyaml", specifier = ">=6.0.0" }, - { name = "requests", specifier = ">=2.31.0" }, - { name = "rich", specifier = ">=13.7.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, - { name = "safety", marker = "extra == 'dev'", specifier = ">=2.3.0" }, - { name = "sphinx", marker = "extra == 'dev'", specifier = ">=7.2.0" }, - { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.2.0" }, - { name = "sphinx-autoapi", marker = "extra == 'docs'", specifier = ">=3.0.0" }, - { name = "sphinx-rtd-theme", marker = "extra == 'dev'", specifier = ">=2.0.0" }, - { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=2.0.0" }, - { name = "sqlparse", specifier = ">=0.4.4" }, - { name = "starlette", specifier = ">=0.27.0" }, - { name = "structlog", specifier = ">=23.2.0" }, - { name = "toml", specifier = ">=0.10.0" }, - { name = "tox", marker = "extra == 'dev'", specifier = ">=4.11.0" }, - { name = "tqdm", specifier = ">=4.66.0" }, - { name = "typer", specifier = ">=0.9.0" }, - { name = "uvicorn", extras = ["standard"], specifier = ">=0.25.0" }, - { name = "uvloop", marker = "extra == 'performance'", specifier = ">=0.19.0" }, - { name = "websockets", specifier = ">=12.0" }, -] -provides-extras = ["dev", "docs", "performance", "monitoring"] - -[package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.11.13" }] - [[package]] name = "mdit-py-plugins" version = "0.4.2"