Skip to content

Commit c8e41e1

Browse files
committed
Fix lint issues, rebae
Signed-off-by: Mihai Criveti <[email protected]>
1 parent 91edccf commit c8e41e1

File tree

9 files changed

+156
-107
lines changed

9 files changed

+156
-107
lines changed

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ contextmanager-decorators=contextlib.contextmanager
575575
# List of members which are set dynamically and missed by pylint inference
576576
# system, and so shouldn't trigger E1101 when accessed. Python regular
577577
# expressions are accepted.
578-
generated-members=
578+
generated-members=sqlalchemy.func.*
579579

580580
# Tells whether to warn about missing members when the owner of the attribute
581581
# is inferred to be None.

mcpgateway/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ def prevent_manual_mcp_creation(cls, values: Dict[str, Any]) -> Dict[str, Any]:
590590
"""
591591
integration_type = values.get("integration_type")
592592
if integration_type == "MCP":
593-
raise ValueError("Cannot manually create MCP tools. Add MCP servers via the Gateways interface - " "tools will be auto-discovered and registered with integration_type='MCP'.")
593+
raise ValueError("Cannot manually create MCP tools. Add MCP servers via the Gateways interface - tools will be auto-discovered and registered with integration_type='MCP'.")
594594
return values
595595

596596

mcpgateway/services/logging_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ async def initialize(self) -> None:
123123
try:
124124
root_logger.addHandler(_get_file_handler())
125125
if settings.log_rotation_enabled:
126-
logging.info(f"File logging enabled with rotation: {settings.log_folder or '.'}/{settings.log_file} " f"(max: {settings.log_max_size_mb}MB, backups: {settings.log_backup_count})")
126+
logging.info(f"File logging enabled with rotation: {settings.log_folder or '.'}/{settings.log_file} (max: {settings.log_max_size_mb}MB, backups: {settings.log_backup_count})")
127127
else:
128128
logging.info(f"File logging enabled (no rotation): {settings.log_folder or '.'}/{settings.log_file}")
129129
except Exception as e:

mcpgateway/services/prompt_service.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
# Third-Party
2525
from jinja2 import Environment, meta, select_autoescape
26-
from sqlalchemy import case, delete, desc, func, not_, select
26+
from sqlalchemy import case, delete, desc, func, not_, select, Float
2727
from sqlalchemy.exc import IntegrityError
2828
from sqlalchemy.orm import Session
2929

@@ -35,6 +35,7 @@
3535
from mcpgateway.plugins import GlobalContext, PluginManager, PluginViolationError, PromptPosthookPayload, PromptPrehookPayload
3636
from mcpgateway.schemas import PromptCreate, PromptRead, PromptUpdate, TopPerformer
3737
from mcpgateway.services.logging_service import LoggingService
38+
from mcpgateway.utils.metrics_common import build_top_performers
3839

3940
# Initialize logging service first
4041
logging_service = LoggingService()
@@ -165,9 +166,13 @@ async def get_top_prompts(self, db: Session, limit: int = 5) -> List[TopPerforme
165166
DbPrompt.name,
166167
func.count(PromptMetric.id).label("execution_count"), # pylint: disable=not-callable
167168
func.avg(PromptMetric.response_time).label("avg_response_time"), # pylint: disable=not-callable
168-
case((func.count(PromptMetric.id) > 0, func.sum(case((PromptMetric.is_success, 1), else_=0)) / func.count(PromptMetric.id) * 100), else_=None).label(
169-
"success_rate"
170-
), # pylint: disable=not-callable
169+
case(
170+
(
171+
func.count(PromptMetric.id) > 0, # pylint: disable=not-callable
172+
func.sum(case((PromptMetric.is_success.is_(True), 1), else_=0)).cast(Float) / func.count(PromptMetric.id) * 100, # pylint: disable=not-callable
173+
),
174+
else_=None,
175+
).label("success_rate"),
171176
func.max(PromptMetric.timestamp).label("last_execution"), # pylint: disable=not-callable
172177
)
173178
.outerjoin(PromptMetric)
@@ -177,17 +182,7 @@ async def get_top_prompts(self, db: Session, limit: int = 5) -> List[TopPerforme
177182
.all()
178183
)
179184

180-
return [
181-
TopPerformer(
182-
id=result.id,
183-
name=result.name,
184-
execution_count=result.execution_count or 0,
185-
avg_response_time=float(result.avg_response_time) if result.avg_response_time else None,
186-
success_rate=float(result.success_rate) if result.success_rate else None,
187-
last_execution=result.last_execution,
188-
)
189-
for result in results
190-
]
185+
return build_top_performers(results)
191186

192187
def _convert_db_prompt(self, db_prompt: DbPrompt) -> Dict[str, Any]:
193188
"""

