Implementation of phase 4.1
This commit is contained in:
+389
@@ -0,0 +1,389 @@
|
||||
# Migration Guide: v1.x → v2.0.0
|
||||
|
||||
## Overview
|
||||
|
||||
Version 2.0.0 introduces **Pydantic v2 models** throughout the codebase, replacing dict-based data structures with type-safe, validated models. This is a **breaking change** that improves type safety, validation, and developer experience.
|
||||
|
||||
## Breaking Changes Summary
|
||||
|
||||
### All API Methods Now Return Pydantic Models
|
||||
|
||||
| Method | v1.x Return Type | v2.0 Return Type |
|
||||
|--------|------------------|------------------|
|
||||
| `autocomplete_address()` | `Dict` | `AutocompleteResponse` |
|
||||
| `get_deals_by_radius()` | `List[Dict]` | `List[Deal]` |
|
||||
| `get_street_deals()` | `List[Dict]` | `List[Deal]` |
|
||||
| `get_neighborhood_deals()` | `List[Dict]` | `List[Deal]` |
|
||||
| `find_recent_deals_for_address()` | `List[Dict]` | `List[Deal]` |
|
||||
| `calculate_deal_statistics()` | `Dict` | `DealStatistics` |
|
||||
| `calculate_market_activity_score()` | `Dict` | `MarketActivityScore` |
|
||||
| `analyze_investment_potential()` | `Dict` | `InvestmentAnalysis` |
|
||||
| `get_market_liquidity()` | `Dict` | `LiquidityMetrics` |
|
||||
| `filter_deals_by_criteria()` | `List[Dict]` | `List[Deal]` |
|
||||
|
||||
## Migration Examples
|
||||
|
||||
### 1. Autocomplete Address
|
||||
|
||||
**Before (v1.x):**
|
||||
```python
|
||||
from nadlan_mcp.govmap import GovmapClient
|
||||
|
||||
client = GovmapClient()
|
||||
result = client.autocomplete_address("סוקולוב 38 חולון")
|
||||
|
||||
# Dict access
|
||||
count = result["resultsCount"]
|
||||
first_result = result["results"][0]
|
||||
address_text = first_result["text"]
|
||||
coords = first_result.get("coordinates") # May not exist
|
||||
```
|
||||
|
||||
**After (v2.0):**
|
||||
```python
|
||||
from nadlan_mcp.govmap import GovmapClient
|
||||
|
||||
client = GovmapClient()
|
||||
result = client.autocomplete_address("סוקולוב 38 חולון")
|
||||
|
||||
# Model attributes with type hints
|
||||
count = result.results_count # int
|
||||
first_result = result.results[0] # AutocompleteResult
|
||||
address_text = first_result.text # str
|
||||
coords = first_result.coordinates # Optional[CoordinatePoint]
|
||||
|
||||
# Access coordinates if available
|
||||
if coords:
|
||||
lon = coords.longitude # float
|
||||
lat = coords.latitude # float
|
||||
```
|
||||
|
||||
### 2. Getting Deals
|
||||
|
||||
**Before (v1.x):**
|
||||
```python
|
||||
deals = client.get_street_deals("polygon123")
|
||||
|
||||
for deal in deals:
|
||||
price = deal.get("dealAmount")
|
||||
area = deal.get("assetArea")
|
||||
|
||||
# Manual price per sqm calculation
|
||||
if price and area and area > 0:
|
||||
price_per_sqm = price / area
|
||||
else:
|
||||
price_per_sqm = None
|
||||
```
|
||||
|
||||
**After (v2.0):**
|
||||
```python
|
||||
deals = client.get_street_deals("polygon123")
|
||||
|
||||
for deal in deals: # deal is a Deal model
|
||||
price = deal.deal_amount # float (camelCase → snake_case)
|
||||
area = deal.asset_area # Optional[float]
|
||||
|
||||
# Computed field - automatically calculated!
|
||||
price_per_sqm = deal.price_per_sqm # Optional[float]
|
||||
```
|
||||
|
||||
### 3. Market Analysis
|
||||
|
||||
**Before (v1.x):**
|
||||
```python
|
||||
stats = client.calculate_deal_statistics(deals)
|
||||
|
||||
# Dict access
|
||||
total = stats["total_deals"]
|
||||
avg_price = stats["price_statistics"]["mean"]
|
||||
property_dist = stats["property_type_distribution"]
|
||||
```
|
||||
|
||||
**After (v2.0):**
|
||||
```python
|
||||
stats = client.calculate_deal_statistics(deals)
|
||||
|
||||
# Model attributes
|
||||
total = stats.total_deals # int
|
||||
avg_price = stats.price_statistics["mean"] # Dict[str, float]
|
||||
property_dist = stats.property_type_distribution # Dict[str, int]
|
||||
|
||||
# Serialize to dict if needed
|
||||
stats_dict = stats.model_dump()
|
||||
stats_json = stats.model_dump_json()
|
||||
```
|
||||
|
||||
### 4. Filtering Deals
|
||||
|
||||
**Before (v1.x):**
|
||||
```python
|
||||
filtered = client.filter_deals_by_criteria(
|
||||
deals,
|
||||
property_type="דירה",
|
||||
min_rooms=3.0,
|
||||
max_rooms=4.0,
|
||||
min_price=1000000.0,
|
||||
max_price=2000000.0
|
||||
)
|
||||
|
||||
# Returns List[Dict]
|
||||
for deal in filtered:
|
||||
rooms = deal.get("rooms")
|
||||
```
|
||||
|
||||
**After (v2.0):**
|
||||
```python
|
||||
# Option 1: Individual parameters (same as before)
|
||||
filtered = client.filter_deals_by_criteria(
|
||||
deals,
|
||||
property_type="דירה",
|
||||
min_rooms=3.0,
|
||||
max_rooms=4.0,
|
||||
min_price=1000000.0,
|
||||
max_price=2000000.0
|
||||
)
|
||||
|
||||
# Option 2: Use DealFilters model (NEW!)
|
||||
from nadlan_mcp.govmap.models import DealFilters
|
||||
|
||||
filters = DealFilters(
|
||||
property_type="דירה",
|
||||
min_rooms=3.0,
|
||||
max_rooms=4.0,
|
||||
min_price=1000000.0,
|
||||
max_price=2000000.0
|
||||
)
|
||||
filtered = client.filter_deals_by_criteria(deals, filters=filters)
|
||||
|
||||
# Returns List[Deal]
|
||||
for deal in filtered:
|
||||
rooms = deal.rooms # Optional[float]
|
||||
```
|
||||
|
||||
### 5. Serialization for MCP/JSON
|
||||
|
||||
**Before (v1.x):**
|
||||
```python
|
||||
import json
|
||||
|
||||
deals = client.get_street_deals("polygon123")
|
||||
# Already dicts, can serialize directly
|
||||
json_str = json.dumps(deals)
|
||||
```
|
||||
|
||||
**After (v2.0):**
|
||||
```python
|
||||
import json
|
||||
|
||||
deals = client.get_street_deals("polygon123")
|
||||
|
||||
# Option 1: Serialize individual models
|
||||
deals_dicts = [deal.model_dump() for deal in deals]
|
||||
json_str = json.dumps(deals_dicts)
|
||||
|
||||
# Option 2: Exclude None values for cleaner output
|
||||
deals_dicts = [deal.model_dump(exclude_none=True) for deal in deals]
|
||||
json_str = json.dumps(deals_dicts, ensure_ascii=False)
|
||||
|
||||
# Option 3: Direct JSON serialization
|
||||
json_str = json.dumps([deal.model_dump() for deal in deals])
|
||||
```
|
||||
|
||||
## Field Name Changes (API → Python)
|
||||
|
||||
Many field names changed from camelCase (API) to snake_case (Python convention). Pydantic handles both via aliases:
|
||||
|
||||
| API Field (camelCase) | Python Attribute (snake_case) |
|
||||
|----------------------|------------------------------|
|
||||
| `dealAmount` | `deal_amount` |
|
||||
| `dealDate` | `deal_date` |
|
||||
| `assetArea` | `asset_area` |
|
||||
| `settlementNameHeb` | `settlement_name_heb` |
|
||||
| `propertyTypeDescription` | `property_type_description` |
|
||||
| `streetName` | `street_name` |
|
||||
| `houseNumber` | `house_number` |
|
||||
| `floorNumber` | `floor_number` |
|
||||
| `sourcePolygonId` | `source_polygon_id` |
|
||||
| `resultsCount` | `results_count` |
|
||||
|
||||
**Note:** When creating models from API responses, use either name:
|
||||
```python
|
||||
# Both work due to Pydantic aliases
|
||||
deal = Deal(dealAmount=1500000, dealDate="2024-01-15", objectid=123)
|
||||
deal = Deal(deal_amount=1500000, deal_date="2024-01-15", objectid=123)
|
||||
```
|
||||
|
||||
## New Features
|
||||
|
||||
### 1. Computed Fields
|
||||
|
||||
Models automatically calculate derived values:
|
||||
|
||||
```python
|
||||
deal = Deal(
|
||||
objectid=123,
|
||||
deal_amount=1500000.0,
|
||||
deal_date="2024-01-15",
|
||||
asset_area=85.0
|
||||
)
|
||||
|
||||
# Automatically computed!
|
||||
assert deal.price_per_sqm == 17647.06
|
||||
```
|
||||
|
||||
### 2. Validation
|
||||
|
||||
Models validate data automatically:
|
||||
|
||||
```python
|
||||
from pydantic import ValidationError
|
||||
|
||||
try:
|
||||
filters = DealFilters(
|
||||
min_rooms=4.0,
|
||||
max_rooms=2.0 # Error: max < min
|
||||
)
|
||||
except ValidationError as e:
|
||||
print(e) # Clear validation error message
|
||||
```
|
||||
|
||||
### 3. Type Hints
|
||||
|
||||
Full IDE autocomplete and type checking:
|
||||
|
||||
```python
|
||||
from nadlan_mcp.govmap.models import Deal
|
||||
|
||||
deal: Deal = client.get_street_deals("polygon123")[0]
|
||||
|
||||
# IDE knows all fields and their types!
|
||||
amount: float = deal.deal_amount
|
||||
area: Optional[float] = deal.asset_area
|
||||
```
|
||||
|
||||
## Updating Tests
|
||||
|
||||
### Mock Data
|
||||
|
||||
**Before (v1.x):**
|
||||
```python
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {
|
||||
"resultsCount": 1,
|
||||
"results": [{"text": "חולון", "id": "123", "type": "city"}]
|
||||
}
|
||||
```
|
||||
|
||||
**After (v2.0):**
|
||||
```python
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {
|
||||
"resultsCount": 1,
|
||||
"results": [{"text": "חולון", "id": "123", "type": "city"}]
|
||||
}
|
||||
# Client will parse this into AutocompleteResponse model
|
||||
```
|
||||
|
||||
### Assertions
|
||||
|
||||
**Before (v1.x):**
|
||||
```python
|
||||
result = client.autocomplete_address("test")
|
||||
assert result["resultsCount"] == 1
|
||||
assert result["results"][0]["text"] == "חולון"
|
||||
```
|
||||
|
||||
**After (v2.0):**
|
||||
```python
|
||||
from nadlan_mcp.govmap.models import AutocompleteResponse
|
||||
|
||||
result = client.autocomplete_address("test")
|
||||
assert isinstance(result, AutocompleteResponse)
|
||||
assert result.results_count == 1
|
||||
assert result.results[0].text == "חולון"
|
||||
```
|
||||
|
||||
### Deal Mocks
|
||||
|
||||
**Before (v1.x):**
|
||||
```python
|
||||
mock_deals = [
|
||||
{"dealAmount": 1500000, "assetArea": 85, "dealDate": "2024-01-15"}
|
||||
]
|
||||
```
|
||||
|
||||
**After (v2.0):**
|
||||
```python
|
||||
# Mock API response (will be parsed into Deal models)
|
||||
mock_response.json.return_value = {
|
||||
"data": [
|
||||
{
|
||||
"objectid": 123, # Required!
|
||||
"dealAmount": 1500000,
|
||||
"dealDate": "2024-01-15",
|
||||
"assetArea": 85
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Or create Deal models directly in tests
|
||||
from nadlan_mcp.govmap.models import Deal
|
||||
|
||||
mock_deals = [
|
||||
Deal(objectid=123, deal_amount=1500000, deal_date="2024-01-15", asset_area=85)
|
||||
]
|
||||
```
|
||||
|
||||
## Gradual Migration Strategy
|
||||
|
||||
If you can't migrate everything at once:
|
||||
|
||||
### 1. Use `.model_dump()` for Compatibility
|
||||
|
||||
```python
|
||||
# Get models from v2.0 API
|
||||
deals = client.get_street_deals("polygon123")
|
||||
|
||||
# Convert to dicts for legacy code
|
||||
deals_dicts = [deal.model_dump() for deal in deals]
|
||||
|
||||
# Now legacy code can use dict access
|
||||
for deal_dict in deals_dicts:
|
||||
price = deal_dict["deal_amount"] # Works!
|
||||
```
|
||||
|
||||
### 2. Wrap in Compatibility Layer
|
||||
|
||||
```python
|
||||
class LegacyClientWrapper:
|
||||
def __init__(self):
|
||||
self.client = GovmapClient()
|
||||
|
||||
def get_street_deals(self, polygon_id, **kwargs):
|
||||
"""Returns dicts for backward compatibility."""
|
||||
deals = self.client.get_street_deals(polygon_id, **kwargs)
|
||||
return [deal.model_dump() for deal in deals]
|
||||
```
|
||||
|
||||
## Benefits of Migration
|
||||
|
||||
✅ **Type Safety** - Full IDE autocomplete and mypy support
|
||||
✅ **Validation** - Automatic data validation with clear errors
|
||||
✅ **Computed Fields** - Price per sqm auto-calculated
|
||||
✅ **Better DX** - Models serve as living documentation
|
||||
✅ **API Compatibility** - Field aliases handle camelCase ↔ snake_case
|
||||
✅ **Clear Errors** - Pydantic validation errors are very descriptive
|
||||
✅ **Performance** - Pydantic v2 is extremely fast (Rust core)
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **Examples:** See `tests/govmap/test_models.py` for comprehensive model usage examples
|
||||
- **Documentation:** Check `ARCHITECTURE.md` for system design
|
||||
- **API Reference:** All models have detailed docstrings
|
||||
- **Issues:** Report migration issues at https://github.com/anthropics/nadlan-mcp/issues
|
||||
|
||||
---
|
||||
|
||||
**Version:** 2.0.0
|
||||
**Date:** 2025-01-26
|
||||
**Breaking Changes:** Yes - all API methods now return Pydantic models
|
||||
Reference in New Issue
Block a user