Both sites were rebuilt and their old endpoints now 404:
- AYM moved to a single-page app backed by a JSON API
(POST /api/core/public/search, kararTipi NormDenetimi/BireyselBasvuru;
full text via {id, size:1} -> "icerik" HTML). Old /Ara, /ND/, /BB/ gone.
- Uyuşmazlık moved to ASP.NET WebForms (viewstate postback to /, GridView
results, decisions served as /Uploads/{EsasNo}.pdf). Old /Arama/Search gone.
Changes:
- New anayasa_mcp_module/api_client.py: shared KBB JSON client, base64url
document-id codec, HTML->Markdown + HTML text stripping helpers.
- Rewrite anayasa client/bireysel_client/unified_client over the new API,
preserving public method names and response models.
- Rewrite uyusmazlik client for the postback flow + GridView parse + PDF
document conversion; simplify request model to text + scope + paging.
- Trim search_anayasa_unified and search_uyusmazlik_decisions tool signatures
to parameters the new APIs actually support; drop dead enums.
Verified live via FastMCP client: search + document retrieval work for AYM
norm/bireysel and Uyuşmazlık.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
151 lines
5.9 KiB
Python
151 lines
5.9 KiB
Python
# anayasa_mcp_module/client.py
|
|
# Norm Denetimi client backed by the new KBB JSON API (see api_client.py).
|
|
#
|
|
# The Anayasa Mahkemesi sites were rebuilt as a single-page app; the old
|
|
# HTML-scraping endpoints on normkararlarbilgibankasi.anayasa.gov.tr/Ara now
|
|
# return HTTP 404. This client maps the rich legacy request model onto the new
|
|
# free-text "query" search and rebuilds the legacy response models from the JSON
|
|
# payload so existing tooling keeps working.
|
|
|
|
import logging
|
|
import math
|
|
from typing import List, Optional
|
|
|
|
from .api_client import (
|
|
AnayasaApiClient,
|
|
KARAR_TIPI_NORM,
|
|
DOCUMENT_MARKDOWN_CHUNK_SIZE,
|
|
build_document_url,
|
|
parse_document_url,
|
|
convert_icerik_to_markdown,
|
|
strip_html_text,
|
|
)
|
|
from .models import (
|
|
AnayasaNormDenetimiSearchRequest,
|
|
AnayasaDecisionSummary,
|
|
AnayasaSearchResult,
|
|
AnayasaDocumentMarkdown,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
if not logger.hasHandlers():
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
|
|
|
def _build_query(params: AnayasaNormDenetimiSearchRequest) -> str:
|
|
"""Derive the free-text query string the new API expects from the legacy model.
|
|
|
|
The new endpoint only supports a single full-text "query" field, so the
|
|
keyword lists are flattened. Esas/Karar numbers are appended when no keyword
|
|
is provided so number-based lookups still return results.
|
|
"""
|
|
terms: List[str] = []
|
|
for bucket in (params.keywords_all, params.keywords_any):
|
|
if bucket:
|
|
terms.extend(t for t in bucket if t)
|
|
if not terms:
|
|
for value in (params.case_number_esas, params.decision_number_karar):
|
|
if value:
|
|
terms.append(value)
|
|
return " ".join(terms).strip()
|
|
|
|
|
|
class AnayasaMahkemesiApiClient:
|
|
"""Norm Denetimi search/document client over the KBB JSON API."""
|
|
|
|
def __init__(self, request_timeout: float = 60.0):
|
|
self.api = AnayasaApiClient(request_timeout)
|
|
|
|
async def search_norm_denetimi_decisions(
|
|
self,
|
|
params: AnayasaNormDenetimiSearchRequest,
|
|
) -> AnayasaSearchResult:
|
|
query = _build_query(params)
|
|
payload = await self.api.search(
|
|
karar_tipi=KARAR_TIPI_NORM,
|
|
query=query,
|
|
page=params.page_to_fetch,
|
|
size=params.results_per_page,
|
|
)
|
|
|
|
total_records = int(payload.get("total") or 0)
|
|
decisions: List[AnayasaDecisionSummary] = []
|
|
for item in payload.get("data") or []:
|
|
esas_no = item.get("esasNo") or ""
|
|
karar_no = item.get("kararNo") or ""
|
|
if esas_no and karar_no:
|
|
reference = f"E.{esas_no}, K.{karar_no}"
|
|
else:
|
|
reference = esas_no or karar_no or ""
|
|
decisions.append(AnayasaDecisionSummary(
|
|
decision_reference_no=reference,
|
|
decision_page_url=build_document_url(KARAR_TIPI_NORM, item.get("id", "")),
|
|
keywords_found_count=item.get("highlightCount") or 0,
|
|
application_type_summary=item.get("basvuruTuruLabel") or "",
|
|
applicant_summary=item.get("basvuranGenelLabel") or "",
|
|
decision_outcome_summary=strip_html_text(item.get("kararKonusu")),
|
|
decision_date_summary=item.get("kararTarihi") or "",
|
|
reviewed_norms=[],
|
|
))
|
|
|
|
return AnayasaSearchResult(
|
|
decisions=decisions,
|
|
total_records_found=total_records,
|
|
retrieved_page_number=params.page_to_fetch,
|
|
)
|
|
|
|
async def get_decision_document_as_markdown(
|
|
self,
|
|
document_url: str,
|
|
page_number: int = 1,
|
|
) -> AnayasaDocumentMarkdown:
|
|
karar_tipi, uuid = parse_document_url(document_url)
|
|
if karar_tipi is None:
|
|
karar_tipi = KARAR_TIPI_NORM
|
|
|
|
record = await self.api.get_decision(karar_tipi, uuid) if uuid else None
|
|
|
|
if not record:
|
|
logger.warning("AnayasaMahkemesiApiClient: No record for document_url %s", document_url)
|
|
return AnayasaDocumentMarkdown(
|
|
source_url=document_url, markdown_chunk=None,
|
|
current_page=page_number, total_pages=0, is_paginated=False,
|
|
)
|
|
|
|
esas_no = record.get("esasNo") or ""
|
|
karar_no = record.get("kararNo") or ""
|
|
reference = f"E.{esas_no}, K.{karar_no}" if (esas_no and karar_no) else (esas_no or karar_no or "")
|
|
rg_tarihi = record.get("resmiGazeteTarihi") or ""
|
|
rg_sayisi = record.get("resmiGazeteSayisi")
|
|
official_gazette = f"{rg_tarihi} / {rg_sayisi}".strip(" /") if (rg_tarihi or rg_sayisi) else ""
|
|
|
|
full_markdown = convert_icerik_to_markdown(record.get("icerik"))
|
|
if not full_markdown:
|
|
return AnayasaDocumentMarkdown(
|
|
source_url=document_url,
|
|
decision_reference_no_from_page=reference,
|
|
decision_date_from_page=record.get("kararTarihi") or "",
|
|
official_gazette_info_from_page=official_gazette,
|
|
markdown_chunk=None, current_page=page_number, total_pages=0, is_paginated=False,
|
|
)
|
|
|
|
total_pages = max(1, math.ceil(len(full_markdown) / DOCUMENT_MARKDOWN_CHUNK_SIZE))
|
|
current_page = max(1, min(page_number, total_pages))
|
|
start = (current_page - 1) * DOCUMENT_MARKDOWN_CHUNK_SIZE
|
|
chunk = full_markdown[start:start + DOCUMENT_MARKDOWN_CHUNK_SIZE]
|
|
|
|
return AnayasaDocumentMarkdown(
|
|
source_url=document_url,
|
|
decision_reference_no_from_page=reference,
|
|
decision_date_from_page=record.get("kararTarihi") or "",
|
|
official_gazette_info_from_page=official_gazette,
|
|
markdown_chunk=chunk,
|
|
current_page=current_page,
|
|
total_pages=total_pages,
|
|
is_paginated=(total_pages > 1),
|
|
)
|
|
|
|
async def close_client_session(self):
|
|
await self.api.close()
|
|
logger.info("AnayasaMahkemesiApiClient (Norm Denetimi): HTTP client session closed.")
|