Add: Decisive appraiser (שמאי מכריע) search via gov.il public API
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>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user