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>
404 lines
23 KiB
Python
404 lines
23 KiB
Python
# rekabet_mcp_module/client.py
|
||
|
||
import asyncio
|
||
import httpx
|
||
from bs4 import BeautifulSoup
|
||
from typing import List, Optional, Tuple, Dict, Any
|
||
import logging
|
||
import html
|
||
import re
|
||
import io # For io.BytesIO
|
||
from urllib.parse import urlencode, urljoin, quote, parse_qs, urlparse
|
||
from markitdown import MarkItDown
|
||
import math
|
||
|
||
# pypdf for PDF processing (lighter alternative to PyMuPDF)
|
||
from pypdf import PdfReader, PdfWriter # PyPDF2'nin devamı niteliğindeki pypdf
|
||
|
||
from .models import (
|
||
RekabetKurumuSearchRequest,
|
||
RekabetDecisionSummary,
|
||
RekabetSearchResult,
|
||
RekabetDocument,
|
||
RekabetKararTuruGuidEnum
|
||
)
|
||
from pydantic import HttpUrl # Ensure HttpUrl is imported from pydantic
|
||
|
||
logger = logging.getLogger(__name__)
|
||
if not logger.hasHandlers(): # Pragma: no cover
|
||
logging.basicConfig(
|
||
level=logging.INFO, # Varsayılan log seviyesi
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
# Debug betiğinde daha detaylı loglama için seviye ayrıca ayarlanabilir.
|
||
|
||
class RekabetKurumuApiClient:
|
||
BASE_URL = "https://www.rekabet.gov.tr"
|
||
SEARCH_PATH = "/tr/Kararlar"
|
||
DECISION_LANDING_PATH_TEMPLATE = "/Karar"
|
||
# PDF sayfa bazlı Markdown döndürüldüğü için bu sabit artık doğrudan kullanılmıyor.
|
||
# DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000
|
||
|
||
def __init__(self, request_timeout: float = 60.0):
|
||
self.http_client = httpx.AsyncClient(
|
||
base_url=self.BASE_URL,
|
||
headers={
|
||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
|
||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||
},
|
||
timeout=request_timeout,
|
||
verify=True,
|
||
follow_redirects=True
|
||
)
|
||
|
||
def _build_search_query_params(self, params: RekabetKurumuSearchRequest) -> List[Tuple[str, str]]:
|
||
query_params: List[Tuple[str, str]] = []
|
||
query_params.append(("sayfaAdi", params.sayfaAdi if params.sayfaAdi is not None else ""))
|
||
query_params.append(("YayinlanmaTarihi", params.YayinlanmaTarihi if params.YayinlanmaTarihi is not None else ""))
|
||
query_params.append(("PdfText", params.PdfText if params.PdfText is not None else ""))
|
||
|
||
karar_turu_id_value = ""
|
||
if params.KararTuruID is not None:
|
||
karar_turu_id_value = params.KararTuruID.value if params.KararTuruID.value != "ALL" else ""
|
||
query_params.append(("KararTuruID", karar_turu_id_value))
|
||
|
||
query_params.append(("KararSayisi", params.KararSayisi if params.KararSayisi is not None else ""))
|
||
query_params.append(("KararTarihi", params.KararTarihi if params.KararTarihi is not None else ""))
|
||
|
||
if params.page and params.page > 1:
|
||
query_params.append(("page", str(params.page)))
|
||
|
||
return query_params
|
||
|
||
async def search_decisions(self, params: RekabetKurumuSearchRequest) -> RekabetSearchResult:
|
||
request_path = self.SEARCH_PATH
|
||
final_query_params = self._build_search_query_params(params)
|
||
logger.info(f"RekabetKurumuApiClient: Performing search. Path: {request_path}, Parameters: {final_query_params}")
|
||
|
||
try:
|
||
response = await self.http_client.get(request_path, params=final_query_params)
|
||
response.raise_for_status()
|
||
html_content = response.text
|
||
except httpx.RequestError as e:
|
||
logger.error(f"RekabetKurumuApiClient: HTTP request error during search: {e}")
|
||
raise
|
||
|
||
soup = BeautifulSoup(html_content, 'html.parser')
|
||
processed_decisions: List[RekabetDecisionSummary] = []
|
||
total_records: Optional[int] = None
|
||
total_pages: Optional[int] = None
|
||
|
||
pagination_div = soup.find("div", class_="yazi01")
|
||
if pagination_div:
|
||
text_content = pagination_div.get_text(separator=" ", strip=True)
|
||
total_match = re.search(r"Toplam\s*:\s*(\d+)", text_content)
|
||
if total_match:
|
||
try:
|
||
total_records = int(total_match.group(1))
|
||
logger.debug(f"Total records found from pagination: {total_records}")
|
||
except ValueError:
|
||
logger.warning(f"Could not convert 'Toplam' value to int: {total_match.group(1)}")
|
||
else:
|
||
logger.warning("'Toplam :' string not found in pagination section.")
|
||
|
||
results_per_page_assumed = 10
|
||
if total_records is not None:
|
||
calculated_total_pages = math.ceil(total_records / results_per_page_assumed)
|
||
total_pages = calculated_total_pages if calculated_total_pages > 0 else (1 if total_records > 0 else 0)
|
||
logger.debug(f"Calculated total pages: {total_pages}")
|
||
|
||
if total_pages is None: # Fallback if total_records couldn't be parsed
|
||
last_page_link = pagination_div.select_one("li.PagedList-skipToLast a")
|
||
if last_page_link and last_page_link.has_attr('href'):
|
||
qs = parse_qs(urlparse(last_page_link['href']).query)
|
||
if 'page' in qs and qs['page']:
|
||
try:
|
||
total_pages = int(qs['page'][0])
|
||
logger.debug(f"Total pages found from 'Last >>' link: {total_pages}")
|
||
except ValueError:
|
||
logger.warning(f"Could not convert page value from 'Last >>' link to int: {qs['page'][0]}")
|
||
elif total_records == 0 : total_pages = 0 # If no records, 0 pages
|
||
elif total_records is not None and total_records > 0 : total_pages = 1 # If records exist but no last page link (e.g. single page)
|
||
else: logger.warning("'Last >>' link not found in pagination section.")
|
||
|
||
decision_tables_container = soup.find("div", id="kararList")
|
||
if not decision_tables_container:
|
||
logger.warning("`div#kararList` (decision list container) not found. HTML structure might have changed or no decisions on this page.")
|
||
else:
|
||
decision_tables = decision_tables_container.find_all("table", class_="equalDivide")
|
||
logger.info(f"Found {len(decision_tables)} 'table' elements with class='equalDivide' for parsing.")
|
||
|
||
if not decision_tables and total_records is not None and total_records > 0 :
|
||
logger.warning(f"Page indicates {total_records} records but no decision tables found with class='equalDivide'.")
|
||
|
||
for idx, table in enumerate(decision_tables):
|
||
logger.debug(f"Processing table {idx + 1}...")
|
||
try:
|
||
rows = table.find_all("tr")
|
||
if len(rows) != 3:
|
||
logger.warning(f"Table {idx + 1} has an unexpected number of rows ({len(rows)} instead of 3). Skipping. HTML snippet:\n{table.prettify()[:500]}")
|
||
continue
|
||
|
||
# Row 1: Publication Date, Decision Number, Related Cases Link
|
||
td_elements_r1 = rows[0].find_all("td")
|
||
pub_date = td_elements_r1[0].get_text(strip=True) if len(td_elements_r1) > 0 else ""
|
||
dec_num = td_elements_r1[1].get_text(strip=True) if len(td_elements_r1) > 1 else ""
|
||
|
||
related_cases_link_tag = td_elements_r1[2].find("a", href=True) if len(td_elements_r1) > 2 else None
|
||
related_cases_url_str: str = ""
|
||
karar_id_from_related: str = ""
|
||
if related_cases_link_tag and related_cases_link_tag.has_attr('href'):
|
||
related_cases_url_str = urljoin(self.BASE_URL, related_cases_link_tag['href'])
|
||
qs_related = parse_qs(urlparse(related_cases_link_tag['href']).query)
|
||
if 'kararId' in qs_related and qs_related['kararId']:
|
||
karar_id_from_related = qs_related['kararId'][0]
|
||
|
||
# Row 2: Decision Date, Decision Type
|
||
td_elements_r2 = rows[1].find_all("td")
|
||
dec_date = td_elements_r2[0].get_text(strip=True) if len(td_elements_r2) > 0 else ""
|
||
dec_type_text = td_elements_r2[1].get_text(strip=True) if len(td_elements_r2) > 1 else ""
|
||
|
||
# Row 3: Title and Main Decision Link
|
||
title_cell = rows[2].find("td", colspan="5")
|
||
decision_link_tag = title_cell.find("a", href=True) if title_cell else None
|
||
|
||
title_text: str = ""
|
||
decision_landing_url_str: str = ""
|
||
karar_id_from_main_link: str = ""
|
||
|
||
if decision_link_tag and decision_link_tag.has_attr('href'):
|
||
title_text = decision_link_tag.get_text(strip=True)
|
||
href_val = decision_link_tag['href']
|
||
if href_val.startswith(self.DECISION_LANDING_PATH_TEMPLATE + "?kararId="): # Ensure it's a decision link
|
||
decision_landing_url_str = urljoin(self.BASE_URL, href_val)
|
||
qs_main = parse_qs(urlparse(href_val).query)
|
||
if 'kararId' in qs_main and qs_main['kararId']:
|
||
karar_id_from_main_link = qs_main['kararId'][0]
|
||
else:
|
||
logger.warning(f"Table {idx+1} decision link has unexpected format: {href_val}")
|
||
else:
|
||
logger.warning(f"Table {idx+1} could not find title/decision link tag.")
|
||
|
||
current_karar_id = karar_id_from_main_link or karar_id_from_related
|
||
|
||
if not current_karar_id:
|
||
logger.warning(f"Table {idx+1} Karar ID not found. Skipping. Title (if any): {title_text}")
|
||
continue
|
||
|
||
processed_decisions.append(RekabetDecisionSummary(
|
||
publication_date=pub_date, decision_number=dec_num, decision_date=dec_date,
|
||
decision_type_text=dec_type_text, title=title_text,
|
||
decision_url=decision_landing_url_str,
|
||
karar_id=current_karar_id,
|
||
related_cases_url=related_cases_url_str
|
||
))
|
||
logger.debug(f"Table {idx+1} parsed successfully: Karar ID '{current_karar_id}', Title '{title_text[:50] if title_text else 'N/A'}...'")
|
||
|
||
except Exception as e:
|
||
logger.warning(f"RekabetKurumuApiClient: Error parsing decision summary {idx+1}: {e}. Problematic Table HTML:\n{table.prettify()}", exc_info=True)
|
||
continue
|
||
|
||
return RekabetSearchResult(
|
||
decisions=processed_decisions, total_records_found=total_records,
|
||
retrieved_page_number=params.page, total_pages=total_pages if total_pages is not None else 0
|
||
)
|
||
|
||
async def _extract_pdf_url_and_landing_page_metadata(self, karar_id: str, landing_page_html: str, landing_page_url: str) -> Dict[str, Any]:
|
||
soup = BeautifulSoup(landing_page_html, 'html.parser')
|
||
data: Dict[str, Any] = {
|
||
"pdf_url": None,
|
||
"title_on_landing_page": soup.title.string.strip() if soup.title and soup.title.string else f"Rekabet Kurumu Kararı {karar_id}",
|
||
}
|
||
# This part needs to be robust and specific to Rekabet Kurumu's landing page structure.
|
||
# Look for common patterns: direct links, download buttons, embedded viewers.
|
||
pdf_anchor = soup.find("a", href=re.compile(r"\.pdf(\?|$)", re.IGNORECASE)) # Basic PDF link
|
||
if not pdf_anchor: # Try other common patterns if the basic one fails
|
||
# Example: Look for links with specific text or class
|
||
pdf_anchor = soup.find("a", string=re.compile(r"karar metni|pdf indir", re.IGNORECASE))
|
||
|
||
if pdf_anchor and pdf_anchor.has_attr('href'):
|
||
pdf_path = pdf_anchor['href']
|
||
data["pdf_url"] = urljoin(landing_page_url, pdf_path)
|
||
logger.info(f"PDF link found on landing page (<a>): {data['pdf_url']}")
|
||
else:
|
||
iframe_pdf = soup.find("iframe", src=re.compile(r"\.pdf(\?|$)", re.IGNORECASE))
|
||
if iframe_pdf and iframe_pdf.has_attr('src'):
|
||
pdf_path = iframe_pdf['src']
|
||
data["pdf_url"] = urljoin(landing_page_url, pdf_path)
|
||
logger.info(f"PDF link found on landing page (<iframe>): {data['pdf_url']}")
|
||
else:
|
||
embed_pdf = soup.find("embed", src=re.compile(r"\.pdf(\?|$)", re.IGNORECASE), type="application/pdf")
|
||
if embed_pdf and embed_pdf.has_attr('src'):
|
||
pdf_path = embed_pdf['src']
|
||
data["pdf_url"] = urljoin(landing_page_url, pdf_path)
|
||
logger.info(f"PDF link found on landing page (<embed>): {data['pdf_url']}")
|
||
else:
|
||
logger.warning(f"No PDF link found on landing page {landing_page_url} for kararId {karar_id} using common selectors.")
|
||
return data
|
||
|
||
async def _download_pdf_bytes(self, pdf_url: str) -> Optional[bytes]:
|
||
try:
|
||
url_to_fetch = pdf_url if pdf_url.startswith(('http://', 'https://')) else urljoin(self.BASE_URL, pdf_url)
|
||
logger.info(f"Downloading PDF from: {url_to_fetch}")
|
||
response = await self.http_client.get(url_to_fetch)
|
||
response.raise_for_status()
|
||
pdf_bytes = await response.aread()
|
||
logger.info(f"PDF content downloaded ({len(pdf_bytes)} bytes) from: {url_to_fetch}")
|
||
return pdf_bytes
|
||
except httpx.RequestError as e:
|
||
logger.error(f"HTTP error downloading PDF from {pdf_url}: {e}")
|
||
except Exception as e:
|
||
logger.error(f"General error downloading PDF from {pdf_url}: {e}")
|
||
return None
|
||
|
||
def _extract_single_pdf_page_as_pdf_bytes(self, original_pdf_bytes: bytes, page_number_to_extract: int) -> Tuple[Optional[bytes], int]:
|
||
total_pages_in_original_pdf = 0
|
||
single_page_pdf_bytes: Optional[bytes] = None
|
||
|
||
if not original_pdf_bytes:
|
||
logger.warning("No original PDF bytes provided for page extraction.")
|
||
return None, 0
|
||
|
||
try:
|
||
pdf_stream = io.BytesIO(original_pdf_bytes)
|
||
reader = PdfReader(pdf_stream)
|
||
total_pages_in_original_pdf = len(reader.pages)
|
||
|
||
if not (0 < page_number_to_extract <= total_pages_in_original_pdf):
|
||
logger.warning(f"Requested page number ({page_number_to_extract}) is out of PDF page range (1-{total_pages_in_original_pdf}).")
|
||
return None, total_pages_in_original_pdf
|
||
|
||
writer = PdfWriter()
|
||
writer.add_page(reader.pages[page_number_to_extract - 1]) # pypdf is 0-indexed
|
||
|
||
output_pdf_stream = io.BytesIO()
|
||
writer.write(output_pdf_stream)
|
||
single_page_pdf_bytes = output_pdf_stream.getvalue()
|
||
|
||
logger.debug(f"Page {page_number_to_extract} of original PDF (total {total_pages_in_original_pdf} pages) extracted as new PDF using pypdf.")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error extracting PDF page using pypdf: {e}", exc_info=True)
|
||
return None, total_pages_in_original_pdf
|
||
return single_page_pdf_bytes, total_pages_in_original_pdf
|
||
|
||
def _convert_pdf_bytes_to_markdown(self, pdf_bytes: bytes, source_url_for_logging: str) -> Optional[str]:
|
||
if not pdf_bytes:
|
||
logger.warning(f"No PDF bytes provided for Markdown conversion (source: {source_url_for_logging}).")
|
||
return None
|
||
|
||
pdf_stream = io.BytesIO(pdf_bytes)
|
||
try:
|
||
md_converter = MarkItDown(enable_plugins=False)
|
||
conversion_result = md_converter.convert(pdf_stream)
|
||
markdown_text = conversion_result.text_content
|
||
|
||
if not markdown_text:
|
||
logger.warning(f"MarkItDown returned empty content from PDF byte stream (source: {source_url_for_logging}). PDF page might be image-based or MarkItDown could not process the PDF stream.")
|
||
return markdown_text
|
||
except Exception as e:
|
||
logger.error(f"MarkItDown conversion error for PDF byte stream (source: {source_url_for_logging}): {e}", exc_info=True)
|
||
return None
|
||
|
||
async def get_decision_document(self, karar_id: str, page_number: int = 1) -> RekabetDocument:
|
||
if not karar_id:
|
||
return RekabetDocument(
|
||
source_landing_page_url=HttpUrl(f"{self.BASE_URL}"),
|
||
karar_id=karar_id or "UNKNOWN_KARAR_ID",
|
||
error_message="karar_id is required.",
|
||
current_page=1, total_pages=0, is_paginated=False )
|
||
|
||
decision_url_path = f"{self.DECISION_LANDING_PATH_TEMPLATE}?kararId={karar_id}"
|
||
full_landing_page_url = urljoin(self.BASE_URL, decision_url_path)
|
||
|
||
logger.info(f"RekabetKurumuApiClient: Getting decision document: {full_landing_page_url}, Requested PDF Page: {page_number}")
|
||
|
||
pdf_url_to_report: Optional[HttpUrl] = None
|
||
title_to_report: Optional[str] = f"Rekabet Kurumu Kararı {karar_id}" # Default
|
||
error_message: Optional[str] = None
|
||
markdown_for_requested_page: Optional[str] = None
|
||
total_pdf_pages: int = 0
|
||
|
||
try:
|
||
async with self.http_client.stream("GET", full_landing_page_url) as response:
|
||
response.raise_for_status()
|
||
content_type = response.headers.get("content-type", "").lower()
|
||
final_url_of_response = HttpUrl(str(response.url))
|
||
original_pdf_bytes: Optional[bytes] = None
|
||
|
||
if "application/pdf" in content_type:
|
||
logger.info(f"URL {final_url_of_response} is a direct PDF. Processing content.")
|
||
pdf_url_to_report = final_url_of_response
|
||
original_pdf_bytes = await response.aread()
|
||
elif "text/html" in content_type:
|
||
logger.info(f"URL {final_url_of_response} is an HTML landing page. Looking for PDF link.")
|
||
landing_page_html_bytes = await response.aread()
|
||
detected_charset = response.charset_encoding or 'utf-8'
|
||
try: landing_page_html = landing_page_html_bytes.decode(detected_charset)
|
||
except UnicodeDecodeError: landing_page_html = landing_page_html_bytes.decode('utf-8', errors='replace')
|
||
|
||
if landing_page_html.strip():
|
||
landing_page_data = self._extract_pdf_url_and_landing_page_metadata(karar_id, landing_page_html, str(final_url_of_response))
|
||
pdf_url_str_from_html = landing_page_data.get("pdf_url")
|
||
if landing_page_data.get("title_on_landing_page"): title_to_report = landing_page_data.get("title_on_landing_page")
|
||
if pdf_url_str_from_html:
|
||
pdf_url_to_report = HttpUrl(pdf_url_str_from_html)
|
||
original_pdf_bytes = await self._download_pdf_bytes(str(pdf_url_to_report))
|
||
else: error_message = (error_message or "") + " PDF URL not found on HTML landing page."
|
||
else: error_message = "Decision landing page content is empty."
|
||
else: error_message = f"Unexpected content type ({content_type}) for URL: {final_url_of_response}"
|
||
|
||
if original_pdf_bytes:
|
||
single_page_pdf_bytes, total_pdf_pages_from_extraction = self._extract_single_pdf_page_as_pdf_bytes(original_pdf_bytes, page_number)
|
||
total_pdf_pages = total_pdf_pages_from_extraction
|
||
|
||
if single_page_pdf_bytes:
|
||
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:
|
||
error_message = (error_message or "") + f"; Could not convert page {page_number} of PDF to Markdown."
|
||
elif total_pdf_pages > 0 :
|
||
error_message = (error_message or "") + f"; Could not extract page {page_number} from PDF (page may be out of range or extraction failed)."
|
||
else:
|
||
error_message = (error_message or "") + "; PDF could not be processed or page count was zero (original PDF might be invalid)."
|
||
elif not error_message:
|
||
error_message = "PDF content could not be downloaded or identified."
|
||
|
||
is_paginated = total_pdf_pages > 1
|
||
current_page_final = page_number
|
||
if total_pdf_pages > 0:
|
||
current_page_final = max(1, min(page_number, total_pdf_pages))
|
||
elif markdown_for_requested_page is None:
|
||
current_page_final = 1
|
||
|
||
# If markdown is None but there was no specific error for markdown conversion (e.g. PDF not found first)
|
||
# make sure error_message reflects that.
|
||
if markdown_for_requested_page is None and pdf_url_to_report and not error_message:
|
||
error_message = (error_message or "") + "; Failed to produce Markdown from PDF page."
|
||
|
||
|
||
return RekabetDocument(
|
||
source_landing_page_url=full_landing_page_url, karar_id=karar_id,
|
||
title_on_landing_page=title_to_report, pdf_url=pdf_url_to_report,
|
||
markdown_chunk=markdown_for_requested_page, current_page=current_page_final,
|
||
total_pages=total_pdf_pages, is_paginated=is_paginated,
|
||
error_message=error_message.strip("; ") if error_message else None )
|
||
|
||
except httpx.HTTPStatusError as e: error_msg_detail = f"HTTP Status error {e.response.status_code} while processing decision page."
|
||
except httpx.RequestError as e: error_msg_detail = f"HTTP Request error while processing decision page: {str(e)}"
|
||
except Exception as e: error_msg_detail = f"General error while processing decision: {str(e)}"
|
||
|
||
exc_info_flag = not isinstance(e, (httpx.HTTPStatusError, httpx.RequestError)) if 'e' in locals() else True
|
||
logger.error(f"RekabetKurumuApiClient: Error processing decision {karar_id} from {full_landing_page_url}: {error_msg_detail}", exc_info=exc_info_flag)
|
||
error_message = (error_message + "; " if error_message else "") + error_msg_detail
|
||
|
||
return RekabetDocument(
|
||
source_landing_page_url=full_landing_page_url, karar_id=karar_id,
|
||
title_on_landing_page=title_to_report, pdf_url=pdf_url_to_report,
|
||
markdown_chunk=None, current_page=page_number, total_pages=0, is_paginated=False,
|
||
error_message=error_message.strip("; ") if error_message else "An unexpected error occurred." )
|
||
|
||
async def close_client_session(self): # Pragma: no cover
|
||
if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed:
|
||
await self.http_client.aclose()
|
||
logger.info("RekabetKurumuApiClient: HTTP client session closed.") |