perata 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- perata/__init__.py +52 -0
- perata/_client.py +86 -0
- perata/_core.py +25 -0
- perata/_errors.py +35 -0
- perata/_pagination.py +32 -0
- perata/_services.py +191 -0
- perata/_version.py +10 -0
- perata/models.py +329 -0
- perata/py.typed +0 -0
- perata/webhooks.py +74 -0
- perata-0.1.0.dist-info/METADATA +63 -0
- perata-0.1.0.dist-info/RECORD +13 -0
- perata-0.1.0.dist-info/WHEEL +4 -0
perata/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Code generated by sdk/generator. DO NOT EDIT.
|
|
2
|
+
"""Peráta's SDK for Python, generated from Peráta's API contract."""
|
|
3
|
+
|
|
4
|
+
from ._client import Perata, Transport, urllib_transport
|
|
5
|
+
from ._errors import PerataError
|
|
6
|
+
from ._pagination import Page
|
|
7
|
+
from ._services import CheckoutsService, CustomersService, OrdersService, PaymentMethodsService, PaymentsService, PricesService, ProductsService, RefundsService, SubscriptionsService
|
|
8
|
+
from ._version import API_VERSION, DEFAULT_BASE_URL, VERSION
|
|
9
|
+
from .models import Card, Checkout, Customer, Order, Pagination, Payment, PaymentMethod, PaymentMethodDetached, Price, Product, Refund, Subscription, CheckoutsCreateParams, CustomersCreateParams, PricesCreateParams, ProductsCreateParams, RefundsCreateParams, SubscriptionsCancelParams
|
|
10
|
+
from .webhooks import WEBHOOK_TOLERANCE_SECONDS, WebhookEvent, WebhookVerificationError, verify_webhook
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"API_VERSION",
|
|
14
|
+
"DEFAULT_BASE_URL",
|
|
15
|
+
"VERSION",
|
|
16
|
+
"WEBHOOK_TOLERANCE_SECONDS",
|
|
17
|
+
"Page",
|
|
18
|
+
"Perata",
|
|
19
|
+
"PerataError",
|
|
20
|
+
"Transport",
|
|
21
|
+
"WebhookEvent",
|
|
22
|
+
"WebhookVerificationError",
|
|
23
|
+
"urllib_transport",
|
|
24
|
+
"verify_webhook",
|
|
25
|
+
"CheckoutsService",
|
|
26
|
+
"CustomersService",
|
|
27
|
+
"OrdersService",
|
|
28
|
+
"PaymentMethodsService",
|
|
29
|
+
"PaymentsService",
|
|
30
|
+
"PricesService",
|
|
31
|
+
"ProductsService",
|
|
32
|
+
"RefundsService",
|
|
33
|
+
"SubscriptionsService",
|
|
34
|
+
"Card",
|
|
35
|
+
"Checkout",
|
|
36
|
+
"Customer",
|
|
37
|
+
"Order",
|
|
38
|
+
"Pagination",
|
|
39
|
+
"Payment",
|
|
40
|
+
"PaymentMethod",
|
|
41
|
+
"PaymentMethodDetached",
|
|
42
|
+
"Price",
|
|
43
|
+
"Product",
|
|
44
|
+
"Refund",
|
|
45
|
+
"Subscription",
|
|
46
|
+
"CheckoutsCreateParams",
|
|
47
|
+
"CustomersCreateParams",
|
|
48
|
+
"PricesCreateParams",
|
|
49
|
+
"ProductsCreateParams",
|
|
50
|
+
"RefundsCreateParams",
|
|
51
|
+
"SubscriptionsCancelParams",
|
|
52
|
+
]
|
perata/_client.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Code generated by sdk/generator. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import urlencode
|
|
11
|
+
|
|
12
|
+
from ._errors import PerataError
|
|
13
|
+
from ._services import CheckoutsService, CustomersService, OrdersService, PaymentMethodsService, PaymentsService, PricesService, ProductsService, RefundsService, SubscriptionsService
|
|
14
|
+
from ._version import DEFAULT_BASE_URL, VERSION
|
|
15
|
+
|
|
16
|
+
Transport = Callable[[str, str, dict[str, str], "bytes | None", float], "tuple[int, dict[str, str], bytes]"]
|
|
17
|
+
"""Sends one request: (method, url, headers, body, timeout) -> (status, headers in lower case, body)."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def urllib_transport(method: str, url: str, headers: dict[str, str], body: bytes | None, timeout: float) -> tuple[int, dict[str, str], bytes]:
|
|
21
|
+
"""The standard library's HTTP, so the SDK depends on nothing."""
|
|
22
|
+
request = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
23
|
+
try:
|
|
24
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
25
|
+
return response.status, {key.lower(): value for key, value in response.headers.items()}, response.read()
|
|
26
|
+
except urllib.error.HTTPError as error:
|
|
27
|
+
return error.code, {key.lower(): value for key, value in error.headers.items()}, error.read()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Perata:
|
|
31
|
+
"""Peráta's API with one organization's API key.
|
|
32
|
+
|
|
33
|
+
The key says the organization and the environment: ``prk_test_…`` is the
|
|
34
|
+
test environment, ``prk_live_…`` the live one.
|
|
35
|
+
|
|
36
|
+
perata = Perata(os.environ["PERATA_API_KEY"])
|
|
37
|
+
product = perata.products.create({"name": "Curso"})
|
|
38
|
+
for product in perata.products.list_iter():
|
|
39
|
+
...
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE_URL, timeout: float = 30.0, transport: Transport | None = None) -> None:
|
|
43
|
+
self._api_key = api_key
|
|
44
|
+
self._base_url = base_url.rstrip("/")
|
|
45
|
+
self._timeout = timeout
|
|
46
|
+
self._transport = transport or urllib_transport
|
|
47
|
+
self.checkouts = CheckoutsService(self)
|
|
48
|
+
self.customers = CustomersService(self)
|
|
49
|
+
self.orders = OrdersService(self)
|
|
50
|
+
self.payment_methods = PaymentMethodsService(self)
|
|
51
|
+
self.payments = PaymentsService(self)
|
|
52
|
+
self.prices = PricesService(self)
|
|
53
|
+
self.products = ProductsService(self)
|
|
54
|
+
self.refunds = RefundsService(self)
|
|
55
|
+
self.subscriptions = SubscriptionsService(self)
|
|
56
|
+
|
|
57
|
+
def request(
|
|
58
|
+
self,
|
|
59
|
+
method: str,
|
|
60
|
+
path: str,
|
|
61
|
+
*,
|
|
62
|
+
query: dict[str, Any] | None = None,
|
|
63
|
+
body: Any = None,
|
|
64
|
+
idempotency_key: str | None = None,
|
|
65
|
+
api_key: str | None = None,
|
|
66
|
+
) -> Any:
|
|
67
|
+
"""Calls the API and answers what it wrapped in ``data``; a refusal raises PerataError."""
|
|
68
|
+
url = self._base_url + path
|
|
69
|
+
values = {name: str(value) for name, value in (query or {}).items() if value is not None and value != ""}
|
|
70
|
+
if values:
|
|
71
|
+
url += "?" + urlencode(values)
|
|
72
|
+
headers = {
|
|
73
|
+
"Authorization": f"Bearer {api_key or self._api_key}",
|
|
74
|
+
"Accept": "application/json",
|
|
75
|
+
"User-Agent": f"perata-python/{VERSION}",
|
|
76
|
+
}
|
|
77
|
+
data = None
|
|
78
|
+
if body is not None:
|
|
79
|
+
data = json.dumps(body).encode()
|
|
80
|
+
headers["Content-Type"] = "application/json"
|
|
81
|
+
if idempotency_key:
|
|
82
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
83
|
+
status, response_headers, raw = self._transport(method, url, headers, data, self._timeout)
|
|
84
|
+
if status >= 300:
|
|
85
|
+
raise PerataError.from_response(status, response_headers, raw)
|
|
86
|
+
return json.loads(raw)["data"]
|
perata/_core.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Requester(Protocol):
|
|
8
|
+
"""What a service calls the API through."""
|
|
9
|
+
|
|
10
|
+
def request(
|
|
11
|
+
self,
|
|
12
|
+
method: str,
|
|
13
|
+
path: str,
|
|
14
|
+
*,
|
|
15
|
+
query: dict[str, Any] | None = None,
|
|
16
|
+
body: Any = None,
|
|
17
|
+
idempotency_key: str | None = None,
|
|
18
|
+
api_key: str | None = None,
|
|
19
|
+
) -> Any: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def segment(value: str) -> str:
|
|
23
|
+
"""A path parameter, escaped."""
|
|
24
|
+
return quote(value, safe="")
|
|
25
|
+
|
perata/_errors.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PerataError(Exception):
|
|
9
|
+
"""A request the API refused.
|
|
10
|
+
|
|
11
|
+
``code`` is the API's: invalid_credential, forbidden, not_found,
|
|
12
|
+
invalid_request, conflict, idempotency_conflict, body_too_large or
|
|
13
|
+
unavailable. ``fields`` says, for invalid_request, what is wrong with each
|
|
14
|
+
field. ``retry_after`` is how many seconds to wait on a 429.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, status: int, code: str, message: str = "", fields: dict[str, list[str]] | None = None, retry_after: int | None = None) -> None:
|
|
18
|
+
super().__init__(f"perata: {status} {code}: {message}" if message else f"perata: {status} {code}")
|
|
19
|
+
self.status = status
|
|
20
|
+
self.code = code
|
|
21
|
+
self.message = message
|
|
22
|
+
self.fields = fields or {}
|
|
23
|
+
self.retry_after = retry_after
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_response(cls, status: int, headers: Mapping[str, str], body: bytes) -> PerataError:
|
|
27
|
+
data: dict[str, Any] = {}
|
|
28
|
+
try:
|
|
29
|
+
loaded = json.loads(body or b"{}")
|
|
30
|
+
if isinstance(loaded, dict):
|
|
31
|
+
data = loaded
|
|
32
|
+
except ValueError:
|
|
33
|
+
pass # A refusal without a JSON body is still a refusal.
|
|
34
|
+
retry_after = headers.get("retry-after", "")
|
|
35
|
+
return cls(status, data.get("error", ""), data.get("message", ""), data.get("fields") or {}, int(retry_after) if retry_after.isdigit() else None)
|
perata/_pagination.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterator
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from .models import Pagination
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Page(Generic[T]):
|
|
14
|
+
"""One page of a list, and where the list ends."""
|
|
15
|
+
|
|
16
|
+
items: list[T]
|
|
17
|
+
pagination: Pagination
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_dict(cls, data: dict[str, Any], item: Callable[[dict[str, Any]], T]) -> Page[T]:
|
|
21
|
+
return cls(items=[item(each) for each in data.get("items") or []], pagination=Pagination.from_dict(data["pagination"]))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def iterate(fetch: Callable[[int], Page[T]], first: int | None = None) -> Iterator[T]:
|
|
25
|
+
"""Walks pages from ``first`` until the last one, as Polar's SDKs do, yielding every item."""
|
|
26
|
+
page = max(first or 1, 1)
|
|
27
|
+
while True:
|
|
28
|
+
result = fetch(page)
|
|
29
|
+
yield from result.items
|
|
30
|
+
if page >= result.pagination.max_page:
|
|
31
|
+
return
|
|
32
|
+
page += 1
|
perata/_services.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Code generated by sdk/generator. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
|
|
7
|
+
from . import models
|
|
8
|
+
from ._core import Requester, segment
|
|
9
|
+
from ._pagination import Page, iterate
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CheckoutsService:
|
|
13
|
+
"""The API's checkouts."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: Requester) -> None:
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
def create(self, params: models.CheckoutsCreateParams, *, idempotency_key: str | None = None, api_key: str | None = None) -> models.Checkout:
|
|
19
|
+
"""Create a checkout address for one product and price."""
|
|
20
|
+
data = self._client.request("POST", "/api/v1/checkouts", body=params, idempotency_key=idempotency_key, api_key=api_key)
|
|
21
|
+
return models.Checkout.from_dict(data)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CustomersService:
|
|
25
|
+
"""The API's customers."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, client: Requester) -> None:
|
|
28
|
+
self._client = client
|
|
29
|
+
|
|
30
|
+
def create(self, params: models.CustomersCreateParams, *, idempotency_key: str | None = None, api_key: str | None = None) -> models.Customer:
|
|
31
|
+
"""Create a customer."""
|
|
32
|
+
data = self._client.request("POST", "/api/v1/customers", body=params, idempotency_key=idempotency_key, api_key=api_key)
|
|
33
|
+
return models.Customer.from_dict(data)
|
|
34
|
+
|
|
35
|
+
def get(self, customer: str, *, api_key: str | None = None) -> models.Customer:
|
|
36
|
+
"""Read a customer."""
|
|
37
|
+
data = self._client.request("GET", f"/api/v1/customers/{segment(customer)}", api_key=api_key)
|
|
38
|
+
return models.Customer.from_dict(data)
|
|
39
|
+
|
|
40
|
+
def list(self, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Page[models.Customer]:
|
|
41
|
+
"""List customers. One page; ``list_iter`` walks every page."""
|
|
42
|
+
data = self._client.request("GET", "/api/v1/customers", query={"page": page, "limit": limit}, api_key=api_key)
|
|
43
|
+
return Page.from_dict(data, models.Customer.from_dict)
|
|
44
|
+
|
|
45
|
+
def list_iter(self, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Iterator[models.Customer]:
|
|
46
|
+
"""Walks every page of customers:list, item by item."""
|
|
47
|
+
return iterate(lambda at: self.list(page=at, limit=limit, api_key=api_key), page)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class OrdersService:
|
|
51
|
+
"""The API's orders."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, client: Requester) -> None:
|
|
54
|
+
self._client = client
|
|
55
|
+
|
|
56
|
+
def list(self, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Page[models.Order]:
|
|
57
|
+
"""List orders: paid checkouts. One page; ``list_iter`` walks every page."""
|
|
58
|
+
data = self._client.request("GET", "/api/v1/orders", query={"page": page, "limit": limit}, api_key=api_key)
|
|
59
|
+
return Page.from_dict(data, models.Order.from_dict)
|
|
60
|
+
|
|
61
|
+
def list_iter(self, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Iterator[models.Order]:
|
|
62
|
+
"""Walks every page of orders:list, item by item."""
|
|
63
|
+
return iterate(lambda at: self.list(page=at, limit=limit, api_key=api_key), page)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PaymentMethodsService:
|
|
67
|
+
"""The API's payment_methods."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, client: Requester) -> None:
|
|
70
|
+
self._client = client
|
|
71
|
+
|
|
72
|
+
def detach(self, payment_method: str, *, idempotency_key: str | None = None, api_key: str | None = None) -> models.PaymentMethodDetached:
|
|
73
|
+
"""Stop charging a stored card; refused while an active subscription charges it."""
|
|
74
|
+
data = self._client.request("POST", f"/api/v1/payment_methods/{segment(payment_method)}/detach", idempotency_key=idempotency_key, api_key=api_key)
|
|
75
|
+
return models.PaymentMethodDetached.from_dict(data)
|
|
76
|
+
|
|
77
|
+
def list(self, customer: str, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Page[models.PaymentMethod]:
|
|
78
|
+
"""List a customer's stored cards: brand, last four digits and expiry. One page; ``list_iter`` walks every page."""
|
|
79
|
+
data = self._client.request("GET", f"/api/v1/customers/{segment(customer)}/payment_methods", query={"page": page, "limit": limit}, api_key=api_key)
|
|
80
|
+
return Page.from_dict(data, models.PaymentMethod.from_dict)
|
|
81
|
+
|
|
82
|
+
def list_iter(self, customer: str, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Iterator[models.PaymentMethod]:
|
|
83
|
+
"""Walks every page of payment_methods:list, item by item."""
|
|
84
|
+
return iterate(lambda at: self.list(customer, page=at, limit=limit, api_key=api_key), page)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PaymentsService:
|
|
88
|
+
"""The API's payments."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, client: Requester) -> None:
|
|
91
|
+
self._client = client
|
|
92
|
+
|
|
93
|
+
def get(self, payment: str, *, api_key: str | None = None) -> models.Payment:
|
|
94
|
+
"""Read a payment."""
|
|
95
|
+
data = self._client.request("GET", f"/api/v1/payments/{segment(payment)}", api_key=api_key)
|
|
96
|
+
return models.Payment.from_dict(data)
|
|
97
|
+
|
|
98
|
+
def list(self, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Page[models.Payment]:
|
|
99
|
+
"""List payments. One page; ``list_iter`` walks every page."""
|
|
100
|
+
data = self._client.request("GET", "/api/v1/payments", query={"page": page, "limit": limit}, api_key=api_key)
|
|
101
|
+
return Page.from_dict(data, models.Payment.from_dict)
|
|
102
|
+
|
|
103
|
+
def list_iter(self, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Iterator[models.Payment]:
|
|
104
|
+
"""Walks every page of payments:list, item by item."""
|
|
105
|
+
return iterate(lambda at: self.list(page=at, limit=limit, api_key=api_key), page)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PricesService:
|
|
109
|
+
"""The API's prices."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, client: Requester) -> None:
|
|
112
|
+
self._client = client
|
|
113
|
+
|
|
114
|
+
def create(self, params: models.PricesCreateParams, *, idempotency_key: str | None = None, api_key: str | None = None) -> models.Price:
|
|
115
|
+
"""Create a BRL price, one-time or recurring."""
|
|
116
|
+
data = self._client.request("POST", "/api/v1/prices", body=params, idempotency_key=idempotency_key, api_key=api_key)
|
|
117
|
+
return models.Price.from_dict(data)
|
|
118
|
+
|
|
119
|
+
def list(self, *, page: int | None = None, limit: int | None = None, product_id: str | None = None, api_key: str | None = None) -> Page[models.Price]:
|
|
120
|
+
"""List prices, optionally of one product (product_id). One page; ``list_iter`` walks every page."""
|
|
121
|
+
data = self._client.request("GET", "/api/v1/prices", query={"page": page, "limit": limit, "product_id": product_id}, api_key=api_key)
|
|
122
|
+
return Page.from_dict(data, models.Price.from_dict)
|
|
123
|
+
|
|
124
|
+
def list_iter(self, *, page: int | None = None, limit: int | None = None, product_id: str | None = None, api_key: str | None = None) -> Iterator[models.Price]:
|
|
125
|
+
"""Walks every page of prices:list, item by item."""
|
|
126
|
+
return iterate(lambda at: self.list(page=at, limit=limit, product_id=product_id, api_key=api_key), page)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ProductsService:
|
|
130
|
+
"""The API's products."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, client: Requester) -> None:
|
|
133
|
+
self._client = client
|
|
134
|
+
|
|
135
|
+
def create(self, params: models.ProductsCreateParams, *, idempotency_key: str | None = None, api_key: str | None = None) -> models.Product:
|
|
136
|
+
"""Create a product."""
|
|
137
|
+
data = self._client.request("POST", "/api/v1/products", body=params, idempotency_key=idempotency_key, api_key=api_key)
|
|
138
|
+
return models.Product.from_dict(data)
|
|
139
|
+
|
|
140
|
+
def get(self, product: str, *, api_key: str | None = None) -> models.Product:
|
|
141
|
+
"""Read a product."""
|
|
142
|
+
data = self._client.request("GET", f"/api/v1/products/{segment(product)}", api_key=api_key)
|
|
143
|
+
return models.Product.from_dict(data)
|
|
144
|
+
|
|
145
|
+
def list(self, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Page[models.Product]:
|
|
146
|
+
"""List products. One page; ``list_iter`` walks every page."""
|
|
147
|
+
data = self._client.request("GET", "/api/v1/products", query={"page": page, "limit": limit}, api_key=api_key)
|
|
148
|
+
return Page.from_dict(data, models.Product.from_dict)
|
|
149
|
+
|
|
150
|
+
def list_iter(self, *, page: int | None = None, limit: int | None = None, api_key: str | None = None) -> Iterator[models.Product]:
|
|
151
|
+
"""Walks every page of products:list, item by item."""
|
|
152
|
+
return iterate(lambda at: self.list(page=at, limit=limit, api_key=api_key), page)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class RefundsService:
|
|
156
|
+
"""The API's refunds."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, client: Requester) -> None:
|
|
159
|
+
self._client = client
|
|
160
|
+
|
|
161
|
+
def create(self, params: models.RefundsCreateParams, *, idempotency_key: str | None = None, api_key: str | None = None) -> models.Refund:
|
|
162
|
+
"""Refund part or all of a payment."""
|
|
163
|
+
data = self._client.request("POST", "/api/v1/refunds", body=params, idempotency_key=idempotency_key, api_key=api_key)
|
|
164
|
+
return models.Refund.from_dict(data)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class SubscriptionsService:
|
|
168
|
+
"""The API's subscriptions."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, client: Requester) -> None:
|
|
171
|
+
self._client = client
|
|
172
|
+
|
|
173
|
+
def cancel(self, subscription: str, params: models.SubscriptionsCancelParams, *, idempotency_key: str | None = None, api_key: str | None = None) -> models.Subscription:
|
|
174
|
+
"""Cancel a subscription, by default at the end of its period."""
|
|
175
|
+
data = self._client.request("POST", f"/api/v1/subscriptions/{segment(subscription)}/cancel", body=params, idempotency_key=idempotency_key, api_key=api_key)
|
|
176
|
+
return models.Subscription.from_dict(data)
|
|
177
|
+
|
|
178
|
+
def get(self, subscription: str, *, api_key: str | None = None) -> models.Subscription:
|
|
179
|
+
"""Read a subscription."""
|
|
180
|
+
data = self._client.request("GET", f"/api/v1/subscriptions/{segment(subscription)}", api_key=api_key)
|
|
181
|
+
return models.Subscription.from_dict(data)
|
|
182
|
+
|
|
183
|
+
def list(self, *, page: int | None = None, limit: int | None = None, customer_id: str | None = None, api_key: str | None = None) -> Page[models.Subscription]:
|
|
184
|
+
"""List subscriptions, optionally of one customer (customer_id). One page; ``list_iter`` walks every page."""
|
|
185
|
+
data = self._client.request("GET", "/api/v1/subscriptions", query={"page": page, "limit": limit, "customer_id": customer_id}, api_key=api_key)
|
|
186
|
+
return Page.from_dict(data, models.Subscription.from_dict)
|
|
187
|
+
|
|
188
|
+
def list_iter(self, *, page: int | None = None, limit: int | None = None, customer_id: str | None = None, api_key: str | None = None) -> Iterator[models.Subscription]:
|
|
189
|
+
"""Walks every page of subscriptions:list, item by item."""
|
|
190
|
+
return iterate(lambda at: self.list(page=at, limit=limit, customer_id=customer_id, api_key=api_key), page)
|
|
191
|
+
|
perata/_version.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code generated by sdk/generator. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
VERSION = "0.1.0"
|
|
4
|
+
"""This SDK's version."""
|
|
5
|
+
|
|
6
|
+
API_VERSION = "0.1.0"
|
|
7
|
+
"""The contract this SDK was generated from."""
|
|
8
|
+
|
|
9
|
+
DEFAULT_BASE_URL = "https://perata.sh"
|
|
10
|
+
"""Peráta's API. The environment, test or live, is the API key's: prk_test_… or prk_live_…."""
|
perata/models.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Code generated by sdk/generator. DO NOT EDIT.
|
|
2
|
+
"""The resources Peráta's API answers and the bodies its writes take."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Card:
|
|
12
|
+
"""Card is a resource of Peráta's API."""
|
|
13
|
+
|
|
14
|
+
brand: str
|
|
15
|
+
exp_month: int | None
|
|
16
|
+
exp_year: int | None
|
|
17
|
+
last4: str | None
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_dict(cls, data: dict[str, Any]) -> Card:
|
|
21
|
+
return cls(
|
|
22
|
+
brand=data["brand"],
|
|
23
|
+
exp_month=data.get("exp_month"),
|
|
24
|
+
exp_year=data.get("exp_year"),
|
|
25
|
+
last4=data.get("last4"),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Checkout:
|
|
31
|
+
"""Checkout is a resource of Peráta's API."""
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
price_id: str
|
|
35
|
+
product_id: str
|
|
36
|
+
url: str
|
|
37
|
+
"""Endereço para onde o comprador é enviado."""
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, data: dict[str, Any]) -> Checkout:
|
|
41
|
+
return cls(
|
|
42
|
+
id=data["id"],
|
|
43
|
+
price_id=data["price_id"],
|
|
44
|
+
product_id=data["product_id"],
|
|
45
|
+
url=data["url"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Customer:
|
|
51
|
+
"""Customer is a resource of Peráta's API."""
|
|
52
|
+
|
|
53
|
+
created_at: str
|
|
54
|
+
email: str
|
|
55
|
+
external_id: str
|
|
56
|
+
id: str
|
|
57
|
+
name: str
|
|
58
|
+
status: Literal["active", "archived"]
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, data: dict[str, Any]) -> Customer:
|
|
62
|
+
return cls(
|
|
63
|
+
created_at=data["created_at"],
|
|
64
|
+
email=data["email"],
|
|
65
|
+
external_id=data["external_id"],
|
|
66
|
+
id=data["id"],
|
|
67
|
+
name=data["name"],
|
|
68
|
+
status=data["status"],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class Order:
|
|
74
|
+
"""Order is a resource of Peráta's API."""
|
|
75
|
+
|
|
76
|
+
amount_cents: int
|
|
77
|
+
currency: Literal["BRL"]
|
|
78
|
+
customer_id: str
|
|
79
|
+
id: str
|
|
80
|
+
paid_at: str | None
|
|
81
|
+
price_id: str
|
|
82
|
+
product_id: str
|
|
83
|
+
status: Literal["open", "confirmed", "succeeded", "failed", "expired"]
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, data: dict[str, Any]) -> Order:
|
|
87
|
+
return cls(
|
|
88
|
+
amount_cents=data["amount_cents"],
|
|
89
|
+
currency=data["currency"],
|
|
90
|
+
customer_id=data["customer_id"],
|
|
91
|
+
id=data["id"],
|
|
92
|
+
paid_at=data.get("paid_at"),
|
|
93
|
+
price_id=data["price_id"],
|
|
94
|
+
product_id=data["product_id"],
|
|
95
|
+
status=data["status"],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class Pagination:
|
|
101
|
+
"""Pagination is a resource of Peráta's API."""
|
|
102
|
+
|
|
103
|
+
max_page: int
|
|
104
|
+
total_count: int
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, data: dict[str, Any]) -> Pagination:
|
|
108
|
+
return cls(
|
|
109
|
+
max_page=data["max_page"],
|
|
110
|
+
total_count=data["total_count"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class Payment:
|
|
116
|
+
"""Payment is a resource of Peráta's API."""
|
|
117
|
+
|
|
118
|
+
amount_cents: int
|
|
119
|
+
captured_cents: int
|
|
120
|
+
checkout_id: str
|
|
121
|
+
created_at: str
|
|
122
|
+
currency: Literal["BRL"]
|
|
123
|
+
customer_id: str
|
|
124
|
+
id: str
|
|
125
|
+
method: Literal["card", "pix", "boleto"]
|
|
126
|
+
refunded_cents: int
|
|
127
|
+
state: Literal["unknown", "pending", "authorized", "captured", "refused", "canceled", "refunded", "partially_refunded"]
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_dict(cls, data: dict[str, Any]) -> Payment:
|
|
131
|
+
return cls(
|
|
132
|
+
amount_cents=data["amount_cents"],
|
|
133
|
+
captured_cents=data["captured_cents"],
|
|
134
|
+
checkout_id=data["checkout_id"],
|
|
135
|
+
created_at=data["created_at"],
|
|
136
|
+
currency=data["currency"],
|
|
137
|
+
customer_id=data["customer_id"],
|
|
138
|
+
id=data["id"],
|
|
139
|
+
method=data["method"],
|
|
140
|
+
refunded_cents=data["refunded_cents"],
|
|
141
|
+
state=data["state"],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class PaymentMethod:
|
|
147
|
+
"""PaymentMethod is a resource of Peráta's API."""
|
|
148
|
+
|
|
149
|
+
card: Card
|
|
150
|
+
created_at: str
|
|
151
|
+
customer_id: str
|
|
152
|
+
id: str
|
|
153
|
+
type: Literal["card"]
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_dict(cls, data: dict[str, Any]) -> PaymentMethod:
|
|
157
|
+
return cls(
|
|
158
|
+
card=Card.from_dict(data["card"]),
|
|
159
|
+
created_at=data["created_at"],
|
|
160
|
+
customer_id=data["customer_id"],
|
|
161
|
+
id=data["id"],
|
|
162
|
+
type=data["type"],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class PaymentMethodDetached:
|
|
168
|
+
"""PaymentMethodDetached is a resource of Peráta's API."""
|
|
169
|
+
|
|
170
|
+
detached: bool
|
|
171
|
+
id: str
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def from_dict(cls, data: dict[str, Any]) -> PaymentMethodDetached:
|
|
175
|
+
return cls(
|
|
176
|
+
detached=data["detached"],
|
|
177
|
+
id=data["id"],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class Price:
|
|
183
|
+
"""Price is a resource of Peráta's API."""
|
|
184
|
+
|
|
185
|
+
amount_cents: int
|
|
186
|
+
"""Valor em centavos."""
|
|
187
|
+
currency: Literal["BRL"]
|
|
188
|
+
id: str
|
|
189
|
+
interval: Literal["none", "month", "year"]
|
|
190
|
+
product_id: str
|
|
191
|
+
status: Literal["active", "archived"]
|
|
192
|
+
type: Literal["one_time", "recurring"]
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def from_dict(cls, data: dict[str, Any]) -> Price:
|
|
196
|
+
return cls(
|
|
197
|
+
amount_cents=data["amount_cents"],
|
|
198
|
+
currency=data["currency"],
|
|
199
|
+
id=data["id"],
|
|
200
|
+
interval=data["interval"],
|
|
201
|
+
product_id=data["product_id"],
|
|
202
|
+
status=data["status"],
|
|
203
|
+
type=data["type"],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(frozen=True)
|
|
208
|
+
class Product:
|
|
209
|
+
"""Product is a resource of Peráta's API."""
|
|
210
|
+
|
|
211
|
+
created_at: str
|
|
212
|
+
description: str
|
|
213
|
+
id: str
|
|
214
|
+
name: str
|
|
215
|
+
status: Literal["active", "archived"]
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def from_dict(cls, data: dict[str, Any]) -> Product:
|
|
219
|
+
return cls(
|
|
220
|
+
created_at=data["created_at"],
|
|
221
|
+
description=data["description"],
|
|
222
|
+
id=data["id"],
|
|
223
|
+
name=data["name"],
|
|
224
|
+
status=data["status"],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@dataclass(frozen=True)
|
|
229
|
+
class Refund:
|
|
230
|
+
"""Refund is a resource of Peráta's API."""
|
|
231
|
+
|
|
232
|
+
amount_cents: int
|
|
233
|
+
id: str
|
|
234
|
+
payment_id: str
|
|
235
|
+
status: Literal["requested", "pending", "unknown", "succeeded", "failed", "canceled"]
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def from_dict(cls, data: dict[str, Any]) -> Refund:
|
|
239
|
+
return cls(
|
|
240
|
+
amount_cents=data["amount_cents"],
|
|
241
|
+
id=data["id"],
|
|
242
|
+
payment_id=data["payment_id"],
|
|
243
|
+
status=data["status"],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@dataclass(frozen=True)
|
|
248
|
+
class Subscription:
|
|
249
|
+
"""Subscription is a resource of Peráta's API."""
|
|
250
|
+
|
|
251
|
+
amount_cents: int
|
|
252
|
+
cancel_at_period_end: bool
|
|
253
|
+
currency: Literal["BRL"]
|
|
254
|
+
current_period_end: str
|
|
255
|
+
current_period_start: str
|
|
256
|
+
customer_id: str
|
|
257
|
+
id: str
|
|
258
|
+
interval: str
|
|
259
|
+
price_id: str
|
|
260
|
+
product_id: str
|
|
261
|
+
status: Literal["trialing", "active", "past_due", "paused", "canceled"]
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def from_dict(cls, data: dict[str, Any]) -> Subscription:
|
|
265
|
+
return cls(
|
|
266
|
+
amount_cents=data["amount_cents"],
|
|
267
|
+
cancel_at_period_end=data["cancel_at_period_end"],
|
|
268
|
+
currency=data["currency"],
|
|
269
|
+
current_period_end=data["current_period_end"],
|
|
270
|
+
current_period_start=data["current_period_start"],
|
|
271
|
+
customer_id=data["customer_id"],
|
|
272
|
+
id=data["id"],
|
|
273
|
+
interval=data["interval"],
|
|
274
|
+
price_id=data["price_id"],
|
|
275
|
+
product_id=data["product_id"],
|
|
276
|
+
status=data["status"],
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class CheckoutsCreateParams(TypedDict):
|
|
281
|
+
"""The body of checkouts:create."""
|
|
282
|
+
|
|
283
|
+
cancel_url: NotRequired[str]
|
|
284
|
+
label: NotRequired[str]
|
|
285
|
+
price_id: str
|
|
286
|
+
product_id: str
|
|
287
|
+
quantity: NotRequired[int]
|
|
288
|
+
success_url: NotRequired[str]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class CustomersCreateParams(TypedDict):
|
|
292
|
+
"""The body of customers:create."""
|
|
293
|
+
|
|
294
|
+
email: str
|
|
295
|
+
external_id: NotRequired[str]
|
|
296
|
+
name: str
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class PricesCreateParams(TypedDict):
|
|
300
|
+
"""The body of prices:create."""
|
|
301
|
+
|
|
302
|
+
amount_cents: int
|
|
303
|
+
interval: NotRequired[Literal["month", "year"]]
|
|
304
|
+
product_id: str
|
|
305
|
+
type: Literal["one_time", "recurring"]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class ProductsCreateParams(TypedDict):
|
|
309
|
+
"""The body of products:create."""
|
|
310
|
+
|
|
311
|
+
description: NotRequired[str]
|
|
312
|
+
name: str
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class RefundsCreateParams(TypedDict):
|
|
316
|
+
"""The body of refunds:create."""
|
|
317
|
+
|
|
318
|
+
amount_cents: int
|
|
319
|
+
note: NotRequired[str]
|
|
320
|
+
payment_id: str
|
|
321
|
+
reason: Literal["requested_by_customer", "duplicate", "fraudulent", "product_unavailable", "service_disruption", "other"]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class SubscriptionsCancelParams(TypedDict):
|
|
325
|
+
"""The body of subscriptions:cancel."""
|
|
326
|
+
|
|
327
|
+
at_period_end: NotRequired[bool]
|
|
328
|
+
reason: NotRequired[str]
|
|
329
|
+
|
perata/py.typed
ADDED
|
File without changes
|
perata/webhooks.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Verifying the webhooks Peráta sends, as Standard Webhooks signs them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
WEBHOOK_TOLERANCE_SECONDS = 300
|
|
15
|
+
"""How far from now a webhook's timestamp may be."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WebhookVerificationError(Exception):
|
|
19
|
+
"""A request Peráta did not sign, or signed too long ago."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str = "perata: webhook signature does not verify") -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class WebhookEvent:
|
|
27
|
+
"""What Peráta posts to an endpoint."""
|
|
28
|
+
|
|
29
|
+
id: str
|
|
30
|
+
type: str
|
|
31
|
+
timestamp: str
|
|
32
|
+
data: dict[str, str]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _header(headers: Mapping[str, str], name: str) -> str:
|
|
36
|
+
for key, value in headers.items():
|
|
37
|
+
if key.lower() == name:
|
|
38
|
+
return value
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def verify_webhook(secret: str, headers: Mapping[str, str], body: bytes | str, now: float | None = None) -> WebhookEvent:
|
|
43
|
+
"""Checks a webhook Peráta sent and answers its event.
|
|
44
|
+
|
|
45
|
+
The signature is an HMAC-SHA256 of ``{webhook-id}.{webhook-timestamp}.{body}``
|
|
46
|
+
under the key the endpoint's ``whsec_`` secret encodes. Pass the body
|
|
47
|
+
exactly as it arrived, before parsing it.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
key = base64.b64decode(secret.removeprefix("whsec_"), validate=True)
|
|
51
|
+
except (binascii.Error, ValueError):
|
|
52
|
+
key = b""
|
|
53
|
+
if not key:
|
|
54
|
+
raise ValueError("perata: the webhook secret is whsec_ and a base64 key")
|
|
55
|
+
raw = body.encode() if isinstance(body, str) else body
|
|
56
|
+
webhook_id = _header(headers, "webhook-id")
|
|
57
|
+
stamp = _header(headers, "webhook-timestamp")
|
|
58
|
+
if not webhook_id or not stamp.isdigit():
|
|
59
|
+
raise WebhookVerificationError()
|
|
60
|
+
if abs((time.time() if now is None else now) - int(stamp)) > WEBHOOK_TOLERANCE_SECONDS:
|
|
61
|
+
raise WebhookVerificationError()
|
|
62
|
+
expected = hmac.new(key, f"{webhook_id}.{stamp}.".encode() + raw, hashlib.sha256).digest()
|
|
63
|
+
for signature in _header(headers, "webhook-signature").split():
|
|
64
|
+
version, _, encoded = signature.partition(",")
|
|
65
|
+
if version != "v1":
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
decoded = base64.b64decode(encoded, validate=True)
|
|
69
|
+
except (binascii.Error, ValueError):
|
|
70
|
+
continue
|
|
71
|
+
if hmac.compare_digest(decoded, expected):
|
|
72
|
+
event = json.loads(raw)
|
|
73
|
+
return WebhookEvent(id=event["id"], type=event["type"], timestamp=event["timestamp"], data=event.get("data") or {})
|
|
74
|
+
raise WebhookVerificationError()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: perata
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Peráta's SDK for Python: products, customers, checkouts, payments, subscriptions and webhooks.
|
|
5
|
+
Project-URL: Homepage, https://perata.sh
|
|
6
|
+
Project-URL: Repository, https://github.com/perata-sh/sdk
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Typing :: Typed
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Peráta SDK para Python
|
|
14
|
+
|
|
15
|
+
SDK oficial da API da Peráta para Python 3.11+, sem dependências. É gerado a partir do contrato da API (`openapi.json`, na raiz de [perata-sh/sdk](https://github.com/perata-sh/sdk)) por `generator/`. Não edite os arquivos desta pasta: regenere.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pip install perata
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import os
|
|
23
|
+
from perata import Perata
|
|
24
|
+
|
|
25
|
+
perata = Perata(os.environ["PERATA_API_KEY"]) # prk_test_… ou prk_live_…
|
|
26
|
+
|
|
27
|
+
product = perata.products.create({"name": "Curso"}, idempotency_key="produto-curso")
|
|
28
|
+
|
|
29
|
+
for product in perata.products.list_iter():
|
|
30
|
+
print(product.name)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Recursos
|
|
34
|
+
|
|
35
|
+
| Serviço | Métodos |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `perata.checkouts` | `create` |
|
|
38
|
+
| `perata.customers` | `create`, `get`, `list`, `list_iter` |
|
|
39
|
+
| `perata.orders` | `list`, `list_iter` |
|
|
40
|
+
| `perata.payment_methods` | `detach`, `list`, `list_iter` |
|
|
41
|
+
| `perata.payments` | `get`, `list`, `list_iter` |
|
|
42
|
+
| `perata.prices` | `create`, `list`, `list_iter` |
|
|
43
|
+
| `perata.products` | `create`, `get`, `list`, `list_iter` |
|
|
44
|
+
| `perata.refunds` | `create` |
|
|
45
|
+
| `perata.subscriptions` | `cancel`, `get`, `list`, `list_iter` |
|
|
46
|
+
|
|
47
|
+
As escritas aceitam `idempotency_key=`, e todas aceitam `api_key=` para usar a chave de outra organização numa chamada.
|
|
48
|
+
|
|
49
|
+
## Erros
|
|
50
|
+
|
|
51
|
+
Uma recusa da API levanta `PerataError`, com `status`, `code` (`invalid_request`, `not_found`…), `fields` (o problema de cada campo) e, num 429, `retry_after` em segundos.
|
|
52
|
+
|
|
53
|
+
## Webhooks
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from perata import verify_webhook
|
|
57
|
+
|
|
58
|
+
event = verify_webhook(os.environ["PERATA_WEBHOOK_SECRET"], request.headers, request.get_data())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A assinatura é a do Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), com tolerância de 5 minutos. Passe o corpo cru, antes de interpretar.
|
|
62
|
+
|
|
63
|
+
Versão do SDK: 0.1.0 · contrato da API: 0.1.0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
perata/__init__.py,sha256=0pulwotQuTc1Bc2kF3Sc9xpFBb5M0vTpzsZ9DsYgP70,1728
|
|
2
|
+
perata/_client.py,sha256=HuQ5AuUXTCypRc4pwaWYS4GvJXoCx2egtSsY6EznUiI,3722
|
|
3
|
+
perata/_core.py,sha256=fuZvnIK1XorCy-8XoRt7A5yUThXEFnIjcJNTUDaZ4qY,534
|
|
4
|
+
perata/_errors.py,sha256=tUdEZv6dTzwxyTtenYnEMKVyi63V9kPbEls0p36VqE0,1468
|
|
5
|
+
perata/_pagination.py,sha256=TDpNR2eLKDlWGVe8z1MwfsS4XNZEWNpSnwQf03qjKno,978
|
|
6
|
+
perata/_services.py,sha256=_YHBKIBRU-ViH6p_M0Hdt9XAF-h2XPqPMaqZ_AVad0I,10442
|
|
7
|
+
perata/_version.py,sha256=5trLUXAo5BHfa6Ok37Mvn8fmSfS9jlirY0FgLc4rIBY,304
|
|
8
|
+
perata/models.py,sha256=963zxtFgY-hcF6HE-9313eVYwfs9bsOSZGstUYP_yMU,8383
|
|
9
|
+
perata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
perata/webhooks.py,sha256=mbXSaLrAXhbOa8WqyIkf4g9hisr1YjZaXBn2FlCA-Lk,2599
|
|
11
|
+
perata-0.1.0.dist-info/METADATA,sha256=ZOixpUkGOCXSMbdyyba2PjhTc1iXxOzbmc7qK3jrcT8,2205
|
|
12
|
+
perata-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
13
|
+
perata-0.1.0.dist-info/RECORD,,
|