mcpgateway/services/resource_service.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
# Third-Party
3535
import parse
36-
from sqlalchemy import case, delete, desc, func, not_, select
36+
from sqlalchemy import case, delete, desc, func, not_, select, Float
3737
from sqlalchemy.exc import IntegrityError
3838
from sqlalchemy.orm import Session
3939

@@ -45,6 +45,7 @@
4545
from mcpgateway.models import ResourceContent, ResourceTemplate, TextContent
4646
from mcpgateway.schemas import ResourceCreate, ResourceMetrics, ResourceRead, ResourceSubscription, ResourceUpdate, TopPerformer
4747
from mcpgateway.services.logging_service import LoggingService
48+
from mcpgateway.utils.metrics_common import build_top_performers
4849

4950
# Initialize logging service first
5051
logging_service = LoggingService()
@@ -138,9 +139,13 @@ async def get_top_resources(self, db: Session, limit: int = 5) -> List[TopPerfor
138139
DbResource.uri.label("name"), # Using URI as the name field for TopPerformer
139140
func.count(ResourceMetric.id).label("execution_count"), # pylint: disable=not-callable
140141
func.avg(ResourceMetric.response_time).label("avg_response_time"), # pylint: disable=not-callable
141-
case((func.count(ResourceMetric.id) > 0, func.sum(case((ResourceMetric.is_success, 1), else_=0)) / func.count(ResourceMetric.id) * 100), else_=None).label(
142-
"success_rate"
143-
), # pylint: disable=not-callable
142+
case(
143+
(
144+
func.count(ResourceMetric.id) > 0, # pylint: disable=not-callable
145+
func.sum(case((ResourceMetric.is_success.is_(True), 1), else_=0)).cast(Float) / func.count(ResourceMetric.id) * 100, # pylint: disable=not-callable
146+
),
147+
else_=None,
148+
).label("success_rate"),
144149
func.max(ResourceMetric.timestamp).label("last_execution"), # pylint: disable=not-callable
145150
)
146151
.outerjoin(ResourceMetric)
@@ -150,17 +155,7 @@ async def get_top_resources(self, db: Session, limit: int = 5) -> List[TopPerfor
150155
.all()
151156
)
152157

153-
return [
154-
TopPerformer(
155-
id=result.id,
156-
name=result.name,
157-
execution_count=result.execution_count or 0,
158-
avg_response_time=float(result.avg_response_time) if result.avg_response_time else None,
159-
success_rate=float(result.success_rate) if result.success_rate else None,
160-
last_execution=result.last_execution,
161-
)
162-
for result in results
163-
]
158+
return build_top_performers(results)
164159

