Skip to content

Commit 7e65f8c

Browse files
inntranclaude
andcommitted
Add source code to make tests pass
This commit adds the core SDK implementation: - Add client.py: Main API client with HTTP methods and error handling - Add environments.py: Environment enum for US, Canada, Australia - Add exceptions.py: Custom exception classes - Add models: BaseModel and BaseResponse for data handling - Add resources/pools.py: PoolsResource for managing mortgage pools - Update __init__.py: Export main classes and set version All 51 tests now pass with 84% code coverage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d29833d commit 7e65f8c

8 files changed

Lines changed: 592 additions & 2 deletions

File tree

src/tmo_api/__init__.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,30 @@
1-
def main() -> None:
2-
print("Hello from tmo-api!")
1+
"""The Mortgage Office API SDK for Python."""
2+
3+
from .client import TheMortgageOfficeClient
4+
from .environments import DEFAULT_ENVIRONMENT, Environment
5+
from .exceptions import (
6+
APIError,
7+
AuthenticationError,
8+
NetworkError,
9+
TheMortgageOfficeError,
10+
ValidationError,
11+
)
12+
from .models import BaseModel, BaseResponse
13+
from .resources import PoolsResource, PoolType
14+
15+
__version__ = "0.0.1"
16+
17+
__all__ = [
18+
"TheMortgageOfficeClient",
19+
"Environment",
20+
"DEFAULT_ENVIRONMENT",
21+
"TheMortgageOfficeError",
22+
"APIError",
23+
"AuthenticationError",
24+
"NetworkError",
25+
"ValidationError",
26+
"BaseModel",
27+
"BaseResponse",
28+
"PoolsResource",
29+
"PoolType",
30+
]

