Initial Python SDK release v1.0.0

This commit is contained in:
garfieldheron
2026-02-19 12:17:09 -05:00
commit 42443872c9
17 changed files with 1037 additions and 0 deletions

153
fetcherpay/client.py Normal file
View 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)