""" 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)