Files
2025-10-26 10:58:46 +02:00

390 lines
9.7 KiB
Markdown

# 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