175 lines
8.0 KiB
Python
175 lines
8.0 KiB
Python
# yargitay_mcp_module/client.py
|
|
|
|
import httpx
|
|
from bs4 import BeautifulSoup # Still needed for pre-processing HTML before markitdown
|
|
from typing import Dict, Any, List, Optional
|
|
import logging
|
|
import html
|
|
import re
|
|
import tempfile
|
|
import os
|
|
from markitdown import MarkItDown
|
|
|
|
from .models import (
|
|
YargitayDetailedSearchRequest,
|
|
YargitayApiSearchResponse,
|
|
YargitayApiDecisionEntry,
|
|
YargitayDocumentMarkdown,
|
|
CompactYargitaySearchResult
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
# Basic logging configuration if no handlers are configured
|
|
if not logger.hasHandlers():
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
|
class YargitayOfficialApiClient:
|
|
"""
|
|
API Client for Yargitay's official decision search system.
|
|
Targets the detailed search endpoint (e.g., /aramadetaylist) based on user-provided payload.
|
|
"""
|
|
BASE_URL = "https://karararama.yargitay.gov.tr"
|
|
# The form action was "/detayliArama". This often maps to an API endpoint like "/aramadetaylist".
|
|
# This should be confirmed with the actual API.
|
|
DETAILED_SEARCH_ENDPOINT = "/aramadetaylist"
|
|
DOCUMENT_ENDPOINT = "/getDokuman"
|
|
|
|
def __init__(self, request_timeout: float = 60.0):
|
|
self.http_client = httpx.AsyncClient(
|
|
base_url=self.BASE_URL,
|
|
headers={
|
|
"Content-Type": "application/json; charset=UTF-8",
|
|
"Accept": "application/json, text/plain, */*",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
"X-KL-KIS-Ajax-Request": "Ajax_Request", # Seen in a Yargitay client example
|
|
"Referer": f"{self.BASE_URL}/" # Some APIs might check referer
|
|
},
|
|
timeout=request_timeout,
|
|
verify=False # SSL verification disabled as per original user code - use with caution
|
|
)
|
|
|
|
async def search_detailed_decisions(
|
|
self,
|
|
search_params: YargitayDetailedSearchRequest
|
|
) -> YargitayApiSearchResponse:
|
|
"""
|
|
Performs a detailed search for decisions in Yargitay
|
|
using the structured search_params.
|
|
"""
|
|
# Create the main payload structure with the 'data' key
|
|
request_payload = {"data": search_params.model_dump(exclude_none=True, by_alias=True)}
|
|
|
|
logger.info(f"YargitayOfficialApiClient: Performing detailed search with payload: {request_payload}")
|
|
|
|
try:
|
|
response = await self.http_client.post(self.DETAILED_SEARCH_ENDPOINT, json=request_payload)
|
|
response.raise_for_status() # Raise an exception for HTTP 4xx or 5xx status codes
|
|
response_json_data = response.json()
|
|
|
|
# Validate and parse the response using Pydantic models
|
|
api_response = YargitayApiSearchResponse(**response_json_data)
|
|
|
|
# Populate the document_url for each decision entry
|
|
if api_response.data and api_response.data.data:
|
|
for decision_item in api_response.data.data:
|
|
decision_item.document_url = f"{self.BASE_URL}{self.DOCUMENT_ENDPOINT}?id={decision_item.id}"
|
|
|
|
return api_response
|
|
|
|
except httpx.RequestError as e:
|
|
logger.error(f"YargitayOfficialApiClient: HTTP request error during detailed search: {e}")
|
|
raise # Re-raise to be handled by the calling MCP tool
|
|
except Exception as e: # Catches Pydantic ValidationErrors as well
|
|
logger.error(f"YargitayOfficialApiClient: Error processing or validating detailed search response: {e}")
|
|
raise
|
|
|
|
def _convert_html_to_markdown(self, html_from_api_data_field: str) -> Optional[str]:
|
|
"""
|
|
Takes raw HTML string (from Yargitay API 'data' field for a document),
|
|
pre-processes it, and converts it to Markdown using MarkItDown.
|
|
Returns only the Markdown string or None if conversion fails.
|
|
"""
|
|
if not html_from_api_data_field:
|
|
return None
|
|
|
|
# Pre-process HTML: unescape entities and fix common escaped sequences
|
|
# Based on user's original fix_html_content
|
|
processed_html = html.unescape(html_from_api_data_field)
|
|
processed_html = processed_html.replace('\\"', '"')
|
|
processed_html = processed_html.replace('\\r\\n', '\n')
|
|
processed_html = processed_html.replace('\\n', '\n')
|
|
processed_html = processed_html.replace('\\t', '\t')
|
|
|
|
# MarkItDown often works best with a full HTML document structure.
|
|
# The Yargitay /getDokuman response already provides a full <html>...</html> string.
|
|
# If it were just a fragment, we might wrap it like:
|
|
# html_to_convert = f"<html><head><meta charset=\"UTF-8\"></head><body>{processed_html}</body></html>"
|
|
# But since it's already a full HTML string in "data":
|
|
html_to_convert = processed_html
|
|
|
|
markdown_output = None
|
|
temp_file_path = None
|
|
try:
|
|
md_converter = MarkItDown() # Plugins disabled as per basic usage
|
|
|
|
# Write the HTML to a temporary file for MarkItDown to process
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html", encoding="utf-8") as tmp_html_file:
|
|
tmp_html_file.write(html_to_convert)
|
|
temp_file_path = tmp_html_file.name
|
|
|
|
conversion_result = md_converter.convert(temp_file_path)
|
|
markdown_output = conversion_result.text_content
|
|
|
|
logger.info("Successfully converted HTML to Markdown.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during MarkItDown HTML to Markdown conversion: {e}")
|
|
finally:
|
|
if temp_file_path and os.path.exists(temp_file_path):
|
|
os.remove(temp_file_path) # Clean up the temporary file
|
|
|
|
return markdown_output
|
|
|
|
async def get_decision_document_as_markdown(self, document_id: str) -> YargitayDocumentMarkdown:
|
|
"""
|
|
Retrieves a specific Yargitay decision by its ID and returns its content
|
|
as Markdown.
|
|
Based on user-provided /getDokuman response structure.
|
|
"""
|
|
document_api_url = f"{self.DOCUMENT_ENDPOINT}?id={document_id}"
|
|
source_url = f"{self.BASE_URL}{document_api_url}" # The original URL of the document
|
|
logger.info(f"YargitayOfficialApiClient: Fetching document for Markdown conversion (ID: {document_id})")
|
|
|
|
try:
|
|
response = await self.http_client.get(document_api_url)
|
|
response.raise_for_status()
|
|
|
|
# Expecting JSON response with HTML content in the 'data' field
|
|
response_json = response.json()
|
|
html_content_from_api = response_json.get("data")
|
|
|
|
if not isinstance(html_content_from_api, str):
|
|
logger.error(f"YargitayOfficialApiClient: 'data' field in API response is not a string or not found (ID: {document_id}).")
|
|
raise ValueError("Expected HTML content not found in API response's 'data' field.")
|
|
|
|
markdown_content = self._convert_html_to_markdown(html_content_from_api)
|
|
|
|
return YargitayDocumentMarkdown(
|
|
document_id=document_id,
|
|
markdown_content=markdown_content,
|
|
source_url=source_url
|
|
)
|
|
except httpx.RequestError as e:
|
|
logger.error(f"YargitayOfficialApiClient: HTTP error fetching document for Markdown (ID: {document_id}): {e}")
|
|
raise
|
|
except ValueError as e: # For JSON parsing errors or missing 'data' field
|
|
logger.error(f"YargitayOfficialApiClient: Error processing document response for Markdown (ID: {document_id}): {e}")
|
|
raise
|
|
except Exception as e: # For other unexpected errors
|
|
logger.error(f"YargitayOfficialApiClient: General error fetching/processing document for Markdown (ID: {document_id}): {e}")
|
|
raise
|
|
|
|
async def close_client_session(self):
|
|
"""Closes the HTTPX client session."""
|
|
await self.http_client.aclose()
|
|
logger.info("YargitayOfficialApiClient: HTTP client session closed.") |