CR Fixes and update markdownlint
This commit is contained in:
+2
-1
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"MD032": false,
|
||||
"MD022": false
|
||||
"MD022": false,
|
||||
"MD013": false
|
||||
}
|
||||
+252
-141
@@ -1,11 +1,12 @@
|
||||
import requests
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
from collections import Counter
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from nadlan_mcp.config import get_config, GovmapConfig
|
||||
import requests
|
||||
|
||||
from nadlan_mcp.config import GovmapConfig, get_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,12 +32,11 @@ class GovmapClient:
|
||||
config: Optional configuration object. If None, uses global config.
|
||||
"""
|
||||
self.config = config or get_config()
|
||||
self.base_url = self.config.base_url.rstrip('/')
|
||||
self.base_url = self.config.base_url.rstrip("/")
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': self.config.user_agent
|
||||
})
|
||||
self.session.headers.update(
|
||||
{"Content-Type": "application/json", "User-Agent": self.config.user_agent}
|
||||
)
|
||||
self.last_request_time = 0.0
|
||||
|
||||
def _rate_limit(self):
|
||||
@@ -101,7 +101,9 @@ class GovmapClient:
|
||||
|
||||
return (lon, lat)
|
||||
|
||||
def _validate_positive_int(self, value: int, name: str, max_value: Optional[int] = None) -> int:
|
||||
def _validate_positive_int(
|
||||
self, value: int, name: str, max_value: Optional[int] = None
|
||||
) -> int:
|
||||
"""
|
||||
Validate positive integer input.
|
||||
|
||||
@@ -145,7 +147,7 @@ class GovmapClient:
|
||||
"searchText": search_text,
|
||||
"language": "he",
|
||||
"isAccurate": False,
|
||||
"maxResults": 10
|
||||
"maxResults": 10,
|
||||
}
|
||||
|
||||
# Retry logic with exponential backoff
|
||||
@@ -153,13 +155,15 @@ class GovmapClient:
|
||||
try:
|
||||
self._rate_limit()
|
||||
|
||||
logger.info(f"Searching for address: {search_text} (attempt {attempt + 1}/{self.config.max_retries + 1})")
|
||||
logger.info(
|
||||
f"Searching for address: {search_text} (attempt {attempt + 1}/{self.config.max_retries + 1})"
|
||||
)
|
||||
timeout = (self.config.connect_timeout, self.config.read_timeout)
|
||||
response = self.session.post(url, json=payload, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
if not data or 'results' not in data:
|
||||
if not data or "results" not in data:
|
||||
raise ValueError("Invalid response format from autocomplete API")
|
||||
|
||||
return data
|
||||
@@ -167,16 +171,22 @@ class GovmapClient:
|
||||
except (requests.RequestException, requests.Timeout) as e:
|
||||
if attempt < self.config.max_retries:
|
||||
wait_time = min(
|
||||
self.config.retry_min_wait * (2 ** attempt),
|
||||
self.config.retry_max_wait
|
||||
self.config.retry_min_wait * (2**attempt),
|
||||
self.config.retry_max_wait,
|
||||
)
|
||||
logger.warning(
|
||||
f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}"
|
||||
)
|
||||
logger.warning(f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Request failed after {self.config.max_retries + 1} attempts: {e}")
|
||||
logger.error(
|
||||
f"Request failed after {self.config.max_retries + 1} attempts: {e}"
|
||||
)
|
||||
raise
|
||||
# This line should never be reached but satisfies type checker
|
||||
raise RuntimeError("Unexpected error: retry loop exited without return or raise")
|
||||
raise RuntimeError(
|
||||
"Unexpected error: retry loop exited without return or raise"
|
||||
)
|
||||
|
||||
def get_gush_helka(self, point: Tuple[float, float]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -195,18 +205,16 @@ class GovmapClient:
|
||||
point = self._validate_coordinates(point)
|
||||
url = f"{self.base_url}/layers-catalog/entitiesByPoint"
|
||||
|
||||
payload = {
|
||||
"point": list(point),
|
||||
"layers": [{"layerId": "16"}],
|
||||
"tolerance": 0
|
||||
}
|
||||
payload = {"point": list(point), "layers": [{"layerId": "16"}], "tolerance": 0}
|
||||
|
||||
# Retry logic with exponential backoff
|
||||
for attempt in range(self.config.max_retries + 1):
|
||||
try:
|
||||
self._rate_limit()
|
||||
|
||||
logger.info(f"Getting Gush/Helka for point: {point} (attempt {attempt + 1}/{self.config.max_retries + 1})")
|
||||
logger.info(
|
||||
f"Getting Gush/Helka for point: {point} (attempt {attempt + 1}/{self.config.max_retries + 1})"
|
||||
)
|
||||
timeout = (self.config.connect_timeout, self.config.read_timeout)
|
||||
response = self.session.post(url, json=payload, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
@@ -217,18 +225,26 @@ class GovmapClient:
|
||||
except (requests.RequestException, requests.Timeout) as e:
|
||||
if attempt < self.config.max_retries:
|
||||
wait_time = min(
|
||||
self.config.retry_min_wait * (2 ** attempt),
|
||||
self.config.retry_max_wait
|
||||
self.config.retry_min_wait * (2**attempt),
|
||||
self.config.retry_max_wait,
|
||||
)
|
||||
logger.warning(
|
||||
f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}"
|
||||
)
|
||||
logger.warning(f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Request failed after {self.config.max_retries + 1} attempts: {e}")
|
||||
logger.error(
|
||||
f"Request failed after {self.config.max_retries + 1} attempts: {e}"
|
||||
)
|
||||
raise
|
||||
# This line should never be reached but satisfies type checker
|
||||
raise RuntimeError("Unexpected error: retry loop exited without return or raise")
|
||||
raise RuntimeError(
|
||||
"Unexpected error: retry loop exited without return or raise"
|
||||
)
|
||||
|
||||
def get_deals_by_radius(self, point: Tuple[float, float], radius: int = 50) -> List[Dict[str, Any]]:
|
||||
def get_deals_by_radius(
|
||||
self, point: Tuple[float, float], radius: int = 50
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Find real estate deals within a specified radius of a point.
|
||||
|
||||
@@ -252,33 +268,48 @@ class GovmapClient:
|
||||
try:
|
||||
self._rate_limit()
|
||||
|
||||
logger.info(f"Getting deals by radius for point: {point}, radius: {radius}m (attempt {attempt + 1}/{self.config.max_retries + 1})")
|
||||
logger.info(
|
||||
f"Getting deals by radius for point: {point}, radius: {radius}m (attempt {attempt + 1}/{self.config.max_retries + 1})"
|
||||
)
|
||||
timeout = (self.config.connect_timeout, self.config.read_timeout)
|
||||
response = self.session.get(url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
if not isinstance(data, list):
|
||||
raise ValueError(f"Expected list response, got {type(data).__name__}")
|
||||
raise ValueError(
|
||||
f"Expected list response, got {type(data).__name__}"
|
||||
)
|
||||
return data
|
||||
|
||||
except (requests.RequestException, requests.Timeout) as e:
|
||||
if attempt < self.config.max_retries:
|
||||
wait_time = min(
|
||||
self.config.retry_min_wait * (2 ** attempt),
|
||||
self.config.retry_max_wait
|
||||
self.config.retry_min_wait * (2**attempt),
|
||||
self.config.retry_max_wait,
|
||||
)
|
||||
logger.warning(
|
||||
f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}"
|
||||
)
|
||||
logger.warning(f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Request failed after {self.config.max_retries + 1} attempts: {e}")
|
||||
logger.error(
|
||||
f"Request failed after {self.config.max_retries + 1} attempts: {e}"
|
||||
)
|
||||
raise
|
||||
# This line should never be reached but satisfies type checker
|
||||
raise RuntimeError("Unexpected error: retry loop exited without return or raise")
|
||||
raise RuntimeError(
|
||||
"Unexpected error: retry loop exited without return or raise"
|
||||
)
|
||||
|
||||
def get_street_deals(self, polygon_id: str, limit: int = 10,
|
||||
start_date: Optional[str] = None, end_date: Optional[str] = None,
|
||||
deal_type: int = 2) -> List[Dict[str, Any]]:
|
||||
def get_street_deals(
|
||||
self,
|
||||
polygon_id: str,
|
||||
limit: int = 10,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
deal_type: int = 2,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve detailed information about deals on a specific street.
|
||||
|
||||
@@ -304,7 +335,9 @@ class GovmapClient:
|
||||
|
||||
limit = self._validate_positive_int(limit, "limit", max_value=1000)
|
||||
if deal_type not in (1, 2):
|
||||
raise ValueError("deal_type must be 1 (first hand/new) or 2 (second hand/used)")
|
||||
raise ValueError(
|
||||
"deal_type must be 1 (first hand/new) or 2 (second hand/used)"
|
||||
)
|
||||
|
||||
url = f"{self.base_url}/real-estate/street-deals/{polygon_id}"
|
||||
|
||||
@@ -319,39 +352,56 @@ class GovmapClient:
|
||||
try:
|
||||
self._rate_limit()
|
||||
|
||||
logger.info(f"Getting street deals for polygon: {polygon_id}, dealType: {deal_type} (attempt {attempt + 1}/{self.config.max_retries + 1})")
|
||||
logger.info(
|
||||
f"Getting street deals for polygon: {polygon_id}, dealType: {deal_type} (attempt {attempt + 1}/{self.config.max_retries + 1})"
|
||||
)
|
||||
timeout = (self.config.connect_timeout, self.config.read_timeout)
|
||||
response = self.session.get(url, params=params, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
# API returns {data: [...], totalCount: ..., limit: ..., offset: ...}
|
||||
if isinstance(data, dict) and 'data' in data:
|
||||
if not isinstance(data['data'], list):
|
||||
raise ValueError(f"Expected list in 'data' field, got {type(data['data']).__name__}")
|
||||
return data['data']
|
||||
if isinstance(data, dict) and "data" in data:
|
||||
if not isinstance(data["data"], list):
|
||||
raise ValueError(
|
||||
f"Expected list in 'data' field, got {type(data['data']).__name__}"
|
||||
)
|
||||
return data["data"]
|
||||
elif isinstance(data, list):
|
||||
return data
|
||||
else:
|
||||
raise ValueError(f"Unexpected response format: {type(data).__name__}")
|
||||
raise ValueError(
|
||||
f"Unexpected response format: {type(data).__name__}"
|
||||
)
|
||||
|
||||
except (requests.RequestException, requests.Timeout) as e:
|
||||
if attempt < self.config.max_retries:
|
||||
wait_time = min(
|
||||
self.config.retry_min_wait * (2 ** attempt),
|
||||
self.config.retry_max_wait
|
||||
self.config.retry_min_wait * (2**attempt),
|
||||
self.config.retry_max_wait,
|
||||
)
|
||||
logger.warning(
|
||||
f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}"
|
||||
)
|
||||
logger.warning(f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Request failed after {self.config.max_retries + 1} attempts: {e}")
|
||||
logger.error(
|
||||
f"Request failed after {self.config.max_retries + 1} attempts: {e}"
|
||||
)
|
||||
raise
|
||||
# This line should never be reached but satisfies type checker
|
||||
raise RuntimeError("Unexpected error: retry loop exited without return or raise")
|
||||
raise RuntimeError(
|
||||
"Unexpected error: retry loop exited without return or raise"
|
||||
)
|
||||
|
||||
def get_neighborhood_deals(self, polygon_id: str, limit: int = 10,
|
||||
start_date: Optional[str] = None, end_date: Optional[str] = None,
|
||||
deal_type: int = 2) -> List[Dict[str, Any]]:
|
||||
def get_neighborhood_deals(
|
||||
self,
|
||||
polygon_id: str,
|
||||
limit: int = 10,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
deal_type: int = 2,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve deals within the same neighborhood as the given polygon_id.
|
||||
|
||||
@@ -377,7 +427,9 @@ class GovmapClient:
|
||||
|
||||
limit = self._validate_positive_int(limit, "limit", max_value=1000)
|
||||
if deal_type not in (1, 2):
|
||||
raise ValueError("deal_type must be 1 (first hand/new) or 2 (second hand/used)")
|
||||
raise ValueError(
|
||||
"deal_type must be 1 (first hand/new) or 2 (second hand/used)"
|
||||
)
|
||||
|
||||
url = f"{self.base_url}/real-estate/neighborhood-deals/{polygon_id}"
|
||||
|
||||
@@ -392,39 +444,56 @@ class GovmapClient:
|
||||
try:
|
||||
self._rate_limit()
|
||||
|
||||
logger.info(f"Getting neighborhood deals for polygon: {polygon_id}, dealType: {deal_type} (attempt {attempt + 1}/{self.config.max_retries + 1})")
|
||||
logger.info(
|
||||
f"Getting neighborhood deals for polygon: {polygon_id}, dealType: {deal_type} (attempt {attempt + 1}/{self.config.max_retries + 1})"
|
||||
)
|
||||
timeout = (self.config.connect_timeout, self.config.read_timeout)
|
||||
response = self.session.get(url, params=params, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
# API returns {data: [...], totalCount: ..., limit: ..., offset: ...}
|
||||
if isinstance(data, dict) and 'data' in data:
|
||||
if not isinstance(data['data'], list):
|
||||
raise ValueError(f"Expected list in 'data' field, got {type(data['data']).__name__}")
|
||||
return data['data']
|
||||
if isinstance(data, dict) and "data" in data:
|
||||
if not isinstance(data["data"], list):
|
||||
raise ValueError(
|
||||
f"Expected list in 'data' field, got {type(data['data']).__name__}"
|
||||
)
|
||||
return data["data"]
|
||||
elif isinstance(data, list):
|
||||
return data
|
||||
else:
|
||||
raise ValueError(f"Unexpected response format: {type(data).__name__}")
|
||||
raise ValueError(
|
||||
f"Unexpected response format: {type(data).__name__}"
|
||||
)
|
||||
|
||||
except (requests.RequestException, requests.Timeout) as e:
|
||||
if attempt < self.config.max_retries:
|
||||
wait_time = min(
|
||||
self.config.retry_min_wait * (2 ** attempt),
|
||||
self.config.retry_max_wait
|
||||
self.config.retry_min_wait * (2**attempt),
|
||||
self.config.retry_max_wait,
|
||||
)
|
||||
logger.warning(
|
||||
f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}"
|
||||
)
|
||||
logger.warning(f"Request failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Request failed after {self.config.max_retries + 1} attempts: {e}")
|
||||
logger.error(
|
||||
f"Request failed after {self.config.max_retries + 1} attempts: {e}"
|
||||
)
|
||||
raise
|
||||
# This line should never be reached but satisfies type checker
|
||||
raise RuntimeError("Unexpected error: retry loop exited without return or raise")
|
||||
raise RuntimeError(
|
||||
"Unexpected error: retry loop exited without return or raise"
|
||||
)
|
||||
|
||||
def find_recent_deals_for_address(self, address: str, years_back: int = 2,
|
||||
radius: int = 30, max_deals: int = 50,
|
||||
deal_type: int = 2) -> List[Dict[str, Any]]:
|
||||
def find_recent_deals_for_address(
|
||||
self,
|
||||
address: str,
|
||||
years_back: int = 2,
|
||||
radius: int = 30,
|
||||
max_deals: int = 50,
|
||||
deal_type: int = 2,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Find all relevant real estate deals for a given address from the last few years.
|
||||
|
||||
@@ -453,25 +522,29 @@ class GovmapClient:
|
||||
radius = self._validate_positive_int(radius, "radius", max_value=5000)
|
||||
max_deals = self._validate_positive_int(max_deals, "max_deals", max_value=10000)
|
||||
if deal_type not in (1, 2):
|
||||
raise ValueError("deal_type must be 1 (first hand/new) or 2 (second hand/used)")
|
||||
raise ValueError(
|
||||
"deal_type must be 1 (first hand/new) or 2 (second hand/used)"
|
||||
)
|
||||
|
||||
try:
|
||||
# Step 1: Get coordinates for the address
|
||||
logger.info(f"Starting search for address: {address}, dealType: {deal_type}")
|
||||
logger.info(
|
||||
f"Starting search for address: {address}, dealType: {deal_type}"
|
||||
)
|
||||
autocomplete_result = self.autocomplete_address(address)
|
||||
|
||||
if not autocomplete_result.get('results'):
|
||||
if not autocomplete_result.get("results"):
|
||||
raise ValueError(f"No results found for address: {address}")
|
||||
|
||||
# Get the best match (first result)
|
||||
best_match = autocomplete_result['results'][0]
|
||||
if 'shape' not in best_match:
|
||||
best_match = autocomplete_result["results"][0]
|
||||
if "shape" not in best_match:
|
||||
raise ValueError("No coordinates found in autocomplete result")
|
||||
|
||||
# Parse coordinates from WKT POINT string
|
||||
# Format: "POINT(longitude latitude)"
|
||||
shape_str = best_match['shape']
|
||||
if not shape_str.startswith('POINT('):
|
||||
shape_str = best_match["shape"]
|
||||
if not shape_str.startswith("POINT("):
|
||||
raise ValueError("Invalid coordinate format in autocomplete result")
|
||||
|
||||
# Extract coordinates from "POINT(x y)"
|
||||
@@ -490,16 +563,16 @@ class GovmapClient:
|
||||
# Extract unique polygon IDs
|
||||
polygon_ids = set()
|
||||
for deal in nearby_deals:
|
||||
if 'polygon_id' in deal:
|
||||
polygon_ids.add(str(deal['polygon_id']))
|
||||
if "polygon_id" in deal:
|
||||
polygon_ids.add(str(deal["polygon_id"]))
|
||||
|
||||
logger.info(f"Found {len(polygon_ids)} unique polygon IDs")
|
||||
|
||||
# Step 3: Calculate date range
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=years_back * 365)
|
||||
start_date_str = start_date.strftime('%Y-%m')
|
||||
end_date_str = end_date.strftime('%Y-%m')
|
||||
start_date_str = start_date.strftime("%Y-%m")
|
||||
end_date_str = end_date.strftime("%Y-%m")
|
||||
|
||||
# Step 4: Get street and neighborhood deals for each polygon
|
||||
# Prioritize: same building (0) > street deals (1) > neighborhood deals (2)
|
||||
@@ -512,14 +585,20 @@ class GovmapClient:
|
||||
try:
|
||||
# Get street deals first (higher priority)
|
||||
current_street_deals = self.get_street_deals(
|
||||
polygon_id, limit=max_deals // 2, # Allocate more to street deals
|
||||
start_date=start_date_str, end_date=end_date_str, deal_type=deal_type
|
||||
polygon_id,
|
||||
limit=max_deals // 2, # Allocate more to street deals
|
||||
start_date=start_date_str,
|
||||
end_date=end_date_str,
|
||||
deal_type=deal_type,
|
||||
)
|
||||
|
||||
# Get neighborhood deals (lower priority)
|
||||
current_neighborhood_deals = self.get_neighborhood_deals(
|
||||
polygon_id, limit=max_deals // 4, # Allocate less to neighborhood deals
|
||||
start_date=start_date_str, end_date=end_date_str, deal_type=deal_type
|
||||
polygon_id,
|
||||
limit=max_deals // 4, # Allocate less to neighborhood deals
|
||||
start_date=start_date_str,
|
||||
end_date=end_date_str,
|
||||
deal_type=deal_type,
|
||||
)
|
||||
|
||||
# Process street deals and separate building deals
|
||||
@@ -527,17 +606,19 @@ class GovmapClient:
|
||||
deal_id = f"{deal.get('dealId', '')}{deal.get('address', '')}{deal.get('dealDate', '')}"
|
||||
if deal_id not in seen_deals:
|
||||
seen_deals.add(deal_id)
|
||||
deal['source_polygon_id'] = polygon_id
|
||||
deal['deal_source'] = 'street'
|
||||
deal["source_polygon_id"] = polygon_id
|
||||
deal["deal_source"] = "street"
|
||||
|
||||
# Check if this is from the same building
|
||||
deal_address = deal.get('address', '').lower().strip()
|
||||
if self._is_same_building(search_address_normalized, deal_address):
|
||||
deal['deal_source'] = 'same_building'
|
||||
deal['priority'] = 0 # Highest priority
|
||||
deal_address = deal.get("address", "").lower().strip()
|
||||
if self._is_same_building(
|
||||
search_address_normalized, deal_address
|
||||
):
|
||||
deal["deal_source"] = "same_building"
|
||||
deal["priority"] = 0 # Highest priority
|
||||
building_deals.append(deal)
|
||||
else:
|
||||
deal['priority'] = 1 # Street deals priority
|
||||
deal["priority"] = 1 # Street deals priority
|
||||
street_deals.append(deal)
|
||||
|
||||
# Add neighborhood deals with lowest priority
|
||||
@@ -545,9 +626,9 @@ class GovmapClient:
|
||||
deal_id = f"{deal.get('dealId', '')}{deal.get('address', '')}{deal.get('dealDate', '')}"
|
||||
if deal_id not in seen_deals:
|
||||
seen_deals.add(deal_id)
|
||||
deal['source_polygon_id'] = polygon_id
|
||||
deal['deal_source'] = 'neighborhood'
|
||||
deal['priority'] = 2 # Lowest priority
|
||||
deal["source_polygon_id"] = polygon_id
|
||||
deal["deal_source"] = "neighborhood"
|
||||
deal["priority"] = 2 # Lowest priority
|
||||
neighborhood_deals.append(deal)
|
||||
|
||||
except Exception as e:
|
||||
@@ -559,8 +640,12 @@ class GovmapClient:
|
||||
|
||||
# Use stable sort: first by date (newest first), then by priority
|
||||
# Since Python's sort is stable, the second sort maintains date order within each priority
|
||||
all_deals.sort(key=lambda x: x.get('dealDate', '1900-01-01'), reverse=True) # Newest first
|
||||
all_deals.sort(key=lambda x: x.get('priority', 3)) # Priority first (0=building, 1=street, 2=neighborhood)
|
||||
all_deals.sort(
|
||||
key=lambda x: x.get("dealDate", "1900-01-01"), reverse=True
|
||||
) # Newest first
|
||||
all_deals.sort(
|
||||
key=lambda x: x.get("priority", 3)
|
||||
) # Priority first (0=building, 1=street, 2=neighborhood)
|
||||
|
||||
# Limit to max_deals
|
||||
if len(all_deals) > max_deals:
|
||||
@@ -568,20 +653,28 @@ class GovmapClient:
|
||||
|
||||
# Add price per square meter calculation and deal type info
|
||||
for deal in all_deals:
|
||||
price = deal.get('dealAmount', 0)
|
||||
area = deal.get('assetArea', 0)
|
||||
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
|
||||
deal['price_per_sqm'] = round(price / area, 2)
|
||||
price = deal.get("dealAmount", 0)
|
||||
area = deal.get("assetArea", 0)
|
||||
if (
|
||||
isinstance(price, (int, float))
|
||||
and isinstance(area, (int, float))
|
||||
and area > 0
|
||||
):
|
||||
deal["price_per_sqm"] = round(price / area, 2)
|
||||
else:
|
||||
deal['price_per_sqm'] = None
|
||||
deal["price_per_sqm"] = None
|
||||
|
||||
# Add deal type description for clarity
|
||||
deal['deal_type'] = deal_type
|
||||
deal['deal_type_description'] = 'first_hand_new' if deal_type == 1 else 'second_hand_used'
|
||||
deal["deal_type"] = deal_type
|
||||
deal["deal_type_description"] = (
|
||||
"first_hand_new" if deal_type == 1 else "second_hand_used"
|
||||
)
|
||||
|
||||
logger.info(f"Found {len(all_deals)} total deals for address: {address} "
|
||||
f"(Building: {len(building_deals)}, Street: {len(street_deals)}, Neighborhood: {len(neighborhood_deals)}) "
|
||||
f"[{deal['deal_type_description'] if all_deals else 'N/A'}]")
|
||||
logger.info(
|
||||
f"Found {len(all_deals)} total deals for address: {address} "
|
||||
f"(Building: {len(building_deals)}, Street: {len(street_deals)}, Neighborhood: {len(neighborhood_deals)}) "
|
||||
f"[{deal['deal_type_description'] if all_deals else 'N/A'}]"
|
||||
)
|
||||
return all_deals
|
||||
|
||||
except Exception as e:
|
||||
@@ -610,8 +703,13 @@ class GovmapClient:
|
||||
def extract_address_parts(addr: str) -> tuple:
|
||||
"""Extract street name and number from address"""
|
||||
# Remove common prefixes/suffixes and normalize
|
||||
addr_clean = addr.replace('רח\'', '').replace('רחוב', '').replace('שד\'', '').replace('שדרות', '')
|
||||
addr_clean = addr_clean.replace(' ', ' ').strip()
|
||||
addr_clean = (
|
||||
addr.replace("רח'", "")
|
||||
.replace("רחוב", "")
|
||||
.replace("שד'", "")
|
||||
.replace("שדרות", "")
|
||||
)
|
||||
addr_clean = addr_clean.replace(" ", " ").strip()
|
||||
|
||||
# Try to extract number and street name
|
||||
parts = addr_clean.split()
|
||||
@@ -620,18 +718,24 @@ class GovmapClient:
|
||||
for i, part in enumerate(parts):
|
||||
if part.isdigit() or any(c.isdigit() for c in part):
|
||||
number = part
|
||||
street_parts = parts[:i] + parts[i+1:]
|
||||
street_name = ' '.join(street_parts).strip()
|
||||
street_parts = parts[:i] + parts[i + 1 :]
|
||||
street_name = " ".join(street_parts).strip()
|
||||
return (street_name, number)
|
||||
|
||||
return (addr_clean, '')
|
||||
return (addr_clean, "")
|
||||
|
||||
search_street, search_number = extract_address_parts(search_address)
|
||||
deal_street, deal_number = extract_address_parts(deal_address)
|
||||
|
||||
# Same street and same number = same building
|
||||
if (search_street and deal_street and search_number and deal_number and
|
||||
search_street == deal_street and search_number == deal_number):
|
||||
if (
|
||||
search_street
|
||||
and deal_street
|
||||
and search_number
|
||||
and deal_number
|
||||
and search_street == deal_street
|
||||
and search_number == deal_number
|
||||
):
|
||||
return True
|
||||
|
||||
# Check if one address is contained in the other (for different formats of same address)
|
||||
@@ -693,12 +797,14 @@ class GovmapClient:
|
||||
for deal in deals:
|
||||
# Property type filter
|
||||
if property_type is not None:
|
||||
deal_type = deal.get('propertyTypeDescription', deal.get('assetTypeHeb', ''))
|
||||
deal_type = deal.get(
|
||||
"propertyTypeDescription", deal.get("assetTypeHeb", "")
|
||||
)
|
||||
if property_type.lower() not in deal_type.lower():
|
||||
continue
|
||||
|
||||
# Room count filter
|
||||
rooms = deal.get('assetRoomNum')
|
||||
rooms = deal.get("assetRoomNum")
|
||||
if rooms is not None:
|
||||
try:
|
||||
rooms = float(rooms)
|
||||
@@ -710,7 +816,7 @@ class GovmapClient:
|
||||
pass # Skip deals with invalid room data
|
||||
|
||||
# Price filter
|
||||
price = deal.get('dealAmount')
|
||||
price = deal.get("dealAmount")
|
||||
if price is not None:
|
||||
try:
|
||||
price = float(price)
|
||||
@@ -722,7 +828,7 @@ class GovmapClient:
|
||||
pass # Skip deals with invalid price data
|
||||
|
||||
# Area filter
|
||||
area = deal.get('assetArea')
|
||||
area = deal.get("assetArea")
|
||||
if area is not None:
|
||||
try:
|
||||
area = float(area)
|
||||
@@ -734,7 +840,7 @@ class GovmapClient:
|
||||
pass # Skip deals with invalid area data
|
||||
|
||||
# Floor filter
|
||||
floor_str = deal.get('floorNo', '')
|
||||
floor_str = deal.get("floorNo", "")
|
||||
if floor_str and isinstance(floor_str, str):
|
||||
# Try to extract floor number (handles Hebrew floor descriptions)
|
||||
floor_num = self._extract_floor_number(floor_str)
|
||||
@@ -763,10 +869,18 @@ class GovmapClient:
|
||||
|
||||
# Hebrew ordinal floor names to numbers
|
||||
hebrew_floors = {
|
||||
'קרקע': 0, 'מרתף': -1,
|
||||
'ראשונה': 1, 'שניה': 2, 'שלישית': 3, 'רביעית': 4,
|
||||
'חמישית': 5, 'שישית': 6, 'שביעית': 7, 'שמינית': 8,
|
||||
'תשיעית': 9, 'עשירית': 10
|
||||
"קרקע": 0,
|
||||
"מרתף": -1,
|
||||
"ראשונה": 1,
|
||||
"שניה": 2,
|
||||
"שלישית": 3,
|
||||
"רביעית": 4,
|
||||
"חמישית": 5,
|
||||
"שישית": 6,
|
||||
"שביעית": 7,
|
||||
"שמינית": 8,
|
||||
"תשיעית": 9,
|
||||
"עשירית": 10,
|
||||
}
|
||||
|
||||
floor_lower = floor_str.lower().strip()
|
||||
@@ -777,8 +891,7 @@ class GovmapClient:
|
||||
return num
|
||||
|
||||
# Try to extract number from string
|
||||
import re
|
||||
numbers = re.findall(r'\d+', floor_str)
|
||||
numbers = re.findall(r"\d+", floor_str)
|
||||
if numbers:
|
||||
try:
|
||||
return int(numbers[0])
|
||||
@@ -787,10 +900,7 @@ class GovmapClient:
|
||||
|
||||
return None
|
||||
|
||||
def calculate_deal_statistics(
|
||||
self,
|
||||
deals: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
def calculate_deal_statistics(self, deals: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate statistical aggregations on deal data.
|
||||
|
||||
@@ -812,7 +922,7 @@ class GovmapClient:
|
||||
"price_stats": {},
|
||||
"area_stats": {},
|
||||
"price_per_sqm_stats": {},
|
||||
"room_distribution": {}
|
||||
"room_distribution": {},
|
||||
}
|
||||
|
||||
# Extract numeric values
|
||||
@@ -822,21 +932,21 @@ class GovmapClient:
|
||||
rooms = []
|
||||
|
||||
for deal in deals:
|
||||
price = deal.get('dealAmount')
|
||||
price = deal.get("dealAmount")
|
||||
if isinstance(price, (int, float)) and price > 0:
|
||||
prices.append(price)
|
||||
|
||||
area = deal.get('assetArea')
|
||||
area = deal.get("assetArea")
|
||||
if isinstance(area, (int, float)) and area > 0:
|
||||
areas.append(area)
|
||||
|
||||
pps = deal.get('price_per_sqm')
|
||||
pps = deal.get("price_per_sqm")
|
||||
if pps is None and price and area and area > 0:
|
||||
pps = price / area
|
||||
if isinstance(pps, (int, float)) and pps > 0:
|
||||
price_per_sqm_values.append(pps)
|
||||
|
||||
room_count = deal.get('assetRoomNum')
|
||||
room_count = deal.get("assetRoomNum")
|
||||
if isinstance(room_count, (int, float)):
|
||||
rooms.append(room_count)
|
||||
|
||||
@@ -853,8 +963,10 @@ class GovmapClient:
|
||||
"max": max(prices),
|
||||
"p25": sorted_prices[len(sorted_prices) // 4],
|
||||
"p75": sorted_prices[(3 * len(sorted_prices)) // 4],
|
||||
"std_dev": round(self._calculate_std_dev(prices), 2) if len(prices) > 1 else 0,
|
||||
"total": sum(prices)
|
||||
"std_dev": round(self._calculate_std_dev(prices), 2)
|
||||
if len(prices) > 1
|
||||
else 0,
|
||||
"total": sum(prices),
|
||||
}
|
||||
|
||||
# Area statistics
|
||||
@@ -866,7 +978,7 @@ class GovmapClient:
|
||||
"min": min(areas),
|
||||
"max": max(areas),
|
||||
"p25": sorted_areas[len(sorted_areas) // 4],
|
||||
"p75": sorted_areas[(3 * len(sorted_areas)) // 4]
|
||||
"p75": sorted_areas[(3 * len(sorted_areas)) // 4],
|
||||
}
|
||||
|
||||
# Price per sqm statistics
|
||||
@@ -878,12 +990,11 @@ class GovmapClient:
|
||||
"min": round(min(price_per_sqm_values), 2),
|
||||
"max": round(max(price_per_sqm_values), 2),
|
||||
"p25": round(sorted_pps[len(sorted_pps) // 4], 2),
|
||||
"p75": round(sorted_pps[(3 * len(sorted_pps)) // 4], 2)
|
||||
"p75": round(sorted_pps[(3 * len(sorted_pps)) // 4], 2),
|
||||
}
|
||||
|
||||
# Room distribution
|
||||
if rooms:
|
||||
from collections import Counter
|
||||
room_counts = Counter(rooms)
|
||||
stats["room_distribution"] = dict(sorted(room_counts.items()))
|
||||
|
||||
@@ -895,4 +1006,4 @@ class GovmapClient:
|
||||
return 0.0
|
||||
mean = sum(values) / len(values)
|
||||
variance = sum((x - mean) ** 2 for x in values) / (len(values) - 1)
|
||||
return variance ** 0.5
|
||||
return variance**0.5
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
requests>=2.31.0,<3.0.0
|
||||
python-dotenv>=1.0.0,<2.0.0
|
||||
pytest>=7.0.0,<8.0.0
|
||||
mcp>=1.0.0,<2.0.0
|
||||
fastmcp>=0.1.0,<1.0.0
|
||||
types-requests>=2.31.0,<3.0.0
|
||||
pydantic>=2.0.0,<3.0.0
|
||||
|
||||
Reference in New Issue
Block a user