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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx&utm_medium=paddle-python-sdk) for information about changes to the Paddle Billing platform, the Paddle API, and other developer tools.

## [Unreleased]

### Added

- Added support for client tokens see [related changelog](https://developer.paddle.com/changelog/2025/client-side-token-api?utm_source=dx&utm_medium=paddle-python-sdk)

## 1.7.0 - 2025-06-27

### Added
Expand Down
2 changes: 2 additions & 0 deletions paddle_billing/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from paddle_billing.Resources.Addresses.AddressesClient import AddressesClient
from paddle_billing.Resources.Adjustments.AdjustmentsClient import AdjustmentsClient
from paddle_billing.Resources.Businesses.BusinessesClient import BusinessesClient
from paddle_billing.Resources.ClientTokens.ClientTokensClient import ClientTokensClient
from paddle_billing.Resources.Customers.CustomersClient import CustomersClient
from paddle_billing.Resources.CustomerPortalSessions.CustomerPortalSessionsClient import CustomerPortalSessionsClient
from paddle_billing.Resources.DiscountGroups.DiscountGroupsClient import DiscountGroupsClient
Expand Down Expand Up @@ -71,6 +72,7 @@ def __init__(
self.addresses = AddressesClient(self)
self.adjustments = AdjustmentsClient(self)
self.businesses = BusinessesClient(self)
self.client_tokens = ClientTokensClient(self)
self.customers = CustomersClient(self)
self.customer_portal_sessions = CustomerPortalSessionsClient(self)
self.discounts = DiscountsClient(self)
Expand Down
33 changes: 33 additions & 0 deletions paddle_billing/Entities/ClientToken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any

from paddle_billing.Entities.Entity import Entity
from paddle_billing.Entities.ClientTokens import ClientTokenStatus


@dataclass
class ClientToken(Entity):
id: str
status: ClientTokenStatus
token: str
name: str
description: str | None
created_at: datetime
updated_at: datetime
revoked_at: datetime | None

@staticmethod
def from_dict(data: dict[str, Any]) -> ClientToken:
return ClientToken(
id=data["id"],
status=ClientTokenStatus(data["status"]),
token=data["token"],
name=data["name"],
description=data.get("description"),
created_at=datetime.fromisoformat(data["created_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
revoked_at=datetime.fromisoformat(data["revoked_at"]) if data.get("revoked_at") else None,
)
6 changes: 6 additions & 0 deletions paddle_billing/Entities/ClientTokens/ClientTokenStatus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta


class ClientTokenStatus(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
Active: "ClientTokenStatus" = "active"
Revoked: "ClientTokenStatus" = "revoked"
1 change: 1 addition & 0 deletions paddle_billing/Entities/ClientTokens/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from paddle_billing.Entities.ClientTokens.ClientTokenStatus import ClientTokenStatus
14 changes: 14 additions & 0 deletions paddle_billing/Entities/Collections/ClientTokenCollection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from __future__ import annotations
from typing import Any

from paddle_billing.Entities.Collections.Collection import Collection
from paddle_billing.Entities.Collections.Paginator import Paginator
from paddle_billing.Entities.ClientToken import ClientToken


class ClientTokenCollection(Collection[ClientToken]):
@classmethod
def from_list(cls, items_data: list[dict[str, Any]], paginator: Paginator | None = None) -> ClientTokenCollection:
items: list[ClientToken] = [ClientToken.from_dict(item) for item in items_data]

return ClientTokenCollection(items, paginator)
1 change: 1 addition & 0 deletions paddle_billing/Entities/Collections/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from paddle_billing.Entities.Collections.AddressCollection import AddressCollection
from paddle_billing.Entities.Collections.AdjustmentCollection import AdjustmentCollection
from paddle_billing.Entities.Collections.BusinessCollection import BusinessCollection
from paddle_billing.Entities.Collections.ClientTokenCollection import ClientTokenCollection
from paddle_billing.Entities.Collections.Collection import Collection
from paddle_billing.Entities.Collections.CreditBalanceCollection import CreditBalanceCollection
from paddle_billing.Entities.Collections.CustomerCollection import CustomerCollection
Expand Down
3 changes: 3 additions & 0 deletions paddle_billing/Entities/Events/EventTypeName.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class EventTypeName(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
BusinessCreated: "EventTypeName" = "business.created"
BusinessImported: "EventTypeName" = "business.imported"
BusinessUpdated: "EventTypeName" = "business.updated"
ClientTokenCreated: "EventTypeName" = "client_token.created"
ClientTokenRevoked: "EventTypeName" = "client_token.revoked"
ClientTokenUpdated: "EventTypeName" = "client_token.updated"
CustomerCreated: "EventTypeName" = "customer.created"
CustomerImported: "EventTypeName" = "customer.imported"
CustomerUpdated: "EventTypeName" = "customer.updated"
Expand Down
33 changes: 33 additions & 0 deletions paddle_billing/Notifications/Entities/ClientToken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any

from paddle_billing.Notifications.Entities.Entity import Entity
from paddle_billing.Notifications.Entities.ClientTokens import ClientTokenStatus


@dataclass
class ClientToken(Entity):
id: str
status: ClientTokenStatus
token: str
name: str
description: str | None
created_at: datetime
updated_at: datetime
revoked_at: datetime | None

@staticmethod
def from_dict(data: dict[str, Any]) -> ClientToken:
return ClientToken(
id=data["id"],
status=ClientTokenStatus(data["status"]),
token=data["token"],
name=data["name"],
description=data.get("description"),
created_at=datetime.fromisoformat(data["created_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
revoked_at=datetime.fromisoformat(data["revoked_at"]) if data.get("revoked_at") else None,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta


class ClientTokenStatus(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
Active: "ClientTokenStatus" = "active"
Revoked: "ClientTokenStatus" = "revoked"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from paddle_billing.Notifications.Entities.ClientTokens.ClientTokenStatus import ClientTokenStatus
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any

from paddle_billing.Notifications.Entities.Simulations.SimulationEntity import SimulationEntity
from paddle_billing.Notifications.Entities.ClientTokens import ClientTokenStatus
from paddle_billing.Undefined import Undefined


@dataclass
class ClientToken(SimulationEntity):
id: str | Undefined = Undefined()
status: ClientTokenStatus | Undefined = Undefined()
token: str | Undefined = Undefined()
name: str | Undefined = Undefined()
description: str | None | Undefined = Undefined()
created_at: datetime | Undefined = Undefined()
updated_at: datetime | Undefined = Undefined()
revoked_at: datetime | None | Undefined = Undefined()

@staticmethod
def from_dict(data: dict[str, Any]) -> ClientToken:
return ClientToken(
id=data.get("id", Undefined()),
status=ClientTokenStatus(data["status"]) if data.get("status") else Undefined(),
token=data.get("token", Undefined()),
name=data.get("name", Undefined()),
description=data.get("description", Undefined()),
created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else Undefined(),
updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else Undefined(),
revoked_at=(
datetime.fromisoformat(data["revoked_at"])
if data.get("revoked_at")
else data.get("revoked_at", Undefined())
),
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from paddle_billing.Notifications.Entities.Simulations.Address import Address
from paddle_billing.Notifications.Entities.Simulations.Adjustment import Adjustment
from paddle_billing.Notifications.Entities.Simulations.Business import Business
from paddle_billing.Notifications.Entities.Simulations.ClientToken import ClientToken
from paddle_billing.Notifications.Entities.Simulations.Customer import Customer
from paddle_billing.Notifications.Entities.Simulations.Discount import Discount
from paddle_billing.Notifications.Entities.Simulations.PaymentMethod import PaymentMethod
Expand Down
17 changes: 17 additions & 0 deletions paddle_billing/Notifications/Events/ClientTokenCreated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from datetime import datetime

from paddle_billing.Entities.Event import Event
from paddle_billing.Entities.Events import EventTypeName

from paddle_billing.Notifications.Entities.ClientToken import ClientToken


class ClientTokenCreated(Event):
def __init__(
self,
event_id: str,
event_type: EventTypeName,
occurred_at: datetime,
data: ClientToken,
):
super().__init__(event_id, event_type, occurred_at, data)
17 changes: 17 additions & 0 deletions paddle_billing/Notifications/Events/ClientTokenRevoked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from datetime import datetime

from paddle_billing.Entities.Event import Event
from paddle_billing.Entities.Events import EventTypeName

from paddle_billing.Notifications.Entities.ClientToken import ClientToken


class ClientTokenRevoked(Event):
def __init__(
self,
event_id: str,
event_type: EventTypeName,
occurred_at: datetime,
data: ClientToken,
):
super().__init__(event_id, event_type, occurred_at, data)
17 changes: 17 additions & 0 deletions paddle_billing/Notifications/Events/ClientTokenUpdated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from datetime import datetime

from paddle_billing.Entities.Event import Event
from paddle_billing.Entities.Events import EventTypeName

from paddle_billing.Notifications.Entities.ClientToken import ClientToken


class ClientTokenUpdated(Event):
def __init__(
self,
event_id: str,
event_type: EventTypeName,
occurred_at: datetime,
data: ClientToken,
):
super().__init__(event_id, event_type, occurred_at, data)
50 changes: 50 additions & 0 deletions paddle_billing/Resources/ClientTokens/ClientTokensClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from paddle_billing.ResponseParser import ResponseParser

from paddle_billing.Entities.Collections import Paginator, ClientTokenCollection
from paddle_billing.Entities.ClientToken import ClientToken
from paddle_billing.Entities.ClientTokens import ClientTokenStatus

from paddle_billing.Resources.ClientTokens.Operations import CreateClientToken, ListClientTokens, UpdateClientToken

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from paddle_billing.Client import Client


class ClientTokensClient:
def __init__(self, client: "Client"):
self.client = client
self.response = None

def list(self, operation: ListClientTokens | None = None) -> ClientTokenCollection:
if operation is None:
operation = ListClientTokens()

self.response = self.client.get_raw("/client-tokens", operation.get_parameters())
parser = ResponseParser(self.response)

return ClientTokenCollection.from_list(
parser.get_list(), Paginator(self.client, parser.get_pagination(), ClientTokenCollection)
)

def get(self, client_token_id: str) -> ClientToken:
self.response = self.client.get_raw(f"/client-tokens/{client_token_id}")
parser = ResponseParser(self.response)

return ClientToken.from_dict(parser.get_dict())

def create(self, operation: CreateClientToken) -> ClientToken:
self.response = self.client.post_raw("/client-tokens", operation)
parser = ResponseParser(self.response)

return ClientToken.from_dict(parser.get_dict())

def update(self, client_token_id: str, operation: UpdateClientToken) -> ClientToken:
self.response = self.client.patch_raw(f"/client-tokens/{client_token_id}", operation)
parser = ResponseParser(self.response)

return ClientToken.from_dict(parser.get_dict())

def revoke(self, client_token_id: str) -> ClientToken:
return self.update(client_token_id, UpdateClientToken(status=ClientTokenStatus.Revoked))
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from dataclasses import dataclass

from paddle_billing.Operation import Operation
from paddle_billing.Undefined import Undefined


@dataclass
class CreateClientToken(Operation):
name: str
description: str | None | Undefined = Undefined()
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from paddle_billing.Entities.ClientToken import ClientTokenStatus
from paddle_billing.EnumStringify import enum_stringify
from paddle_billing.HasParameters import HasParameters

from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException
from paddle_billing.Resources.Shared.Operations import Pager


class ListClientTokens(HasParameters):
def __init__(
self,
pager: Pager | None = None,
statuses: list[ClientTokenStatus] | None = None,
):
self.pager = pager
self.statuses = statuses if statuses is not None else []

# Validation
for field_name, field_value, field_type in [
("statuses", self.statuses, ClientTokenStatus),
]:
invalid_items = [item for item in field_value if not isinstance(item, field_type)]
if invalid_items:
raise InvalidArgumentException.array_contains_invalid_types(
field_name, field_type.__name__, invalid_items
)

def get_parameters(self) -> dict[str, str]:
parameters = {}
if self.pager:
parameters.update(self.pager.get_parameters())
if self.statuses:
parameters["status"] = ",".join(map(enum_stringify, self.statuses))

return parameters
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from dataclasses import dataclass

from paddle_billing.Operation import Operation
from paddle_billing.Undefined import Undefined
from paddle_billing.Entities.ClientTokens import ClientTokenStatus


@dataclass
class UpdateClientToken(Operation):
status: ClientTokenStatus | Undefined = Undefined()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from paddle_billing.Resources.ClientTokens.Operations.CreateClientToken import CreateClientToken
from paddle_billing.Resources.ClientTokens.Operations.ListClientTokens import ListClientTokens
from paddle_billing.Resources.ClientTokens.Operations.UpdateClientToken import UpdateClientToken
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Pricing page integration",
"description": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Pricing page integration",
"description": "Used to display prices and open checkout within our pricing page on our marketing domain."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Pricing page integration",
"description": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"status": "revoked"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"data": {
"id": "ctkn_01ghbkd0frb9k95cnhwd1bxpvk",
"token": "live_7d279f61a3499fed520f7cd8c08",
"name": "Pricing page integration",
"description": "Used to display prices and open checkout within our pricing page on our marketing domain.",
"status": "active",
"created_at": "2025-06-26T14:36:14.695000Z",
"updated_at": "2025-06-26T14:36:14.695000Z",
"revoked_at": null
},
"meta": {
"request_id": "badf3eb7-3eb8-4d4a-92f9-bc798790b7bc"
}
}
Loading