Initial Python SDK release v1.0.0
This commit is contained in:
158
README.md
Normal file
158
README.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# FetcherPay Python SDK
|
||||
|
||||
Official Python SDK for the FetcherPay API — One API. Every Rail.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install fetcherpay
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from fetcherpay import FetcherPay
|
||||
|
||||
client = FetcherPay(
|
||||
api_key='fp_test_your_key',
|
||||
environment='sandbox' # or 'production'
|
||||
)
|
||||
|
||||
# Create a payment
|
||||
payment = client.payments.create(
|
||||
amount=10000, # $100.00 in cents
|
||||
currency='USD',
|
||||
source={'payment_method_id': 'pm_123'},
|
||||
destination={'payment_method_id': 'pm_456'},
|
||||
rail='auto' # Auto-select optimal rail
|
||||
)
|
||||
|
||||
print(payment['id'], payment['status'])
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `api_key` | str | required | Your FetcherPay API key |
|
||||
| `environment` | str | 'sandbox' | 'sandbox' or 'production' |
|
||||
| `base_url` | str | auto | Override base URL |
|
||||
| `timeout` | int | 30 | Request timeout (seconds) |
|
||||
|
||||
## API Reference
|
||||
|
||||
### Payments
|
||||
|
||||
```python
|
||||
# Create payment
|
||||
payment = client.payments.create(
|
||||
amount=10000,
|
||||
source={'payment_method_id': 'pm_123'},
|
||||
destination={'payment_method_id': 'pm_456'},
|
||||
idempotency_key='unique-key'
|
||||
)
|
||||
|
||||
# Retrieve payment
|
||||
payment = client.payments.retrieve('pay_xxx')
|
||||
|
||||
# List payments
|
||||
payments = client.payments.list(limit=10)
|
||||
|
||||
# Cancel payment
|
||||
client.payments.cancel('pay_xxx', reason='Customer request')
|
||||
|
||||
# Refund payment
|
||||
client.payments.refund('pay_xxx', amount=5000, reason='Partial refund')
|
||||
```
|
||||
|
||||
### Ledger
|
||||
|
||||
```python
|
||||
# List accounts
|
||||
accounts = client.ledger.list_accounts()
|
||||
|
||||
# Get account balance
|
||||
account = client.ledger.retrieve_account('la_xxx')
|
||||
print(account['balance']['available'])
|
||||
|
||||
# List entries
|
||||
entries = client.ledger.list_entries(account_id='la_xxx')
|
||||
```
|
||||
|
||||
### Payment Methods
|
||||
|
||||
```python
|
||||
# Create bank account
|
||||
pm = client.payment_methods.create(
|
||||
type='bank_account',
|
||||
bank_account={
|
||||
'account_number': '000123456789',
|
||||
'routing_number': '011000015',
|
||||
'account_type': 'checking'
|
||||
}
|
||||
)
|
||||
|
||||
# List payment methods
|
||||
methods = client.payment_methods.list()
|
||||
```
|
||||
|
||||
### Webhooks
|
||||
|
||||
```python
|
||||
# Create webhook endpoint
|
||||
webhook = client.webhooks.create(
|
||||
url='https://your-app.com/webhooks',
|
||||
events=['payment.settled', 'payment.failed']
|
||||
)
|
||||
|
||||
# Verify webhook signature
|
||||
is_valid = client.verify_webhook_signature(
|
||||
payload,
|
||||
signature, # from X-FetcherPay-Signature header
|
||||
webhook['secret']
|
||||
)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
from fetcherpay import (
|
||||
FetcherPayError,
|
||||
AuthenticationError,
|
||||
ValidationError,
|
||||
NotFoundError
|
||||
)
|
||||
|
||||
try:
|
||||
client.payments.create(...)
|
||||
except AuthenticationError:
|
||||
print('Invalid API key')
|
||||
except ValidationError as e:
|
||||
print(f'Validation failed: {e.param}')
|
||||
except FetcherPayError as e:
|
||||
print(f'API error: {e.type} ({e.status_code})')
|
||||
```
|
||||
|
||||
## Webhook Verification
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
# In your webhook handler
|
||||
def handle_webhook(request):
|
||||
payload = request.body
|
||||
signature = request.headers.get('X-FetcherPay-Signature')
|
||||
secret = 'whsec_your_webhook_secret'
|
||||
|
||||
if client.verify_webhook_signature(payload, signature, secret):
|
||||
event = json.loads(payload)
|
||||
if event['type'] == 'payment.settled':
|
||||
handle_payment_settled(event['data'])
|
||||
return 'OK', 200
|
||||
else:
|
||||
return 'Invalid signature', 401
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
51
examples/basic.py
Normal file
51
examples/basic.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
FetcherPay Python SDK - Basic Example
|
||||
"""
|
||||
|
||||
import os
|
||||
from fetcherpay import FetcherPay
|
||||
|
||||
# Initialize client
|
||||
client = FetcherPay(
|
||||
api_key=os.environ.get('FETCHERPAY_API_KEY', 'sandbox'),
|
||||
environment='sandbox'
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Create a payment
|
||||
print('Creating payment...')
|
||||
payment = client.payments.create(
|
||||
amount=10000, # $100.00 in cents
|
||||
currency='USD',
|
||||
source={'payment_method_id': 'pm_bank_123'},
|
||||
destination={'payment_method_id': 'pm_merchant_456'},
|
||||
rail='auto'
|
||||
)
|
||||
print(f'Payment created: {payment["id"]} Status: {payment["status"]}')
|
||||
|
||||
# Retrieve the payment
|
||||
print('\nRetrieving payment...')
|
||||
retrieved = client.payments.retrieve(payment['id'])
|
||||
print(f'Retrieved: {retrieved["id"]} Timeline: {len(retrieved["timeline"])} events')
|
||||
|
||||
# List payments
|
||||
print('\nListing payments...')
|
||||
payments = client.payments.list(limit=5)
|
||||
print(f'Found {len(payments["data"])} payments')
|
||||
|
||||
# List ledger accounts
|
||||
print('\nListing ledger accounts...')
|
||||
accounts = client.ledger.list_accounts()
|
||||
print(f'Found {len(accounts["data"])} accounts')
|
||||
for acc in accounts['data']:
|
||||
print(f' - {acc["name"]}: ${acc["balance"]["available"] / 100} available')
|
||||
|
||||
except Exception as e:
|
||||
print(f'Error: {e}')
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
41
examples/webhook_verification.py
Normal file
41
examples/webhook_verification.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
FetcherPay Python SDK - Webhook Verification Example
|
||||
"""
|
||||
|
||||
import json
|
||||
from fetcherpay import FetcherPay
|
||||
|
||||
# Initialize client
|
||||
client = FetcherPay(
|
||||
api_key='sandbox',
|
||||
environment='sandbox'
|
||||
)
|
||||
|
||||
# Example webhook payload
|
||||
payload = json.dumps({
|
||||
'id': 'evt_123',
|
||||
'type': 'payment.settled',
|
||||
'created_at': '2026-02-18T20:00:00Z',
|
||||
'data': {
|
||||
'id': 'pay_456',
|
||||
'status': 'settled',
|
||||
'amount': 10000
|
||||
}
|
||||
})
|
||||
|
||||
# Your webhook secret from the dashboard
|
||||
webhook_secret = 'whsec_your_secret_here'
|
||||
|
||||
# Simulate receiving a webhook
|
||||
signature = 'sha256=...' # From X-FetcherPay-Signature header
|
||||
|
||||
# Verify the signature
|
||||
is_valid = client.verify_webhook_signature(payload, signature, webhook_secret)
|
||||
|
||||
if is_valid:
|
||||
print('✅ Webhook signature verified')
|
||||
# Process the webhook event
|
||||
event = json.loads(payload)
|
||||
print(f'Event type: {event["type"]}')
|
||||
else:
|
||||
print('❌ Invalid webhook signature')
|
||||
51
fetcherpay/__init__.py
Normal file
51
fetcherpay/__init__.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
FetcherPay Python SDK
|
||||
One API. Every Rail.
|
||||
|
||||
Example:
|
||||
>>> from fetcherpay import FetcherPay
|
||||
>>>
|
||||
>>> client = FetcherPay(
|
||||
... api_key='fp_test_your_key',
|
||||
... environment='sandbox'
|
||||
... )
|
||||
>>>
|
||||
>>> payment = client.payments.create(
|
||||
... amount=10000,
|
||||
... currency='USD',
|
||||
... source={'payment_method_id': 'pm_123'},
|
||||
... destination={'payment_method_id': 'pm_456'}
|
||||
... )
|
||||
>>> print(payment.id)
|
||||
"""
|
||||
|
||||
from .client import FetcherPay
|
||||
from .exceptions import (
|
||||
FetcherPayError,
|
||||
AuthenticationError,
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
)
|
||||
from .types import (
|
||||
Payment,
|
||||
PaymentMethod,
|
||||
LedgerAccount,
|
||||
LedgerEntry,
|
||||
WebhookEndpoint,
|
||||
CreatePaymentRequest,
|
||||
)
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__all__ = [
|
||||
'FetcherPay',
|
||||
'FetcherPayError',
|
||||
'AuthenticationError',
|
||||
'ValidationError',
|
||||
'NotFoundError',
|
||||
'Payment',
|
||||
'PaymentMethod',
|
||||
'LedgerAccount',
|
||||
'LedgerEntry',
|
||||
'WebhookEndpoint',
|
||||
'CreatePaymentRequest',
|
||||
]
|
||||
BIN
fetcherpay/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
fetcherpay/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
fetcherpay/__pycache__/client.cpython-311.pyc
Normal file
BIN
fetcherpay/__pycache__/client.cpython-311.pyc
Normal file
Binary file not shown.
BIN
fetcherpay/__pycache__/exceptions.cpython-311.pyc
Normal file
BIN
fetcherpay/__pycache__/exceptions.cpython-311.pyc
Normal file
Binary file not shown.
BIN
fetcherpay/__pycache__/types.cpython-311.pyc
Normal file
BIN
fetcherpay/__pycache__/types.cpython-311.pyc
Normal file
Binary file not shown.
1
fetcherpay/api/__init__.py
Normal file
1
fetcherpay/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API modules
|
||||
53
fetcherpay/api/ledger.py
Normal file
53
fetcherpay/api/ledger.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Ledger API
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class LedgerAPI:
|
||||
"""Ledger API client"""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
def list_accounts(
|
||||
self,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None,
|
||||
type: Optional[str] = None
|
||||
) -> dict:
|
||||
"""List ledger accounts"""
|
||||
params = {'limit': limit}
|
||||
if cursor:
|
||||
params['cursor'] = cursor
|
||||
if type:
|
||||
params['type'] = type
|
||||
|
||||
return self.client._request('GET', '/ledger/accounts', params=params)
|
||||
|
||||
def retrieve_account(self, account_id: str) -> dict:
|
||||
"""Retrieve a ledger account"""
|
||||
return self.client._request('GET', f'/ledger/accounts/{account_id}')
|
||||
|
||||
def list_entries(
|
||||
self,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None,
|
||||
account_id: Optional[str] = None,
|
||||
payment_id: Optional[str] = None
|
||||
) -> dict:
|
||||
"""List ledger entries"""
|
||||
params = {'limit': limit}
|
||||
if cursor:
|
||||
params['cursor'] = cursor
|
||||
if account_id:
|
||||
params['account_id'] = account_id
|
||||
if payment_id:
|
||||
params['payment_id'] = payment_id
|
||||
|
||||
return self.client._request('GET', '/ledger/entries', params=params)
|
||||
|
||||
def retrieve_entry(self, entry_id: str) -> dict:
|
||||
"""Retrieve a ledger entry"""
|
||||
return self.client._request('GET', f'/ledger/entries/{entry_id}')
|
||||
73
fetcherpay/api/payment_methods.py
Normal file
73
fetcherpay/api/payment_methods.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Payment Methods API
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class PaymentMethodsAPI:
|
||||
"""Payment Methods API client"""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
def create(
|
||||
self,
|
||||
type: str,
|
||||
bank_account: Optional[Dict[str, Any]] = None,
|
||||
card: Optional[Dict[str, Any]] = None,
|
||||
usdc_wallet: Optional[Dict[str, Any]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
idempotency_key: Optional[str] = None
|
||||
) -> dict:
|
||||
"""
|
||||
Create a payment method
|
||||
|
||||
Args:
|
||||
type: 'bank_account', 'card', or 'usdc_wallet'
|
||||
bank_account: Bank account details
|
||||
card: Card details
|
||||
usdc_wallet: USDC wallet details
|
||||
metadata: Optional metadata
|
||||
idempotency_key: Idempotency key
|
||||
"""
|
||||
data = {'type': type}
|
||||
|
||||
if bank_account:
|
||||
data['bank_account'] = bank_account
|
||||
if card:
|
||||
data['card'] = card
|
||||
if usdc_wallet:
|
||||
data['usdc_wallet'] = usdc_wallet
|
||||
if metadata:
|
||||
data['metadata'] = metadata
|
||||
|
||||
return self.client._request(
|
||||
'POST',
|
||||
'/payment-methods',
|
||||
json=data,
|
||||
idempotency_key=idempotency_key
|
||||
)
|
||||
|
||||
def retrieve(self, payment_method_id: str) -> dict:
|
||||
"""Retrieve a payment method"""
|
||||
return self.client._request('GET', f'/payment-methods/{payment_method_id}')
|
||||
|
||||
def list(
|
||||
self,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None,
|
||||
type: Optional[str] = None
|
||||
) -> dict:
|
||||
"""List payment methods"""
|
||||
params = {'limit': limit}
|
||||
if cursor:
|
||||
params['cursor'] = cursor
|
||||
if type:
|
||||
params['type'] = type
|
||||
|
||||
return self.client._request('GET', '/payment-methods', params=params)
|
||||
|
||||
def delete(self, payment_method_id: str) -> None:
|
||||
"""Delete a payment method"""
|
||||
self.client._request('DELETE', f'/payment-methods/{payment_method_id}')
|
||||
133
fetcherpay/api/payments.py
Normal file
133
fetcherpay/api/payments.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Payments API
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
|
||||
class PaymentsAPI:
|
||||
"""Payments API client"""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
def create(
|
||||
self,
|
||||
amount: int,
|
||||
source: Dict[str, str],
|
||||
destination: Dict[str, str],
|
||||
currency: str = 'USD',
|
||||
rail: str = 'auto',
|
||||
description: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
idempotency_key: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new payment
|
||||
|
||||
Args:
|
||||
amount: Amount in cents (e.g., 10000 for $100.00)
|
||||
source: Payment source with payment_method_id
|
||||
destination: Payment destination with payment_method_id
|
||||
currency: Currency code (default: USD)
|
||||
rail: Payment rail (auto, ach, rtp, card, crypto)
|
||||
description: Optional payment description
|
||||
metadata: Optional metadata dictionary
|
||||
idempotency_key: Optional idempotency key
|
||||
|
||||
Returns:
|
||||
Payment object
|
||||
"""
|
||||
data = {
|
||||
'amount': amount,
|
||||
'currency': currency,
|
||||
'rail': rail,
|
||||
'source': source,
|
||||
'destination': destination,
|
||||
}
|
||||
|
||||
if description:
|
||||
data['description'] = description
|
||||
if metadata:
|
||||
data['metadata'] = metadata
|
||||
|
||||
return self.client._request(
|
||||
'POST',
|
||||
'/payments',
|
||||
json=data,
|
||||
idempotency_key=idempotency_key
|
||||
)
|
||||
|
||||
def retrieve(self, payment_id: str) -> Dict[str, Any]:
|
||||
"""Retrieve a payment by ID"""
|
||||
return self.client._request('GET', f'/payments/{payment_id}')
|
||||
|
||||
def list(
|
||||
self,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
rail: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
List payments
|
||||
|
||||
Args:
|
||||
limit: Number of results (1-100)
|
||||
cursor: Pagination cursor
|
||||
status: Filter by status
|
||||
rail: Filter by rail
|
||||
"""
|
||||
params = {'limit': limit}
|
||||
if cursor:
|
||||
params['cursor'] = cursor
|
||||
if status:
|
||||
params['status'] = status
|
||||
if rail:
|
||||
params['rail'] = rail
|
||||
|
||||
return self.client._request('GET', '/payments', params=params)
|
||||
|
||||
def cancel(
|
||||
self,
|
||||
payment_id: str,
|
||||
reason: Optional[str] = None,
|
||||
idempotency_key: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Cancel a pending payment"""
|
||||
data = {'reason': reason} if reason else {}
|
||||
return self.client._request(
|
||||
'POST',
|
||||
f'/payments/{payment_id}/cancel',
|
||||
json=data,
|
||||
idempotency_key=idempotency_key
|
||||
)
|
||||
|
||||
def refund(
|
||||
self,
|
||||
payment_id: str,
|
||||
amount: Optional[int] = None,
|
||||
reason: Optional[str] = None,
|
||||
idempotency_key: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Refund a settled payment
|
||||
|
||||
Args:
|
||||
payment_id: Payment ID to refund
|
||||
amount: Amount to refund (omit for full refund)
|
||||
reason: Refund reason
|
||||
idempotency_key: Idempotency key
|
||||
"""
|
||||
data = {}
|
||||
if amount:
|
||||
data['amount'] = amount
|
||||
if reason:
|
||||
data['reason'] = reason
|
||||
|
||||
return self.client._request(
|
||||
'POST',
|
||||
f'/payments/{payment_id}/refund',
|
||||
json=data,
|
||||
idempotency_key=idempotency_key
|
||||
)
|
||||
80
fetcherpay/api/webhooks.py
Normal file
80
fetcherpay/api/webhooks.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Webhooks API
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
|
||||
class WebhooksAPI:
|
||||
"""Webhooks API client"""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
def create(
|
||||
self,
|
||||
url: str,
|
||||
events: Optional[List[str]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
idempotency_key: Optional[str] = None
|
||||
) -> dict:
|
||||
"""
|
||||
Create a webhook endpoint
|
||||
|
||||
Args:
|
||||
url: Endpoint URL
|
||||
events: List of events to subscribe to
|
||||
metadata: Optional metadata
|
||||
idempotency_key: Idempotency key
|
||||
"""
|
||||
data = {'url': url}
|
||||
|
||||
if events:
|
||||
data['events'] = events
|
||||
if metadata:
|
||||
data['metadata'] = metadata
|
||||
|
||||
return self.client._request(
|
||||
'POST',
|
||||
'/webhooks',
|
||||
json=data,
|
||||
idempotency_key=idempotency_key
|
||||
)
|
||||
|
||||
def retrieve(self, webhook_id: str) -> dict:
|
||||
"""Retrieve a webhook endpoint"""
|
||||
return self.client._request('GET', f'/webhooks/{webhook_id}')
|
||||
|
||||
def list(
|
||||
self,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None
|
||||
) -> dict:
|
||||
"""List webhook endpoints"""
|
||||
params = {'limit': limit}
|
||||
if cursor:
|
||||
params['cursor'] = cursor
|
||||
|
||||
return self.client._request('GET', '/webhooks', params=params)
|
||||
|
||||
def update(
|
||||
self,
|
||||
webhook_id: str,
|
||||
url: Optional[str] = None,
|
||||
events: Optional[List[str]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
) -> dict:
|
||||
"""Update a webhook endpoint"""
|
||||
data = {}
|
||||
if url:
|
||||
data['url'] = url
|
||||
if events:
|
||||
data['events'] = events
|
||||
if metadata:
|
||||
data['metadata'] = metadata
|
||||
|
||||
return self.client._request('PUT', f'/webhooks/{webhook_id}', json=data)
|
||||
|
||||
def delete(self, webhook_id: str) -> None:
|
||||
"""Delete a webhook endpoint"""
|
||||
self.client._request('DELETE', f'/webhooks/{webhook_id}')
|
||||
153
fetcherpay/client.py
Normal file
153
fetcherpay/client.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
FetcherPay API Client
|
||||
"""
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
from typing import Optional
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from .exceptions import (
|
||||
FetcherPayError,
|
||||
AuthenticationError,
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
IdempotencyError,
|
||||
)
|
||||
from .api.payments import PaymentsAPI
|
||||
from .api.ledger import LedgerAPI
|
||||
from .api.payment_methods import PaymentMethodsAPI
|
||||
from .api.webhooks import WebhooksAPI
|
||||
|
||||
|
||||
class FetcherPay:
|
||||
"""
|
||||
Main FetcherPay client
|
||||
|
||||
Args:
|
||||
api_key: Your FetcherPay API key
|
||||
environment: 'sandbox' or 'production'
|
||||
base_url: Optional custom base URL
|
||||
timeout: Request timeout in seconds (default: 30)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
environment: str = 'sandbox',
|
||||
base_url: Optional[str] = None,
|
||||
timeout: int = 30
|
||||
):
|
||||
self.api_key = api_key
|
||||
self.environment = environment
|
||||
self.timeout = timeout
|
||||
|
||||
if base_url:
|
||||
self.base_url = base_url
|
||||
elif environment == 'production':
|
||||
self.base_url = 'https://api.fetcherpay.com/v1'
|
||||
else:
|
||||
self.base_url = 'https://sandbox.fetcherpay.com/v1'
|
||||
|
||||
# Create session with retries
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
|
||||
# Initialize API modules
|
||||
self.payments = PaymentsAPI(self)
|
||||
self.ledger = LedgerAPI(self)
|
||||
self.payment_methods = PaymentMethodsAPI(self)
|
||||
self.webhooks = WebhooksAPI(self)
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
params: Optional[dict] = None,
|
||||
json: Optional[dict] = None,
|
||||
headers: Optional[dict] = None,
|
||||
idempotency_key: Optional[str] = None
|
||||
) -> dict:
|
||||
"""Make an HTTP request"""
|
||||
url = urljoin(self.base_url, path)
|
||||
|
||||
request_headers = {}
|
||||
if headers:
|
||||
request_headers.update(headers)
|
||||
if idempotency_key:
|
||||
request_headers['Idempotency-Key'] = idempotency_key
|
||||
|
||||
try:
|
||||
response = self.session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
params=params,
|
||||
json=json,
|
||||
headers=request_headers,
|
||||
timeout=self.timeout
|
||||
)
|
||||
|
||||
# Handle errors
|
||||
if response.status_code >= 400:
|
||||
self._handle_error(response)
|
||||
|
||||
return response.json() if response.content else {}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
raise FetcherPayError('Request timeout', 'timeout_error')
|
||||
except requests.exceptions.ConnectionError:
|
||||
raise FetcherPayError('Connection error', 'connection_error')
|
||||
|
||||
def _handle_error(self, response):
|
||||
"""Handle API errors"""
|
||||
try:
|
||||
data = response.json()
|
||||
error = data.get('error', {})
|
||||
except:
|
||||
error = {}
|
||||
|
||||
message = error.get('message', 'An error occurred')
|
||||
error_type = error.get('type', 'api_error')
|
||||
param = error.get('param')
|
||||
code = error.get('code')
|
||||
|
||||
if response.status_code == 401:
|
||||
raise AuthenticationError(message)
|
||||
elif response.status_code == 404:
|
||||
raise NotFoundError(message)
|
||||
elif response.status_code == 422:
|
||||
raise ValidationError(message, param)
|
||||
elif response.status_code == 409:
|
||||
raise IdempotencyError(message)
|
||||
else:
|
||||
raise FetcherPayError(message, error_type, response.status_code, param, code)
|
||||
|
||||
def verify_webhook_signature(
|
||||
self,
|
||||
payload: str,
|
||||
signature: str,
|
||||
secret: str
|
||||
) -> bool:
|
||||
"""
|
||||
Verify a webhook signature
|
||||
|
||||
Args:
|
||||
payload: The raw webhook payload string
|
||||
signature: The signature from X-FetcherPay-Signature header
|
||||
secret: Your webhook secret
|
||||
|
||||
Returns:
|
||||
True if signature is valid
|
||||
"""
|
||||
expected = hmac.new(
|
||||
secret.encode('utf-8'),
|
||||
payload.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(signature, expected)
|
||||
49
fetcherpay/exceptions.py
Normal file
49
fetcherpay/exceptions.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
FetcherPay SDK Exceptions
|
||||
"""
|
||||
|
||||
|
||||
class FetcherPayError(Exception):
|
||||
"""Base FetcherPay error"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
type: str = 'api_error',
|
||||
status_code: int = None,
|
||||
param: str = None,
|
||||
code: str = None
|
||||
):
|
||||
super().__init__(message)
|
||||
self.type = type
|
||||
self.status_code = status_code
|
||||
self.param = param
|
||||
self.code = code
|
||||
|
||||
|
||||
class AuthenticationError(FetcherPayError):
|
||||
"""Authentication failed"""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message, 'authentication_error', 401)
|
||||
|
||||
|
||||
class ValidationError(FetcherPayError):
|
||||
"""Validation failed"""
|
||||
|
||||
def __init__(self, message: str, param: str = None):
|
||||
super().__init__(message, 'validation_error', 422, param)
|
||||
|
||||
|
||||
class NotFoundError(FetcherPayError):
|
||||
"""Resource not found"""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message, 'not_found', 404)
|
||||
|
||||
|
||||
class IdempotencyError(FetcherPayError):
|
||||
"""Idempotency key conflict"""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message, 'idempotency_error', 409)
|
||||
153
fetcherpay/types.py
Normal file
153
fetcherpay/types.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
FetcherPay SDK Types
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class PaymentStatus(str, Enum):
|
||||
PENDING = 'pending'
|
||||
AUTHORIZED = 'authorized'
|
||||
PROCESSING = 'processing'
|
||||
SETTLED = 'settled'
|
||||
FAILED = 'failed'
|
||||
CANCELLED = 'cancelled'
|
||||
REFUNDED = 'refunded'
|
||||
PARTIALLY_REFUNDED = 'partially_refunded'
|
||||
|
||||
|
||||
class Rail(str, Enum):
|
||||
AUTO = 'auto'
|
||||
ACH = 'ach'
|
||||
RTP = 'rtp'
|
||||
CARD = 'card'
|
||||
CRYPTO = 'crypto'
|
||||
|
||||
|
||||
class AccountType(str, Enum):
|
||||
ASSET = 'asset'
|
||||
LIABILITY = 'liability'
|
||||
REVENUE = 'revenue'
|
||||
EXPENSE = 'expense'
|
||||
EQUITY = 'equity'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fee:
|
||||
amount: int
|
||||
rate: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TimelineEvent:
|
||||
status: str
|
||||
timestamp: str
|
||||
detail: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Refund:
|
||||
id: str
|
||||
payment_id: str
|
||||
amount: int
|
||||
reason: Optional[str]
|
||||
status: str
|
||||
created_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Payment:
|
||||
id: str
|
||||
object: str
|
||||
status: PaymentStatus
|
||||
amount: int
|
||||
currency: str
|
||||
rail: str
|
||||
rail_selected: str
|
||||
description: Optional[str]
|
||||
source: Dict[str, Any]
|
||||
destination: Dict[str, Any]
|
||||
fee: Optional[Fee]
|
||||
timeline: List[TimelineEvent]
|
||||
ledger_entry_ids: List[str]
|
||||
refunds: List[Refund]
|
||||
idempotency_key: Optional[str]
|
||||
metadata: Dict[str, Any]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaymentMethod:
|
||||
id: str
|
||||
object: str
|
||||
type: str
|
||||
status: str
|
||||
bank_account: Optional[Dict[str, Any]]
|
||||
card: Optional[Dict[str, Any]]
|
||||
usdc_wallet: Optional[Dict[str, Any]]
|
||||
metadata: Dict[str, Any]
|
||||
created_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerBalance:
|
||||
pending: int
|
||||
posted: int
|
||||
available: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerAccount:
|
||||
id: str
|
||||
object: str
|
||||
name: str
|
||||
type: AccountType
|
||||
currency: str
|
||||
balance: LedgerBalance
|
||||
metadata: Dict[str, Any]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerEntry:
|
||||
id: str
|
||||
object: str
|
||||
journal_id: str
|
||||
account_id: str
|
||||
payment_id: Optional[str]
|
||||
entry_type: str
|
||||
amount: int
|
||||
currency: str
|
||||
status: str
|
||||
description: Optional[str]
|
||||
metadata: Dict[str, Any]
|
||||
created_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebhookEndpoint:
|
||||
id: str
|
||||
object: str
|
||||
url: str
|
||||
events: List[str]
|
||||
status: str
|
||||
secret: str
|
||||
metadata: Dict[str, Any]
|
||||
created_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListResponse:
|
||||
data: List[Any]
|
||||
has_more: bool
|
||||
next_cursor: Optional[str]
|
||||
|
||||
|
||||
# Request types
|
||||
CreatePaymentRequest = Dict[str, Any]
|
||||
CreatePaymentMethodRequest = Dict[str, Any]
|
||||
CreateWebhookRequest = Dict[str, Any]
|
||||
41
setup.py
Normal file
41
setup.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(
|
||||
name="fetcherpay",
|
||||
version="1.0.0",
|
||||
author="FetcherPay",
|
||||
author_email="support@fetcherpay.com",
|
||||
description="FetcherPay Python SDK",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/fetcherpay/fetcherpay-python",
|
||||
packages=find_packages(),
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
],
|
||||
python_requires=">=3.8",
|
||||
install_requires=[
|
||||
"requests>=2.25.0",
|
||||
"typing-extensions>=4.0.0",
|
||||
],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"pytest>=6.0",
|
||||
"pytest-cov>=2.0",
|
||||
"black>=21.0",
|
||||
"isort>=5.0",
|
||||
"mypy>=0.910",
|
||||
]
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user