8d6639bc4c
Adds a new MCP tool `search_decisive_appraisals` that queries the Ministry of Justice public registry (~30K published decisions) by block (גוש), plot (חלקה), appraiser name, committee, decision/publicity date ranges, or free text — and returns metadata + direct PDF URLs. Implementation notes: - New `nadlan_mcp/govil/` package, parallel to `nadlan_mcp/govmap/`, for gov.il APIs that are not Govmap. Pydantic v2 models match the upstream PascalCase response via aliases. - Upstream sits behind an F5 WAF that rejects standard `requests`; uses `curl_cffi` with Chrome 120 impersonation to traverse it. - Static `x-client-id` header (issued to the gov.il SPA, public, visible in any DevTools session) is required by the gateway — without it every call returns a generic 500. - 14 unit tests cover model parsing, body shape, pagination, and the 500-is-fatal contract (configuration error, not retryable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
2.6 KiB
Python
64 lines
2.6 KiB
Python
"""
|
|
Pydantic models for the Ministry of Justice "Decisive Appraiser" API.
|
|
|
|
The upstream API at pub-justice.openapi.gov.il returns decisions in a
|
|
nested wrapper:
|
|
|
|
{"Results": [{"Data": {...decision fields...}}, ...],
|
|
"TotalResults": 30398, "Status": ..., "message": ...}
|
|
|
|
These models flatten the wrapper for ergonomic use, while preserving the
|
|
original Hebrew field labels via aliases.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class AppraisalDocument(BaseModel):
|
|
"""A single PDF attached to an appraisal decision."""
|
|
|
|
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
|
|
file_url: str = Field(alias="FileName")
|
|
display_name: Optional[str] = Field(default=None, alias="DisplayName")
|
|
extension: Optional[str] = Field(default=None, alias="Extension")
|
|
|
|
|
|
class AppraisalDecision(BaseModel):
|
|
"""
|
|
One published decision by a certified decisive appraiser (שמאי מכריע).
|
|
|
|
Field names are snake_case in Python; aliases match the upstream
|
|
PascalCase JSON keys so the model can be constructed from a raw API
|
|
payload via `AppraisalDecision.model_validate(data)`.
|
|
"""
|
|
|
|
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
|
|
appraisal_header: Optional[str] = Field(default=None, alias="AppraisalHeader")
|
|
appraisal_type: Optional[str] = Field(default=None, alias="AppraisalType")
|
|
appraisal_version: Optional[str] = Field(default=None, alias="AppraisalVersion")
|
|
decisive_appraiser: Optional[str] = Field(default=None, alias="DecisiveAppraiser")
|
|
appraiser_type: Optional[str] = Field(default=None, alias="AppraiserType")
|
|
block: Optional[str] = Field(default=None, alias="Block")
|
|
plot: Optional[str] = Field(default=None, alias="Plot")
|
|
committee: Optional[str] = Field(default=None, alias="Committee")
|
|
decision_date: Optional[datetime] = Field(default=None, alias="DecisionDate")
|
|
publicity_date: Optional[datetime] = Field(default=None, alias="PublicityDate")
|
|
documents: List[AppraisalDocument] = Field(default_factory=list, alias="Document")
|
|
doc_summary: Dict[str, Any] = Field(default_factory=dict, alias="DocSummary")
|
|
|
|
|
|
class DecisiveAppraiserSearchResponse(BaseModel):
|
|
"""Top-level wrapper of the SearchDecisions endpoint."""
|
|
|
|
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
|
|
total_results: int = Field(default=0, alias="TotalResults")
|
|
results: List[AppraisalDecision] = Field(default_factory=list)
|
|
status: Optional[Any] = Field(default=None, alias="Status")
|
|
message: Optional[Any] = Field(default=None)
|