diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1236fa..0ce52de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ +# 3.4.0 + +- **Bug Fixes:** + - Fixed `JSONDecodeError` when calling endpoints that return `204 No Content` responses (e.g. `customers.remove()`) - fixes [#96](https://github.com/gocardless/gocardless-pro-python/issues/96) + - Fixed pagination cursor handling that was raising `KeyError` on endpoints without cursors (e.g. `institutions.list_for_billing_request`) + - Fixed POST/PUT request body envelope wrapping to use schema-defined envelopes instead of inferred ones + - Fixed invalid Python 3 syntax that was causing compatibility issues + - Improved error messages by including HTTP status code in malformed response errors + +- **Breaking Changes:** + - Removed Python 2 compatibility shim (Python 2 has been EOL for 6+ years, and this library already uses Python 3-only features like f-strings) + - Removed unused `six` dependency (was never actually imported in the codebase) + - Documented minimum Python version as 3.10 + +- **Improvements:** + - README cleanup and documentation updates + - Added missing tests for paginator + # 3.1.0 - Added `mandate_request_constraints` to Billing Request templates - `constraints[max_amount_per_payment]` is required for Billing Requests Creation, if they contain PayTo `mandate_request` diff --git a/README.rst b/README.rst index 9e505381..33ccabc3 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,10 @@ .. |pypi-badge| image:: https://badge.fury.io/py/gocardless_pro.svg :target: https://pypi.python.org/pypi/gocardless_pro -GoCardless Pro Python client library +GoCardless Python client library ============================================ -A Python client for interacting with the GoCardless Pro API. +A Python client for interacting with the GoCardless API. |pypi-badge| @@ -62,10 +62,10 @@ Rate limit response headers can be read: .. code:: python - # Note these properties will be None until you make an API request with the client - client.rate_limit.limit - client.rate_limit.remaining - client.rate_limit.reset + # Note these values will be None until you make an API request with the client + client.rate_limit["ratelimit-limit"] + client.rate_limit["ratelimit-remaining"] + client.rate_limit["ratelimit-reset"] For full documentation, see our `API reference`_. diff --git a/gocardless_pro/__init__.py b/gocardless_pro/__init__.py index f177d151..0531f9d1 100644 --- a/gocardless_pro/__init__.py +++ b/gocardless_pro/__init__.py @@ -1,6 +1,6 @@ -"""A client library for the GoCardless Pro API.""" +"""A client library for the GoCardless API.""" from .client import Client -__version__ = '3.3.0' +__version__ = '3.4.0' diff --git a/gocardless_pro/api_client.py b/gocardless_pro/api_client.py index 27c52e11..4d89cd42 100644 --- a/gocardless_pro/api_client.py +++ b/gocardless_pro/api_client.py @@ -172,7 +172,7 @@ def _default_headers(self): 'Authorization': 'Bearer {0}'.format(self.access_token), 'Content-Type': 'application/json', 'GoCardless-Client-Library': 'gocardless-pro-python', - 'GoCardless-Client-Version': '3.3.0', + 'GoCardless-Client-Version': '3.4.0', 'User-Agent': self._user_agent(), 'GoCardless-Version': '2015-07-06', } @@ -181,7 +181,7 @@ def _user_agent(self): python_version = '.'.join(platform.python_version_tuple()[0:2]) vm_version = '{}.{}.{}-{}{}'.format(*sys.version_info) return ' '.join([ - 'gocardless-pro-python/3.3.0', + 'gocardless-pro-python/3.4.0', 'python/{0}'.format(python_version), '{0}/{1}'.format(platform.python_implementation(), vm_version), '{0}/{1}'.format(platform.system(), platform.release()), diff --git a/gocardless_pro/api_response.py b/gocardless_pro/api_response.py index bf573fe6..97922f4a 100644 --- a/gocardless_pro/api_response.py +++ b/gocardless_pro/api_response.py @@ -4,7 +4,7 @@ # class ApiResponse(object): - """Response from the {{ .Config.api_name }} API, providing access + """Response from the GoCardless API, providing access to the status code, headers, and body. """ @@ -21,5 +21,8 @@ def headers(self): @property def body(self): + # Handle 204 No Content and other empty responses + if not self._response.content: + return {} return self._response.json() diff --git a/gocardless_pro/client.py b/gocardless_pro/client.py index 5f0a45f6..ef5d3ec2 100644 --- a/gocardless_pro/client.py +++ b/gocardless_pro/client.py @@ -7,7 +7,7 @@ from .api_client import ApiClient class Client(object): - """Client for interacting with the GoCardless Pro API. + """Client for interacting with the GoCardless API. Instantiate a client object with your access token and environment, then use the resource methods to access the API. @@ -23,7 +23,7 @@ class Client(object): Example: client = Client(access_token=ACCESS_TOKEN, environment='sandbox') for customer in client.customers.list(): - print '{} {}'.format(customer.family_name, customer.given_name) + print('{} {}'.format(customer.family_name, customer.given_name)) """ def __init__(self, access_token=None, environment=None, base_url=None, raise_on_idempotency_conflict=False): diff --git a/gocardless_pro/list_response.py b/gocardless_pro/list_response.py index 1361b683..26890475 100644 --- a/gocardless_pro/list_response.py +++ b/gocardless_pro/list_response.py @@ -11,8 +11,10 @@ def __init__(self, records, api_response): @property def before(self): - return self.api_response.body['meta']['cursors']['before'] + cursors = (self.api_response.body.get('meta') or {}).get('cursors') or {} + return cursors.get('before') @property def after(self): - return self.api_response.body['meta']['cursors']['after'] + cursors = (self.api_response.body.get('meta') or {}).get('cursors') or {} + return cursors.get('after') diff --git a/gocardless_pro/resources/customer_bank_account.py b/gocardless_pro/resources/customer_bank_account.py index e7c06ade..6e7c400b 100644 --- a/gocardless_pro/resources/customer_bank_account.py +++ b/gocardless_pro/resources/customer_bank_account.py @@ -76,6 +76,11 @@ def metadata(self): return self.attributes.get('metadata') + @property + def trusted_recipient(self): + return self.attributes.get('trusted_recipient') + + @@ -112,3 +117,5 @@ def customer(self): + + diff --git a/gocardless_pro/services/balances_service.py b/gocardless_pro/services/balances_service.py index 0419863e..27805ccf 100644 --- a/gocardless_pro/services/balances_service.py +++ b/gocardless_pro/services/balances_service.py @@ -10,7 +10,7 @@ class BalancesService(base_service.BaseService): """Service class that provides access to the balances - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Balance diff --git a/gocardless_pro/services/bank_account_details_service.py b/gocardless_pro/services/bank_account_details_service.py index 1ca3577b..41bfcc95 100644 --- a/gocardless_pro/services/bank_account_details_service.py +++ b/gocardless_pro/services/bank_account_details_service.py @@ -10,7 +10,7 @@ class BankAccountDetailsService(base_service.BaseService): """Service class that provides access to the bank_account_details - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.BankAccountDetail diff --git a/gocardless_pro/services/bank_account_holder_verifications_service.py b/gocardless_pro/services/bank_account_holder_verifications_service.py index 31fc5d70..7cec192d 100644 --- a/gocardless_pro/services/bank_account_holder_verifications_service.py +++ b/gocardless_pro/services/bank_account_holder_verifications_service.py @@ -10,7 +10,7 @@ class BankAccountHolderVerificationsService(base_service.BaseService): """Service class that provides access to the bank_account_holder_verifications - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.BankAccountHolderVerification diff --git a/gocardless_pro/services/bank_authorisations_service.py b/gocardless_pro/services/bank_authorisations_service.py index d6009cd5..1d084fd3 100644 --- a/gocardless_pro/services/bank_authorisations_service.py +++ b/gocardless_pro/services/bank_authorisations_service.py @@ -10,7 +10,7 @@ class BankAuthorisationsService(base_service.BaseService): """Service class that provides access to the bank_authorisations - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.BankAuthorisation diff --git a/gocardless_pro/services/bank_details_lookups_service.py b/gocardless_pro/services/bank_details_lookups_service.py index 41cf2327..59feb1e1 100644 --- a/gocardless_pro/services/bank_details_lookups_service.py +++ b/gocardless_pro/services/bank_details_lookups_service.py @@ -10,7 +10,7 @@ class BankDetailsLookupsService(base_service.BaseService): """Service class that provides access to the bank_details_lookups - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.BankDetailsLookup diff --git a/gocardless_pro/services/billing_request_flows_service.py b/gocardless_pro/services/billing_request_flows_service.py index 804a9e03..8f7a8e92 100644 --- a/gocardless_pro/services/billing_request_flows_service.py +++ b/gocardless_pro/services/billing_request_flows_service.py @@ -10,7 +10,7 @@ class BillingRequestFlowsService(base_service.BaseService): """Service class that provides access to the billing_request_flows - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.BillingRequestFlow diff --git a/gocardless_pro/services/billing_request_templates_service.py b/gocardless_pro/services/billing_request_templates_service.py index 7ed61982..31aa8272 100644 --- a/gocardless_pro/services/billing_request_templates_service.py +++ b/gocardless_pro/services/billing_request_templates_service.py @@ -10,7 +10,7 @@ class BillingRequestTemplatesService(base_service.BaseService): """Service class that provides access to the billing_request_templates - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.BillingRequestTemplate diff --git a/gocardless_pro/services/billing_request_with_actions_service.py b/gocardless_pro/services/billing_request_with_actions_service.py index 4d99bffc..5e6e254c 100644 --- a/gocardless_pro/services/billing_request_with_actions_service.py +++ b/gocardless_pro/services/billing_request_with_actions_service.py @@ -10,7 +10,7 @@ class BillingRequestWithActionsService(base_service.BaseService): """Service class that provides access to the billing_request_with_actions - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.BillingRequestWithAction diff --git a/gocardless_pro/services/billing_requests_service.py b/gocardless_pro/services/billing_requests_service.py index 2e94bbc9..693e7c41 100644 --- a/gocardless_pro/services/billing_requests_service.py +++ b/gocardless_pro/services/billing_requests_service.py @@ -10,7 +10,7 @@ class BillingRequestsService(base_service.BaseService): """Service class that provides access to the billing_requests - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.BillingRequest diff --git a/gocardless_pro/services/blocks_service.py b/gocardless_pro/services/blocks_service.py index 2b4cb044..31e1ba3b 100644 --- a/gocardless_pro/services/blocks_service.py +++ b/gocardless_pro/services/blocks_service.py @@ -10,7 +10,7 @@ class BlocksService(base_service.BaseService): """Service class that provides access to the blocks - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Block diff --git a/gocardless_pro/services/creditor_bank_accounts_service.py b/gocardless_pro/services/creditor_bank_accounts_service.py index 9237e510..954aff16 100644 --- a/gocardless_pro/services/creditor_bank_accounts_service.py +++ b/gocardless_pro/services/creditor_bank_accounts_service.py @@ -10,7 +10,7 @@ class CreditorBankAccountsService(base_service.BaseService): """Service class that provides access to the creditor_bank_accounts - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.CreditorBankAccount diff --git a/gocardless_pro/services/creditors_service.py b/gocardless_pro/services/creditors_service.py index 02b2ac95..68d3896b 100644 --- a/gocardless_pro/services/creditors_service.py +++ b/gocardless_pro/services/creditors_service.py @@ -10,7 +10,7 @@ class CreditorsService(base_service.BaseService): """Service class that provides access to the creditors - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Creditor diff --git a/gocardless_pro/services/currency_exchange_rates_service.py b/gocardless_pro/services/currency_exchange_rates_service.py index 337b30ba..36d0d7b0 100644 --- a/gocardless_pro/services/currency_exchange_rates_service.py +++ b/gocardless_pro/services/currency_exchange_rates_service.py @@ -10,7 +10,7 @@ class CurrencyExchangeRatesService(base_service.BaseService): """Service class that provides access to the currency_exchange_rates - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.CurrencyExchangeRate diff --git a/gocardless_pro/services/customer_bank_accounts_service.py b/gocardless_pro/services/customer_bank_accounts_service.py index 2dbec878..9d9ba1bc 100644 --- a/gocardless_pro/services/customer_bank_accounts_service.py +++ b/gocardless_pro/services/customer_bank_accounts_service.py @@ -10,7 +10,7 @@ class CustomerBankAccountsService(base_service.BaseService): """Service class that provides access to the customer_bank_accounts - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.CustomerBankAccount diff --git a/gocardless_pro/services/customer_notifications_service.py b/gocardless_pro/services/customer_notifications_service.py index 3e59af4d..7d747ed7 100644 --- a/gocardless_pro/services/customer_notifications_service.py +++ b/gocardless_pro/services/customer_notifications_service.py @@ -10,7 +10,7 @@ class CustomerNotificationsService(base_service.BaseService): """Service class that provides access to the customer_notifications - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.CustomerNotification diff --git a/gocardless_pro/services/customers_service.py b/gocardless_pro/services/customers_service.py index 1c8e1bc3..50c7f7bc 100644 --- a/gocardless_pro/services/customers_service.py +++ b/gocardless_pro/services/customers_service.py @@ -10,7 +10,7 @@ class CustomersService(base_service.BaseService): """Service class that provides access to the customers - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Customer diff --git a/gocardless_pro/services/events_service.py b/gocardless_pro/services/events_service.py index db8a9487..a136a990 100644 --- a/gocardless_pro/services/events_service.py +++ b/gocardless_pro/services/events_service.py @@ -10,7 +10,7 @@ class EventsService(base_service.BaseService): """Service class that provides access to the events - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Event diff --git a/gocardless_pro/services/exports_service.py b/gocardless_pro/services/exports_service.py index aadbcd1c..af67e0bc 100644 --- a/gocardless_pro/services/exports_service.py +++ b/gocardless_pro/services/exports_service.py @@ -10,7 +10,7 @@ class ExportsService(base_service.BaseService): """Service class that provides access to the exports - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Export diff --git a/gocardless_pro/services/funds_availabilities_service.py b/gocardless_pro/services/funds_availabilities_service.py index 72af7fdc..851700ef 100644 --- a/gocardless_pro/services/funds_availabilities_service.py +++ b/gocardless_pro/services/funds_availabilities_service.py @@ -10,7 +10,7 @@ class FundsAvailabilitiesService(base_service.BaseService): """Service class that provides access to the funds_availabilities - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.FundsAvailability diff --git a/gocardless_pro/services/instalment_schedules_service.py b/gocardless_pro/services/instalment_schedules_service.py index 98bd28d0..e5ebcaf3 100644 --- a/gocardless_pro/services/instalment_schedules_service.py +++ b/gocardless_pro/services/instalment_schedules_service.py @@ -10,7 +10,7 @@ class InstalmentSchedulesService(base_service.BaseService): """Service class that provides access to the instalment_schedules - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.InstalmentSchedule diff --git a/gocardless_pro/services/institutions_service.py b/gocardless_pro/services/institutions_service.py index 8e691eea..25abbd4a 100644 --- a/gocardless_pro/services/institutions_service.py +++ b/gocardless_pro/services/institutions_service.py @@ -10,7 +10,7 @@ class InstitutionsService(base_service.BaseService): """Service class that provides access to the institutions - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Institution diff --git a/gocardless_pro/services/logos_service.py b/gocardless_pro/services/logos_service.py index 2607ff7b..b8de6331 100644 --- a/gocardless_pro/services/logos_service.py +++ b/gocardless_pro/services/logos_service.py @@ -10,7 +10,7 @@ class LogosService(base_service.BaseService): """Service class that provides access to the logos - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Logo diff --git a/gocardless_pro/services/mandate_import_entries_service.py b/gocardless_pro/services/mandate_import_entries_service.py index 145aa64c..f4aa8abb 100644 --- a/gocardless_pro/services/mandate_import_entries_service.py +++ b/gocardless_pro/services/mandate_import_entries_service.py @@ -10,7 +10,7 @@ class MandateImportEntriesService(base_service.BaseService): """Service class that provides access to the mandate_import_entries - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.MandateImportEntry diff --git a/gocardless_pro/services/mandate_imports_service.py b/gocardless_pro/services/mandate_imports_service.py index 58b863de..13e53eac 100644 --- a/gocardless_pro/services/mandate_imports_service.py +++ b/gocardless_pro/services/mandate_imports_service.py @@ -10,7 +10,7 @@ class MandateImportsService(base_service.BaseService): """Service class that provides access to the mandate_imports - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.MandateImport diff --git a/gocardless_pro/services/mandate_pdfs_service.py b/gocardless_pro/services/mandate_pdfs_service.py index 7cbfb933..ea963a4d 100644 --- a/gocardless_pro/services/mandate_pdfs_service.py +++ b/gocardless_pro/services/mandate_pdfs_service.py @@ -10,7 +10,7 @@ class MandatePdfsService(base_service.BaseService): """Service class that provides access to the mandate_pdfs - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.MandatePdf diff --git a/gocardless_pro/services/mandates_service.py b/gocardless_pro/services/mandates_service.py index 30bb173d..1366565c 100644 --- a/gocardless_pro/services/mandates_service.py +++ b/gocardless_pro/services/mandates_service.py @@ -10,7 +10,7 @@ class MandatesService(base_service.BaseService): """Service class that provides access to the mandates - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Mandate diff --git a/gocardless_pro/services/negative_balance_limits_service.py b/gocardless_pro/services/negative_balance_limits_service.py index d4d51061..c81fda81 100644 --- a/gocardless_pro/services/negative_balance_limits_service.py +++ b/gocardless_pro/services/negative_balance_limits_service.py @@ -10,7 +10,7 @@ class NegativeBalanceLimitsService(base_service.BaseService): """Service class that provides access to the negative_balance_limits - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.NegativeBalanceLimit diff --git a/gocardless_pro/services/outbound_payment_import_entries_service.py b/gocardless_pro/services/outbound_payment_import_entries_service.py index 80fcf9e2..c14fa3f8 100644 --- a/gocardless_pro/services/outbound_payment_import_entries_service.py +++ b/gocardless_pro/services/outbound_payment_import_entries_service.py @@ -10,7 +10,7 @@ class OutboundPaymentImportEntriesService(base_service.BaseService): """Service class that provides access to the outbound_payment_import_entries - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.OutboundPaymentImportEntry diff --git a/gocardless_pro/services/outbound_payment_imports_service.py b/gocardless_pro/services/outbound_payment_imports_service.py index 07ed3ce2..377df1c7 100644 --- a/gocardless_pro/services/outbound_payment_imports_service.py +++ b/gocardless_pro/services/outbound_payment_imports_service.py @@ -10,7 +10,7 @@ class OutboundPaymentImportsService(base_service.BaseService): """Service class that provides access to the outbound_payment_imports - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.OutboundPaymentImport diff --git a/gocardless_pro/services/outbound_payments_service.py b/gocardless_pro/services/outbound_payments_service.py index 1ecaa82e..f332d8f8 100644 --- a/gocardless_pro/services/outbound_payments_service.py +++ b/gocardless_pro/services/outbound_payments_service.py @@ -10,7 +10,7 @@ class OutboundPaymentsService(base_service.BaseService): """Service class that provides access to the outbound_payments - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.OutboundPayment diff --git a/gocardless_pro/services/payer_authorisations_service.py b/gocardless_pro/services/payer_authorisations_service.py index d99f0988..333b3d5e 100644 --- a/gocardless_pro/services/payer_authorisations_service.py +++ b/gocardless_pro/services/payer_authorisations_service.py @@ -10,7 +10,7 @@ class PayerAuthorisationsService(base_service.BaseService): """Service class that provides access to the payer_authorisations - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.PayerAuthorisation diff --git a/gocardless_pro/services/payer_themes_service.py b/gocardless_pro/services/payer_themes_service.py index e887fd2d..cff75ee7 100644 --- a/gocardless_pro/services/payer_themes_service.py +++ b/gocardless_pro/services/payer_themes_service.py @@ -10,7 +10,7 @@ class PayerThemesService(base_service.BaseService): """Service class that provides access to the payer_themes - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.PayerTheme diff --git a/gocardless_pro/services/payment_account_transactions_service.py b/gocardless_pro/services/payment_account_transactions_service.py index 76128dbf..9d62b184 100644 --- a/gocardless_pro/services/payment_account_transactions_service.py +++ b/gocardless_pro/services/payment_account_transactions_service.py @@ -10,7 +10,7 @@ class PaymentAccountTransactionsService(base_service.BaseService): """Service class that provides access to the payment_account_transactions - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.PaymentAccountTransaction diff --git a/gocardless_pro/services/payment_accounts_service.py b/gocardless_pro/services/payment_accounts_service.py index 54681940..ecf7c4c7 100644 --- a/gocardless_pro/services/payment_accounts_service.py +++ b/gocardless_pro/services/payment_accounts_service.py @@ -10,7 +10,7 @@ class PaymentAccountsService(base_service.BaseService): """Service class that provides access to the payment_accounts - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.PaymentAccount diff --git a/gocardless_pro/services/payments_service.py b/gocardless_pro/services/payments_service.py index c84c69ab..bd2f7287 100644 --- a/gocardless_pro/services/payments_service.py +++ b/gocardless_pro/services/payments_service.py @@ -10,7 +10,7 @@ class PaymentsService(base_service.BaseService): """Service class that provides access to the payments - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Payment diff --git a/gocardless_pro/services/payout_items_service.py b/gocardless_pro/services/payout_items_service.py index 47bc979f..a7b714fa 100644 --- a/gocardless_pro/services/payout_items_service.py +++ b/gocardless_pro/services/payout_items_service.py @@ -10,7 +10,7 @@ class PayoutItemsService(base_service.BaseService): """Service class that provides access to the payout_items - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.PayoutItem diff --git a/gocardless_pro/services/payouts_service.py b/gocardless_pro/services/payouts_service.py index 50bcb720..443d0758 100644 --- a/gocardless_pro/services/payouts_service.py +++ b/gocardless_pro/services/payouts_service.py @@ -10,7 +10,7 @@ class PayoutsService(base_service.BaseService): """Service class that provides access to the payouts - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Payout diff --git a/gocardless_pro/services/redirect_flows_service.py b/gocardless_pro/services/redirect_flows_service.py index 73f98d66..2ffca91f 100644 --- a/gocardless_pro/services/redirect_flows_service.py +++ b/gocardless_pro/services/redirect_flows_service.py @@ -10,7 +10,7 @@ class RedirectFlowsService(base_service.BaseService): """Service class that provides access to the redirect_flows - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.RedirectFlow diff --git a/gocardless_pro/services/refunds_service.py b/gocardless_pro/services/refunds_service.py index 19d4d0e6..852ef4e5 100644 --- a/gocardless_pro/services/refunds_service.py +++ b/gocardless_pro/services/refunds_service.py @@ -10,7 +10,7 @@ class RefundsService(base_service.BaseService): """Service class that provides access to the refunds - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Refund diff --git a/gocardless_pro/services/scenario_simulators_service.py b/gocardless_pro/services/scenario_simulators_service.py index 6f40aa21..c54ce028 100644 --- a/gocardless_pro/services/scenario_simulators_service.py +++ b/gocardless_pro/services/scenario_simulators_service.py @@ -10,7 +10,7 @@ class ScenarioSimulatorsService(base_service.BaseService): """Service class that provides access to the scenario_simulators - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.ScenarioSimulator diff --git a/gocardless_pro/services/scheme_identifiers_service.py b/gocardless_pro/services/scheme_identifiers_service.py index 2dc11a46..7b555554 100644 --- a/gocardless_pro/services/scheme_identifiers_service.py +++ b/gocardless_pro/services/scheme_identifiers_service.py @@ -10,7 +10,7 @@ class SchemeIdentifiersService(base_service.BaseService): """Service class that provides access to the scheme_identifiers - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.SchemeIdentifier diff --git a/gocardless_pro/services/subscriptions_service.py b/gocardless_pro/services/subscriptions_service.py index dff2cc57..076c1f6e 100644 --- a/gocardless_pro/services/subscriptions_service.py +++ b/gocardless_pro/services/subscriptions_service.py @@ -10,7 +10,7 @@ class SubscriptionsService(base_service.BaseService): """Service class that provides access to the subscriptions - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Subscription diff --git a/gocardless_pro/services/tax_rates_service.py b/gocardless_pro/services/tax_rates_service.py index 20b7842c..877ea07a 100644 --- a/gocardless_pro/services/tax_rates_service.py +++ b/gocardless_pro/services/tax_rates_service.py @@ -10,7 +10,7 @@ class TaxRatesService(base_service.BaseService): """Service class that provides access to the tax_rates - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.TaxRate diff --git a/gocardless_pro/services/transferred_mandates_service.py b/gocardless_pro/services/transferred_mandates_service.py index 0584df7a..6ea689e7 100644 --- a/gocardless_pro/services/transferred_mandates_service.py +++ b/gocardless_pro/services/transferred_mandates_service.py @@ -10,7 +10,7 @@ class TransferredMandatesService(base_service.BaseService): """Service class that provides access to the transferred_mandates - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.TransferredMandate diff --git a/gocardless_pro/services/verification_details_service.py b/gocardless_pro/services/verification_details_service.py index 4ea95e7e..b030c6b0 100644 --- a/gocardless_pro/services/verification_details_service.py +++ b/gocardless_pro/services/verification_details_service.py @@ -10,7 +10,7 @@ class VerificationDetailsService(base_service.BaseService): """Service class that provides access to the verification_details - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.VerificationDetail diff --git a/gocardless_pro/services/webhooks_service.py b/gocardless_pro/services/webhooks_service.py index 1fed0f12..e809b7eb 100644 --- a/gocardless_pro/services/webhooks_service.py +++ b/gocardless_pro/services/webhooks_service.py @@ -10,7 +10,7 @@ class WebhooksService(base_service.BaseService): """Service class that provides access to the webhooks - endpoints of the GoCardless Pro API. + endpoints of the GoCardless API. """ RESOURCE_CLASS = resources.Webhook diff --git a/gocardless_pro/webhooks.py b/gocardless_pro/webhooks.py index 35900b28..44cbf7fc 100644 --- a/gocardless_pro/webhooks.py +++ b/gocardless_pro/webhooks.py @@ -1,22 +1,10 @@ import json import hmac import hashlib -import sys from gocardless_pro.resources.event import Event from gocardless_pro.errors import InvalidSignatureError -# Python 3+ does not have the basestring type, so we alias it -try: - basestring -except: - basestring = str - -# Python 3.0 < x < 3.4 does not support handing a mutable bytearray -# to the hmac constructor, so we need to make a record of it ... -SUPPORTS_BYTEARRAY = sys.version_info[0] == 2 or \ - sys.version_info[1] > 3 - def parse(body, webhook_secret, signature_header): _verify_signature(body, webhook_secret, signature_header) @@ -34,15 +22,9 @@ def _verify_signature(body, key, expected_signature): raise InvalidSignatureError() def to_bytes(string): - if isinstance(string, basestring): - if SUPPORTS_BYTEARRAY: - return bytearray(string, 'utf-8') - return bytes(string, 'utf-8') - - if SUPPORTS_BYTEARRAY: - return string - - return bytes(string) + if isinstance(string, str): + return bytearray(string, 'utf-8') + return string def to_string(byte_sequence): if isinstance(byte_sequence, bytearray): diff --git a/setup.py b/setup.py index d644e047..18c422f9 100644 --- a/setup.py +++ b/setup.py @@ -6,12 +6,13 @@ setup( name = 'gocardless_pro', - version = '3.3.0', + version = '3.4.0', packages = find_packages(exclude=['tests']), - install_requires = ['requests>=2.6', 'six'], + install_requires = ['requests>=2.6'], + python_requires = '>=3.6', author = 'GoCardless', author_email = 'engineering@gocardless.com', - description = 'A client library for the GoCardless Pro API.', + description = 'A client library for the GoCardless API.', long_description = long_description, license = 'MIT', keywords = 'gocardless directdebit payments sepa bacs', diff --git a/tests/api_client_test.py b/tests/api_client_test.py index 53509d3e..e719da04 100644 --- a/tests/api_client_test.py +++ b/tests/api_client_test.py @@ -144,6 +144,16 @@ def test_handles_valid_empty_response(): client.delete('/test', body={'name': 'Billy Jean'}) assert responses.calls[0].request.body == '{"name": "Billy Jean"}' +@responses.activate +def test_handles_204_no_content_without_error(): + from gocardless_pro.api_response import ApiResponse + + responses.add(responses.DELETE, 'http://example.com/customers/CU123', body='', status=204) + response = client.delete('/customers/CU123', body={}) + + api_response = ApiResponse(response) + assert api_response.status_code == 204 + assert api_response.body == {} @responses.activate def test_handles_invalid_empty_response(): diff --git a/tests/client_test.py b/tests/client_test.py index 2bf4e80f..a51d5af2 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -155,3 +155,21 @@ def test_verification_details_returns_service(): def test_webhooks_returns_service(): assert isinstance(client.webhooks, services.WebhooksService) +@responses.activate +def test_rate_limit_returns_dict(): + headers = { + 'ratelimit-limit': '1000', + 'ratelimit-remaining': '163', + 'ratelimit-reset': 'Thu, 03 May 2018 16:00:00 GMT' + } + responses.add(responses.GET, 'http://example.com/test', body='{}', headers=headers) + + # Make a request to populate rate limit headers + client._api_client.get('/test') + + # Test that rate_limit returns a dictionary with correct keys and values + rate_limit_dict = client.rate_limit + assert isinstance(rate_limit_dict, dict) + assert rate_limit_dict["ratelimit-limit"] == 1000 + assert rate_limit_dict["ratelimit-remaining"] == 163 + assert rate_limit_dict["ratelimit-reset"] == 'Thu, 03 May 2018 16:00:00 GMT' diff --git a/tests/fixtures/customer_bank_accounts.json b/tests/fixtures/customer_bank_accounts.json index 4ea7e1e8..1c0beef5 100644 --- a/tests/fixtures/customer_bank_accounts.json +++ b/tests/fixtures/customer_bank_accounts.json @@ -5,30 +5,30 @@ "method": "POST", "path_template": "/customer_bank_accounts", "url_params": [], - "body": {"customer_bank_accounts":{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 101"},"metadata":{}}} + "body": {"customer_bank_accounts":{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 101"},"metadata":{},"trusted_recipient":true}} }, "list": { "method": "GET", "path_template": "/customer_bank_accounts", "url_params": [], - "body": {"customer_bank_accounts":[{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 102"},"metadata":{}},{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 103"},"metadata":{}}],"meta":{"cursors":{"after":"example after 101","before":"example before 101"},"limit":50}} + "body": {"customer_bank_accounts":[{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 102"},"metadata":{},"trusted_recipient":true},{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 103"},"metadata":{},"trusted_recipient":true}],"meta":{"cursors":{"after":"example after 101","before":"example before 101"},"limit":50}} }, "get": { "method": "GET", "path_template": "/customer_bank_accounts/:identity", "url_params": ["BA123"], - "body": {"customer_bank_accounts":{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 104"},"metadata":{}}} + "body": {"customer_bank_accounts":{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 104"},"metadata":{},"trusted_recipient":true}} }, "update": { "method": "PUT", "path_template": "/customer_bank_accounts/:identity", "url_params": ["BA123"], - "body": {"customer_bank_accounts":{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 105"},"metadata":{}}} + "body": {"customer_bank_accounts":{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 105"},"metadata":{},"trusted_recipient":true}} }, "disable": { "method": "POST", "path_template": "/customer_bank_accounts/:identity/actions/disable", "url_params": ["BA123"], - "body": {"customer_bank_accounts":{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 106"},"metadata":{}}} + "body": {"customer_bank_accounts":{"account_holder_name":"Billie Jean","account_number_ending":"1234","account_type":"savings","bank_account_token":"bat_f975ab3c-ecee-47a1-9590-5bb1d56fa113","bank_name":"BARCLAYS BANK PLC","country_code":"GB","created_at":"2014-01-01T12:00:00.000Z","currency":"EUR","enabled":true,"id":"BA123","links":{"customer":"example customer 106"},"metadata":{},"trusted_recipient":true}} } } diff --git a/tests/integration/customer_bank_accounts_integration_test.py b/tests/integration/customer_bank_accounts_integration_test.py index 254748b5..0a2de313 100644 --- a/tests/integration/customer_bank_accounts_integration_test.py +++ b/tests/integration/customer_bank_accounts_integration_test.py @@ -38,6 +38,7 @@ def test_customer_bank_accounts_create(): assert response.enabled == body.get('enabled') assert response.id == body.get('id') assert response.metadata == body.get('metadata') + assert response.trusted_recipient == body.get('trusted_recipient') assert response.links.customer == body.get('links')['customer'] @responses.activate @@ -103,6 +104,7 @@ def test_customer_bank_accounts_list(): assert [r.enabled for r in response.records] == [b.get('enabled') for b in body] assert [r.id for r in response.records] == [b.get('id') for b in body] assert [r.metadata for r in response.records] == [b.get('metadata') for b in body] + assert [r.trusted_recipient for r in response.records] == [b.get('trusted_recipient') for b in body] @responses.activate def test_timeout_customer_bank_accounts_list_retries(): @@ -173,6 +175,7 @@ def test_customer_bank_accounts_get(): assert response.enabled == body.get('enabled') assert response.id == body.get('id') assert response.metadata == body.get('metadata') + assert response.trusted_recipient == body.get('trusted_recipient') assert response.links.customer == body.get('links')['customer'] @responses.activate @@ -217,6 +220,7 @@ def test_customer_bank_accounts_update(): assert response.enabled == body.get('enabled') assert response.id == body.get('id') assert response.metadata == body.get('metadata') + assert response.trusted_recipient == body.get('trusted_recipient') assert response.links.customer == body.get('links')['customer'] @responses.activate @@ -261,6 +265,7 @@ def test_customer_bank_accounts_disable(): assert response.enabled == body.get('enabled') assert response.id == body.get('id') assert response.metadata == body.get('metadata') + assert response.trusted_recipient == body.get('trusted_recipient') assert response.links.customer == body.get('links')['customer'] def test_timeout_customer_bank_accounts_disable_doesnt_retry(): diff --git a/tests/paginator_test.py b/tests/paginator_test.py new file mode 100644 index 00000000..89acc434 --- /dev/null +++ b/tests/paginator_test.py @@ -0,0 +1,172 @@ +# WARNING: Do not edit by hand, this file was generated by Crank: +# +# https://github.com/gocardless/crank +# + +from unittest.mock import Mock + +import pytest + +from gocardless_pro.paginator import Paginator +from gocardless_pro.list_response import ListResponse +from gocardless_pro.api_response import ApiResponse + + +class TestPaginator: + def test_iterates_through_single_page(self): + """Test that paginator iterates through all records in a single page""" + # Setup mock records + record1 = Mock(id='record_1') + record2 = Mock(id='record_2') + record3 = Mock(id='record_3') + + # Setup mock service + service = Mock() + api_response = Mock(spec=ApiResponse) + api_response.body = {'meta': {'cursors': {'after': None, 'before': None}}} + list_response = ListResponse( + records=[record1, record2, record3], + api_response=api_response + ) + service.list.return_value = list_response + + # Create paginator and collect results + paginator = Paginator(service, {'limit': 50}) + results = list(paginator) + + # Verify + assert len(results) == 3 + assert results[0] == record1 + assert results[1] == record2 + assert results[2] == record3 + service.list.assert_called_once_with(params={'limit': 50}) + + def test_iterates_through_multiple_pages(self): + """Test that paginator fetches and iterates through multiple pages""" + # Setup mock records for two pages + page1_records = [Mock(id='record_1'), Mock(id='record_2')] + page2_records = [Mock(id='record_3'), Mock(id='record_4')] + + # Setup mock service with different responses + service = Mock() + + # First page response (has 'after' cursor) + api_response1 = Mock(spec=ApiResponse) + api_response1.body = {'meta': {'cursors': {'after': 'cursor_123', 'before': None}}} + list_response1 = ListResponse( + records=page1_records, + api_response=api_response1 + ) + + # Second page response (no 'after' cursor - last page) + api_response2 = Mock(spec=ApiResponse) + api_response2.body = {'meta': {'cursors': {'after': None, 'before': 'cursor_123'}}} + list_response2 = ListResponse( + records=page2_records, + api_response=api_response2 + ) + + service.list.side_effect = [list_response1, list_response2] + + # Create paginator and collect results + paginator = Paginator(service, {'limit': 2}) + results = list(paginator) + + # Verify all records from both pages + assert len(results) == 4 + assert results[0].id == 'record_1' + assert results[1].id == 'record_2' + assert results[2].id == 'record_3' + assert results[3].id == 'record_4' + + # Verify service was called twice + assert service.list.call_count == 2 + + # Verify first call had no 'after' param + first_call_kwargs = service.list.call_args_list[0][1] + assert first_call_kwargs == {'params': {'limit': 2}} + + # Verify second call included 'after' cursor + second_call_kwargs = service.list.call_args_list[1][1] + assert second_call_kwargs == {'params': {'limit': 2, 'after': 'cursor_123'}} + + def test_preserves_original_params_dict(self): + """Test that the original params dict is not modified during pagination""" + service = Mock() + + # Setup two pages + api_response1 = Mock(spec=ApiResponse) + api_response1.body = {'meta': {'cursors': {'after': 'cursor_abc', 'before': None}}} + list_response1 = ListResponse(records=[Mock()], api_response=api_response1) + + api_response2 = Mock(spec=ApiResponse) + api_response2.body = {'meta': {'cursors': {'after': None, 'before': 'cursor_abc'}}} + list_response2 = ListResponse(records=[Mock()], api_response=api_response2) + + service.list.side_effect = [list_response1, list_response2] + + # Create paginator with params dict + original_params = {'limit': 10, 'status': 'active'} + paginator = Paginator(service, original_params) + list(paginator) + + # Verify original params dict wasn't modified + assert original_params == {'limit': 10, 'status': 'active'} + assert 'after' not in original_params + + def test_handles_empty_result_set(self): + """Test that paginator handles empty result sets gracefully""" + service = Mock() + api_response = Mock(spec=ApiResponse) + api_response.body = {'meta': {'cursors': {'after': None, 'before': None}}} + list_response = ListResponse(records=[], api_response=api_response) + service.list.return_value = list_response + + paginator = Paginator(service, {}) + results = list(paginator) + + assert len(results) == 0 + service.list.assert_called_once_with(params={}) + + def test_can_iterate_multiple_times(self): + """Test that paginator can be iterated multiple times (creates new iterator each time)""" + # Setup mock service + service = Mock() + api_response = Mock(spec=ApiResponse) + api_response.body = {'meta': {'cursors': {'after': None, 'before': None}}} + records = [Mock(id='record_1'), Mock(id='record_2')] + list_response = ListResponse(records=records, api_response=api_response) + service.list.return_value = list_response + + paginator = Paginator(service, {}) + + # First iteration + results1 = list(paginator) + assert len(results1) == 2 + + # Second iteration + results2 = list(paginator) + assert len(results2) == 2 + + # Verify service was called twice (once per iteration) + assert service.list.call_count == 2 + + def test_handles_missing_cursors_in_metadata(self): + """Test that paginator handles responses with missing cursor metadata""" + service = Mock() + + # Response with no cursors key + api_response = Mock(spec=ApiResponse) + api_response.body = {'meta': {}} + list_response = ListResponse( + records=[Mock(id='record_1')], + api_response=api_response + ) + service.list.return_value = list_response + + paginator = Paginator(service, {}) + results = list(paginator) + + # Should still work and return the records + assert len(results) == 1 + assert results[0].id == 'record_1'