fix httpx efficiency

This commit is contained in:
saidsurucu
2025-07-21 22:38:52 +03:00
parent 90a7a23064
commit 673f996f5f
2 changed files with 141 additions and 113 deletions
+94 -75
View File
@@ -348,6 +348,19 @@ from fastmcp import FastMCP
# Placeholder app for decorators - will be replaced in create_app() after all tools are defined # Placeholder app for decorators - will be replaced in create_app() after all tools are defined
app = FastMCP("Yargı MCP Server Placeholder") app = FastMCP("Yargı MCP Server Placeholder")
# --- Shared HTTP Client for Health Checks ---
shared_health_check_client = None
def get_or_create_health_check_client():
"""Get or create shared httpx client for health checks."""
global shared_health_check_client
if shared_health_check_client is None:
shared_health_check_client = httpx.AsyncClient(
timeout=30.0,
verify=False
)
return shared_health_check_client
# --- API Client Instances --- # --- API Client Instances ---
yargitay_client_instance = YargitayOfficialApiClient() yargitay_client_instance = YargitayOfficialApiClient()
danistay_client_instance = DanistayApiClient() danistay_client_instance = DanistayApiClient()
@@ -1417,6 +1430,14 @@ def perform_cleanup():
] ]
async def close_all_clients_async(): async def close_all_clients_async():
tasks = [] tasks = []
# Close shared health check client first
global shared_health_check_client
if shared_health_check_client:
logger.info("Scheduling close for shared health check client")
tasks.append(shared_health_check_client.aclose())
# Close all module clients
for client_instance in clients_to_close: for client_instance in clients_to_close:
if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session): if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}") logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
@@ -1467,50 +1488,49 @@ async def check_government_servers_health() -> Dict[str, Any]:
} }
} }
async with httpx.AsyncClient( client = get_or_create_health_check_client()
headers={ headers = {
"Accept": "*/*", "Accept": "*/*",
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
"Connection": "keep-alive", "Connection": "keep-alive",
"Content-Type": "application/json; charset=UTF-8", "Content-Type": "application/json; charset=UTF-8",
"Origin": "https://karararama.yargitay.gov.tr", "Origin": "https://karararama.yargitay.gov.tr",
"Referer": "https://karararama.yargitay.gov.tr/", "Referer": "https://karararama.yargitay.gov.tr/",
"Sec-Fetch-Dest": "empty", "Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin", "Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"X-Requested-With": "XMLHttpRequest" "X-Requested-With": "XMLHttpRequest"
}, }
timeout=30.0,
verify=False response = await client.post(
) as client: "https://karararama.yargitay.gov.tr/aramalist",
response = await client.post( json=yargitay_payload,
"https://karararama.yargitay.gov.tr/aramalist", headers=headers
json=yargitay_payload )
)
if response.status_code == 200:
response_data = response.json()
records_total = response_data.get("data", {}).get("recordsTotal", 0)
if response.status_code == 200: if records_total > 0:
response_data = response.json() health_results["yargitay"] = {
records_total = response_data.get("data", {}).get("recordsTotal", 0) "status": "healthy",
"response_time_ms": response.elapsed.total_seconds() * 1000
if records_total > 0: }
health_results["yargitay"] = {
"status": "healthy",
"response_time_ms": response.elapsed.total_seconds() * 1000
}
else:
health_results["yargitay"] = {
"status": "unhealthy",
"reason": "recordsTotal is 0 or missing",
"response_time_ms": response.elapsed.total_seconds() * 1000
}
else: else:
health_results["yargitay"] = { health_results["yargitay"] = {
"status": "unhealthy", "status": "unhealthy",
"reason": f"HTTP {response.status_code}", "reason": "recordsTotal is 0 or missing",
"response_time_ms": response.elapsed.total_seconds() * 1000 "response_time_ms": response.elapsed.total_seconds() * 1000
} }
else:
health_results["yargitay"] = {
"status": "unhealthy",
"reason": f"HTTP {response.status_code}",
"response_time_ms": response.elapsed.total_seconds() * 1000
}
except Exception as e: except Exception as e:
health_results["yargitay"] = { health_results["yargitay"] = {
"status": "unhealthy", "status": "unhealthy",
@@ -1532,50 +1552,49 @@ async def check_government_servers_health() -> Dict[str, Any]:
"paging": True "paging": True
} }
async with httpx.AsyncClient( client = get_or_create_health_check_client()
headers={ headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", "Accept": "application/json",
"User-Agent": "Mozilla/5.0 Health Check" "User-Agent": "Mozilla/5.0 Health Check"
}, }
timeout=30.0,
verify=False response = await client.post(
) as client: "https://bedesten.adalet.gov.tr/emsal-karar/searchDocuments",
response = await client.post( json=bedesten_payload,
"https://bedesten.adalet.gov.tr/emsal-karar/searchDocuments", headers=headers
json=bedesten_payload )
)
if response.status_code == 200:
if response.status_code == 200: response_data = response.json()
response_data = response.json() logger.debug(f"Bedesten API response: {response_data}")
logger.debug(f"Bedesten API response: {response_data}") if response_data and isinstance(response_data, dict):
if response_data and isinstance(response_data, dict): data_section = response_data.get("data")
data_section = response_data.get("data") if data_section and isinstance(data_section, dict):
if data_section and isinstance(data_section, dict): total_found = data_section.get("total", 0)
total_found = data_section.get("total", 0)
else:
total_found = 0
else: else:
total_found = 0 total_found = 0
else:
if total_found > 0: total_found = 0
health_results["bedesten"] = {
"status": "healthy", if total_found > 0:
"response_time_ms": response.elapsed.total_seconds() * 1000 health_results["bedesten"] = {
} "status": "healthy",
else: "response_time_ms": response.elapsed.total_seconds() * 1000
health_results["bedesten"] = { }
"status": "unhealthy",
"reason": "total is 0 or missing in data field",
"response_time_ms": response.elapsed.total_seconds() * 1000
}
else: else:
health_results["bedesten"] = { health_results["bedesten"] = {
"status": "unhealthy", "status": "unhealthy",
"reason": f"HTTP {response.status_code}", "reason": "total is 0 or missing in data field",
"response_time_ms": response.elapsed.total_seconds() * 1000 "response_time_ms": response.elapsed.total_seconds() * 1000
} }
else:
health_results["bedesten"] = {
"status": "unhealthy",
"reason": f"HTTP {response.status_code}",
"response_time_ms": response.elapsed.total_seconds() * 1000
}
except Exception as e: except Exception as e:
health_results["bedesten"] = { health_results["bedesten"] = {
"status": "unhealthy", "status": "unhealthy",
+47 -38
View File
@@ -1,7 +1,6 @@
# uyusmazlik_mcp_module/client.py # uyusmazlik_mcp_module/client.py
import httpx import httpx
import aiohttp
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from typing import Dict, Any, List, Optional, Union, Tuple from typing import Dict, Any, List, Optional, Union, Tuple
import logging import logging
@@ -9,7 +8,7 @@ import html
import re import re
import io import io
from markitdown import MarkItDown from markitdown import MarkItDown
from urllib.parse import urljoin, urlencode # urlencode for aiohttp form data from urllib.parse import urljoin
from .models import ( from .models import (
UyusmazlikSearchRequest, UyusmazlikSearchRequest,
@@ -56,17 +55,21 @@ class UyusmazlikApiClient:
# Individual documents are fetched by their full URLs obtained from search results. # Individual documents are fetched by their full URLs obtained from search results.
def __init__(self, request_timeout: float = 30.0): def __init__(self, request_timeout: float = 30.0):
self.request_timeout = request_timeout # Store timeout for aiohttp and httpx self.request_timeout = request_timeout
# Headers for aiohttp search. httpx for docs will create its own. # Create shared httpx client for all requests
self.default_aiohttp_search_headers = { self.http_client = httpx.AsyncClient(
"Accept": "*/*", # Mimicking browser headers provided by user base_url=self.BASE_URL,
"Accept-Encoding": "gzip, deflate, br, zstd", headers={
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", "Accept": "*/*",
"X-Requested-With": "XMLHttpRequest", "Accept-Encoding": "gzip, deflate, br, zstd",
"Origin": self.BASE_URL, "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
"Referer": self.BASE_URL + "/", "X-Requested-With": "XMLHttpRequest",
"Origin": self.BASE_URL,
} "Referer": self.BASE_URL + "/",
},
timeout=request_timeout,
verify=False
)
async def search_decisions( async def search_decisions(
@@ -107,32 +110,36 @@ class UyusmazlikApiClient:
add_to_form_data("Hepsi", params.hepsi) add_to_form_data("Hepsi", params.hepsi)
add_to_form_data("Herhangibirisi", params.herhangi_birisi) add_to_form_data("Herhangibirisi", params.herhangi_birisi)
add_to_form_data("NotHepsi", params.not_hepsi) add_to_form_data("NotHepsi", params.not_hepsi)
# X-Requested-With is handled by default_aiohttp_search_headers
search_url = urljoin(self.BASE_URL, self.SEARCH_ENDPOINT) # Convert form data to dict for httpx
# For aiohttp, data for application/x-www-form-urlencoded should be a dict or str. form_data_dict = {}
# Using urlencode for list of tuples. for key, value in form_data_list:
encoded_form_payload = urlencode(form_data_list, encoding='UTF-8') if key in form_data_dict:
# Handle multiple values (like KararSonucuList)
if not isinstance(form_data_dict[key], list):
form_data_dict[key] = [form_data_dict[key]]
form_data_dict[key].append(value)
else:
form_data_dict[key] = value
logger.info(f"UyusmazlikApiClient (aiohttp): Performing search to {search_url} with form_data: {encoded_form_payload}") logger.info(f"UyusmazlikApiClient (httpx): Performing search to {self.SEARCH_ENDPOINT} with form_data: {form_data_dict}")
html_content = ""
aiohttp_headers = self.default_aiohttp_search_headers.copy()
aiohttp_headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
try: try:
# Create a new session for each call for simplicity with aiohttp here # Use shared httpx client
async with aiohttp.ClientSession(headers=aiohttp_headers) as session: response = await self.http_client.post(
async with session.post(search_url, data=encoded_form_payload, timeout=self.request_timeout) as response: self.SEARCH_ENDPOINT,
response.raise_for_status() # Raises ClientResponseError for 400-599 data=form_data_dict,
html_content = await response.text(encoding='utf-8') # Ensure correct encoding headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
logger.debug("UyusmazlikApiClient (aiohttp): Received HTML response for search.") )
response.raise_for_status()
html_content = response.text
logger.debug("UyusmazlikApiClient (httpx): Received HTML response for search.")
except aiohttp.ClientError as e: except httpx.HTTPError as e:
logger.error(f"UyusmazlikApiClient (aiohttp): HTTP client error during search: {e}") logger.error(f"UyusmazlikApiClient (httpx): HTTP client error during search: {e}")
raise # Re-raise to be handled by the MCP tool raise # Re-raise to be handled by the MCP tool
except Exception as e: except Exception as e:
logger.error(f"UyusmazlikApiClient (aiohttp): Error processing search request: {e}") logger.error(f"UyusmazlikApiClient (httpx): Error processing search request: {e}")
raise raise
# --- HTML Parsing (remains the same as previous version) --- # --- HTML Parsing (remains the same as previous version) ---
@@ -215,10 +222,8 @@ class UyusmazlikApiClient:
""" """
logger.info(f"UyusmazlikApiClient (httpx for docs): Fetching Uyuşmazlık document for Markdown from URL: {document_url}") logger.info(f"UyusmazlikApiClient (httpx for docs): Fetching Uyuşmazlık document for Markdown from URL: {document_url}")
try: try:
# Using a new httpx.AsyncClient instance for this GET request for simplicity # Use the existing shared http_client instead of creating a new one
async with httpx.AsyncClient(verify=False, timeout=self.request_timeout) as doc_fetch_client: get_response = await self.http_client.get(document_url, headers={"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"})
get_response = await doc_fetch_client.get(document_url, headers={"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"})
get_response.raise_for_status() get_response.raise_for_status()
html_content_from_api = get_response.text html_content_from_api = get_response.text
@@ -236,5 +241,9 @@ class UyusmazlikApiClient:
raise raise
async def close_client_session(self): async def close_client_session(self):
"""Close the shared httpx client session."""
logger.info("UyusmazlikApiClient: No persistent client session from __init__ to close.") if hasattr(self, 'http_client') and self.http_client:
await self.http_client.aclose()
logger.info("UyusmazlikApiClient: HTTP client session closed.")
else:
logger.info("UyusmazlikApiClient: No HTTP client session to close.")