diff --git a/method/resource.py b/method/resource.py index 045f76b..e983ee9 100644 --- a/method/resource.py +++ b/method/resource.py @@ -160,8 +160,17 @@ def _make_request(self, method: str, path: Optional[str] = None, data: Optional[ options = {} if data: options['data'] = json.dumps(data) + # Note (Reza): Automatically appends '[]' to list-type query params (e.g., 'expand') + # so users can pass them naturally without needing to format keys manually. if params: - options['params'] = params + fixed = {} + for k, v in params.items(): + if isinstance(v, list): + fixed[f"{k}[]"] = v + else: + fixed[k] = v + options['params'] = fixed + if headers: options['headers'] = headers @@ -211,11 +220,11 @@ def _list(self, params: Optional[Dict] = None) -> MethodResponse[List[T]]: return self._make_request('GET', params=params) @MethodError.catch - def _create(self, data: Dict, request_opts: Optional[RequestOpts] = None) -> MethodResponse[T]: + def _create(self, data: Dict, params: Optional[Dict] = None, request_opts: Optional[RequestOpts] = None) -> MethodResponse[T]: headers = {} if request_opts and request_opts.get('idempotency_key'): headers['Idempotency-Key'] = request_opts.get('idempotency_key') - return self._make_request('POST', data=data, headers=headers) + return self._make_request('POST', data=data, headers=headers, params=params) @MethodError.catch def _create_with_sub_path(self, path: str, data: Dict) -> MethodResponse[T]: diff --git a/method/resources/Entities/Connect.py b/method/resources/Entities/Connect.py index 9cdb9d2..71bde1d 100644 --- a/method/resources/Entities/Connect.py +++ b/method/resources/Entities/Connect.py @@ -6,10 +6,33 @@ EntityConnectResponseStatusLiterals = Literal[ - 'completed', - 'in_progress', - 'pending', - 'failed' + "completed", "in_progress", "pending", "failed" +] + +AccountConnectResponseExpandLiterals = Literal[ + "accounts", + "accounts.sensitive", + "accounts.balance", + "accounts.card_brand", + "accounts.attribute", + "accounts.payoff", + "accounts.transaction", + "accounts.update", + "accounts.payment_instrument", + "accounts.latest_verification_session", +] + +AccountProductsEligibleForAutomaticExecutionLiteral = Literal[ + "account_attribute", + "balance", + "card_brand", + "update", + "payoff", +] + + +AccountSubscriptionsEligibleForAutomaticExecutionLiteral = Literal[ + "card_brand", "update", "update.snapshot", "transaction" ] @@ -22,15 +45,36 @@ class EntityConnect(TypedDict): updated_at: str +class ConnectExpandOpts(TypedDict): + expand: AccountConnectResponseExpandLiterals + + +class ConnectResourceListOpts(ResourceListOpts, ConnectExpandOpts): + pass + +class ConnectCreateOpts(TypedDict): + products: Optional[List[AccountProductsEligibleForAutomaticExecutionLiteral]] + subscriptions: Optional[List[AccountSubscriptionsEligibleForAutomaticExecutionLiteral]] + + + + class EntityConnectResource(Resource): def __init__(self, config: Configuration): - super(EntityConnectResource, self).__init__(config.add_path('connect')) + super(EntityConnectResource, self).__init__(config.add_path("connect")) - def retrieve(self, cxn_id: str) -> MethodResponse[EntityConnect]: - return super(EntityConnectResource, self)._get_with_id(cxn_id) - - def list(self, params: Optional[ResourceListOpts] = None) -> MethodResponse[List[EntityConnect]]: - return super(EntityConnectResource, self)._list(params) + def retrieve(self, cxn_id: str, opts: Optional[ConnectExpandOpts] = None) -> MethodResponse[EntityConnect]: + return super(EntityConnectResource, self)._get_with_sub_path_and_params(cxn_id, params=opts) - def create(self) -> MethodResponse[EntityConnect]: - return super(EntityConnectResource, self)._create({}) + def list( + self, opts: Optional[ConnectResourceListOpts] = None + ) -> MethodResponse[List[EntityConnect]]: + return super(EntityConnectResource, self)._list(opts) + + def create( + self, + opts: ConnectCreateOpts = {}, + params: Optional[ConnectExpandOpts] = None + ) -> MethodResponse[EntityConnect]: + return super(EntityConnectResource, self)._create(data=opts, params=params) + \ No newline at end of file diff --git a/method/resources/Entities/Subscriptions.py b/method/resources/Entities/Subscriptions.py index 76ee8bc..d640632 100644 --- a/method/resources/Entities/Subscriptions.py +++ b/method/resources/Entities/Subscriptions.py @@ -1,4 +1,4 @@ -from typing import TypedDict, Optional, Literal, Dict, Any +from typing import TypedDict, Optional, Literal, Dict, Any, Union from method.resource import MethodResponse, Resource from method.configuration import Configuration @@ -54,7 +54,7 @@ def retrieve(self, sub_id: str) -> MethodResponse[EntitySubscriptionResponseOpts def list(self) -> MethodResponse[EntitySubscriptionListResponse]: return super(EntitySubscriptionsResource, self)._list() - def create(self, opts: EntitySubscriptionCreateOpts | EntitySubscriptionNamesLiterals) -> MethodResponse[EntitySubscriptionResponseOpts]: + def create(self, opts: Union[EntitySubscriptionCreateOpts, EntitySubscriptionNamesLiterals]) -> MethodResponse[EntitySubscriptionResponseOpts]: if isinstance(opts, str): opts = {'enroll': opts} return super(EntitySubscriptionsResource, self)._create(opts) diff --git a/test/resources/Account_test.py b/test/resources/Account_test.py index 7a2c8d8..9777b1d 100644 --- a/test/resources/Account_test.py +++ b/test/resources/Account_test.py @@ -1,6 +1,7 @@ import os from typing import List import pytest +import time from method import Method from dotenv import load_dotenv from utils import await_results @@ -300,7 +301,7 @@ def test_create_card_brands(setup): 'id': card_brand_create_response['id'], 'account_id': test_credit_card_account['id'], 'network': 'visa', - 'status': 'completed', + 'status': 'in_progress', 'issuer': card_brand_create_response['issuer'], 'last4': '1580', 'brands': card_brand_create_response['brands'], @@ -311,52 +312,62 @@ def test_create_card_brands(setup): 'updated_at': card_brand_create_response['updated_at'], } + time.sleep(5) assert card_brand_create_response == expect_results def test_retrieve_card_brands(setup): test_credit_card_account = setup['test_credit_card_account'] + card_retrieve_response = method.accounts(test_credit_card_account['id']).card_brands.retrieve(card_brand_create_response['id']) - expect_results: AccountCardBrand = { + expect_results = { 'id': card_brand_create_response['id'], 'account_id': test_credit_card_account['id'], 'network': 'visa', 'status': 'completed', 'issuer': card_brand_create_response['issuer'], 'last4': '1580', - 'brands': card_brand_create_response['brands'], 'shared': False, - 'source': card_brand_create_response['source'], + 'source': "network", 'error': None, 'created_at': card_retrieve_response['created_at'], 'updated_at': card_retrieve_response['updated_at'], } - assert card_retrieve_response == expect_results + for k, v in expect_results.items(): + assert card_retrieve_response[k] == v + + brand = card_retrieve_response['brands'][0] + assert brand['id'] == 'brand_UBwVzXjpP4PJ6' + assert brand['name'] == 'Chase Sapphire Reserve' + assert brand['url'] == 'https://static.methodfi.com/card_brands/1b7ccaba6535cb837f802d968add4700.png' + assert isinstance(brand['art_id'], str) and brand['art_id'].startswith('art_') @pytest.mark.asyncio async def test_list_card_brands(setup): test_credit_card_account = setup['test_credit_card_account'] - - card_brands_list_response = method.accounts(test_credit_card_account['id']).card_brands.list() - - expect_results: AccountCardBrand = { - 'id': card_brand_create_response['id'], - 'account_id': test_credit_card_account['id'], - 'network': 'visa', - 'status': 'completed', - 'issuer': card_brand_create_response['issuer'], - 'last4': '1580', - 'brands': card_brand_create_response['brands'], - 'shared': False, - 'source': card_brand_create_response['source'], - 'error': None, - 'created_at': card_brands_list_response[0]['created_at'], - 'updated_at': card_brands_list_response[0]['updated_at'], - } - assert card_brands_list_response[0] == expect_results + card_brands_list_response = method.accounts(test_credit_card_account['id']).card_brands.list() + result = card_brands_list_response[0] + + assert result['id'] == card_brand_create_response['id'] + assert result['account_id'] == test_credit_card_account['id'] + assert result['network'] == 'visa' + assert result['status'] == 'completed' + assert result['issuer'] == card_brand_create_response['issuer'] + assert result['last4'] == '1580' + assert result['shared'] is False + assert result['source'] == 'network' + assert result['error'] is None + assert result['created_at'] == result['created_at'] + assert result['updated_at'] == result['updated_at'] + + brand = result['brands'][0] + assert brand['id'] == 'brand_UBwVzXjpP4PJ6' + assert brand['name'] == 'Chase Sapphire Reserve' + assert brand['url'] == 'https://static.methodfi.com/card_brands/1b7ccaba6535cb837f802d968add4700.png' + assert isinstance(brand['art_id'], str) and brand['art_id'].startswith('art_') def test_create_payoffs(setup): global payoff_create_response diff --git a/test/resources/Report_test.py b/test/resources/Report_test.py index 939dab1..e9269cc 100644 --- a/test/resources/Report_test.py +++ b/test/resources/Report_test.py @@ -25,7 +25,7 @@ def test_create_report(): "type": "payments.created.current", "url": f"https://dev.methodfi.com/reports/{report_create_response['id']}/download", "status": "completed", - "metadata": None, + "metadata": report_create_response["metadata"], "created_at": report_create_response["created_at"], "updated_at": report_create_response["updated_at"], } @@ -43,7 +43,7 @@ def test_retrieve_report(): "type": "payments.created.current", "url": f"https://dev.methodfi.com/reports/{report_create_response['id']}/download", "status": "completed", - "metadata": None, + "metadata": report_create_response["metadata"], "created_at": report_retrieve_response["created_at"], "updated_at": report_retrieve_response["updated_at"], }