perf(server): unblock event loop on rate-limit waits and markitdown
Two complementary changes to mitigate intermittent TLS handshake
timeouts and "notifications/cancelled: Bad Request" seen against the
single-worker uvicorn deployment.
1. bedesten rate-limiter back-pressure
- Add optional ``max_wait`` to ``_TokenBucket.acquire``: if the next
wait would exceed it, raise ``BedestenRateLimited`` immediately
instead of sleeping. After a server-side 429 the bucket pauses for
up to 30s; previously a queued request sat in ``asyncio.sleep``
for that whole window, holding the worker slot and pushing the
MCP client past its cancellation timeout.
- ``search_bedesten_unified`` / ``get_bedesten_document_markdown``
catch ``BedestenRateLimited`` and reuse the existing structured
429-style response, so callers get a fast, clean retry signal.
- Tunable via ``BEDESTEN_RATE_MAX_WAIT_S`` (default 8.0s).
2. Offload sync markitdown conversions to a thread
- Every ``markitdown.convert*`` call site is now wrapped in
``asyncio.to_thread(...)`` across 14 modules (bedesten, yargitay,
danistay, anayasa norm + bireysel, uyusmazlik, emsal, rekabet,
gib, kvkk, sayistay, bddk, sigorta_tahkim, kik_v2). PDF / large
HTML parsing was stalling the event loop for seconds, which on a
single-worker deployment delayed every other in-flight request
and queued new TLS handshakes until they timed out.
Verified locally:
- ``ast.parse`` + ``importlib.import_module`` on all 15 modified files
- ``mcp_server_main.create_app()`` constructs successfully
- New ``_TokenBucket.acquire(max_wait=...)`` smoke-tested across 6
paths: capacity-available, no-arg backward compat, max_wait raise,
max_wait wait+succeed, ``penalize_until`` + max_wait fast-raise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
26aa3dacc6
commit
96a5a538b2
@@ -1,6 +1,7 @@
|
|||||||
# anayasa_mcp_module/bireysel_client.py
|
# anayasa_mcp_module/bireysel_client.py
|
||||||
# This client is for Bireysel Başvuru: https://kararlarbilgibankasi.anayasa.gov.tr
|
# This client is for Bireysel Başvuru: https://kararlarbilgibankasi.anayasa.gov.tr
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from bs4 import BeautifulSoup, Tag
|
from bs4 import BeautifulSoup, Tag
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
@@ -302,7 +303,7 @@ class AnayasaBireyselBasvuruApiClient:
|
|||||||
elif "Karar Tarihi" in key and not karar_tarihi_from_page: karar_tarihi_from_page = value
|
elif "Karar Tarihi" in key and not karar_tarihi_from_page: karar_tarihi_from_page = value
|
||||||
elif "Resmi Gazete Tarih / Sayı" in key: resmi_gazete_info_from_page = value
|
elif "Resmi Gazete Tarih / Sayı" in key: resmi_gazete_info_from_page = value
|
||||||
|
|
||||||
full_markdown_content = self._convert_html_to_markdown_bireysel(html_content_from_api)
|
full_markdown_content = await asyncio.to_thread(self._convert_html_to_markdown_bireysel, html_content_from_api)
|
||||||
|
|
||||||
if not full_markdown_content:
|
if not full_markdown_content:
|
||||||
return AnayasaBireyselBasvuruDocumentMarkdown(
|
return AnayasaBireyselBasvuruDocumentMarkdown(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# anayasa_mcp_module/client.py
|
# anayasa_mcp_module/client.py
|
||||||
# This client is for Norm Denetimi: https://normkararlarbilgibankasi.anayasa.gov.tr
|
# This client is for Norm Denetimi: https://normkararlarbilgibankasi.anayasa.gov.tr
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
@@ -309,7 +310,7 @@ class AnayasaMahkemesiApiClient:
|
|||||||
official_gazette_from_page = rg_text_content.replace("Resmî Gazete tarih ve sayısı:", "").replace("Resmi Gazete tarih/sayı:", "").strip()
|
official_gazette_from_page = rg_text_content.replace("Resmî Gazete tarih ve sayısı:", "").replace("Resmi Gazete tarih/sayı:", "").strip()
|
||||||
|
|
||||||
|
|
||||||
full_markdown_content = self._convert_html_to_markdown_norm_denetimi(html_content_from_api)
|
full_markdown_content = await asyncio.to_thread(self._convert_html_to_markdown_norm_denetimi, html_content_from_api)
|
||||||
|
|
||||||
if not full_markdown_content:
|
if not full_markdown_content:
|
||||||
return AnayasaDocumentMarkdown(
|
return AnayasaDocumentMarkdown(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# bddk_mcp_module/client.py
|
# bddk_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
import logging
|
import logging
|
||||||
@@ -210,14 +211,19 @@ class BddkApiClient:
|
|||||||
|
|
||||||
# Convert to Markdown based on content type
|
# Convert to Markdown based on content type
|
||||||
if "pdf" in content_type:
|
if "pdf" in content_type:
|
||||||
# Handle PDF documents
|
# Handle PDF documents. markitdown is sync; offload to thread
|
||||||
|
# so PDF parsing doesn't block the event-loop / other requests.
|
||||||
pdf_stream = io.BytesIO(response.content)
|
pdf_stream = io.BytesIO(response.content)
|
||||||
result = self.markitdown.convert_stream(pdf_stream, file_extension=".pdf")
|
result = await asyncio.to_thread(
|
||||||
|
self.markitdown.convert_stream, pdf_stream, file_extension=".pdf"
|
||||||
|
)
|
||||||
markdown_content = result.text_content
|
markdown_content = result.text_content
|
||||||
else:
|
else:
|
||||||
# Handle HTML documents
|
# Handle HTML documents (sync conversion offloaded to thread)
|
||||||
html_stream = io.BytesIO(response.content)
|
html_stream = io.BytesIO(response.content)
|
||||||
result = self.markitdown.convert_stream(html_stream, file_extension=".html")
|
result = await asyncio.to_thread(
|
||||||
|
self.markitdown.convert_stream, html_stream, file_extension=".html"
|
||||||
|
)
|
||||||
markdown_content = result.text_content
|
markdown_content = result.text_content
|
||||||
|
|
||||||
# Clean up the markdown content
|
# Clean up the markdown content
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ from .enums import get_full_birim_adi
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BedestenRateLimited(Exception):
|
||||||
|
"""Raised when the local rate-limit bucket would block longer than allowed.
|
||||||
|
|
||||||
|
Carries the suggested retry-after (seconds) so callers can surface a
|
||||||
|
structured 429-style response to the MCP client instead of silently
|
||||||
|
blocking the event-loop slot for the full bucket-pause window.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, retry_after: float) -> None:
|
||||||
|
self.retry_after = retry_after
|
||||||
|
super().__init__(f"local bucket would block {retry_after:.1f}s")
|
||||||
|
|
||||||
|
|
||||||
class _TokenBucket:
|
class _TokenBucket:
|
||||||
"""Asyncio token bucket with explicit back-pressure.
|
"""Asyncio token bucket with explicit back-pressure.
|
||||||
|
|
||||||
@@ -40,7 +53,12 @@ class _TokenBucket:
|
|||||||
self._not_before = 0.0
|
self._not_before = 0.0
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
async def acquire(self) -> None:
|
async def acquire(self, max_wait: Optional[float] = None) -> None:
|
||||||
|
"""Acquire one token. If ``max_wait`` is set and the next wait would
|
||||||
|
exceed it, raise :class:`BedestenRateLimited` immediately instead of
|
||||||
|
sleeping — keeps a single rate-limited request from holding the
|
||||||
|
worker-slot for the full bucket-pause window (up to ~30s on 429)."""
|
||||||
|
deadline = (time.monotonic() + max_wait) if max_wait is not None else None
|
||||||
while True:
|
while True:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
@@ -56,6 +74,10 @@ class _TokenBucket:
|
|||||||
self._tokens -= 1.0
|
self._tokens -= 1.0
|
||||||
return
|
return
|
||||||
wait_s = (1.0 - self._tokens) / self.refill_per_s
|
wait_s = (1.0 - self._tokens) / self.refill_per_s
|
||||||
|
if deadline is not None:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if wait_s > remaining:
|
||||||
|
raise BedestenRateLimited(retry_after=wait_s)
|
||||||
await asyncio.sleep(wait_s)
|
await asyncio.sleep(wait_s)
|
||||||
|
|
||||||
def penalize_until(self, monotonic_deadline: float) -> None:
|
def penalize_until(self, monotonic_deadline: float) -> None:
|
||||||
@@ -78,8 +100,11 @@ class BedestenApiClient:
|
|||||||
# 3.5s spacing (no burst, ~14% safety margin). Override via env:
|
# 3.5s spacing (no burst, ~14% safety margin). Override via env:
|
||||||
# BEDESTEN_RATE_CAPACITY (default 1)
|
# BEDESTEN_RATE_CAPACITY (default 1)
|
||||||
# BEDESTEN_RATE_REFILL_S (default 3.5; seconds per token)
|
# BEDESTEN_RATE_REFILL_S (default 3.5; seconds per token)
|
||||||
|
# BEDESTEN_RATE_MAX_WAIT_S (default 8.0; max seconds to wait in the
|
||||||
|
# local bucket before returning a structured 429 to the caller)
|
||||||
_DEFAULT_CAPACITY = int(os.getenv("BEDESTEN_RATE_CAPACITY", "1"))
|
_DEFAULT_CAPACITY = int(os.getenv("BEDESTEN_RATE_CAPACITY", "1"))
|
||||||
_DEFAULT_REFILL_S = float(os.getenv("BEDESTEN_RATE_REFILL_S", "3.5"))
|
_DEFAULT_REFILL_S = float(os.getenv("BEDESTEN_RATE_REFILL_S", "3.5"))
|
||||||
|
_DEFAULT_MAX_WAIT_S = float(os.getenv("BEDESTEN_RATE_MAX_WAIT_S", "8.0"))
|
||||||
|
|
||||||
def __init__(self, request_timeout: float = 60.0):
|
def __init__(self, request_timeout: float = 60.0):
|
||||||
self.http_client = httpx.AsyncClient(
|
self.http_client = httpx.AsyncClient(
|
||||||
@@ -137,7 +162,7 @@ class BedestenApiClient:
|
|||||||
if not request_dict["data"]["birimAdi"]: # Remove if empty string
|
if not request_dict["data"]["birimAdi"]: # Remove if empty string
|
||||||
del request_dict["data"]["birimAdi"]
|
del request_dict["data"]["birimAdi"]
|
||||||
|
|
||||||
await self._bucket.acquire()
|
await self._bucket.acquire(max_wait=self._DEFAULT_MAX_WAIT_S)
|
||||||
response = await self.http_client.post(
|
response = await self.http_client.post(
|
||||||
self.SEARCH_ENDPOINT,
|
self.SEARCH_ENDPOINT,
|
||||||
json=request_dict
|
json=request_dict
|
||||||
@@ -171,7 +196,7 @@ class BedestenApiClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Get document
|
# Get document
|
||||||
await self._bucket.acquire()
|
await self._bucket.acquire(max_wait=self._DEFAULT_MAX_WAIT_S)
|
||||||
response = await self.http_client.post(
|
response = await self.http_client.post(
|
||||||
self.DOCUMENT_ENDPOINT,
|
self.DOCUMENT_ENDPOINT,
|
||||||
json=doc_request.model_dump()
|
json=doc_request.model_dump()
|
||||||
@@ -202,12 +227,20 @@ class BedestenApiClient:
|
|||||||
|
|
||||||
logger.info(f"BedestenApiClient: Document mime type: {mime_type}")
|
logger.info(f"BedestenApiClient: Document mime type: {mime_type}")
|
||||||
|
|
||||||
# Convert to markdown based on mime type
|
# Convert to markdown based on mime type. markitdown is sync and
|
||||||
|
# PDF parsing in particular can block the event-loop for seconds,
|
||||||
|
# which on a single-worker uvicorn deployment stalls every other
|
||||||
|
# in-flight MCP request and new TLS handshakes. Offload to a
|
||||||
|
# thread so the event-loop stays responsive.
|
||||||
if mime_type == "text/html":
|
if mime_type == "text/html":
|
||||||
html_content = content_bytes.decode('utf-8')
|
html_content = content_bytes.decode('utf-8')
|
||||||
markdown_content = self._convert_html_to_markdown(html_content)
|
markdown_content = await asyncio.to_thread(
|
||||||
|
self._convert_html_to_markdown, html_content
|
||||||
|
)
|
||||||
elif mime_type == "application/pdf":
|
elif mime_type == "application/pdf":
|
||||||
markdown_content = self._convert_pdf_to_markdown(content_bytes)
|
markdown_content = await asyncio.to_thread(
|
||||||
|
self._convert_pdf_to_markdown, content_bytes
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unsupported mime type: {mime_type}")
|
logger.warning(f"Unsupported mime type: {mime_type}")
|
||||||
markdown_content = f"Unsupported content type: {mime_type}. Unable to convert to markdown."
|
markdown_content = f"Unsupported content type: {mime_type}. Unable to convert to markdown."
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# danistay_mcp_module/client.py
|
# danistay_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
@@ -170,7 +171,7 @@ class DanistayApiClient:
|
|||||||
source_url=source_url
|
source_url=source_url
|
||||||
)
|
)
|
||||||
|
|
||||||
markdown_content = self._convert_html_to_markdown_danistay(html_content_from_api)
|
markdown_content = await asyncio.to_thread(self._convert_html_to_markdown_danistay, html_content_from_api)
|
||||||
|
|
||||||
return DanistayDocumentMarkdown(
|
return DanistayDocumentMarkdown(
|
||||||
id=id,
|
id=id,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# emsal_mcp_module/client.py
|
# emsal_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
# from bs4 import BeautifulSoup # Uncomment if needed for advanced HTML pre-processing
|
# from bs4 import BeautifulSoup # Uncomment if needed for advanced HTML pre-processing
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
@@ -153,7 +154,7 @@ class EmsalApiClient:
|
|||||||
logger.warning(f"EmsalApiClient: Received empty or non-string HTML in 'data' field for Emsal ID {id}.")
|
logger.warning(f"EmsalApiClient: Received empty or non-string HTML in 'data' field for Emsal ID {id}.")
|
||||||
return EmsalDocumentMarkdown(id=id, markdown_content=None, source_url=source_url)
|
return EmsalDocumentMarkdown(id=id, markdown_content=None, source_url=source_url)
|
||||||
|
|
||||||
markdown_content = self._clean_html_and_convert_to_markdown_emsal(html_content_from_api)
|
markdown_content = await asyncio.to_thread(self._clean_html_and_convert_to_markdown_emsal, html_content_from_api)
|
||||||
|
|
||||||
return EmsalDocumentMarkdown(
|
return EmsalDocumentMarkdown(
|
||||||
id=id,
|
id=id,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# gib_mcp_module/client.py
|
# gib_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
@@ -302,7 +303,7 @@ class GibApiClient:
|
|||||||
|
|
||||||
item = content[0] if isinstance(content[0], dict) else {}
|
item = content[0] if isinstance(content[0], dict) else {}
|
||||||
description_html = item.get("description") or ""
|
description_html = item.get("description") or ""
|
||||||
markdown_body = self._convert_html_to_markdown(description_html) or ""
|
markdown_body = (await asyncio.to_thread(self._convert_html_to_markdown, description_html)) or ""
|
||||||
header_block = self._build_header_block(item)
|
header_block = self._build_header_block(item)
|
||||||
|
|
||||||
if header_block and markdown_body:
|
if header_block and markdown_body:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# kik_mcp_module/client_v2.py
|
# kik_mcp_module/client_v2.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
@@ -439,7 +440,9 @@ class KikV2ApiClient:
|
|||||||
html_bytes = html_content.encode('utf-8')
|
html_bytes = html_content.encode('utf-8')
|
||||||
html_stream = BytesIO(html_bytes)
|
html_stream = BytesIO(html_bytes)
|
||||||
|
|
||||||
result = md.convert_stream(html_stream, file_extension=".html")
|
# markitdown is sync; offload to thread so HTML parsing doesn't
|
||||||
|
# block the event-loop / other in-flight MCP requests.
|
||||||
|
result = await asyncio.to_thread(md.convert_stream, html_stream, file_extension=".html")
|
||||||
markdown_content = result.text_content
|
markdown_content = result.text_content
|
||||||
|
|
||||||
return KikV2DocumentMarkdown(
|
return KikV2DocumentMarkdown(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# kvkk_mcp_module/client.py
|
# kvkk_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
@@ -291,7 +292,7 @@ class KvkkApiClient:
|
|||||||
# Convert HTML content to Markdown
|
# Convert HTML content to Markdown
|
||||||
full_markdown_content = None
|
full_markdown_content = None
|
||||||
if extracted_data["html_content"]:
|
if extracted_data["html_content"]:
|
||||||
full_markdown_content = self._convert_html_to_markdown(extracted_data["html_content"])
|
full_markdown_content = await asyncio.to_thread(self._convert_html_to_markdown, extracted_data["html_content"])
|
||||||
|
|
||||||
if not full_markdown_content:
|
if not full_markdown_content:
|
||||||
return KvkkDocumentMarkdown(
|
return KvkkDocumentMarkdown(
|
||||||
|
|||||||
+38
-1
@@ -258,7 +258,7 @@ def create_app():
|
|||||||
|
|
||||||
# --- Module Imports ---
|
# --- Module Imports ---
|
||||||
from yargitay_mcp_module.client import YargitayOfficialApiClient
|
from yargitay_mcp_module.client import YargitayOfficialApiClient
|
||||||
from bedesten_mcp_module.client import BedestenApiClient
|
from bedesten_mcp_module.client import BedestenApiClient, BedestenRateLimited
|
||||||
from bedesten_mcp_module.models import (
|
from bedesten_mcp_module.models import (
|
||||||
BedestenSearchRequest, BedestenSearchData,
|
BedestenSearchRequest, BedestenSearchData,
|
||||||
BedestenDocumentMarkdown, BedestenCourtTypeEnum
|
BedestenDocumentMarkdown, BedestenCourtTypeEnum
|
||||||
@@ -1182,6 +1182,26 @@ For best results, use exact phrases with quotes for legal terms."""),
|
|||||||
"page_size": pageSize,
|
"page_size": pageSize,
|
||||||
"searched_courts": court_types
|
"searched_courts": court_types
|
||||||
}
|
}
|
||||||
|
except BedestenRateLimited as e:
|
||||||
|
retry_after = f"{e.retry_after:.1f}"
|
||||||
|
logger.warning(f"Bedesten local rate-limit bucket full for search; retry-after={retry_after}s")
|
||||||
|
return {
|
||||||
|
"decisions": [],
|
||||||
|
"total_records": 0,
|
||||||
|
"requested_page": pageNumber,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"searched_courts": court_types,
|
||||||
|
"error": "rate_limit_exceeded",
|
||||||
|
"status_code": 429,
|
||||||
|
"retry_after": retry_after,
|
||||||
|
"message": (
|
||||||
|
"Bedesten istemci tarafı eşzamanlılık sınırına ulaşıldı "
|
||||||
|
"(yerel token-bucket dolu). Lütfen kısa bir süre bekleyip "
|
||||||
|
"aramayı tekrar deneyin. Alternatif olarak yargi-cli aracını "
|
||||||
|
"kullanabilirsiniz: https://github.com/saidsurucu/yargi-cli "
|
||||||
|
"(kullanım talimatları için repo README'sini okuyun)."
|
||||||
|
),
|
||||||
|
}
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
if e.response.status_code == 429:
|
if e.response.status_code == 429:
|
||||||
retry_after = e.response.headers.get("Retry-After", "")
|
retry_after = e.response.headers.get("Retry-After", "")
|
||||||
@@ -1227,6 +1247,23 @@ async def get_bedesten_document_markdown(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return await bedesten_client_instance.get_document_as_markdown(documentId)
|
return await bedesten_client_instance.get_document_as_markdown(documentId)
|
||||||
|
except BedestenRateLimited as e:
|
||||||
|
retry_after = f"{e.retry_after:.1f}"
|
||||||
|
logger.warning(f"Bedesten local rate-limit bucket full for document {documentId}; retry-after={retry_after}s")
|
||||||
|
message = (
|
||||||
|
"Bedesten istemci tarafı eşzamanlılık sınırına ulaşıldı "
|
||||||
|
"(yerel token-bucket dolu). Lütfen kısa bir süre bekleyip "
|
||||||
|
"belgeyi tekrar talep edin. Alternatif olarak yargi-cli aracını "
|
||||||
|
"kullanabilirsiniz: https://github.com/saidsurucu/yargi-cli "
|
||||||
|
"(kullanım talimatları için repo README'sini okuyun). "
|
||||||
|
f"Retry-After: {retry_after}"
|
||||||
|
)
|
||||||
|
return BedestenDocumentMarkdown(
|
||||||
|
documentId=documentId,
|
||||||
|
markdown_content=f"ERROR (rate_limit_exceeded, HTTP 429): {message}",
|
||||||
|
source_url=f"https://mevzuat.adalet.gov.tr/ictihat/{documentId}",
|
||||||
|
mime_type=None,
|
||||||
|
)
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
if e.response.status_code == 429:
|
if e.response.status_code == 429:
|
||||||
retry_after = e.response.headers.get("Retry-After", "")
|
retry_after = e.response.headers.get("Retry-After", "")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# rekabet_mcp_module/client.py
|
# rekabet_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from typing import List, Optional, Tuple, Dict, Any
|
from typing import List, Optional, Tuple, Dict, Any
|
||||||
@@ -353,7 +354,7 @@ class RekabetKurumuApiClient:
|
|||||||
total_pdf_pages = total_pdf_pages_from_extraction
|
total_pdf_pages = total_pdf_pages_from_extraction
|
||||||
|
|
||||||
if single_page_pdf_bytes:
|
if single_page_pdf_bytes:
|
||||||
markdown_for_requested_page = self._convert_pdf_bytes_to_markdown(single_page_pdf_bytes, str(pdf_url_to_report or full_landing_page_url))
|
markdown_for_requested_page = await asyncio.to_thread(self._convert_pdf_bytes_to_markdown, single_page_pdf_bytes, str(pdf_url_to_report or full_landing_page_url))
|
||||||
if not markdown_for_requested_page:
|
if not markdown_for_requested_page:
|
||||||
error_message = (error_message or "") + f"; Could not convert page {page_number} of PDF to Markdown."
|
error_message = (error_message or "") + f"; Could not convert page {page_number} of PDF to Markdown."
|
||||||
elif total_pdf_pages > 0 :
|
elif total_pdf_pages > 0 :
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# sayistay_mcp_module/client.py
|
# sayistay_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
import re
|
import re
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
@@ -657,7 +658,7 @@ class SayistayApiClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Convert HTML to Markdown using existing method
|
# Convert HTML to Markdown using existing method
|
||||||
markdown_content = self._convert_html_to_markdown(html_content)
|
markdown_content = await asyncio.to_thread(self._convert_html_to_markdown, html_content)
|
||||||
|
|
||||||
if markdown_content and "Error converting HTML content" not in markdown_content:
|
if markdown_content and "Error converting HTML content" not in markdown_content:
|
||||||
logger.info(f"Successfully retrieved and converted document {decision_id} to Markdown")
|
logger.info(f"Successfully retrieved and converted document {decision_id} to Markdown")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# sigorta_tahkim_mcp_module/client.py
|
# sigorta_tahkim_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import logging
|
import logging
|
||||||
@@ -202,7 +203,11 @@ class SigortaTahkimApiClient:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
pdf_stream = io.BytesIO(response.content)
|
pdf_stream = io.BytesIO(response.content)
|
||||||
result = self.markitdown.convert_stream(pdf_stream, file_extension=".pdf")
|
# markitdown is sync; offload to thread so PDF parsing doesn't block
|
||||||
|
# the event-loop / other in-flight MCP requests.
|
||||||
|
result = await asyncio.to_thread(
|
||||||
|
self.markitdown.convert_stream, pdf_stream, file_extension=".pdf"
|
||||||
|
)
|
||||||
return result.text_content.strip(), pdf_url
|
return result.text_content.strip(), pdf_url
|
||||||
|
|
||||||
def _split_into_decisions(self, markdown_content: str) -> list[tuple[str, str]]:
|
def _split_into_decisions(self, markdown_content: str) -> list[tuple[str, str]]:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# uyusmazlik_mcp_module/client.py
|
# uyusmazlik_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
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
|
||||||
@@ -232,7 +233,7 @@ class UyusmazlikApiClient:
|
|||||||
logger.warning(f"UyusmazlikApiClient: Received empty or non-string HTML from URL {document_url}.")
|
logger.warning(f"UyusmazlikApiClient: Received empty or non-string HTML from URL {document_url}.")
|
||||||
return UyusmazlikDocumentMarkdown(source_url=document_url, markdown_content=None)
|
return UyusmazlikDocumentMarkdown(source_url=document_url, markdown_content=None)
|
||||||
|
|
||||||
markdown_content = self._convert_html_to_markdown_uyusmazlik(html_content_from_api)
|
markdown_content = await asyncio.to_thread(self._convert_html_to_markdown_uyusmazlik, html_content_from_api)
|
||||||
return UyusmazlikDocumentMarkdown(source_url=document_url, markdown_content=markdown_content)
|
return UyusmazlikDocumentMarkdown(source_url=document_url, markdown_content=markdown_content)
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
logger.error(f"UyusmazlikApiClient (httpx for docs): HTTP error fetching Uyuşmazlık document from {document_url}: {e}")
|
logger.error(f"UyusmazlikApiClient (httpx for docs): HTTP error fetching Uyuşmazlık document from {document_url}: {e}")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# yargitay_mcp_module/client.py
|
# yargitay_mcp_module/client.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from bs4 import BeautifulSoup # Still needed for pre-processing HTML before markitdown
|
from bs4 import BeautifulSoup # Still needed for pre-processing HTML before markitdown
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
@@ -159,7 +160,7 @@ class YargitayOfficialApiClient:
|
|||||||
logger.error(f"YargitayOfficialApiClient: 'data' field in API response is not a string or not found (ID: {id}).")
|
logger.error(f"YargitayOfficialApiClient: 'data' field in API response is not a string or not found (ID: {id}).")
|
||||||
raise ValueError("Expected HTML content not found in API response's 'data' field.")
|
raise ValueError("Expected HTML content not found in API response's 'data' field.")
|
||||||
|
|
||||||
markdown_content = self._convert_html_to_markdown(html_content_from_api)
|
markdown_content = await asyncio.to_thread(self._convert_html_to_markdown, html_content_from_api)
|
||||||
|
|
||||||
return YargitayDocumentMarkdown(
|
return YargitayDocumentMarkdown(
|
||||||
id=id,
|
id=id,
|
||||||
|
|||||||
Reference in New Issue
Block a user