Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
753 changes: 749 additions & 4 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pytest = "^7.4.0"
flake8 = "^6.1.0"
black = "^23.7.0"
isort = "^5.12.0"
aiohttp = "^3.14.1"


[build-system]
Expand Down
11 changes: 9 additions & 2 deletions src/shade/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
from .gateway import Gateway
from .http import AsyncHTTPClient, SyncHTTPClient
from .errors import (
AuthenticationError,
InvalidRequestError,
NetworkError,
NotFoundError,
HTTPError,
RateLimitError,
ShadeError,
)
from .gateway import Gateway

__version__ = "0.1.0"

__all__ = [
"AsyncHTTPClient",
"AuthenticationError",
"Gateway",
"HTTPError",
"InvalidRequestError",
"NetworkError",
"NotFoundError",
"RateLimitError",
"ShadeError",
]
"SyncHTTPClient",
]
47 changes: 46 additions & 1 deletion src/shade/errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Shade SDK exceptions.
"""
from __future__ import annotations

from typing import Optional
Expand All @@ -23,6 +26,48 @@ def __str__(self) -> str:
return f"{self.message} (status code: {self.status_code})"


class HTTPError(ShadeError):
"""Raised for non-2xx responses that are not handled by a more specific error."""

def __init__(
self,
message: str,
status_code: int,
response_body: Optional[str] = None,
) -> None:
super().__init__(message, status_code=status_code, response_body=response_body)


class RateLimitError(HTTPError):
"""
Raised when the API returns HTTP 429 Too Many Requests and either:
- auto-retry is disabled, or
- ``max_retries`` has been exhausted.

Attributes
----------
retry_after : int | None
Seconds to wait before the next attempt, parsed from the
``Retry-After`` response header. ``None`` if the header was absent.
"""

def __init__(
self,
message: str,
retry_after: Optional[int] = None,
status_code: int = 429,
response_body: Optional[str] = None,
) -> None:
super().__init__(message, status_code=status_code, response_body=response_body)
self.retry_after = retry_after

def __str__(self) -> str: # pragma: no cover
base = super().__str__()
if self.retry_after is not None:
return f"{base} (retry after {self.retry_after}s)"
return base


class AuthenticationError(ShadeError):
"""Raised when authentication fails or credentials are invalid."""

Expand All @@ -36,4 +81,4 @@ class NotFoundError(ShadeError):


class NetworkError(ShadeError):
"""Raised when the SDK cannot complete a network request."""
"""Raised when the SDK cannot complete a network request."""
88 changes: 82 additions & 6 deletions src/shade/gateway.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,89 @@
from __future__ import annotations

from typing import Any, Dict, Optional

from .http import AsyncHTTPClient, SyncHTTPClient, DEFAULT_MAX_RETRIES


class Gateway:
"""
Main entry point for the Shade Payment Gateway.

Parameters
----------
api_key : str
Your Shade API key.
base_url : str
Override the default API base URL (useful for testing).
max_retries : int
Number of automatic retries on HTTP 429. Defaults to
``DEFAULT_MAX_RETRIES`` (3). Set to ``0`` to disable.
timeout : float
Per-request socket timeout in seconds.
"""
def __init__(self):
pass

def process_payment(self, amount: float, currency: str):
_DEFAULT_BASE_URL = "https://api.shadeprotocol.io/v1"

def __init__(
self,
api_key: str = "",
base_url: str = "",
max_retries: int = DEFAULT_MAX_RETRIES,
timeout: float = 30.0,
) -> None:
if not api_key:
raise ValueError("api_key must be a non-empty string")
self.api_key = api_key
self._base_url = base_url or self._DEFAULT_BASE_URL
self._http = SyncHTTPClient(
base_url=self._base_url,
api_key=api_key,
max_retries=max_retries,
timeout=timeout,
)
self._async_http = AsyncHTTPClient(
base_url=self._base_url,
api_key=api_key,
max_retries=max_retries,
Comment thread
bukkybyte marked this conversation as resolved.
timeout=timeout,
)

# ------------------------------------------------------------------
# Sync API
# ------------------------------------------------------------------

def process_payment(self, amount: float, currency: str) -> Dict[str, Any]:
"""
Process a payment (placeholder).
Process a payment (sync).

Parameters
----------
amount : float
Payment amount.
currency : str
ISO 4217 currency code (e.g. ``"USD"``).

Returns
-------
dict
API response body.
"""
print(f"Processing payment of {amount} {currency}...")
return True
return self._http.request(
"POST",
"/payments",
{"amount": amount, "currency": currency},
)

# ------------------------------------------------------------------
# Async API
# ------------------------------------------------------------------

async def process_payment_async(
self, amount: float, currency: str
) -> Dict[str, Any]:
"""Async variant of :meth:`process_payment`."""
return await self._async_http.request(
"POST",
"/payments",
{"amount": amount, "currency": currency},
)
Loading
Loading