[BUG]Further fix the at_eof problem caused by aiomysql (#9)

* fix at_eof bug

* update uv.lock
This commit is contained in:
Yijia Su
2025-07-02 19:29:37 +08:00
committed by GitHub
parent e3b9bf96ab
commit 282a1c0bd9
2 changed files with 390 additions and 243 deletions

View File

@@ -210,7 +210,8 @@ class DorisConnectionManager:
if not self.config.database.password: if not self.config.database.password:
self.logger.warning("Database password is empty, this may cause connection issues") 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( self.pool = await aiomysql.create_pool(
host=self.config.database.host, host=self.config.database.host,
port=self.config.database.port, port=self.config.database.port,
@@ -218,22 +219,22 @@ class DorisConnectionManager:
password=self.config.database.password, password=self.config.database.password,
db=self.config.database.database, db=self.config.database.database,
charset="utf8", 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, maxsize=self.config.database.max_connections or 20,
autocommit=True, autocommit=True,
connect_timeout=self.connection_timeout, connect_timeout=self.connection_timeout,
# Additional parameters for stability # Enhanced stability parameters
pool_recycle=3600, # Recycle connections every hour pool_recycle=7200, # Recycle connections every 2 hours
echo=False, # Don't echo SQL statements echo=False, # Don't echo SQL statements
) )
# Test the connection pool # Test the connection pool with a more robust test
if not await self.test_connection(): if not await self._robust_connection_test():
raise RuntimeError("Connection pool test failed") raise RuntimeError("Connection pool robust test failed")
self.logger.info( self.logger.info(
f"Connection pool initialized successfully, min connections: {self.config.database.min_connections}, " f"Connection pool initialized successfully with on-demand connection creation, "
f"max connections: {self.config.database.max_connections}" f"max connections: {self.config.database.max_connections or 20}"
) )
# Start background monitoring tasks # Start background monitoring tasks
@@ -252,63 +253,178 @@ class DorisConnectionManager:
self.pool = None self.pool = None
raise 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: 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 Supports session-level connection reuse to improve performance and consistency
""" """
# Check if there's an existing session connection # Check if there's an existing session connection
if session_id in self.session_connections: if session_id in self.session_connections:
conn = self.session_connections[session_id] conn = self.session_connections[session_id]
# Check connection health # Enhanced connection health check
if await conn.ping(): if await self._comprehensive_connection_health_check(conn):
return conn return conn
else: else:
# Connection is unhealthy, clean up and create new one # 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) await self._cleanup_session_connection(session_id)
# Create new connection # Create new connection with retry logic
return await self._create_new_connection(session_id) return await self._create_new_connection_with_retry(session_id)
async def _create_new_connection(self, session_id: str) -> DorisConnection: async def _comprehensive_connection_health_check(self, conn: DorisConnection) -> bool:
"""Create new database connection""" """Perform comprehensive connection health check"""
try: try:
if not self.pool: # Check basic connection state
raise RuntimeError("Connection pool not initialized") if not conn.connection or conn.connection.closed:
return False
# Get connection from pool
raw_connection = await self.pool.acquire()
# Validate the raw connection # Instead of checking internal state, perform a simple ping test
if not raw_connection: # This is more reliable and less dependent on aiomysql internals
raise RuntimeError(f"Failed to acquire connection from pool for session {session_id}") if not await conn.ping():
return False
# Verify the connection is not closed return True
if raw_connection.closed:
raise RuntimeError(f"Acquired connection is already closed for session {session_id}")
# 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: except Exception as e:
self.metrics.connection_errors += 1 # Check for at_eof errors specifically
self.logger.error(f"Failed to create connection for session {session_id}: {e}") error_str = str(e).lower()
raise 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): async def release_connection(self, session_id: str):
"""Release session connection""" """Release session connection"""
@@ -316,26 +432,47 @@ class DorisConnectionManager:
await self._cleanup_session_connection(session_id) await self._cleanup_session_connection(session_id)
async def _cleanup_session_connection(self, session_id: str): 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: if session_id in self.session_connections:
conn = self.session_connections[session_id] conn = self.session_connections[session_id]
try: 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 if (self.pool and
conn.connection and conn.connection and
not conn.connection.closed and not conn.connection.closed):
hasattr(conn.connection, '_reader') and
conn.connection._reader is not None): # 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:
# Try to gracefully return to pool
self.pool.release(conn.connection) self.pool.release(conn.connection)
self.logger.debug(f"Successfully returned connection to pool for session {session_id}")
except Exception as pool_error: except Exception as pool_error:
self.logger.debug(f"Failed to return connection to pool for session {session_id}: {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: try:
await conn.connection.ensure_closed() await conn.connection.ensure_closed()
except Exception: 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 # Close connection wrapper
await conn.close() await conn.close()
@@ -365,24 +502,24 @@ class DorisConnectionManager:
self.logger.error(f"Health check error: {e}") self.logger.error(f"Health check error: {e}")
async def _perform_health_check(self): async def _perform_health_check(self):
"""Perform health check""" """Perform enhanced health check"""
try: try:
unhealthy_sessions = [] unhealthy_sessions = []
# First pass: check basic connectivity # Enhanced health check with comprehensive validation
for session_id, conn in self.session_connections.items(): 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) 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() current_time = datetime.utcnow()
stale_sessions = [] stale_sessions = []
for session_id, conn in self.session_connections.items(): for session_id, conn in self.session_connections.items():
if session_id not in unhealthy_sessions: # Don't double-check if session_id not in unhealthy_sessions: # Don't double-check
last_used_delta = (current_time - conn.last_used).total_seconds() last_used_delta = (current_time - conn.last_used).total_seconds()
if last_used_delta > 1800: # 30 minutes if last_used_delta > 1800: # 30 minutes
# Force a ping check for stale connections # Force a comprehensive health check for stale connections
if not await conn.ping(): if not await self._comprehensive_connection_health_check(conn):
stale_sessions.append(session_id) stale_sessions.append(session_id)
all_problematic_sessions = list(set(unhealthy_sessions + stale_sessions)) all_problematic_sessions = list(set(unhealthy_sessions + stale_sessions))
@@ -453,9 +590,29 @@ class DorisConnectionManager:
async def execute_query( async def execute_query(
self, session_id: str, sql: str, params: tuple | None = None, auth_context=None self, session_id: str, sql: str, params: tuple | None = None, auth_context=None
) -> QueryResult: ) -> QueryResult:
"""Execute query""" """Execute query with enhanced error handling and retry logic"""
conn = await self.get_connection(session_id) max_retries = 2
return await conn.execute(sql, params, auth_context) 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 @asynccontextmanager
async def get_connection_context(self, session_id: str): async def get_connection_context(self, session_id: str):
@@ -500,20 +657,8 @@ class DorisConnectionManager:
self.logger.error(f"Error closing connection manager: {e}") self.logger.error(f"Error closing connection manager: {e}")
async def test_connection(self) -> bool: async def test_connection(self) -> bool:
"""Test database connection""" """Test database connection using robust connection test"""
try: return await self._robust_connection_test()
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
async def diagnose_connection_health(self) -> Dict[str, Any]: async def diagnose_connection_health(self) -> Dict[str, Any]:
"""Diagnose connection pool and session health""" """Diagnose connection pool and session health"""
@@ -680,3 +825,5 @@ class ConnectionPoolMonitor:
report["recommendations"].append("Connection pool utilization is high, consider increasing pool size") report["recommendations"].append("Connection pool utilization is high, consider increasing pool size")
return report return report

328
uv.lock generated
View File

@@ -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 }, { 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]] [[package]]
name = "dparse" name = "dparse"
version = "0.6.4" 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 }, { 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]] [[package]]
name = "mdit-py-plugins" name = "mdit-py-plugins"
version = "0.4.2" version = "0.4.2"