165160
def _convert_resource_to_read(self, resource: DbResource) -> ResourceRead:
166161
"""

mcpgateway/services/server_service.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
# Third-Party
2121
import httpx
22-
from sqlalchemy import case, delete, desc, func, not_, select
22+
from sqlalchemy import case, delete, desc, func, not_, select, Float
2323
from sqlalchemy.exc import IntegrityError
2424
from sqlalchemy.orm import Session
2525

@@ -32,6 +32,7 @@
3232
from mcpgateway.db import Tool as DbTool
3333
from mcpgateway.schemas import ServerCreate, ServerMetrics, ServerRead, ServerUpdate, TopPerformer
3434
from mcpgateway.services.logging_service import LoggingService
35+
from mcpgateway.utils.metrics_common import build_top_performers
3536

3637
# Initialize logging service first
3738
logging_service = LoggingService()
@@ -153,9 +154,13 @@ async def get_top_servers(self, db: Session, limit: int = 5) -> List[TopPerforme
153154
DbServer.name,
154155
func.count(ServerMetric.id).label("execution_count"), # pylint: disable=not-callable
155156
func.avg(ServerMetric.response_time).label("avg_response_time"), # pylint: disable=not-callable
156-
case((func.count(ServerMetric.id) > 0, func.sum(case((ServerMetric.is_success, 1), else_=0)) / func.count(ServerMetric.id) * 100), else_=None).label(
157-
"success_rate"
158-
), # pylint: disable=not-callable
157+
case(
158+
(
159+
func.count(ServerMetric.id) > 0, # pylint: disable=not-callable
160+
func.sum(case((ServerMetric.is_success.is_(True), 1), else_=0)).cast(Float) / func.count(ServerMetric.id) * 100, # pylint: disable=not-callable
161+
),
162+
else_=None,
163+
).label("success_rate"),
159164
func.max(ServerMetric.timestamp).label("last_execution"), # pylint: disable=not-callable
160165
)
161166
.outerjoin(ServerMetric)
@@ -165,17 +170,7 @@ async def get_top_servers(self, db: Session, limit: int = 5) -> List[TopPerforme
165170
.all()
166171
)
167172

168-
return [
169-
TopPerformer(
170-
id=result.id,
171-
name=result.name,
172-
execution_count=result.execution_count or 0,
173-
avg_response_time=float(result.avg_response_time) if result.avg_response_time else None,
174-
success_rate=float(result.success_rate) if result.success_rate else None,
175-
last_execution=result.last_execution,
176-
)
177-
for result in results
178-
]
173+
return build_top_performers(results)
179174

180175
def _convert_server_to_read(self, server: DbServer) -> ServerRead:
181176
"""

mcpgateway/services/tool_service.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from mcp import ClientSession
2929
from mcp.client.sse import sse_client
3030
from mcp.client.streamable_http import streamablehttp_client
31-
from sqlalchemy import case, delete, desc, func, not_, select
31+
from sqlalchemy import case, delete, desc, func, not_, select, Float
3232
from sqlalchemy.exc import IntegrityError
3333
from sqlalchemy.orm import Session
3434

@@ -44,6 +44,7 @@
4444
from mcpgateway.schemas import ToolCreate, ToolRead, ToolUpdate, TopPerformer
4545
from mcpgateway.services.logging_service import LoggingService
4646
from mcpgateway.utils.create_slug import slugify
47+
from mcpgateway.utils.metrics_common import build_top_performers
4748
from mcpgateway.utils.passthrough_headers import get_passthrough_headers
4849
from mcpgateway.utils.retry_manager import ResilientHttpClient
4950
from mcpgateway.utils.services_auth import decode_auth
@@ -215,9 +216,13 @@ async def get_top_tools(self, db: Session, limit: int = 5) -> List[TopPerformer]
215216
DbTool.name,
216217
func.count(ToolMetric.id).label("execution_count"), # pylint: disable=not-callable
217218
func.avg(ToolMetric.response_time).label("avg_response_time"), # pylint: disable=not-callable
218-
case((func.count(ToolMetric.id) > 0, func.sum(case((ToolMetric.is_success, 1), else_=0)) / func.count(ToolMetric.id) * 100), else_=None).label(
219-
"success_rate"
220-
), # pylint: disable=not-callable
219+
case(
220+
(
221+
func.count(ToolMetric.id) > 0, # pylint: disable=not-callable
222+
func.sum(case((ToolMetric.is_success.is_(True), 1), else_=0)).cast(Float) / func.count(ToolMetric.id) * 100, # pylint: disable=not-callable
223+
),
224+
else_=None,
225+
).label("success_rate"),
221226
func.max(ToolMetric.timestamp).label("last_execution"), # pylint: disable=not-callable
222227
)
223228
.outerjoin(ToolMetric)
@@ -227,17 +232,7 @@ async def get_top_tools(self, db: Session, limit: int = 5) -> List[TopPerformer]
227232
.all()
228233
)
229234

230-
return [
231-
TopPerformer(
232-
id=result.id,
233-
name=result.name,
234-
execution_count=result.execution_count or 0,
235-
avg_response_time=float(result.avg_response_time) if result.avg_response_time else None,
236-
success_rate=float(result.success_rate) if result.success_rate else None,
237-
last_execution=result.last_execution,
238-
)
239-
for result in results
240-
]
235+
return build_top_performers(results)
241236

