securelend 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
securelend/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """
2
+ SecureLend SDK for Python
3
+
4
+ Official SDK for SecureLend - Financial services infrastructure for AI
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from .client import SecureLend
10
+ from .utils.errors import (
11
+ AuthenticationError,
12
+ NetworkError,
13
+ NotFoundError,
14
+ RateLimitError,
15
+ SecureLendError,
16
+ ServerError,
17
+ ValidationError,
18
+ )
19
+ from . import types
20
+
21
+ __all__ = [
22
+ "SecureLend",
23
+ "SecureLendError",
24
+ "AuthenticationError",
25
+ "RateLimitError",
26
+ "ValidationError",
27
+ "NotFoundError",
28
+ "NetworkError",
29
+ "ServerError",
30
+ "types",
31
+ ]
securelend/client.py ADDED
@@ -0,0 +1,348 @@
1
+ import json
2
+ from typing import Any, Dict, Optional, Type, TypeVar
3
+
4
+ from pydantic import BaseModel, ValidationError as PydanticValidationError
5
+
6
+ from . import types
7
+ from .utils.errors import SecureLendError, ValidationError
8
+ from .utils.mcp import MCPClient
9
+
10
+ T = TypeVar("T", bound=BaseModel)
11
+
12
+
13
+ class SecureLend:
14
+ """
15
+ SecureLend MCP-native SDK Client
16
+
17
+ Example:
18
+ >>> import asyncio
19
+ >>> from securelend import SecureLend
20
+ >>>
21
+ >>> async def main():
22
+ ... securelend = SecureLend(api_key="sk_test_...")
23
+ ... loans = await securelend.compare_business_loans({
24
+ ... "loanAmount": 200000,
25
+ ... "purpose": "equipment",
26
+ ... "annualRevenue": 1200000,
27
+ ... })
28
+ ... print(loans)
29
+ """
30
+
31
+ def __init__(
32
+ self, *, api_key: Optional[str] = None, server_url: Optional[str] = None
33
+ ):
34
+ mcp_url = server_url or "https://mcp.securelend.ai/mcp"
35
+ self._mcp_client = MCPClient(api_key=api_key or "", mcp_url=mcp_url)
36
+
37
+ # --- Loan Comparison ---
38
+
39
+ async def compare_personal_loans(
40
+ self, request: Dict[str, Any]
41
+ ) -> types.LoanComparisonResponse:
42
+ return await self._validated_call_tool(
43
+ "compare_personal_loans",
44
+ request,
45
+ types.PersonalLoanSearchParams,
46
+ types.LoanComparisonResponse,
47
+ )
48
+
49
+ async def compare_business_loans(
50
+ self, request: Dict[str, Any]
51
+ ) -> types.LoanComparisonResponse:
52
+ return await self._validated_call_tool(
53
+ "compare_business_loans",
54
+ request,
55
+ types.BusinessLoanSearchParams,
56
+ types.LoanComparisonResponse,
57
+ )
58
+
59
+ async def compare_personal_mortgages(
60
+ self, request: Dict[str, Any]
61
+ ) -> types.LoanComparisonResponse:
62
+ return await self._validated_call_tool(
63
+ "compare_personal_mortgages",
64
+ request,
65
+ types.MortgageSearchParams,
66
+ types.LoanComparisonResponse,
67
+ )
68
+
69
+ async def compare_business_mortgages(
70
+ self, request: Dict[str, Any]
71
+ ) -> types.LoanComparisonResponse:
72
+ return await self._validated_call_tool(
73
+ "compare_business_mortgages",
74
+ request,
75
+ types.MortgageSearchParams,
76
+ types.LoanComparisonResponse,
77
+ )
78
+
79
+ async def compare_car_loans(
80
+ self, request: Dict[str, Any]
81
+ ) -> types.LoanComparisonResponse:
82
+ return await self._validated_call_tool(
83
+ "compare_car_loans",
84
+ request,
85
+ types.AutoLoanSearchParams,
86
+ types.LoanComparisonResponse,
87
+ )
88
+
89
+ async def compare_student_loans(
90
+ self, request: Dict[str, Any]
91
+ ) -> types.LoanComparisonResponse:
92
+ return await self._validated_call_tool(
93
+ "compare_student_loans",
94
+ request,
95
+ types.StudentLoanSearchParams,
96
+ types.LoanComparisonResponse,
97
+ )
98
+
99
+ # --- Banking & Credit Cards ---
100
+
101
+ async def compare_business_banking(
102
+ self, request: Dict[str, Any]
103
+ ) -> types.BusinessBankingComparisonResponse:
104
+ return await self._validated_call_tool(
105
+ "compare_business_banking",
106
+ request,
107
+ types.BusinessBankingSearchSchema,
108
+ types.BusinessBankingComparisonResponse,
109
+ )
110
+
111
+ async def compare_personal_banking(
112
+ self, request: Dict[str, Any]
113
+ ) -> types.PersonalBankingComparisonResponse:
114
+ return await self._validated_call_tool(
115
+ "compare_personal_banking",
116
+ request,
117
+ types.PersonalBankingSearchSchema,
118
+ types.PersonalBankingComparisonResponse,
119
+ )
120
+
121
+ async def compare_savings_accounts(
122
+ self, request: Dict[str, Any]
123
+ ) -> types.SavingsAccountComparisonResponse:
124
+ return await self._validated_call_tool(
125
+ "compare_savings_accounts",
126
+ request,
127
+ types.SavingsSearchSchema,
128
+ types.SavingsAccountComparisonResponse,
129
+ )
130
+
131
+ async def compare_business_credit_cards(
132
+ self, request: Dict[str, Any]
133
+ ) -> types.BusinessCreditCardComparisonResponse:
134
+ return await self._validated_call_tool(
135
+ "compare_business_credit_cards",
136
+ request,
137
+ types.BusinessCreditCardSearchParams,
138
+ types.BusinessCreditCardComparisonResponse,
139
+ )
140
+
141
+ async def compare_personal_credit_cards(
142
+ self, request: Dict[str, Any]
143
+ ) -> types.PersonalCreditCardComparisonResponse:
144
+ return await self._validated_call_tool(
145
+ "compare_personal_credit_cards",
146
+ request,
147
+ types.PersonalCreditCardSearchSchema,
148
+ types.PersonalCreditCardComparisonResponse,
149
+ )
150
+
151
+ # --- Financial Calculators ---
152
+
153
+ async def calculate_loan_payment(
154
+ self, request: Dict[str, Any]
155
+ ) -> types.LoanCalculationResponse:
156
+ return await self._validated_call_tool(
157
+ "calculate_loan_payment",
158
+ request,
159
+ types.LoanPaymentParams,
160
+ types.LoanCalculationResponse,
161
+ )
162
+
163
+ async def calculate_mortgage_payment(
164
+ self, request: Dict[str, Any]
165
+ ) -> types.MortgageCalculationResponse:
166
+ return await self._validated_call_tool(
167
+ "calculate_mortgage_payment",
168
+ request,
169
+ types.MortgagePaymentParams,
170
+ types.MortgageCalculationResponse,
171
+ )
172
+
173
+ async def compare_lease_vs_purchase(
174
+ self, request: Dict[str, Any]
175
+ ) -> types.LeaseVsPurchaseResponse:
176
+ return await self._validated_call_tool(
177
+ "compare_lease_vs_purchase",
178
+ request,
179
+ types.LeaseVsPurchaseParams,
180
+ types.LeaseVsPurchaseResponse,
181
+ )
182
+
183
+ # --- Application Management ---
184
+
185
+ async def get_offer(self, request: Dict[str, Any]) -> types.PersonalApplication:
186
+ return await self._validated_call_tool(
187
+ "get_offer", request, types.GetOfferParams, types.PersonalApplication
188
+ )
189
+
190
+ async def get_multiple_offers(
191
+ self, request: Dict[str, Any]
192
+ ) -> types.PersonalApplication:
193
+ return await self._validated_call_tool(
194
+ "get_multiple_offers",
195
+ request,
196
+ types.GetMultipleOffersParams,
197
+ types.PersonalApplication,
198
+ )
199
+
200
+ async def display_offer_form(
201
+ self, request: Dict[str, Any]
202
+ ) -> types.DisplayOfferFormResponse:
203
+ return await self._validated_call_tool(
204
+ "display_offer_form",
205
+ request,
206
+ types.DisplayOfferFormParams,
207
+ types.DisplayOfferFormResponse,
208
+ )
209
+
210
+ async def track_offer_status(
211
+ self, request: Dict[str, Any]
212
+ ) -> types.TrackOfferStatusResponse:
213
+ return await self._validated_call_tool(
214
+ "track_offer_status",
215
+ request,
216
+ types.TrackOfferStatusParams,
217
+ types.TrackOfferStatusResponse,
218
+ )
219
+
220
+ async def display_upload_documents_form(
221
+ self, request: Dict[str, Any]
222
+ ) -> types.DisplayUploadDocumentsFormResponse:
223
+ return await self._validated_call_tool(
224
+ "display_upload_documents_form",
225
+ request,
226
+ types.DisplayUploadDocumentsFormParams,
227
+ types.DisplayUploadDocumentsFormResponse,
228
+ )
229
+
230
+ async def submit_documents(
231
+ self, request: Dict[str, Any]
232
+ ) -> types.SubmitDocumentsResponse:
233
+ return await self._validated_call_tool(
234
+ "submit_documents",
235
+ request,
236
+ types.SubmitDocumentsParams,
237
+ types.SubmitDocumentsResponse,
238
+ )
239
+
240
+ # --- Core Methods ---
241
+
242
+ async def connect(self) -> None:
243
+ """Manually connect to the MCP server."""
244
+ await self._mcp_client.connect()
245
+
246
+ def set_api_key(self, api_key: str) -> None:
247
+ """Update API key for multi-tenant applications."""
248
+ self._mcp_client.set_api_key(api_key)
249
+
250
+ def enable_debug(self) -> None:
251
+ """Enable debug logging."""
252
+ self._mcp_client.enable_debug()
253
+
254
+ def disable_debug(self) -> None:
255
+ """Disable debug logging."""
256
+ self._mcp_client.disable_debug()
257
+
258
+ async def aclose(self):
259
+ """Gracefully close the connection to the MCP server."""
260
+ await self._mcp_client.close()
261
+
262
+ async def __aenter__(self):
263
+ return self
264
+
265
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
266
+ await self.aclose()
267
+
268
+ # --- Private Helpers ---
269
+
270
+ async def _validated_call_tool(
271
+ self,
272
+ tool_name: str,
273
+ request_data: Dict[str, Any],
274
+ request_cls: Type[BaseModel],
275
+ response_cls: Type[T],
276
+ ) -> T:
277
+ try:
278
+ validated_request = request_cls.model_validate(request_data)
279
+ except PydanticValidationError as e:
280
+ raise ValidationError("Invalid request parameters", errors=e.errors()) from e
281
+ return await self._call_tool(tool_name, validated_request, response_cls)
282
+
283
+ async def _call_tool(
284
+ self, tool_name: str, request: BaseModel, response_cls: Type[T]
285
+ ) -> T:
286
+ # Pydantic models are passed in, dump to dict for MCP call
287
+ request_data = request.model_dump(by_alias=True, exclude_none=True)
288
+ tool_result_dict = await self._mcp_client.call_tool(tool_name, request_data)
289
+ tool_result = types.ToolResult.model_validate(tool_result_dict)
290
+ data = self._parse_json_response(tool_result)
291
+ data["widget"] = self._get_widget(tool_result)
292
+
293
+ try:
294
+ return response_cls.model_validate(data)
295
+ except PydanticValidationError as e:
296
+ raise ValidationError(
297
+ f"Invalid response from MCP server: failed to validate JSON content for tool '{tool_name}'",
298
+ errors=e.errors(),
299
+ ) from e
300
+
301
+ def _parse_json_response(self, tool_result: types.ToolResult) -> Dict[str, Any]:
302
+ if tool_result.structured_content:
303
+ return tool_result.structured_content
304
+
305
+ content_list = tool_result.content
306
+ if not isinstance(content_list, list):
307
+ raise SecureLendError(
308
+ "Invalid response: content is not a list", "server_error"
309
+ )
310
+
311
+ for item in content_list:
312
+ if item.type == "text" and item.text:
313
+ try:
314
+ return json.loads(item.text)
315
+ except json.JSONDecodeError as e:
316
+ raise SecureLendError(
317
+ f"Failed to parse JSON response: {e}", "server_error"
318
+ ) from e
319
+ elif (
320
+ item.type == "resource"
321
+ and item.resource
322
+ and item.resource.mime_type == "application/json"
323
+ and item.resource.text
324
+ ):
325
+ try:
326
+ return json.loads(item.resource.text)
327
+ except json.JSONDecodeError as e:
328
+ raise SecureLendError(
329
+ f"Failed to parse JSON response: {e}", "server_error"
330
+ ) from e
331
+
332
+ raise SecureLendError(
333
+ "Invalid response from MCP server: missing JSON content", "server_error"
334
+ )
335
+
336
+ def _get_widget(self, tool_result: types.ToolResult) -> Optional[str]:
337
+ content_list = tool_result.content
338
+ if not isinstance(content_list, list):
339
+ return None
340
+
341
+ for item in content_list:
342
+ if (
343
+ item.type == "resource"
344
+ and item.resource
345
+ and item.resource.mime_type == "text/html"
346
+ ):
347
+ return item.resource.text
348
+ return None
securelend/types.py ADDED
@@ -0,0 +1,475 @@
1
+ from __future__ import annotations
2
+ from typing import List, Literal, Optional, Union, Dict, Any
3
+ from pydantic import BaseModel, Field, model_validator
4
+ from enum import Enum
5
+
6
+ # ============================================================================
7
+ # Enums
8
+ # ============================================================================
9
+
10
+ class CreditTier(str, Enum):
11
+ PRIME = "PRIME"
12
+ NEAR_PRIME = "NEAR_PRIME"
13
+ SUBPRIME = "SUBPRIME"
14
+ DEEP_SUBPRIME = "DEEP_SUBPRIME"
15
+ UNKNOWN = "UNKNOWN"
16
+
17
+ class ProductType(str, Enum):
18
+ INSTALLMENT_LOAN = "INSTALLMENT_LOAN"
19
+ LINE_OF_CREDIT = "LINE_OF_CREDIT"
20
+ SHORT_TERM_CREDIT = "SHORT_TERM_CREDIT"
21
+ BNPL = "BNPL"
22
+ MORTGAGE = "MORTGAGE"
23
+ AUTO_LOAN = "AUTO_LOAN"
24
+ AUTO_REFINANCE = "AUTO_REFINANCE"
25
+ STUDENT_LOAN = "STUDENT_LOAN"
26
+ STUDENT_LOAN_REFINANCE = "STUDENT_LOAN_REFINANCE"
27
+ BUSINESS_LOAN = "BUSINESS_LOAN"
28
+ BUSINESS_BANKING = "BUSINESS_BANKING"
29
+ BUSINESS_CREDIT_CARD = "BUSINESS_CREDIT_CARD"
30
+ PERSONAL_BANKING = "PERSONAL_BANKING"
31
+ SAVINGS_ACCOUNT = "SAVINGS_ACCOUNT"
32
+ PERSONAL_CREDIT_CARD = "PERSONAL_CREDIT_CARD"
33
+
34
+ class ApplicationType(str, Enum):
35
+ PERSONAL = "PERSONAL"
36
+ BUSINESS = "BUSINESS"
37
+
38
+ class ApplicationStatus(str, Enum):
39
+ DRAFT = "DRAFT"
40
+ SUBMITTED = "SUBMITTED"
41
+ IN_REVIEW = "IN_REVIEW"
42
+ ACTION_REQUIRED = "ACTION_REQUIRED"
43
+ APPROVED = "APPROVED"
44
+ DECLINED = "DECLINED"
45
+ CLOSED = "CLOSED"
46
+
47
+ class ProviderStatus(str, Enum):
48
+ PENDING = "PENDING"
49
+ SUBMITTED = "SUBMITTED"
50
+ REFERRED = "REFERRED"
51
+ ERROR = "ERROR"
52
+ RECEIVED = "RECEIVED"
53
+
54
+ # ============================================================================
55
+ # Common & Base Types
56
+ # ============================================================================
57
+
58
+ class Money(BaseModel):
59
+ amount: float
60
+ currency: Optional[str] = None
61
+
62
+ class Resource(BaseModel):
63
+ mime_type: Optional[str] = Field(None, alias="mimeType")
64
+ text: Optional[str] = None
65
+
66
+ class ContentItem(BaseModel):
67
+ type: Optional[str] = None
68
+ resource: Optional[Resource] = None
69
+ text: Optional[str] = None
70
+
71
+ class ToolResult(BaseModel):
72
+ content: List[ContentItem] = []
73
+ structured_content: Optional[Any] = Field(None, alias="structuredContent")
74
+ request_id: Optional[str] = Field(None, alias="requestId")
75
+
76
+ class SecureLendConfig(BaseModel):
77
+ api_key: Optional[str] = Field(None, alias="apiKey")
78
+ server_url: Optional[str] = Field(None, alias="serverUrl")
79
+
80
+ # ============================================================================
81
+ # Loan Comparison
82
+ # ============================================================================
83
+
84
+ class PersonalLoanSearchParams(BaseModel):
85
+ loan_amount: float = Field(..., alias="loanAmount", description="The desired loan amount in USD (e.g., 25000).")
86
+ purpose: Literal["debt_consolidation", "home_improvement", "major_purchase", "medical", "vacation", "other"] = Field(..., description="The purpose of the loan.")
87
+ credit_score: Optional[int] = Field(None, alias="creditScore", description="The applicant's estimated credit score (300-850).")
88
+ monthly_income: Optional[float] = Field(None, alias="monthlyIncome", description="Applicant's gross monthly income in USD.")
89
+ employment_status: Optional[Literal["employed", "self_employed", "retired", "unemployed"]] = Field(None, alias="employmentStatus", description="The applicant's employment status.")
90
+ state: Optional[str] = Field(None, description="The applicant's state of residence (2-letter code).")
91
+
92
+ class BusinessLoanSearchParams(BaseModel):
93
+ loan_amount: float = Field(..., alias="loanAmount", description="The desired loan amount, e.g., 50000 for $50,000.")
94
+ purpose: str = Field(..., description="The reason for the loan, e.g., 'working capital'.")
95
+ annual_revenue: Optional[float] = Field(None, alias="annualRevenue", description="The business's gross annual revenue in USD.")
96
+ industry: Optional[str] = Field(None, description="The industry the business operates in, e.g., 'technology'.")
97
+ state: Optional[str] = Field(None, description="The state where the business is located (2-letter code).")
98
+
99
+ class MortgageSearchParams(BaseModel):
100
+ loan_amount: float = Field(..., alias="loanAmount", description="The desired mortgage loan amount in USD.")
101
+ home_price: Optional[float] = Field(None, alias="homePrice", description="The purchase price of the home in USD.")
102
+ down_payment: Optional[float] = Field(None, alias="downPayment", description="The amount of the down payment in USD.")
103
+ credit_score: Optional[int] = Field(None, alias="creditScore", description="The applicant's estimated credit score (500-850).")
104
+ loan_type: Literal["conventional", "fha", "va", "jumbo", "refinance"] = Field(..., alias="loanType", description="The type of mortgage loan.")
105
+ property_type: Optional[Literal["primary", "secondary", "investment"]] = Field(None, alias="propertyType", description="The intended use of the property.")
106
+ state: Optional[str] = Field(None, description="The state where the property is located (2-letter code).")
107
+
108
+ class AutoLoanSearchParams(BaseModel):
109
+ loan_amount: float = Field(..., alias="loanAmount", description="The desired auto loan amount in USD.")
110
+ credit_score: Optional[int] = Field(None, alias="creditScore", description="The applicant's estimated credit score (300-850).")
111
+ is_new: bool = Field(..., alias="isNew", description="Specifies if the vehicle is new (true) or used (false).")
112
+ state: Optional[str] = Field(None, description="The applicant's state of residence (2-letter code).")
113
+
114
+ class StudentLoanSearchParams(BaseModel):
115
+ loan_amount: float = Field(..., alias="loanAmount", description="The total amount needed for the student loan in USD.")
116
+ credit_score: Optional[int] = Field(None, alias="creditScore", description="The student's estimated credit score (300-850).")
117
+ co_signer_credit_score: Optional[int] = Field(None, alias="coSignerCreditScore", description="The co-signer's estimated credit score (300-850), if applicable.")
118
+ degree_type: Literal["undergraduate", "graduate", "mba", "medical", "law"] = Field(..., alias="degreeType", description="The type of degree the loan is for.")
119
+ state: Optional[str] = Field(None, description="The student's state of residence (2-letter code).")
120
+
121
+ class Lender(BaseModel):
122
+ id: str
123
+ name: str
124
+ type: str
125
+ logo_url: Optional[str] = Field(None, alias="logoUrl")
126
+
127
+ class Product(BaseModel):
128
+ name: str
129
+ type: str
130
+ description: Optional[str] = None
131
+
132
+ class InterestRate(BaseModel):
133
+ type: Literal["fixed", "variable"]
134
+ rate: Optional[float] = None
135
+ apr: float
136
+
137
+ class Payment(BaseModel):
138
+ frequency: Optional[str] = None
139
+ amount: Money
140
+
141
+ class Repayment(BaseModel):
142
+ total_repayment_amount: float = Field(..., alias="totalRepaymentAmount")
143
+ cost_of_financing: float = Field(..., alias="costOfFinancing")
144
+
145
+ class LoanTerms(BaseModel):
146
+ amount: Money
147
+ interest_rate: InterestRate = Field(..., alias="interestRate")
148
+ term_months: int = Field(..., alias="termMonths")
149
+ payment: Payment
150
+ total_cost: Optional[Money] = Field(None, alias="totalCost")
151
+ repayment: Optional[Repayment] = None
152
+
153
+ class OriginationFee(BaseModel):
154
+ amount: Optional[Money] = None
155
+ percentage: Optional[float] = None
156
+
157
+ class LoanFees(BaseModel):
158
+ origination: Optional[OriginationFee] = None
159
+
160
+ class Matching(BaseModel):
161
+ approval_probability: Optional[float] = Field(None, alias="approvalProbability")
162
+ match_score: Optional[float] = Field(None, alias="matchScore")
163
+ match_reasons: Optional[List[str]] = Field(None, alias="matchReasons")
164
+
165
+ class FundingSpeed(BaseModel):
166
+ description: str
167
+
168
+ class Process(BaseModel):
169
+ application_method: str = Field(..., alias="applicationMethod")
170
+ application_url: Optional[str] = Field(None, alias="applicationUrl")
171
+ funding_speed: Optional[FundingSpeed] = Field(None, alias="fundingSpeed")
172
+
173
+ class LoanOffer(BaseModel):
174
+ offer_id: str = Field(..., alias="offerId")
175
+ lender: Lender
176
+ product: Product
177
+ terms: LoanTerms
178
+ fees: Optional[LoanFees] = None
179
+ matching: Optional[Matching] = None
180
+ process: Optional[Process] = None
181
+ lender_info: Optional[str] = Field(None, alias="lenderInfo")
182
+ badges: Optional[List[str]] = None
183
+ is_secure_lend_tenant: Optional[bool] = Field(None, alias="isSecureLendTenant")
184
+
185
+ class LoanComparisonSummary(BaseModel):
186
+ total_offers: int = Field(..., alias="totalOffers")
187
+ best_rate: float = Field(..., alias="bestRate")
188
+ best_approval_probability: Optional[float] = Field(None, alias="bestApprovalProbability")
189
+ fastest_funding: Optional[str] = Field(None, alias="fastestFunding")
190
+
191
+ class Metadata(BaseModel):
192
+ query_id: str = Field(..., alias="queryId")
193
+ timestamp: str
194
+ session_id: Optional[str] = Field(None, alias="sessionId")
195
+
196
+ class LoanComparisonResponse(BaseModel):
197
+ offers: List[LoanOffer]
198
+ summary: LoanComparisonSummary
199
+ search_criteria: Optional[Union[PersonalLoanSearchParams, BusinessLoanSearchParams, MortgageSearchParams, AutoLoanSearchParams, StudentLoanSearchParams]] = Field(None, alias="searchCriteria")
200
+ metadata: Metadata
201
+ widget: Optional[str] = None
202
+
203
+ # ============================================================================
204
+ # Banking & Credit Cards
205
+ # ============================================================================
206
+
207
+ class BusinessBankingSearchSchema(BaseModel):
208
+ industry: Optional[str] = Field(None, description="The business's industry.")
209
+ monthly_transactions: Optional[int] = Field(None, alias="monthlyTransactions", description="Estimated number of monthly transactions.")
210
+
211
+ class BusinessBankingOffer(BaseModel):
212
+ account_id: str = Field(..., alias="accountId")
213
+ name: str
214
+ issuer: str
215
+ image_url: str = Field(..., alias="imageUrl")
216
+ rating: float
217
+ best_for: str = Field(..., alias="bestFor")
218
+ apy: str
219
+ interest_rate: Optional[str] = Field(None, alias="interestRate")
220
+ monthly_fee: str = Field(..., alias="monthlyFee")
221
+ bonus: str
222
+
223
+ class BusinessBankingComparisonResponse(BaseModel):
224
+ offers: List[BusinessBankingOffer]
225
+ search_criteria: Optional[BusinessBankingSearchSchema] = Field(None, alias="searchCriteria")
226
+ metadata: Metadata
227
+ widget: Optional[str] = None
228
+
229
+ class PersonalBankingSearchSchema(BaseModel):
230
+ features: Optional[List[str]] = Field(None, description="Desired account features.")
231
+
232
+ class PersonalBankingOffer(BaseModel):
233
+ account_id: str = Field(..., alias="accountId")
234
+ name: str
235
+ issuer: str
236
+ image_url: str = Field(..., alias="imageUrl")
237
+ rating: float
238
+ best_for: str = Field(..., alias="bestFor")
239
+ apy: str
240
+ interest_rate: Optional[str] = Field(None, alias="interestRate")
241
+ bonus: str
242
+ monthly_fee: str = Field(..., alias="monthlyFee")
243
+ overdraft_rating: float = Field(..., alias="overdraftRating")
244
+
245
+ class PersonalBankingComparisonResponse(BaseModel):
246
+ offers: List[PersonalBankingOffer]
247
+ search_criteria: Optional[PersonalBankingSearchSchema] = Field(None, alias="searchCriteria")
248
+ metadata: Metadata
249
+ widget: Optional[str] = None
250
+
251
+ class SavingsSearchSchema(BaseModel):
252
+ initial_deposit: Optional[float] = Field(None, alias="initialDeposit", description="The initial deposit amount in USD.")
253
+
254
+ class SavingsOffer(BaseModel):
255
+ account_id: str = Field(..., alias="accountId")
256
+ name: str
257
+ issuer: str
258
+ image_url: str = Field(..., alias="imageUrl")
259
+ rating: float
260
+ apy: str
261
+ interest_rate: Optional[str] = Field(None, alias="interestRate")
262
+ min_balance_for_apy: str = Field(..., alias="minBalanceForApy")
263
+ monthly_fee: str = Field(..., alias="monthlyFee")
264
+
265
+ class SavingsAccountComparisonResponse(BaseModel):
266
+ offers: List[SavingsOffer]
267
+ search_criteria: Optional[SavingsSearchSchema] = Field(None, alias="searchCriteria")
268
+ metadata: Metadata
269
+ widget: Optional[str] = None
270
+
271
+ class BusinessCreditCardSearchParams(BaseModel):
272
+ credit_score: Optional[int] = Field(None, alias="creditScore", description="The applicant's estimated credit score (300-850).")
273
+ annual_revenue: Optional[float] = Field(None, alias="annualRevenue", description="The business's annual revenue in USD.")
274
+ business_age_in_years: Optional[float] = Field(None, alias="businessAgeInYears", description="The age of the business in years.")
275
+
276
+ class BusinessCreditCardTerms(BaseModel):
277
+ apr: float
278
+ annual_fee: float = Field(..., alias="annualFee")
279
+ credit_limit: Money = Field(..., alias="creditLimit")
280
+
281
+ class BusinessCreditCardOffer(BaseModel):
282
+ card_id: str = Field(..., alias="cardId")
283
+ name: str
284
+ issuer: str
285
+ image_url: str = Field(..., alias="imageUrl")
286
+ rating: float
287
+ credit_score_range: str = Field(..., alias="creditScoreRange")
288
+ welcome_bonus: str = Field(..., alias="welcomeBonus")
289
+ terms: BusinessCreditCardTerms
290
+
291
+ class BusinessCreditCardComparisonResponse(BaseModel):
292
+ offers: List[BusinessCreditCardOffer]
293
+ search_criteria: Optional[BusinessCreditCardSearchParams] = Field(None, alias="searchCriteria")
294
+ metadata: Metadata
295
+ widget: Optional[str] = None
296
+
297
+ class PersonalCreditCardSearchSchema(BaseModel):
298
+ credit_score: Optional[int] = Field(None, alias="creditScore", description="The applicant's estimated credit score (300-850).")
299
+ rewards_type: Optional[Literal["cash_back", "travel", "points"]] = Field(None, alias="rewardsType", description="Preferred rewards type.")
300
+
301
+ class PersonalCreditCardAPRs(BaseModel):
302
+ purchase: float
303
+ balance_transfer: float = Field(..., alias="balanceTransfer")
304
+ penalty: Optional[float] = None
305
+
306
+ class PersonalCreditCardFees(BaseModel):
307
+ annual: float
308
+
309
+ class PersonalCreditCardTerms(BaseModel):
310
+ apr: PersonalCreditCardAPRs
311
+ fees: PersonalCreditCardFees
312
+
313
+ class PersonalCreditCardOffer(BaseModel):
314
+ card_id: str = Field(..., alias="cardId")
315
+ name: str
316
+ issuer: str
317
+ image_url: str = Field(..., alias="imageUrl")
318
+ rating: float
319
+ best_for: str = Field(..., alias="bestFor")
320
+ rewards_rate: str = Field(..., alias="rewardsRate")
321
+ intro_offer: str = Field(..., alias="introOffer")
322
+ recommended_credit_score: str = Field(..., alias="recommendedCreditScore")
323
+ terms: PersonalCreditCardTerms
324
+ application_url: Optional[str] = Field(None, alias="applicationUrl")
325
+
326
+ class PersonalCreditCardComparisonResponse(BaseModel):
327
+ offers: List[PersonalCreditCardOffer]
328
+ search_criteria: Optional[PersonalCreditCardSearchSchema] = Field(None, alias="searchCriteria")
329
+ metadata: Metadata
330
+ widget: Optional[str] = None
331
+
332
+ # ============================================================================
333
+ # Financial Calculators
334
+ # ============================================================================
335
+
336
+ class LoanPaymentParams(BaseModel):
337
+ loan_amount: float = Field(..., alias="loanAmount")
338
+ interest_rate: float = Field(..., alias="interestRate")
339
+ loan_term_in_months: int = Field(..., alias="loanTermInMonths")
340
+
341
+ class LoanCalculationResponse(BaseModel):
342
+ monthly_payment: float = Field(..., alias="monthlyPayment")
343
+ total_payment: float = Field(..., alias="totalPayment")
344
+ total_interest: float = Field(..., alias="totalInterest")
345
+ widget: Optional[str] = None
346
+
347
+ class MortgagePaymentParams(BaseModel):
348
+ property_value: float = Field(..., alias="propertyValue")
349
+ down_payment: float = Field(..., alias="downPayment")
350
+ interest_rate: float = Field(..., alias="interestRate")
351
+ loan_term_in_years: int = Field(..., alias="loanTermInYears")
352
+ property_tax_rate: float = Field(..., alias="propertyTaxRate")
353
+ home_insurance: float = Field(..., alias="homeInsurance")
354
+
355
+ class MortgageCalculationResponse(BaseModel):
356
+ loan_amount: float = Field(..., alias="loanAmount")
357
+ principal_and_interest: float = Field(..., alias="principalAndInterest")
358
+ monthly_property_tax: float = Field(..., alias="monthlyPropertyTax")
359
+ monthly_home_insurance: float = Field(..., alias="monthlyHomeInsurance")
360
+ total_monthly_payment: float = Field(..., alias="totalMonthlyPayment")
361
+ widget: Optional[str] = None
362
+
363
+ class LeaseVsPurchaseParams(BaseModel):
364
+ purchase_price: float = Field(..., alias="purchasePrice")
365
+ down_payment: float = Field(..., alias="downPayment")
366
+ loan_term_in_months: int = Field(..., alias="loanTermInMonths")
367
+ interest_rate: float = Field(..., alias="interestRate")
368
+ sales_tax_rate: float = Field(..., alias="salesTaxRate")
369
+ lease_term_in_months: int = Field(..., alias="leaseTermInMonths")
370
+ monthly_lease_payment: float = Field(..., alias="monthlyLeasePayment")
371
+ money_factor: float = Field(..., alias="moneyFactor")
372
+ acquisition_fee: float = Field(..., alias="acquisitionFee")
373
+ security_deposit: float = Field(..., alias="securityDeposit")
374
+ residual_value_percentage: float = Field(..., alias="residualValuePercentage")
375
+ expected_ownership_in_months: int = Field(..., alias="expectedOwnershipInMonths")
376
+
377
+ class LeaseVsPurchaseResponse(BaseModel):
378
+ purchase_analysis: Dict[str, Any] = Field(..., alias="purchaseAnalysis")
379
+ lease_analysis: Dict[str, Any] = Field(..., alias="leaseAnalysis")
380
+ comparison: Dict[str, Any]
381
+ widget: Optional[str] = None
382
+
383
+ # ============================================================================
384
+ # Application Management
385
+ # ============================================================================
386
+
387
+ class PersonalApplicant(BaseModel):
388
+ first_name: str = Field(..., alias="firstName", description="The applicant's first name.")
389
+ last_name: str = Field(..., alias="lastName", description="The applicant's last name.")
390
+ email: str = Field(..., description="The applicant's email address.")
391
+ phone: Optional[str] = Field(None, description="The applicant's phone number.")
392
+
393
+ class Provider(BaseModel):
394
+ provider_id: str = Field(..., alias="providerId", description="The ID of the selected provider.")
395
+ provider_name: str = Field(..., alias="providerName", description="The name of the selected provider.")
396
+
397
+ class GetOfferParams(BaseModel):
398
+ product_type: ProductType = Field(..., alias="productType", description="The type of financial product being applied for.")
399
+ applicant: PersonalApplicant = Field(..., description="Personal details of the applicant.")
400
+ application_data: Dict[str, Any] = Field(..., alias="applicationData", description="The original search parameters or form data for the application.")
401
+ provider: Provider = Field(..., description="The selected provider to submit the application to.")
402
+
403
+ class GetMultipleOffersParams(BaseModel):
404
+ product_type: ProductType = Field(..., alias="productType", description="The type of financial product being applied for.")
405
+ applicant: PersonalApplicant = Field(..., description="Personal details of the applicant.")
406
+ application_data: Dict[str, Any] = Field(..., alias="applicationData", description="The original search parameters or form data for the application.")
407
+ providers: List[Provider] = Field(..., min_length=1, description="The list of selected providers to submit the application to.")
408
+
409
+ class ApplicationProvider(BaseModel):
410
+ provider_id: str = Field(..., alias="providerId")
411
+ provider_name: str = Field(..., alias="providerName")
412
+ status: ProviderStatus
413
+ submitted_at: Optional[str] = Field(None, alias="submittedAt")
414
+ provider_application_id: Optional[str] = Field(None, alias="providerApplicationId")
415
+ referral_url: Optional[str] = Field(None, alias="referralUrl")
416
+ error: Optional[str] = None
417
+
418
+ class PersonalApplication(BaseModel):
419
+ id: str
420
+ created_at: str = Field(..., alias="createdAt")
421
+ updated_at: str = Field(..., alias="updatedAt")
422
+ application_type: ApplicationType = Field(..., alias="applicationType")
423
+ product_type: Union[ProductType, str] = Field(..., alias="productType")
424
+ status: ApplicationStatus
425
+ applicant: PersonalApplicant
426
+ application_data: Dict[str, Any] = Field(..., alias="applicationData")
427
+ providers: List[ApplicationProvider]
428
+ email: str
429
+ widget: Optional[str] = None
430
+
431
+ class DisplayOfferFormParams(BaseModel):
432
+ session_id: Optional[str] = Field(None, alias="sessionId")
433
+ offer_id: Optional[str] = Field(None, alias="offerId")
434
+
435
+ AnyOffer = Union[LoanOffer, BusinessCreditCardOffer, BusinessBankingOffer, PersonalBankingOffer, SavingsOffer, PersonalCreditCardOffer]
436
+
437
+ class DisplayOfferFormResponse(BaseModel):
438
+ offer: AnyOffer
439
+ all_offers: List[AnyOffer] = Field(..., alias="allOffers")
440
+ application_data: Dict[str, Any] = Field(..., alias="applicationData")
441
+ product_type: str = Field(..., alias="productType")
442
+ widget: Optional[str] = None
443
+
444
+ class TrackOfferStatusParams(BaseModel):
445
+ application_id: Optional[str] = Field(None, alias="applicationId")
446
+ email: Optional[str] = None
447
+
448
+ @model_validator(mode="after")
449
+ def check_application_id_or_email(self) -> "TrackOfferStatusParams":
450
+ if not self.application_id and not self.email:
451
+ raise ValueError("Either applicationId or email must be provided.")
452
+ return self
453
+
454
+ class TrackOfferStatusResponse(BaseModel):
455
+ applications: List[PersonalApplication]
456
+ widget: Optional[str] = None
457
+
458
+ class DisplayUploadDocumentsFormParams(BaseModel):
459
+ application_id: Optional[str] = Field(None, alias="applicationId")
460
+
461
+ class DisplayUploadDocumentsFormResponse(BaseModel):
462
+ widget: Optional[str] = None
463
+
464
+ class SubmitDocumentsParams(BaseModel):
465
+ application_id: str = Field(..., alias="applicationId")
466
+ file_name: str = Field(..., alias="fileName")
467
+ document_type: str = Field(..., alias="documentType")
468
+
469
+ class SubmitDocumentsResponse(BaseModel):
470
+ success: bool
471
+ message: str
472
+ upload_url: Optional[str] = Field(None, alias="uploadUrl")
473
+ upload_fields: Optional[Dict[str, str]] = Field(None, alias="uploadFields")
474
+ document_id: Optional[str] = Field(None, alias="documentId")
475
+ widget: Optional[str] = None
@@ -0,0 +1 @@
1
+ # This file makes the `utils` directory a Python package.
@@ -0,0 +1,38 @@
1
+ class SecureLendError(Exception):
2
+ """Base exception for all SecureLend SDK errors."""
3
+
4
+ def __init__(self, message: str, error_type: str, details: any = None):
5
+ super().__init__(message)
6
+ self.type = error_type
7
+ self.details = details
8
+
9
+
10
+ class AuthenticationError(SecureLendError):
11
+ def __init__(self, message: str):
12
+ super().__init__(message, "authentication_error")
13
+
14
+
15
+ class RateLimitError(SecureLendError):
16
+ def __init__(self, message: str, retry_after: int | None = None):
17
+ super().__init__(message, "rate_limit_error")
18
+ self.retry_after = retry_after
19
+
20
+
21
+ class ValidationError(SecureLendError):
22
+ def __init__(self, message: str, errors: any = None):
23
+ super().__init__(message, "validation_error", errors)
24
+
25
+
26
+ class NotFoundError(SecureLendError):
27
+ def __init__(self, message: str):
28
+ super().__init__(message, "not_found")
29
+
30
+
31
+ class NetworkError(SecureLendError):
32
+ def __init__(self, message: str):
33
+ super().__init__(message, "network_error")
34
+
35
+
36
+ class ServerError(SecureLendError):
37
+ def __init__(self, message: str):
38
+ super().__init__(message, "server_error")
@@ -0,0 +1,115 @@
1
+ from contextlib import AsyncExitStack
2
+ from typing import Any, Dict, Optional
3
+
4
+ import httpx
5
+ from mcp import ClientSession, McpError
6
+ from mcp.client.streamable_http import streamablehttp_client
7
+
8
+ from ..utils.errors import (
9
+ AuthenticationError,
10
+ NetworkError,
11
+ RateLimitError,
12
+ ServerError,
13
+ )
14
+
15
+
16
+ class MCPClient:
17
+ def __init__(self, *, api_key: str, mcp_url: str):
18
+ self._api_key = api_key
19
+ self._mcp_url = mcp_url
20
+ self._debug = False
21
+ self._session: Optional[ClientSession] = None
22
+ self._exit_stack = AsyncExitStack()
23
+ self._reconnect_required = False
24
+
25
+ async def _get_session(self) -> ClientSession:
26
+ if self._session is None or self._reconnect_required:
27
+ await self.connect()
28
+ self._reconnect_required = False
29
+ assert self._session is not None
30
+ return self._session
31
+
32
+ async def connect(self) -> None:
33
+ if self._session:
34
+ await self.close()
35
+
36
+ self._exit_stack = AsyncExitStack()
37
+ headers = {
38
+ "User-Agent": f"securelend-python/{__import__('securelend').__version__}",
39
+ }
40
+ if self._api_key:
41
+ headers["Authorization"] = f"Bearer {self._api_key}"
42
+
43
+ if self._debug:
44
+ print(f"[SecureLend SDK] Connecting to MCP server at {self._mcp_url}")
45
+
46
+ try:
47
+ transport_context = streamablehttp_client(
48
+ url=self._mcp_url, headers=headers
49
+ )
50
+ read_stream, write_stream, _ = await self._exit_stack.enter_async_context(
51
+ transport_context
52
+ )
53
+ session_context = ClientSession(read_stream, write_stream)
54
+ self._session = await self._exit_stack.enter_async_context(session_context)
55
+ await self._session.initialize()
56
+ except httpx.HTTPStatusError as e:
57
+ if e.response.status_code in (401, 403):
58
+ raise AuthenticationError("Authentication failed.") from e
59
+ if e.response.status_code == 429:
60
+ retry_after_header = e.response.headers.get("Retry-After")
61
+ retry_after = None
62
+ if retry_after_header and retry_after_header.isdigit():
63
+ retry_after = int(retry_after_header)
64
+ raise RateLimitError(
65
+ "Rate limit exceeded.", retry_after=retry_after
66
+ ) from e
67
+ if e.response.status_code >= 500:
68
+ raise ServerError(f"Server error: {e.response.text}") from e
69
+ raise NetworkError(f"HTTP error: {e}") from e
70
+ except httpx.RequestError as e:
71
+ raise NetworkError(f"Network error: {e}") from e
72
+ except Exception as e:
73
+ raise NetworkError(f"Failed to connect to MCP server: {e}") from e
74
+
75
+ async def call_tool(self, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
76
+ session = await self._get_session()
77
+ try:
78
+ if self._debug:
79
+ print(f"[SecureLend SDK] Calling tool '{name}' with args: {args}")
80
+
81
+ result = await session.call_tool(name, args)
82
+
83
+ content_list = []
84
+ for item in result.content:
85
+ content_item = {"type": item.type, "text": getattr(item, "text", None)}
86
+ if hasattr(item, "resource") and item.resource:
87
+ content_item["resource"] = {
88
+ "mimeType": item.resource.mimeType,
89
+ "text": item.resource.text,
90
+ }
91
+ content_list.append(content_item)
92
+
93
+ return {
94
+ "content": content_list,
95
+ "structuredContent": getattr(result, "structuredContent", None),
96
+ "requestId": getattr(result, "requestId", None),
97
+ }
98
+ except McpError as e:
99
+ raise ServerError(f"MCP tool call failed: {e.message}") from e
100
+
101
+ async def close(self):
102
+ if self._session:
103
+ await self._exit_stack.aclose()
104
+ self._session = None
105
+ self._exit_stack = AsyncExitStack()
106
+
107
+ def set_api_key(self, api_key: str) -> None:
108
+ self._api_key = api_key
109
+ self._reconnect_required = True
110
+
111
+ def enable_debug(self) -> None:
112
+ self._debug = True
113
+
114
+ def disable_debug(self) -> None:
115
+ self._debug = False
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: securelend
3
+ Version: 0.1.0
4
+ Summary: SecureLend SDK for Python
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/SecureLend/sdk/tree/main/packages/python
7
+ Project-URL: Bug Tracker, https://github.com/SecureLend/sdk/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: mcp>=1.2.0
13
+ Requires-Dist: httpx>=0.27.0
14
+ Requires-Dist: pydantic>=2.0
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7.0; extra == "test"
17
+ Requires-Dist: pytest-asyncio>=0.20; extra == "test"
18
+
19
+ # SecureLend SDK for Python [![PyPI version](https://img.shields.io/pypi/v/securelend.svg)](https://pypi.org/project/securelend/)
20
+
21
+ The official Python SDK for SecureLend - Financial services infrastructure for AI assistants.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install securelend
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```python
32
+ import asyncio
33
+ import os
34
+ from securelend import SecureLend
35
+
36
+ async def main():
37
+ # The API server is public and does not require an API key.
38
+ # A key can optionally be provided for usage tracking.
39
+ async with SecureLend(api_key=os.environ.get("SECURELEND_API_KEY")) as securelend:
40
+ response = await securelend.compare_business_loans({
41
+ "loanAmount": 200000,
42
+ "purpose": "equipment",
43
+ "annualRevenue": 1200000,
44
+ })
45
+
46
+ print(f"Found {response.summary.total_offers} loan offers.")
47
+
48
+ if response.offers:
49
+ top_offer = response.offers[0]
50
+ apr = top_offer.terms.interest_rate.apr * 100
51
+ print(f"Top offer from {top_offer.lender.name} with {apr:.2f}% APR.")
52
+
53
+ # Calculate payment details for the top offer
54
+ payment_details = await securelend.calculate_loan_payment({
55
+ "loanAmount": top_offer.terms.amount.amount,
56
+ "interestRate": apr,
57
+ "loanTermInMonths": top_offer.terms.term_months,
58
+ })
59
+ print(f" - Estimated Monthly Payment: ${payment_details.monthly_payment:,.2f}")
60
+
61
+
62
+ if __name__ == "__main__":
63
+ asyncio.run(main())
64
+ ```
65
+
66
+ ## Documentation
67
+
68
+ - [Full Documentation](https://docs.securelend.ai)
69
+ - [API Reference](https://docs.securelend.ai/api)
70
+ - [Guides](https://docs.securelend.ai/guides)
@@ -0,0 +1,10 @@
1
+ securelend/__init__.py,sha256=HIaVtRuOEOiTKDxnYFIKyuwNtuPivfWzp9whktUb-Og,553
2
+ securelend/client.py,sha256=WdmIH_LnBk9F20SPXx0pZ9JJqjkZwjQEUD33EL0uQEs,11660
3
+ securelend/types.py,sha256=-f43TX5oNaJ-16jeoH3S6AMHRO_cVJCV-rYO_bjiLGs,21899
4
+ securelend/utils/__init__.py,sha256=1IwFDtwJ76i1O7_iM4LLGqwgtt11y0PIV0DWubh8nLU,58
5
+ securelend/utils/errors.py,sha256=rPT22bGnPKM8sYubDjI2Mx3VQ_mMGcsYd5pONwMSzLI,1150
6
+ securelend/utils/mcp.py,sha256=1yvEweKcvmrOhd1iHp13uEx67ANLJF1CUY4uElEq_ls,4296
7
+ securelend-0.1.0.dist-info/METADATA,sha256=HgutbST1RKPcLUC7qbY7lA_ezVx4CGtaMblwPoIdVkE,2304
8
+ securelend-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
9
+ securelend-0.1.0.dist-info/top_level.txt,sha256=my2T0PhysHbgmRHqTy7ZaLnHpLFa9o9zB9rJQZWE9BI,11
10
+ securelend-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ securelend