src/tmo_api/client.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
"""Base client for The Mortgage Office API."""
2+
3+
import json
4+
import sys
5+
from typing import Any, Dict, Optional, Union
6+
from urllib.parse import urljoin
7+
8+
import requests
9+
10+
from .environments import DEFAULT_ENVIRONMENT, Environment
11+
from .exceptions import APIError, AuthenticationError, NetworkError
12+
from .resources import PoolsResource
13+
14+
15+
class TheMortgageOfficeClient:
16+
"""Base client for The Mortgage Office API."""
17+
18+
def __init__(
19+
self,
20+
token: str,
21+
database: str,
22+
environment: Union[Environment, str] = DEFAULT_ENVIRONMENT,
23+
timeout: int = 30,
24+
debug: bool = False,
25+
) -> None:
26+
"""Initialize the client.
27+
28+
Args:
29+
token: Your API token assigned by Applied Business Software
30+
database: The name of your company database
31+
environment: API environment (US, CANADA, AUSTRALIA) or custom URL
32+
timeout: Request timeout in seconds (default: 30)
33+
debug: Enable debug logging (default: False)
34+
"""
35+
self.token: str = token
36+
self.database: str = database
37+
self.timeout: int = timeout
38+
self.debug: bool = debug
39+
40+
# Handle environment parameter
41+
if isinstance(environment, str):
42+
# If string, treat as custom URL
43+
self.base_url: str = environment
44+
else:
45+
# If Environment enum, use its value
46+
self.base_url = environment.value
47+
48+
self.session: requests.Session = requests.Session()
49+
50+
# Set default headers
51+
self.session.headers.update(
52+
{
53+
"Token": self.token,
54+
"Database": self.database,
55+
"Content-Type": "application/json",
56+
"User-Agent": "themortgageoffice-sdk-python",
57+
}
58+
)
59+
60+
# Import PoolType here to avoid circular imports
61+
from .resources.pools import PoolType
62+
63+
# Initialize Shares resources
64+
self.shares_pools: PoolsResource = PoolsResource(self, PoolType.SHARES)
65+
self.shares_partners: PoolsResource = PoolsResource(self, PoolType.SHARES)
66+
self.shares_distributions: PoolsResource = PoolsResource(self, PoolType.SHARES)
67+
self.shares_certificates: PoolsResource = PoolsResource(self, PoolType.SHARES)
68+
self.shares_history: PoolsResource = PoolsResource(self, PoolType.SHARES)
69+
70+
# Initialize Capital resources
71+
self.capital_pools: PoolsResource = PoolsResource(self, PoolType.CAPITAL)
72+
self.capital_partners: PoolsResource = PoolsResource(self, PoolType.CAPITAL)
73+
self.capital_distributions: PoolsResource = PoolsResource(self, PoolType.CAPITAL)
74+
self.capital_history: PoolsResource = PoolsResource(self, PoolType.CAPITAL)
75+
76+
def _debug_log(self, message: str) -> None:
77+
"""Log debug message to stderr if debug mode is enabled."""
78+
if self.debug:
79+
print(f"DEBUG: {message}", file=sys.stderr)
80+
81+
def _debug_log_request(
82+
self,
83+
method: str,
84+
url: str,
85+
headers: Dict[str, str],
86+
params: Optional[Dict[str, Any]] = None,
87+
json_data: Optional[Dict[str, Any]] = None,
88+
) -> None:
89+
"""Log request details if debug mode is enabled."""
90+
if not self.debug:
91+
return
92+
93+
print("DEBUG: === REQUEST ===", file=sys.stderr)
94+
print(f"DEBUG: {method} {url}", file=sys.stderr)
95+
print("DEBUG: Headers:", file=sys.stderr)
96+
for key, value in headers.items():
97+
# Mask sensitive headers
98+
if key.lower() in ["token", "authorization"]:
99+
masked_value = (
100+
"*" * min(len(value), 8) + value[-4:] if len(value) > 4 else "*" * len(value)
101+
)
102+
print(f"DEBUG: {key}: {masked_value}", file=sys.stderr)
103+
else:
104+
print(f"DEBUG: {key}: {value}", file=sys.stderr)
105+
106+
if params:
107+
print("DEBUG: Query Parameters:", file=sys.stderr)
108+
for key, value in params.items():
109+
print(f"DEBUG: {key}: {value}", file=sys.stderr)
110+
111+
if json_data:
112+
print("DEBUG: Request Body:", file=sys.stderr)
113+
print(f"DEBUG: {json.dumps(json_data, indent=2)}", file=sys.stderr)
114+
115+
def _debug_log_response(
116+
self, response: requests.Response, response_data: Dict[str, Any]
117+
) -> None:
118+
"""Log response details if debug mode is enabled."""
119+
if not self.debug:
120+
return
121+
122+
print("DEBUG: === RESPONSE ===", file=sys.stderr)
123+
print(f"DEBUG: Status: {response.status_code}", file=sys.stderr)
124+
print("DEBUG: Response Headers:", file=sys.stderr)
125+
for key, value in response.headers.items():
126+
print(f"DEBUG: {key}: {value}", file=sys.stderr)
127+
128+
print("DEBUG: Response Body:", file=sys.stderr)
129+
print(
130+
f"DEBUG: {json.dumps(response_data, indent=2, default=str)}",
131+
file=sys.stderr,
132+
)
133+
print("DEBUG: ==================", file=sys.stderr)
134+
135+
def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
136+
"""Make a request to the API.
137+
138+
Args:
139+
method: HTTP method (GET, POST, PUT, DELETE)
140+
endpoint: API endpoint path
141+
**kwargs: Additional arguments to pass to requests
142+
143+
Returns:
144+
API response data
145+
146+
Raises:
147+
AuthenticationError: If authentication fails
148+
APIError: If the API returns an error
149+
NetworkError: If a network error occurs
150+
"""
151+
url: str = urljoin(self.base_url + "/", endpoint)
152+
153+
# Log request details if debug mode is enabled
154+
self._debug_log_request(
155+
method=method,
156+
url=url,
157+
headers={k: str(v) for k, v in self.session.headers.items()},
158+
params=kwargs.get("params"),
159+
json_data=kwargs.get("json"),
160+
)
161+
162+
try:
163+
response = self.session.request(method=method, url=url, timeout=self.timeout, **kwargs)
164+
response.raise_for_status()
165+
166+
except requests.exceptions.Timeout:
167+
self._debug_log("Request timed out")
168+
raise NetworkError("Request timed out")
169+
except requests.exceptions.ConnectionError:
170+
self._debug_log("Connection error occurred")
171+
raise NetworkError("Connection error occurred")
172+
except requests.exceptions.HTTPError as e:
173+
self._debug_log(f"HTTP error: {response.status_code}")
174+
if response.status_code == 401:
175+
raise AuthenticationError("Invalid token or database")
176+
elif response.status_code == 403:
177+
raise AuthenticationError("Access denied")
178+
else:
179+
raise NetworkError(f"HTTP {response.status_code}: {str(e)}")
180+
except requests.exceptions.RequestException as e:
181+
self._debug_log(f"Request exception: {str(e)}")
182+
raise NetworkError(f"Request failed: {str(e)}")
183+
184+
try:
185+
data: Dict[str, Any] = response.json()
186+
except ValueError:
187+
self._debug_log("Failed to parse JSON response")
188+
raise APIError("Invalid JSON response from API")
189+
190+
# Log response details if debug mode is enabled
191+
self._debug_log_response(response, data)
192+
193+
# Check for API-level errors
194+
if data.get("Status") != 0:
195+
error_message: str = data.get("ErrorMessage", "Unknown API error")
196+
error_number: Optional[int] = data.get("ErrorNumber")
197+
self._debug_log(f"API error: {error_message} (Number: {error_number})")
198+
raise APIError(error_message, error_number)
199+
200+
return data
201+
202+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
203+
"""Make a GET request.
204+
205+
Args:
206+
endpoint: API endpoint path
207+
params: Query parameters
208+
209+
Returns:
210+
API response data
211+
"""
212+
return self._make_request("GET", endpoint, params=params)
213+
214+
def post(self, endpoint: str, json: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
215+
"""Make a POST request.
216+
217+
Args:
218+
endpoint: API endpoint path
219+
json: JSON data to send
220+
221+
Returns:
222+
API response data
223+
"""
224+
return self._make_request("POST", endpoint, json=json)
225+
226+
def put(self, endpoint: str, json: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
227+
"""Make a PUT request.
228+
229+
Args:
230+
endpoint: API endpoint path
231+
json: JSON data to send
232+
233+
Returns:
234+
API response data
235+
"""
236+
return self._make_request("PUT", endpoint, json=json)
237+
238+
def delete(self, endpoint: str) -> Dict[str, Any]:
239+
"""Make a DELETE request.
240+
241+
Args:
242+
endpoint: API endpoint path
243+
244+
Returns:
245+
API response data
246+
"""
247+
return self._make_request("DELETE", endpoint)

src/tmo_api/environments.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Environment configurations for The Mortgage Office SDK."""
2+
3+
from enum import Enum
4+
from typing import Final
5+
6+
7+
class Environment(Enum):
8+
"""Supported API environments."""
9+
10+
US = "https://api.themortgageoffice.com"
11+
CANADA = "https://api-ca.themortgageoffice.com"
12+
AUSTRALIA = "https://api-aus.themortgageoffice.com"
13+
14+
15+
# Default environment
16+
DEFAULT_ENVIRONMENT: Final[Environment] = Environment.US

src/tmo_api/exceptions.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Custom exceptions for The Mortgage Office SDK."""
2+
3+
from typing import Optional
4+
5+
6+
class TheMortgageOfficeError(Exception):
7+
"""Base exception for The Mortgage Office SDK."""
8+
9+
def __init__(self, message: str, error_number: Optional[int] = None) -> None:
10+
super().__init__(message)
11+
self.message: str = message
12+
self.error_number: Optional[int] = error_number
13+
14+
15+
class AuthenticationError(TheMortgageOfficeError):
16+
"""Raised when authentication fails."""
17+
18+
pass
19+
20+
21+
class APIError(TheMortgageOfficeError):
22+
"""Raised when the API returns an error response."""
23+
24+
pass
25+
26+
27+
class ValidationError(TheMortgageOfficeError):
28+
"""Raised when request validation fails."""
29+
30+
pass
31+
32+
33+
class NetworkError(TheMortgageOfficeError):
34+
"""Raised when network-related errors occur."""
35+
36+
pass

src/tmo_api/models/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Models package for The Mortgage Office SDK."""
2+
3+
from .base import BaseModel, BaseResponse
4+
5+
__all__ = [
6+
"BaseModel",
7+
"BaseResponse",
8+
]

0 commit comments

Comments
 (0)