242237
def _convert_tool_to_read(self, tool: DbTool) -> ToolRead:
243238
"""Converts a DbTool instance into a ToolRead model, including aggregated metrics and
@@ -734,18 +729,21 @@ async def invoke_tool(self, db: Session, name: str, arguments: Dict[str, Any], r
734729
# Handle 204 No Content responses that have no body
735730
if response.status_code == 204:
736731
tool_result = ToolResult(content=[TextContent(type="text", text="Request completed successfully (No Content)")])
732+
# Mark as successful only after all operations complete successfully
733+
success = True
737734
elif response.status_code not in [200, 201, 202, 206]:
738735
result = response.json()
739736
tool_result = ToolResult(
740737
content=[TextContent(type="text", text=str(result["error"]) if "error" in result else "Tool error encountered")],
741738
is_error=True,
742739
)
740+
# Don't mark as successful for error responses - success remains False
743741
else:
744742
result = response.json()
745743
filtered_response = extract_using_jq(result, tool.jsonpath_filter)
746744
tool_result = ToolResult(content=[TextContent(type="text", text=json.dumps(filtered_response, indent=2))])
747-
748-
success = True
745+
# Mark as successful only after all operations complete successfully
746+
success = True
749747
elif tool.integration_type == "MCP":
750748
transport = tool.request_type.lower()
751749
gateway = db.execute(select(DbGateway).where(DbGateway.id == tool.gateway_id).where(DbGateway.enabled)).scalar_one_or_none()
@@ -795,9 +793,10 @@ async def connect_to_streamablehttp_server(server_url: str):
795793
tool_call_result = await connect_to_streamablehttp_server(tool_gateway.url)
796794
content = tool_call_result.model_dump(by_alias=True).get("content", [])
797795

798-
success = True
799796
filtered_response = extract_using_jq(content, tool.jsonpath_filter)
800797
tool_result = ToolResult(content=filtered_response)
798+
# Mark as successful only after all operations complete successfully
799+
success = True
801800
else:
802801
tool_result = ToolResult(content=[TextContent(type="text", text="Invalid tool type")])
803802

mcpgateway/utils/metrics_common.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Common utilities for metrics handling across service modules.
4+
5+
Copyright 2025
6+
SPDX-License-Identifier: Apache-2.0
7+
Authors: Mihai Criveti
8+
"""
9+
10+
# Standard
11+
from typing import List
12+
13+
# First-Party
14+
from mcpgateway.schemas import TopPerformer
15+
16+
17+
def build_top_performers(results: List) -> List[TopPerformer]:
18+
"""
19+
Convert database query results to TopPerformer objects.
20+
21+
This utility function eliminates code duplication across service modules
22+
that need to convert database query results with metrics into TopPerformer objects.
23+
24+
Args:
25+
results: List of database query results, each containing:
26+
- id: Entity ID
27+
- name: Entity name
28+
- execution_count: Total executions
29+
- avg_response_time: Average response time
30+
- success_rate: Success rate percentage
31+
- last_execution: Last execution timestamp
32+
33+
Returns:
34+
List[TopPerformer]: List of TopPerformer objects with proper type conversions
35+
36+
Examples:
37+
>>> from unittest.mock import MagicMock
38+
>>> result = MagicMock()
39+
>>> result.id = 1
40+
>>> result.name = "test"
41+
>>> result.execution_count = 10
42+
>>> result.avg_response_time = 1.5
43+
>>> result.success_rate = 85.0
44+
>>> result.last_execution = None
45+
>>> performers = build_top_performers([result])
46+
>>> len(performers)
47+
1
48+
>>> performers[0].id
49+
1
50+
>>> performers[0].execution_count
51+
10
52+
>>> performers[0].avg_response_time
53+
1.5
54+
"""
55+
return [
56+
TopPerformer(
57+
id=result.id,
58+
name=result.name,
59+
execution_count=result.execution_count or 0,
60+
avg_response_time=float(result.avg_response_time) if result.avg_response_time else None,
61+
success_rate=float(result.success_rate) if result.success_rate else None,
62+
last_execution=result.last_execution,
63+
)
64+
for result in results
65+
]

0 commit comments

Comments
 (0)