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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx

### Added

- Added support for metrics explore. See [related changelog](https://developer.paddle.com/changelog/2026/explore-metrics-api?utm_source=dx&utm_medium=paddle-python-sdk)
- `Client.metrics.list_explore_entities`
- `Client.metrics.run_explore_query`
- Added support for `subscriptions` report type. See [related changelog](https://developer.paddle.com/changelog/2026/subscriptions-report?utm_source=dx&utm_medium=paddle-python-sdk) and [scheduled change filter changelog](https://developer.paddle.com/changelog/2026/subscriptions-report-scheduled-change-filter?utm_source=dx&utm_medium=paddle-python-sdk)
- Added `filter`, `start` (sent as `from`), and `end` (sent as `to`) parameters to list events operation. See [related changelog](https://developer.paddle.com/changelog/2026/event-notification-log-filtering?utm_source=dx&utm_medium=paddle-python-sdk)
- Added `paypal` to transaction payment `method_details`, showing the PayPal account used for a payment attempt. See [related changelog](https://developer.paddle.com/changelog/2026/paypal-transaction-payments?utm_source=dx&utm_medium=paddle-python-sdk)
Expand Down
9 changes: 9 additions & 0 deletions paddle_billing/Entities/Metrics/MetricEntity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta


class MetricEntity(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
TransactionsCompleted: "MetricEntity" = "transactions.completed"
AdjustmentsRefunds: "MetricEntity" = "adjustments.refunds"
AdjustmentsChargebacks: "MetricEntity" = "adjustments.chargebacks"
Checkouts: "MetricEntity" = "checkouts"
Subscriptions: "MetricEntity" = "subscriptions"
26 changes: 26 additions & 0 deletions paddle_billing/Entities/Metrics/MetricEntityDefinition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

from paddle_billing.Entities.Entity import Entity
from paddle_billing.Entities.Metrics.MetricEntity import MetricEntity
from paddle_billing.Entities.Metrics.MetricEntityDimension import MetricEntityDimension
from paddle_billing.Entities.Metrics.MetricEntityMeasure import MetricEntityMeasure
from paddle_billing.Entities.Metrics.MetricEntityTimeDimension import MetricEntityTimeDimension


@dataclass
class MetricEntityDefinition(Entity):
entity: MetricEntity
time_dimension: MetricEntityTimeDimension
dimensions: list[MetricEntityDimension]
measures: list[MetricEntityMeasure]

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricEntityDefinition:
return MetricEntityDefinition(
entity=MetricEntity(data["entity"]),
time_dimension=MetricEntityTimeDimension.from_dict(data["time_dimension"]),
dimensions=[MetricEntityDimension.from_dict(dimension) for dimension in data["dimensions"]],
measures=[MetricEntityMeasure.from_dict(measure) for measure in data["measures"]],
)
23 changes: 23 additions & 0 deletions paddle_billing/Entities/Metrics/MetricEntityDimension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

from paddle_billing.Entities.Metrics.MetricsFieldType import MetricsFieldType
from paddle_billing.Entities.Metrics.MetricsFilterOperator import MetricsFilterOperator


@dataclass
class MetricEntityDimension:
name: str
type: MetricsFieldType
filter_ops: list[MetricsFilterOperator]
multi_valued: bool | None = None

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricEntityDimension:
return MetricEntityDimension(
name=data["name"],
type=MetricsFieldType(data["type"]),
filter_ops=[MetricsFilterOperator(op) for op in data["filter_ops"]],
multi_valued=data.get("multi_valued"),
)
23 changes: 23 additions & 0 deletions paddle_billing/Entities/Metrics/MetricEntityMeasure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

from paddle_billing.Entities.Metrics.MetricsAggregation import MetricsAggregation
from paddle_billing.Entities.Metrics.MetricsFieldType import MetricsFieldType


@dataclass
class MetricEntityMeasure:
name: str
type: MetricsFieldType
aggs: list[MetricsAggregation] | None

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricEntityMeasure:
aggs = data.get("aggs")

return MetricEntityMeasure(
name=data["name"],
type=MetricsFieldType(data["type"]),
aggs=[MetricsAggregation(agg) for agg in aggs] if aggs is not None else None,
)
18 changes: 18 additions & 0 deletions paddle_billing/Entities/Metrics/MetricEntityTimeDimension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

from paddle_billing.Entities.Metrics.MetricsBucketInterval import MetricsBucketInterval


@dataclass
class MetricEntityTimeDimension:
intervals: list[MetricsBucketInterval]
max_lookback_days: int

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricEntityTimeDimension:
return MetricEntityTimeDimension(
intervals=[MetricsBucketInterval(interval) for interval in data["intervals"]],
max_lookback_days=data["max_lookback_days"],
)
7 changes: 7 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsAggregation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta


class MetricsAggregation(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
Count: "MetricsAggregation" = "count"
CountDistinct: "MetricsAggregation" = "count_distinct"
Sum: "MetricsAggregation" = "sum"
7 changes: 7 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsBucketInterval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta


class MetricsBucketInterval(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
Day: "MetricsBucketInterval" = "day"
Week: "MetricsBucketInterval" = "week"
Month: "MetricsBucketInterval" = "month"
36 changes: 36 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsExploreData.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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.Metrics.MetricEntity import MetricEntity
from paddle_billing.Entities.Metrics.MetricsBucketInterval import MetricsBucketInterval
from paddle_billing.Entities.Metrics.MetricsExploreFields import MetricsExploreFields
from paddle_billing.Entities.Metrics.MetricsExploreSeries import MetricsExploreSeries
from paddle_billing.Entities.Shared import CurrencyCode


@dataclass
class MetricsExploreData(Entity):
entity: MetricEntity
interval: MetricsBucketInterval
starts_at: datetime
ends_at: datetime
updated_at: datetime
fields: MetricsExploreFields
series: list[MetricsExploreSeries]
currency_code: CurrencyCode | None = None

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricsExploreData:
return MetricsExploreData(
entity=MetricEntity(data["entity"]),
interval=MetricsBucketInterval(data["interval"]),
starts_at=datetime.fromisoformat(data["starts_at"]),
ends_at=datetime.fromisoformat(data["ends_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
fields=MetricsExploreFields.from_dict(data["fields"]),
series=[MetricsExploreSeries.from_dict(series) for series in data["series"]],
currency_code=CurrencyCode(data["currency_code"]) if data.get("currency_code") else None,
)
13 changes: 13 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsExploreDataPoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any


@dataclass
class MetricsExploreDataPoint:
timestamp: str
measures: dict[str, str]

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricsExploreDataPoint:
return MetricsExploreDataPoint(timestamp=data["timestamp"], measures=dict(data["measures"]))
15 changes: 15 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsExploreFieldDimension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

from paddle_billing.Entities.Metrics.MetricsFieldType import MetricsFieldType


@dataclass
class MetricsExploreFieldDimension:
name: str
type: MetricsFieldType

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricsExploreFieldDimension:
return MetricsExploreFieldDimension(name=data["name"], type=MetricsFieldType(data["type"]))
23 changes: 23 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsExploreFieldMeasure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

from paddle_billing.Entities.Metrics.MetricsAggregation import MetricsAggregation
from paddle_billing.Entities.Metrics.MetricsFieldType import MetricsFieldType


@dataclass
class MetricsExploreFieldMeasure:
name: str
type: MetricsFieldType
field: str | None = None
agg: MetricsAggregation | None = None

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricsExploreFieldMeasure:
return MetricsExploreFieldMeasure(
name=data["name"],
type=MetricsFieldType(data["type"]),
field=data.get("field"),
agg=MetricsAggregation(data["agg"]) if data.get("agg") else None,
)
19 changes: 19 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsExploreFields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

from paddle_billing.Entities.Metrics.MetricsExploreFieldDimension import MetricsExploreFieldDimension
from paddle_billing.Entities.Metrics.MetricsExploreFieldMeasure import MetricsExploreFieldMeasure


@dataclass
class MetricsExploreFields:
dimensions: list[MetricsExploreFieldDimension]
measures: list[MetricsExploreFieldMeasure]

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricsExploreFields:
return MetricsExploreFields(
dimensions=[MetricsExploreFieldDimension.from_dict(dimension) for dimension in data["dimensions"]],
measures=[MetricsExploreFieldMeasure.from_dict(measure) for measure in data["measures"]],
)
34 changes: 34 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsExploreResponse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from urllib.parse import parse_qs, urlparse

from paddle_billing.Entities.Metrics.MetricsExploreData import MetricsExploreData
from paddle_billing.Entities.Shared import MetaPaginated


@dataclass
class MetricsExploreResponse:
data: MetricsExploreData
meta: MetaPaginated

@property
def has_more(self) -> bool:
return self.meta.pagination.has_more

@property
def next_cursor(self) -> str | None:
"""The `after` value to pass to the next run_explore_query call, or None on the last page."""
if not self.has_more or not self.meta.pagination.next:
return None

after = parse_qs(urlparse(self.meta.pagination.next).query).get("after")

return after[0] if after else None

@staticmethod
def from_dict(body: dict[str, Any]) -> MetricsExploreResponse:
return MetricsExploreResponse(
data=MetricsExploreData.from_dict(body["data"]),
meta=MetaPaginated.from_dict(body["meta"]),
)
18 changes: 18 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsExploreSeries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

from paddle_billing.Entities.Metrics.MetricsExploreDataPoint import MetricsExploreDataPoint


@dataclass
class MetricsExploreSeries:
dimensions: dict[str, str]
timeseries: list[MetricsExploreDataPoint]

@staticmethod
def from_dict(data: dict[str, Any]) -> MetricsExploreSeries:
return MetricsExploreSeries(
dimensions=dict(data["dimensions"]),
timeseries=[MetricsExploreDataPoint.from_dict(point) for point in data["timeseries"]],
)
8 changes: 8 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsFieldType.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta


class MetricsFieldType(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
String: "MetricsFieldType" = "string"
Integer: "MetricsFieldType" = "integer"
Decimal: "MetricsFieldType" = "decimal"
Currency: "MetricsFieldType" = "currency"
5 changes: 5 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsFilterOperator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta


class MetricsFilterOperator(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
In: "MetricsFilterOperator" = "in"
6 changes: 6 additions & 0 deletions paddle_billing/Entities/Metrics/MetricsSortDirection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta


class MetricsSortDirection(PaddleStrEnum, metaclass=PaddleStrEnumMeta):
Asc: "MetricsSortDirection" = "asc"
Desc: "MetricsSortDirection" = "desc"
17 changes: 17 additions & 0 deletions paddle_billing/Entities/Metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,20 @@
from paddle_billing.Entities.Metrics.MetricsRefunds import MetricsRefunds
from paddle_billing.Entities.Metrics.MetricsChargebacks import MetricsChargebacks
from paddle_billing.Entities.Metrics.MetricsCheckoutConversion import MetricsCheckoutConversion
from paddle_billing.Entities.Metrics.MetricEntity import MetricEntity
from paddle_billing.Entities.Metrics.MetricsBucketInterval import MetricsBucketInterval
from paddle_billing.Entities.Metrics.MetricsAggregation import MetricsAggregation
from paddle_billing.Entities.Metrics.MetricsFieldType import MetricsFieldType
from paddle_billing.Entities.Metrics.MetricsFilterOperator import MetricsFilterOperator
from paddle_billing.Entities.Metrics.MetricsSortDirection import MetricsSortDirection
from paddle_billing.Entities.Metrics.MetricEntityTimeDimension import MetricEntityTimeDimension
from paddle_billing.Entities.Metrics.MetricEntityDimension import MetricEntityDimension
from paddle_billing.Entities.Metrics.MetricEntityMeasure import MetricEntityMeasure
from paddle_billing.Entities.Metrics.MetricEntityDefinition import MetricEntityDefinition
from paddle_billing.Entities.Metrics.MetricsExploreFieldDimension import MetricsExploreFieldDimension
from paddle_billing.Entities.Metrics.MetricsExploreFieldMeasure import MetricsExploreFieldMeasure
from paddle_billing.Entities.Metrics.MetricsExploreFields import MetricsExploreFields
from paddle_billing.Entities.Metrics.MetricsExploreDataPoint import MetricsExploreDataPoint
from paddle_billing.Entities.Metrics.MetricsExploreSeries import MetricsExploreSeries
from paddle_billing.Entities.Metrics.MetricsExploreData import MetricsExploreData
from paddle_billing.Entities.Metrics.MetricsExploreResponse import MetricsExploreResponse
23 changes: 22 additions & 1 deletion paddle_billing/Resources/Metrics/MetricsClient.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
from paddle_billing.ResponseParser import ResponseParser

from paddle_billing.Entities.Metrics.MetricEntityDefinition import MetricEntityDefinition
from paddle_billing.Entities.Metrics.MetricsActiveSubscribers import MetricsActiveSubscribers
from paddle_billing.Entities.Metrics.MetricsChargebacks import MetricsChargebacks
from paddle_billing.Entities.Metrics.MetricsCheckoutConversion import MetricsCheckoutConversion
from paddle_billing.Entities.Metrics.MetricsExploreResponse import MetricsExploreResponse
from paddle_billing.Entities.Metrics.MetricsMonthlyRecurringRevenue import MetricsMonthlyRecurringRevenue
from paddle_billing.Entities.Metrics.MetricsMonthlyRecurringRevenueChange import MetricsMonthlyRecurringRevenueChange
from paddle_billing.Entities.Metrics.MetricsRefunds import MetricsRefunds
from paddle_billing.Entities.Metrics.MetricsRevenue import MetricsRevenue

from paddle_billing.Resources.Metrics.Operations import GetMetrics
from paddle_billing.Resources.Metrics.Operations import (
GetMetrics,
ListExploreMetricEntities,
RunExploreMetricsQuery,
)

from typing import TYPE_CHECKING

Expand Down Expand Up @@ -62,3 +68,18 @@ def get_checkout_conversion(self, operation: GetMetrics) -> MetricsCheckoutConve
parser = ResponseParser(self.response)

return MetricsCheckoutConversion.from_dict(parser.get_dict())

def list_explore_entities(self, operation: ListExploreMetricEntities | None = None) -> list[MetricEntityDefinition]:
if operation is None:
operation = ListExploreMetricEntities()

self.response = self.client.get_raw("/metrics/explore/entities", operation.get_parameters())
parser = ResponseParser(self.response)

return [MetricEntityDefinition.from_dict(entity) for entity in parser.get_list()]

def run_explore_query(self, operation: RunExploreMetricsQuery, after: str | None = None) -> MetricsExploreResponse:
parameters = {"after": after} if after else None
self.response = self.client.post_raw("/metrics/explore", operation, parameters)

return MetricsExploreResponse.from_dict(self.response.json())
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from paddle_billing.Entities.Metrics import MetricEntity
from paddle_billing.HasParameters import HasParameters


class ListExploreMetricEntities(HasParameters):
def __init__(self, entity: MetricEntity | None = None):
self.entity = entity

def get_parameters(self) -> dict[str, str]:
if self.entity is None:
return {}

return {"entity": self.entity.value}
Loading
Loading