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 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any