From 8479fd2335ed9732ff83504fdaaf741fca046371 Mon Sep 17 00:00:00 2001 From: Yinchuan Song <562997+inntran@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:25:08 -0500 Subject: [PATCH 1/2] Add Pool data models with comprehensive test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add OtherAsset model with DateLastEvaluated date parsing - Add OtherLiability model with MaturityDate and PaymentNextDue parsing - Add Pool model with nested objects and multiple date field support - Add PoolResponse wrapper for single pool API responses - Add PoolsResponse wrapper handling both list and single pool responses - Update PoolsResource to return typed Pool objects instead of dicts - Add 14 comprehensive tests covering all Pool model classes - Fix PoolsResponse to handle empty dict edge case correctly - All 111 tests passing with 91% code coverage maintained 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/tmo_api/models/__init__.py | 6 + src/tmo_api/models/pool.py | 84 ++++++++++++++ src/tmo_api/resources/pools.py | 16 ++- tests/test_models_pool.py | 203 +++++++++++++++++++++++++++++++++ tests/test_resources_pools.py | 3 + 5 files changed, 306 insertions(+), 6 deletions(-) create mode 100644 src/tmo_api/models/pool.py create mode 100644 tests/test_models_pool.py diff --git a/src/tmo_api/models/__init__.py b/src/tmo_api/models/__init__.py index ce4500d..f7bf64a 100644 --- a/src/tmo_api/models/__init__.py +++ b/src/tmo_api/models/__init__.py @@ -1,8 +1,14 @@ """Models package for The Mortgage Office SDK.""" from .base import BaseModel, BaseResponse +from .pool import OtherAsset, OtherLiability, Pool, PoolResponse, PoolsResponse __all__ = [ "BaseModel", "BaseResponse", + "Pool", + "PoolResponse", + "PoolsResponse", + "OtherAsset", + "OtherLiability", ] diff --git a/src/tmo_api/models/pool.py b/src/tmo_api/models/pool.py new file mode 100644 index 0000000..e8fb4ee --- /dev/null +++ b/src/tmo_api/models/pool.py @@ -0,0 +1,84 @@ +"""Pool-related models for The Mortgage Office SDK.""" + +from typing import Any, Dict, List, Optional + +from .base import BaseModel, BaseResponse + + +class OtherAsset(BaseModel): + """Represents an other asset in a mortgage pool.""" + + def _parse_data(self, data: Dict[str, Any]) -> None: + super()._parse_data(data) + + # Parse dates specifically (since they need conversion) + if "DateLastEvaluated" in data: + self.DateLastEvaluated = self._parse_date(data.get("DateLastEvaluated")) + + +class OtherLiability(BaseModel): + """Represents an other liability in a mortgage pool.""" + + def _parse_data(self, data: Dict[str, Any]) -> None: + super()._parse_data(data) + + # Parse dates specifically (since they need conversion) + if "MaturityDate" in data: + self.MaturityDate = self._parse_date(data.get("MaturityDate")) + if "PaymentNextDue" in data: + self.PaymentNextDue = self._parse_date(data.get("PaymentNextDue")) + + +class Pool(BaseModel): + """Represents a mortgage pool.""" + + def _parse_data(self, data: Dict[str, Any]) -> None: + super()._parse_data(data) + + # Parse dates specifically (since they need conversion) + if "InceptionDate" in data: + self.InceptionDate = self._parse_date(data.get("InceptionDate")) + if "LastEvaluation" in data: + self.LastEvaluation = self._parse_date(data.get("LastEvaluation")) + if "SysTimeStamp" in data: + self.SysTimeStamp = self._parse_date(data.get("SysTimeStamp")) + + # Parse nested objects (override the raw arrays with parsed objects) + if "OtherAssets" in data: + self.OtherAssets: List[OtherAsset] = [] + for asset_data in data.get("OtherAssets", []): + self.OtherAssets.append(OtherAsset(asset_data)) + + if "OtherLiabilities" in data: + self.OtherLiabilities: List[OtherLiability] = [] + for liability_data in data.get("OtherLiabilities", []): + self.OtherLiabilities.append(OtherLiability(liability_data)) + + +class PoolResponse(BaseResponse): + """Response containing pool data.""" + + pool: Optional["Pool"] + + def __init__(self, data: Dict[str, Any]) -> None: + super().__init__(data) + if self.data: + self.pool = Pool(self.data) + else: + self.pool = None + + +class PoolsResponse(BaseResponse): + """Response containing multiple pools.""" + + def __init__(self, data: Dict[str, Any]) -> None: + super().__init__(data) + self.pools: List[Pool] = [] + + # Handle both single pool and list of pools + pool_data: Any = self.data + if isinstance(pool_data, list): + for item in pool_data: + self.pools.append(Pool(item)) + elif isinstance(pool_data, dict) and pool_data: + self.pools.append(Pool(pool_data)) diff --git a/src/tmo_api/resources/pools.py b/src/tmo_api/resources/pools.py index a97cdbd..d1370c4 100644 --- a/src/tmo_api/resources/pools.py +++ b/src/tmo_api/resources/pools.py @@ -1,7 +1,9 @@ """Pools resource for The Mortgage Office SDK.""" from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, cast +from typing import TYPE_CHECKING, Any, List, cast + +from ..models.pool import Pool, PoolResponse, PoolsResponse if TYPE_CHECKING: from ..client import TheMortgageOfficeClient @@ -30,14 +32,14 @@ def __init__( self.pool_type = pool_type self.base_path = f"LSS.svc/{pool_type.value}" - def get_pool(self, account: str) -> Dict[str, Any]: + def get_pool(self, account: str) -> Pool: """Get pool details by account. Args: account: The pool account identifier Returns: - Pool data dictionary + Pool object with detailed information Raises: APIError: If the API returns an error @@ -50,7 +52,8 @@ def get_pool(self, account: str) -> Dict[str, Any]: endpoint = f"{self.base_path}/Pools/{account}" response_data = self.client.get(endpoint) - return response_data + response = PoolResponse(response_data) + return response.pool # type: ignore def get_pool_partners(self, account: str) -> list: """Get pool partners by account. @@ -140,7 +143,7 @@ def get_pool_attachments(self, account: str) -> list: response_data = self.client.get(endpoint) return cast(List[Any], response_data.get("Data", [])) - def list_all(self) -> list: + def list_all(self) -> List[Pool]: """List all pools. Returns: @@ -151,4 +154,5 @@ def list_all(self) -> list: """ endpoint = f"{self.base_path}/Pools" response_data = self.client.get(endpoint) - return cast(List[Any], response_data.get("Data", [])) + response = PoolsResponse(response_data) + return response.pools diff --git a/tests/test_models_pool.py b/tests/test_models_pool.py new file mode 100644 index 0000000..d04844e --- /dev/null +++ b/tests/test_models_pool.py @@ -0,0 +1,203 @@ +"""Tests for Pool models.""" + +from datetime import datetime + +import pytest + +from tmo_api.models.pool import OtherAsset, OtherLiability, Pool, PoolResponse, PoolsResponse + + +class TestOtherAsset: + """Test OtherAsset model.""" + + def test_other_asset_initialization(self): + """Test OtherAsset initialization.""" + data = { + "rec_id": 123, + "Description": "Test Asset", + "Value": 10000.00, + "DateLastEvaluated": "12/31/2024", + } + asset = OtherAsset(data) + + assert asset.rec_id == 123 + assert asset.Description == "Test Asset" + assert asset.Value == 10000.00 + assert isinstance(asset.DateLastEvaluated, datetime) + assert asset.DateLastEvaluated == datetime(2024, 12, 31) + + def test_other_asset_without_date(self): + """Test OtherAsset without date.""" + data = {"rec_id": 456, "Description": "Asset without date"} + asset = OtherAsset(data) + + assert asset.rec_id == 456 + assert asset.Description == "Asset without date" + + +class TestOtherLiability: + """Test OtherLiability model.""" + + def test_other_liability_initialization(self): + """Test OtherLiability initialization.""" + data = { + "rec_id": 789, + "Description": "Test Liability", + "Balance": 50000.00, + "MaturityDate": "06/30/2025", + "PaymentNextDue": "01/15/2025", + } + liability = OtherLiability(data) + + assert liability.rec_id == 789 + assert liability.Description == "Test Liability" + assert liability.Balance == 50000.00 + assert isinstance(liability.MaturityDate, datetime) + assert liability.MaturityDate == datetime(2025, 6, 30) + assert isinstance(liability.PaymentNextDue, datetime) + assert liability.PaymentNextDue == datetime(2025, 1, 15) + + def test_other_liability_without_dates(self): + """Test OtherLiability without dates.""" + data = {"rec_id": 101, "Description": "Liability without dates"} + liability = OtherLiability(data) + + assert liability.rec_id == 101 + assert liability.Description == "Liability without dates" + + +class TestPool: + """Test Pool model.""" + + def test_pool_initialization(self): + """Test Pool initialization with basic data.""" + data = { + "rec_id": 1, + "Account": "POOL001", + "Name": "Test Pool", + "InceptionDate": "01/01/2024", + "LastEvaluation": "12/31/2024", + } + pool = Pool(data) + + assert pool.rec_id == 1 + assert pool.Account == "POOL001" + assert pool.Name == "Test Pool" + assert isinstance(pool.InceptionDate, datetime) + assert pool.InceptionDate == datetime(2024, 1, 1) + assert isinstance(pool.LastEvaluation, datetime) + assert pool.LastEvaluation == datetime(2024, 12, 31) + + def test_pool_with_nested_objects(self): + """Test Pool with nested OtherAssets and OtherLiabilities.""" + data = { + "rec_id": 2, + "Account": "POOL002", + "OtherAssets": [ + {"rec_id": 10, "Description": "Asset 1", "Value": 1000}, + {"rec_id": 11, "Description": "Asset 2", "Value": 2000}, + ], + "OtherLiabilities": [ + {"rec_id": 20, "Description": "Liability 1", "Balance": 5000}, + ], + } + pool = Pool(data) + + assert pool.rec_id == 2 + assert len(pool.OtherAssets) == 2 + assert isinstance(pool.OtherAssets[0], OtherAsset) + assert pool.OtherAssets[0].Description == "Asset 1" + assert pool.OtherAssets[1].Description == "Asset 2" + + assert len(pool.OtherLiabilities) == 1 + assert isinstance(pool.OtherLiabilities[0], OtherLiability) + assert pool.OtherLiabilities[0].Description == "Liability 1" + + def test_pool_repr(self): + """Test Pool string representation.""" + data = {"rec_id": 999, "Account": "POOL999"} + pool = Pool(data) + assert repr(pool) == "Pool(999)" + + +class TestPoolResponse: + """Test PoolResponse model.""" + + def test_pool_response_with_data(self): + """Test PoolResponse with pool data.""" + response_data = { + "Status": 0, + "Data": { + "rec_id": 1, + "Account": "POOL001", + "Name": "Test Pool", + }, + } + response = PoolResponse(response_data) + + assert response.status == 0 + assert response.pool is not None + assert isinstance(response.pool, Pool) + assert response.pool.Account == "POOL001" + + def test_pool_response_without_data(self): + """Test PoolResponse without pool data.""" + response_data = {"Status": 1, "ErrorMessage": "Not found", "ErrorNumber": 404} + response = PoolResponse(response_data) + + assert response.status == 1 + assert response.pool is None + assert response.error_message == "Not found" + + def test_pool_response_repr(self): + """Test PoolResponse string representation.""" + response_data = {"Status": 0, "Data": {"rec_id": 1}} + response = PoolResponse(response_data) + assert repr(response) == "PoolResponse(status=0)" + + +class TestPoolsResponse: + """Test PoolsResponse model.""" + + def test_pools_response_with_list(self): + """Test PoolsResponse with list of pools.""" + response_data = { + "Status": 0, + "Data": [ + {"rec_id": 1, "Account": "POOL001"}, + {"rec_id": 2, "Account": "POOL002"}, + {"rec_id": 3, "Account": "POOL003"}, + ], + } + response = PoolsResponse(response_data) + + assert response.status == 0 + assert len(response.pools) == 3 + assert all(isinstance(pool, Pool) for pool in response.pools) + assert response.pools[0].Account == "POOL001" + assert response.pools[1].Account == "POOL002" + assert response.pools[2].Account == "POOL003" + + def test_pools_response_with_single_pool(self): + """Test PoolsResponse with single pool dict.""" + response_data = {"Status": 0, "Data": {"rec_id": 1, "Account": "POOL001"}} + response = PoolsResponse(response_data) + + assert response.status == 0 + assert len(response.pools) == 1 + assert isinstance(response.pools[0], Pool) + assert response.pools[0].Account == "POOL001" + + def test_pools_response_empty(self): + """Test PoolsResponse with empty data.""" + response_data = {"Status": 0, "Data": []} + response = PoolsResponse(response_data) + + assert response.status == 0 + assert len(response.pools) == 0 + + def test_pools_response_repr(self): + """Test PoolsResponse string representation.""" + response_data = {"Status": 0, "Data": []} + response = PoolsResponse(response_data) + assert repr(response) == "PoolsResponse(status=0)" diff --git a/tests/test_resources_pools.py b/tests/test_resources_pools.py index bbe5e8a..5ad9d5c 100644 --- a/tests/test_resources_pools.py +++ b/tests/test_resources_pools.py @@ -6,6 +6,7 @@ from tmo_api.client import TheMortgageOfficeClient from tmo_api.exceptions import ValidationError +from tmo_api.models.pool import Pool from tmo_api.resources.pools import PoolsResource, PoolType @@ -40,6 +41,7 @@ def test_get_pool_success(self, mock_get, client, mock_pool_account, mock_api_re mock_get.assert_called_once_with(f"LSS.svc/Shares/Pools/{mock_pool_account}") assert pool is not None + assert isinstance(pool, Pool) @patch.object(TheMortgageOfficeClient, "get") def test_get_pool_empty_account(self, mock_get, client): @@ -106,6 +108,7 @@ def test_list_all_pools(self, mock_get, client, mock_pools_response): mock_get.assert_called_once_with("LSS.svc/Shares/Pools") assert isinstance(pools, list) + assert all(isinstance(pool, Pool) for pool in pools) def test_pool_type_enum(self): """Test PoolType enum values.""" From d8402bb39973b1e2e25acce511ea11855f20461d Mon Sep 17 00:00:00 2001 From: Yinchuan Song <562997+inntran@users.noreply.github.com> Date: Thu, 6 Nov 2025 10:27:26 -0500 Subject: [PATCH 2/2] Add test coverage for Pool.SysTimeStamp date parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add SysTimeStamp field to test_pool_initialization test data - Add assertions to verify SysTimeStamp is parsed correctly - Improves code coverage from 91% to 92% - Fixes codecov missing line 44 in pool.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/test_models_pool.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_models_pool.py b/tests/test_models_pool.py index d04844e..3a25245 100644 --- a/tests/test_models_pool.py +++ b/tests/test_models_pool.py @@ -77,6 +77,7 @@ def test_pool_initialization(self): "Name": "Test Pool", "InceptionDate": "01/01/2024", "LastEvaluation": "12/31/2024", + "SysTimeStamp": "11/15/2024", } pool = Pool(data) @@ -87,6 +88,8 @@ def test_pool_initialization(self): assert pool.InceptionDate == datetime(2024, 1, 1) assert isinstance(pool.LastEvaluation, datetime) assert pool.LastEvaluation == datetime(2024, 12, 31) + assert isinstance(pool.SysTimeStamp, datetime) + assert pool.SysTimeStamp == datetime(2024, 11, 15) def test_pool_with_nested_objects(self): """Test Pool with nested OtherAssets and OtherLiabilities."""