Files
nadlan-mcp/nadlan_mcp/govil/models.py
T
chaim 8d6639bc4c 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>
2026-04-25 10:45:25 +00:00

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)