From 06d0c8043abee5e960b15604215733bfd8611dc0 Mon Sep 17 00:00:00 2001 From: David Grayston Date: Fri, 27 Mar 2026 16:21:54 +0000 Subject: [PATCH 1/2] feat: Add support for metrics --- CHANGELOG.md | 13 ++ paddle_billing/Client.py | 4 +- .../Metrics/MetricsActiveSubscribers.py | 26 ++++ .../Metrics/MetricsAmountDatapoint.py | 16 +++ .../Entities/Metrics/MetricsChargebacks.py | 26 ++++ .../Metrics/MetricsCheckoutConversion.py | 26 ++++ .../MetricsCheckoutConversionDatapoint.py | 20 ++++ .../Entities/Metrics/MetricsCountDatapoint.py | 16 +++ .../Entities/Metrics/MetricsInterval.py | 5 + .../Metrics/MetricsMonthlyRecurringRevenue.py | 29 +++++ .../MetricsMonthlyRecurringRevenueChange.py | 29 +++++ .../Entities/Metrics/MetricsRefunds.py | 29 +++++ .../Entities/Metrics/MetricsRevenue.py | 29 +++++ .../Metrics/MetricsRevenueDatapoint.py | 18 +++ paddle_billing/Entities/Metrics/__init__.py | 12 ++ .../Resources/Metrics/MetricsClient.py | 64 ++++++++++ .../Metrics/Operations/GetMetrics.py | 33 ++++++ .../Resources/Metrics/Operations/__init__.py | 1 + paddle_billing/Resources/Metrics/__init__.py | 0 setup.py | 2 +- .../Functional/Resources/Metrics/__init__.py | 0 .../response/active_subscribers.json | 17 +++ .../_fixtures/response/chargebacks.json | 17 +++ .../response/checkout_conversion.json | 17 +++ .../response/monthly_recurring_revenue.json | 18 +++ .../monthly_recurring_revenue_change.json | 18 +++ .../Metrics/_fixtures/response/refunds.json | 18 +++ .../Metrics/_fixtures/response/revenue.json | 18 +++ .../Resources/Metrics/test_MetricsClient.py | 112 ++++++++++++++++++ .../Resources/Metrics/Operations/__init__.py | 0 .../Metrics/Operations/test_GetMetrics.py | 48 ++++++++ tests/Unit/Resources/Metrics/__init__.py | 0 32 files changed, 679 insertions(+), 2 deletions(-) create mode 100644 paddle_billing/Entities/Metrics/MetricsActiveSubscribers.py create mode 100644 paddle_billing/Entities/Metrics/MetricsAmountDatapoint.py create mode 100644 paddle_billing/Entities/Metrics/MetricsChargebacks.py create mode 100644 paddle_billing/Entities/Metrics/MetricsCheckoutConversion.py create mode 100644 paddle_billing/Entities/Metrics/MetricsCheckoutConversionDatapoint.py create mode 100644 paddle_billing/Entities/Metrics/MetricsCountDatapoint.py create mode 100644 paddle_billing/Entities/Metrics/MetricsInterval.py create mode 100644 paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenue.py create mode 100644 paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenueChange.py create mode 100644 paddle_billing/Entities/Metrics/MetricsRefunds.py create mode 100644 paddle_billing/Entities/Metrics/MetricsRevenue.py create mode 100644 paddle_billing/Entities/Metrics/MetricsRevenueDatapoint.py create mode 100644 paddle_billing/Entities/Metrics/__init__.py create mode 100644 paddle_billing/Resources/Metrics/MetricsClient.py create mode 100644 paddle_billing/Resources/Metrics/Operations/GetMetrics.py create mode 100644 paddle_billing/Resources/Metrics/Operations/__init__.py create mode 100644 paddle_billing/Resources/Metrics/__init__.py create mode 100644 tests/Functional/Resources/Metrics/__init__.py create mode 100644 tests/Functional/Resources/Metrics/_fixtures/response/active_subscribers.json create mode 100644 tests/Functional/Resources/Metrics/_fixtures/response/chargebacks.json create mode 100644 tests/Functional/Resources/Metrics/_fixtures/response/checkout_conversion.json create mode 100644 tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue.json create mode 100644 tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue_change.json create mode 100644 tests/Functional/Resources/Metrics/_fixtures/response/refunds.json create mode 100644 tests/Functional/Resources/Metrics/_fixtures/response/revenue.json create mode 100644 tests/Functional/Resources/Metrics/test_MetricsClient.py create mode 100644 tests/Unit/Resources/Metrics/Operations/__init__.py create mode 100644 tests/Unit/Resources/Metrics/Operations/test_GetMetrics.py create mode 100644 tests/Unit/Resources/Metrics/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f73bee..bfb4984c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ 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. +## 1.14.0 - 2026-03-30 + +### Added + +- Added support for metrics endpoints. See [related changelog](https://developer.paddle.com/changelog/2026/metrics-api?utm_source=dx&utm_medium=paddle-python-sdk) + - `Client.metrics.get_monthly_recurring_revenue` + - `Client.metrics.get_monthly_recurring_revenue_change` + - `Client.metrics.get_active_subscribers` + - `Client.metrics.get_revenue` + - `Client.metrics.get_refunds` + - `Client.metrics.get_chargebacks` + - `Client.metrics.get_checkout_conversion` + ## 1.13.1 - 2026-03-25 ### Added diff --git a/paddle_billing/Client.py b/paddle_billing/Client.py index 6bafc67d..249c6320 100644 --- a/paddle_billing/Client.py +++ b/paddle_billing/Client.py @@ -39,6 +39,7 @@ from paddle_billing.Resources.SimulationRunEvents.SimulationRunEventsClient import SimulationRunEventsClient from paddle_billing.Resources.SimulationTypes.SimulationTypesClient import SimulationTypesClient from paddle_billing.Resources.Subscriptions.SubscriptionsClient import SubscriptionsClient +from paddle_billing.Resources.Metrics.MetricsClient import MetricsClient from paddle_billing.Resources.Transactions.TransactionsClient import TransactionsClient @@ -94,6 +95,7 @@ def __init__( self.subscriptions = SubscriptionsClient(self) self.transactions = TransactionsClient(self) self.ip_addresses = IPAddressesClient(self) + self.metrics = MetricsClient(self) @staticmethod def null_logger() -> Logger: @@ -200,7 +202,7 @@ def build_request_session(self) -> Session: "Authorization": f"Bearer {self.__api_key}", "Content-Type": "application/json", "Paddle-Version": str(self.use_api_version), - "User-Agent": "PaddleSDK/python 1.13.1", + "User-Agent": "PaddleSDK/python 1.14.0", } ) diff --git a/paddle_billing/Entities/Metrics/MetricsActiveSubscribers.py b/paddle_billing/Entities/Metrics/MetricsActiveSubscribers.py new file mode 100644 index 00000000..36ff2820 --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsActiveSubscribers.py @@ -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.MetricsCountDatapoint import MetricsCountDatapoint +from paddle_billing.Entities.Metrics.MetricsInterval import MetricsInterval + + +@dataclass +class MetricsActiveSubscribers(Entity): + timeseries: list[MetricsCountDatapoint] + starts_at: str + ends_at: str + interval: MetricsInterval + updated_at: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsActiveSubscribers: + return MetricsActiveSubscribers( + timeseries=[MetricsCountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], + starts_at=data["starts_at"], + ends_at=data["ends_at"], + interval=MetricsInterval(data["interval"]), + updated_at=data["updated_at"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsAmountDatapoint.py b/paddle_billing/Entities/Metrics/MetricsAmountDatapoint.py new file mode 100644 index 00000000..4d2a20ae --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsAmountDatapoint.py @@ -0,0 +1,16 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + + +@dataclass +class MetricsAmountDatapoint: + timestamp: str + amount: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsAmountDatapoint: + return MetricsAmountDatapoint( + timestamp=data["timestamp"], + amount=data["amount"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsChargebacks.py b/paddle_billing/Entities/Metrics/MetricsChargebacks.py new file mode 100644 index 00000000..188b3d44 --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsChargebacks.py @@ -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.MetricsCountDatapoint import MetricsCountDatapoint +from paddle_billing.Entities.Metrics.MetricsInterval import MetricsInterval + + +@dataclass +class MetricsChargebacks(Entity): + timeseries: list[MetricsCountDatapoint] + starts_at: str + ends_at: str + interval: MetricsInterval + updated_at: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsChargebacks: + return MetricsChargebacks( + timeseries=[MetricsCountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], + starts_at=data["starts_at"], + ends_at=data["ends_at"], + interval=MetricsInterval(data["interval"]), + updated_at=data["updated_at"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsCheckoutConversion.py b/paddle_billing/Entities/Metrics/MetricsCheckoutConversion.py new file mode 100644 index 00000000..d67f2165 --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsCheckoutConversion.py @@ -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.MetricsCheckoutConversionDatapoint import MetricsCheckoutConversionDatapoint +from paddle_billing.Entities.Metrics.MetricsInterval import MetricsInterval + + +@dataclass +class MetricsCheckoutConversion(Entity): + timeseries: list[MetricsCheckoutConversionDatapoint] + starts_at: str + ends_at: str + interval: MetricsInterval + updated_at: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsCheckoutConversion: + return MetricsCheckoutConversion( + timeseries=[MetricsCheckoutConversionDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], + starts_at=data["starts_at"], + ends_at=data["ends_at"], + interval=MetricsInterval(data["interval"]), + updated_at=data["updated_at"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsCheckoutConversionDatapoint.py b/paddle_billing/Entities/Metrics/MetricsCheckoutConversionDatapoint.py new file mode 100644 index 00000000..76711a27 --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsCheckoutConversionDatapoint.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + + +@dataclass +class MetricsCheckoutConversionDatapoint: + timestamp: str + count: int + completed_count: int + rate: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsCheckoutConversionDatapoint: + return MetricsCheckoutConversionDatapoint( + timestamp=data["timestamp"], + count=data["count"], + completed_count=data["completed_count"], + rate=data["rate"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsCountDatapoint.py b/paddle_billing/Entities/Metrics/MetricsCountDatapoint.py new file mode 100644 index 00000000..d51559a4 --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsCountDatapoint.py @@ -0,0 +1,16 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + + +@dataclass +class MetricsCountDatapoint: + timestamp: str + count: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsCountDatapoint: + return MetricsCountDatapoint( + timestamp=data["timestamp"], + count=data["count"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsInterval.py b/paddle_billing/Entities/Metrics/MetricsInterval.py new file mode 100644 index 00000000..0e980f61 --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsInterval.py @@ -0,0 +1,5 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class MetricsInterval(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Day: "MetricsInterval" = "day" diff --git a/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenue.py b/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenue.py new file mode 100644 index 00000000..be31650c --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenue.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.Metrics.MetricsAmountDatapoint import MetricsAmountDatapoint +from paddle_billing.Entities.Metrics.MetricsInterval import MetricsInterval +from paddle_billing.Entities.Shared import CurrencyCode + + +@dataclass +class MetricsMonthlyRecurringRevenue(Entity): + currency_code: CurrencyCode + timeseries: list[MetricsAmountDatapoint] + starts_at: str + ends_at: str + interval: MetricsInterval + updated_at: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsMonthlyRecurringRevenue: + return MetricsMonthlyRecurringRevenue( + currency_code=CurrencyCode(data["currency_code"]), + timeseries=[MetricsAmountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], + starts_at=data["starts_at"], + ends_at=data["ends_at"], + interval=MetricsInterval(data["interval"]), + updated_at=data["updated_at"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenueChange.py b/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenueChange.py new file mode 100644 index 00000000..efef0248 --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenueChange.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.Metrics.MetricsAmountDatapoint import MetricsAmountDatapoint +from paddle_billing.Entities.Metrics.MetricsInterval import MetricsInterval +from paddle_billing.Entities.Shared import CurrencyCode + + +@dataclass +class MetricsMonthlyRecurringRevenueChange(Entity): + currency_code: CurrencyCode + timeseries: list[MetricsAmountDatapoint] + starts_at: str + ends_at: str + interval: MetricsInterval + updated_at: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsMonthlyRecurringRevenueChange: + return MetricsMonthlyRecurringRevenueChange( + currency_code=CurrencyCode(data["currency_code"]), + timeseries=[MetricsAmountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], + starts_at=data["starts_at"], + ends_at=data["ends_at"], + interval=MetricsInterval(data["interval"]), + updated_at=data["updated_at"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsRefunds.py b/paddle_billing/Entities/Metrics/MetricsRefunds.py new file mode 100644 index 00000000..971f589d --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsRefunds.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.Metrics.MetricsAmountDatapoint import MetricsAmountDatapoint +from paddle_billing.Entities.Metrics.MetricsInterval import MetricsInterval +from paddle_billing.Entities.Shared import CurrencyCode + + +@dataclass +class MetricsRefunds(Entity): + currency_code: CurrencyCode + timeseries: list[MetricsAmountDatapoint] + starts_at: str + ends_at: str + interval: MetricsInterval + updated_at: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsRefunds: + return MetricsRefunds( + currency_code=CurrencyCode(data["currency_code"]), + timeseries=[MetricsAmountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], + starts_at=data["starts_at"], + ends_at=data["ends_at"], + interval=MetricsInterval(data["interval"]), + updated_at=data["updated_at"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsRevenue.py b/paddle_billing/Entities/Metrics/MetricsRevenue.py new file mode 100644 index 00000000..93884f2e --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsRevenue.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.Metrics.MetricsInterval import MetricsInterval +from paddle_billing.Entities.Metrics.MetricsRevenueDatapoint import MetricsRevenueDatapoint +from paddle_billing.Entities.Shared import CurrencyCode + + +@dataclass +class MetricsRevenue(Entity): + currency_code: CurrencyCode + timeseries: list[MetricsRevenueDatapoint] + starts_at: str + ends_at: str + interval: MetricsInterval + updated_at: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsRevenue: + return MetricsRevenue( + currency_code=CurrencyCode(data["currency_code"]), + timeseries=[MetricsRevenueDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], + starts_at=data["starts_at"], + ends_at=data["ends_at"], + interval=MetricsInterval(data["interval"]), + updated_at=data["updated_at"], + ) diff --git a/paddle_billing/Entities/Metrics/MetricsRevenueDatapoint.py b/paddle_billing/Entities/Metrics/MetricsRevenueDatapoint.py new file mode 100644 index 00000000..b9f83100 --- /dev/null +++ b/paddle_billing/Entities/Metrics/MetricsRevenueDatapoint.py @@ -0,0 +1,18 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + + +@dataclass +class MetricsRevenueDatapoint: + timestamp: str + amount: str + count: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> MetricsRevenueDatapoint: + return MetricsRevenueDatapoint( + timestamp=data["timestamp"], + amount=data["amount"], + count=data["count"], + ) diff --git a/paddle_billing/Entities/Metrics/__init__.py b/paddle_billing/Entities/Metrics/__init__.py new file mode 100644 index 00000000..51c17a6a --- /dev/null +++ b/paddle_billing/Entities/Metrics/__init__.py @@ -0,0 +1,12 @@ +from paddle_billing.Entities.Metrics.MetricsInterval import MetricsInterval +from paddle_billing.Entities.Metrics.MetricsAmountDatapoint import MetricsAmountDatapoint +from paddle_billing.Entities.Metrics.MetricsCountDatapoint import MetricsCountDatapoint +from paddle_billing.Entities.Metrics.MetricsRevenueDatapoint import MetricsRevenueDatapoint +from paddle_billing.Entities.Metrics.MetricsCheckoutConversionDatapoint import MetricsCheckoutConversionDatapoint +from paddle_billing.Entities.Metrics.MetricsMonthlyRecurringRevenue import MetricsMonthlyRecurringRevenue +from paddle_billing.Entities.Metrics.MetricsMonthlyRecurringRevenueChange import MetricsMonthlyRecurringRevenueChange +from paddle_billing.Entities.Metrics.MetricsActiveSubscribers import MetricsActiveSubscribers +from paddle_billing.Entities.Metrics.MetricsRevenue import MetricsRevenue +from paddle_billing.Entities.Metrics.MetricsRefunds import MetricsRefunds +from paddle_billing.Entities.Metrics.MetricsChargebacks import MetricsChargebacks +from paddle_billing.Entities.Metrics.MetricsCheckoutConversion import MetricsCheckoutConversion diff --git a/paddle_billing/Resources/Metrics/MetricsClient.py b/paddle_billing/Resources/Metrics/MetricsClient.py new file mode 100644 index 00000000..982a5403 --- /dev/null +++ b/paddle_billing/Resources/Metrics/MetricsClient.py @@ -0,0 +1,64 @@ +from paddle_billing.ResponseParser import ResponseParser + +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.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 typing import TYPE_CHECKING + +if TYPE_CHECKING: + from paddle_billing.Client import Client + + +class MetricsClient: + def __init__(self, client: "Client"): + self.client = client + self.response = None + + def get_monthly_recurring_revenue(self, operation: GetMetrics) -> MetricsMonthlyRecurringRevenue: + self.response = self.client.get_raw("/metrics/monthly-recurring-revenue", operation.get_parameters()) + parser = ResponseParser(self.response) + + return MetricsMonthlyRecurringRevenue.from_dict(parser.get_dict()) + + def get_monthly_recurring_revenue_change(self, operation: GetMetrics) -> MetricsMonthlyRecurringRevenueChange: + self.response = self.client.get_raw("/metrics/monthly-recurring-revenue-change", operation.get_parameters()) + parser = ResponseParser(self.response) + + return MetricsMonthlyRecurringRevenueChange.from_dict(parser.get_dict()) + + def get_active_subscribers(self, operation: GetMetrics) -> MetricsActiveSubscribers: + self.response = self.client.get_raw("/metrics/active-subscribers", operation.get_parameters()) + parser = ResponseParser(self.response) + + return MetricsActiveSubscribers.from_dict(parser.get_dict()) + + def get_revenue(self, operation: GetMetrics) -> MetricsRevenue: + self.response = self.client.get_raw("/metrics/revenue", operation.get_parameters()) + parser = ResponseParser(self.response) + + return MetricsRevenue.from_dict(parser.get_dict()) + + def get_refunds(self, operation: GetMetrics) -> MetricsRefunds: + self.response = self.client.get_raw("/metrics/refunds", operation.get_parameters()) + parser = ResponseParser(self.response) + + return MetricsRefunds.from_dict(parser.get_dict()) + + def get_chargebacks(self, operation: GetMetrics) -> MetricsChargebacks: + self.response = self.client.get_raw("/metrics/chargebacks", operation.get_parameters()) + parser = ResponseParser(self.response) + + return MetricsChargebacks.from_dict(parser.get_dict()) + + def get_checkout_conversion(self, operation: GetMetrics) -> MetricsCheckoutConversion: + self.response = self.client.get_raw("/metrics/checkout-conversion", operation.get_parameters()) + parser = ResponseParser(self.response) + + return MetricsCheckoutConversion.from_dict(parser.get_dict()) diff --git a/paddle_billing/Resources/Metrics/Operations/GetMetrics.py b/paddle_billing/Resources/Metrics/Operations/GetMetrics.py new file mode 100644 index 00000000..d87d8382 --- /dev/null +++ b/paddle_billing/Resources/Metrics/Operations/GetMetrics.py @@ -0,0 +1,33 @@ +from datetime import date + +from paddle_billing.HasParameters import HasParameters + + +class GetMetrics(HasParameters): + def __init__( + self, + date_from: str, + date_to: str, + ): + self.date_from = date_from + self.date_to = date_to + + for field_name, field_value in [ + ("date_from", self.date_from), + ("date_to", self.date_to), + ]: + try: + date.fromisoformat(field_value) + except (ValueError, TypeError): + raise ValueError(f"'{field_name}' must be a date string in YYYY-MM-DD format, got '{field_value}'") + + if "T" in field_value: + raise ValueError( + f"'{field_name}' must be a date string in YYYY-MM-DD format without time, got '{field_value}'" + ) + + def get_parameters(self) -> dict[str, str]: + return { + "from": self.date_from, + "to": self.date_to, + } diff --git a/paddle_billing/Resources/Metrics/Operations/__init__.py b/paddle_billing/Resources/Metrics/Operations/__init__.py new file mode 100644 index 00000000..4aa9524a --- /dev/null +++ b/paddle_billing/Resources/Metrics/Operations/__init__.py @@ -0,0 +1 @@ +from paddle_billing.Resources.Metrics.Operations.GetMetrics import GetMetrics diff --git a/paddle_billing/Resources/Metrics/__init__.py b/paddle_billing/Resources/Metrics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py index 92dbb2ae..f4e7f578 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup( - version="1.13.1", + version="1.14.0", author="Paddle and contributors", author_email="team-dx@paddle.com", description="Paddle's Python SDK for Paddle Billing", diff --git a/tests/Functional/Resources/Metrics/__init__.py b/tests/Functional/Resources/Metrics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/active_subscribers.json b/tests/Functional/Resources/Metrics/_fixtures/response/active_subscribers.json new file mode 100644 index 00000000..902cf50c --- /dev/null +++ b/tests/Functional/Resources/Metrics/_fixtures/response/active_subscribers.json @@ -0,0 +1,17 @@ +{ + "data": { + "timeseries": [ + { "timestamp": "2025-09-01T00:00:00Z", "count": 1250 }, + { "timestamp": "2025-09-02T00:00:00Z", "count": 1267 }, + { "timestamp": "2025-09-03T00:00:00Z", "count": 1284 }, + { "timestamp": "2025-09-04T00:00:00Z", "count": 1291 } + ], + "starts_at": "2025-09-01T00:00:00Z", + "ends_at": "2025-09-05T00:00:00Z", + "interval": "day", + "updated_at": "2025-09-04T20:30:00Z" + }, + "meta": { + "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" + } +} diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/chargebacks.json b/tests/Functional/Resources/Metrics/_fixtures/response/chargebacks.json new file mode 100644 index 00000000..00053153 --- /dev/null +++ b/tests/Functional/Resources/Metrics/_fixtures/response/chargebacks.json @@ -0,0 +1,17 @@ +{ + "data": { + "timeseries": [ + { "timestamp": "2025-09-01T00:00:00Z", "count": 1 }, + { "timestamp": "2025-09-02T00:00:00Z", "count": 2 }, + { "timestamp": "2025-09-03T00:00:00Z", "count": 0 }, + { "timestamp": "2025-09-04T00:00:00Z", "count": 1 } + ], + "starts_at": "2025-09-01T00:00:00Z", + "ends_at": "2025-09-05T00:00:00Z", + "interval": "day", + "updated_at": "2026-03-03T06:30:32.811Z" + }, + "meta": { + "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" + } +} diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/checkout_conversion.json b/tests/Functional/Resources/Metrics/_fixtures/response/checkout_conversion.json new file mode 100644 index 00000000..8c282289 --- /dev/null +++ b/tests/Functional/Resources/Metrics/_fixtures/response/checkout_conversion.json @@ -0,0 +1,17 @@ +{ + "data": { + "timeseries": [ + { "timestamp": "2025-09-01T00:00:00Z", "count": 151, "completed_count": 5, "rate": "0.033113" }, + { "timestamp": "2025-09-02T00:00:00Z", "count": 66, "completed_count": 11, "rate": "0.166667" }, + { "timestamp": "2025-09-03T00:00:00Z", "count": 139, "completed_count": 12, "rate": "0.086331" }, + { "timestamp": "2025-09-04T00:00:00Z", "count": 210, "completed_count": 28, "rate": "0.133333" } + ], + "starts_at": "2025-09-01T00:00:00Z", + "ends_at": "2025-09-05T00:00:00Z", + "interval": "day", + "updated_at": "2025-09-04T20:30:00Z" + }, + "meta": { + "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" + } +} diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue.json b/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue.json new file mode 100644 index 00000000..b309174c --- /dev/null +++ b/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue.json @@ -0,0 +1,18 @@ +{ + "data": { + "currency_code": "USD", + "timeseries": [ + { "timestamp": "2025-09-01T00:00:00Z", "amount": "1286023068" }, + { "timestamp": "2025-09-02T00:00:00Z", "amount": "1345678901" }, + { "timestamp": "2025-09-03T00:00:00Z", "amount": "1398765432" }, + { "timestamp": "2025-09-04T00:00:00Z", "amount": "1420987654" } + ], + "starts_at": "2025-09-01T00:00:00Z", + "ends_at": "2025-09-05T00:00:00Z", + "interval": "day", + "updated_at": "2025-09-04T20:30:00Z" + }, + "meta": { + "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" + } +} diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue_change.json b/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue_change.json new file mode 100644 index 00000000..fa1ef803 --- /dev/null +++ b/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue_change.json @@ -0,0 +1,18 @@ +{ + "data": { + "currency_code": "USD", + "timeseries": [ + { "timestamp": "2025-09-01T00:00:00Z", "amount": "125000" }, + { "timestamp": "2025-09-02T00:00:00Z", "amount": "-75000" }, + { "timestamp": "2025-09-03T00:00:00Z", "amount": "200000" }, + { "timestamp": "2025-09-04T00:00:00Z", "amount": "50000" } + ], + "starts_at": "2025-09-01T00:00:00Z", + "ends_at": "2025-09-05T00:00:00Z", + "interval": "day", + "updated_at": "2025-09-04T20:30:00Z" + }, + "meta": { + "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" + } +} diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/refunds.json b/tests/Functional/Resources/Metrics/_fixtures/response/refunds.json new file mode 100644 index 00000000..8661f552 --- /dev/null +++ b/tests/Functional/Resources/Metrics/_fixtures/response/refunds.json @@ -0,0 +1,18 @@ +{ + "data": { + "currency_code": "USD", + "timeseries": [ + { "timestamp": "2025-09-01T00:00:00Z", "amount": "10000" }, + { "timestamp": "2025-09-02T00:00:00Z", "amount": "0" }, + { "timestamp": "2025-09-03T00:00:00Z", "amount": "0" }, + { "timestamp": "2025-09-04T00:00:00Z", "amount": "0" } + ], + "starts_at": "2025-09-01T00:00:00Z", + "ends_at": "2025-09-05T00:00:00Z", + "interval": "day", + "updated_at": "2025-09-04T20:30:00Z" + }, + "meta": { + "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" + } +} diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/revenue.json b/tests/Functional/Resources/Metrics/_fixtures/response/revenue.json new file mode 100644 index 00000000..a6818819 --- /dev/null +++ b/tests/Functional/Resources/Metrics/_fixtures/response/revenue.json @@ -0,0 +1,18 @@ +{ + "data": { + "currency_code": "USD", + "timeseries": [ + { "timestamp": "2025-09-01T00:00:00Z", "amount": "1286023068", "count": 100 }, + { "timestamp": "2025-09-02T00:00:00Z", "amount": "1345678901", "count": 100 }, + { "timestamp": "2025-09-03T00:00:00Z", "amount": "1398765432", "count": 100 }, + { "timestamp": "2025-09-04T00:00:00Z", "amount": "1420987654", "count": 100 } + ], + "starts_at": "2025-09-01T00:00:00Z", + "ends_at": "2025-09-05T00:00:00Z", + "interval": "day", + "updated_at": "2025-09-04T20:30:00Z" + }, + "meta": { + "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" + } +} diff --git a/tests/Functional/Resources/Metrics/test_MetricsClient.py b/tests/Functional/Resources/Metrics/test_MetricsClient.py new file mode 100644 index 00000000..00819331 --- /dev/null +++ b/tests/Functional/Resources/Metrics/test_MetricsClient.py @@ -0,0 +1,112 @@ +from json import dumps, loads +from paddle_billing.Json import PayloadEncoder +from pytest import mark +from urllib.parse import unquote + +from paddle_billing.Entities.Metrics import ( + MetricsActiveSubscribers, + MetricsChargebacks, + MetricsCheckoutConversion, + MetricsMonthlyRecurringRevenue, + MetricsMonthlyRecurringRevenueChange, + MetricsRefunds, + MetricsRevenue, +) + +from paddle_billing.Resources.Metrics.Operations import GetMetrics + +from tests.Utils.ReadsFixture import ReadsFixtures + + +class TestMetricsClient: + @mark.parametrize( + "method, operation, expected_response_body, expected_url, expected_entity_type", + [ + ( + "get_monthly_recurring_revenue", + GetMetrics(date_from="2025-09-01", date_to="2025-09-05"), + ReadsFixtures.read_raw_json_fixture("response/monthly_recurring_revenue"), + "/metrics/monthly-recurring-revenue?from=2025-09-01&to=2025-09-05", + MetricsMonthlyRecurringRevenue, + ), + ( + "get_monthly_recurring_revenue_change", + GetMetrics(date_from="2025-09-01", date_to="2025-09-05"), + ReadsFixtures.read_raw_json_fixture("response/monthly_recurring_revenue_change"), + "/metrics/monthly-recurring-revenue-change?from=2025-09-01&to=2025-09-05", + MetricsMonthlyRecurringRevenueChange, + ), + ( + "get_active_subscribers", + GetMetrics(date_from="2025-09-01", date_to="2025-09-05"), + ReadsFixtures.read_raw_json_fixture("response/active_subscribers"), + "/metrics/active-subscribers?from=2025-09-01&to=2025-09-05", + MetricsActiveSubscribers, + ), + ( + "get_revenue", + GetMetrics(date_from="2025-09-01", date_to="2025-09-05"), + ReadsFixtures.read_raw_json_fixture("response/revenue"), + "/metrics/revenue?from=2025-09-01&to=2025-09-05", + MetricsRevenue, + ), + ( + "get_refunds", + GetMetrics(date_from="2025-09-01", date_to="2025-09-05"), + ReadsFixtures.read_raw_json_fixture("response/refunds"), + "/metrics/refunds?from=2025-09-01&to=2025-09-05", + MetricsRefunds, + ), + ( + "get_chargebacks", + GetMetrics(date_from="2025-09-01", date_to="2025-09-05"), + ReadsFixtures.read_raw_json_fixture("response/chargebacks"), + "/metrics/chargebacks?from=2025-09-01&to=2025-09-05", + MetricsChargebacks, + ), + ( + "get_checkout_conversion", + GetMetrics(date_from="2025-09-01", date_to="2025-09-05"), + ReadsFixtures.read_raw_json_fixture("response/checkout_conversion"), + "/metrics/checkout-conversion?from=2025-09-01&to=2025-09-05", + MetricsCheckoutConversion, + ), + ], + ids=[ + "Get monthly recurring revenue", + "Get monthly recurring revenue change", + "Get active subscribers", + "Get revenue", + "Get refunds", + "Get chargebacks", + "Get checkout conversion", + ], + ) + def test_metrics_returns_expected_response( + self, + test_client, + mock_requests, + method, + operation, + expected_response_body, + expected_url, + expected_entity_type, + ): + expected_url = f"{test_client.base_url}{expected_url}" + mock_requests.get(expected_url, status_code=200, text=expected_response_body) + + response = getattr(test_client.client.metrics, method)(operation) + response_json = test_client.client.metrics.response.json() + last_request = mock_requests.last_request + + assert isinstance(response, expected_entity_type) + assert last_request is not None + assert last_request.method == "GET" + assert test_client.client.status_code == 200 + assert unquote(last_request.url) == expected_url, "The URL does not match the expected URL" + assert ( + loads(dumps(response, cls=PayloadEncoder)) == loads(str(expected_response_body))["data"] + ), "The metrics response object doesn't match the expected fixture JSON" + assert response_json == loads( + str(expected_response_body) + ), "The response JSON doesn't match the expected fixture JSON" diff --git a/tests/Unit/Resources/Metrics/Operations/__init__.py b/tests/Unit/Resources/Metrics/Operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Unit/Resources/Metrics/Operations/test_GetMetrics.py b/tests/Unit/Resources/Metrics/Operations/test_GetMetrics.py new file mode 100644 index 00000000..728ce1f6 --- /dev/null +++ b/tests/Unit/Resources/Metrics/Operations/test_GetMetrics.py @@ -0,0 +1,48 @@ +from pytest import mark, raises + +from paddle_billing.Resources.Metrics.Operations import GetMetrics + + +class TestGetMetrics: + def test_accepts_valid_date_strings(self): + operation = GetMetrics(date_from="2025-09-01", date_to="2025-09-05") + + assert operation.get_parameters() == {"from": "2025-09-01", "to": "2025-09-05"} + + @mark.parametrize( + "date_from, date_to", + [ + ("2025-09-01T00:00:00Z", "2025-09-05"), + ("2025-09-01", "2025-09-05T23:59:59Z"), + ("2025-09-01T00:00:00.000000Z", "2025-09-05"), + ], + ids=[ + "date_from with datetime", + "date_to with datetime", + "date_from with microseconds", + ], + ) + def test_rejects_datetime_strings(self, date_from, date_to): + with raises(ValueError, match="YYYY-MM-DD format"): + GetMetrics(date_from=date_from, date_to=date_to) + + @mark.parametrize( + "date_from, date_to", + [ + ("not-a-date", "2025-09-05"), + ("2025-09-01", "invalid"), + ("", "2025-09-05"), + ("2025/09/01", "2025-09-05"), + ("01-09-2025", "2025-09-05"), + ], + ids=[ + "date_from not a date", + "date_to not a date", + "date_from empty string", + "date_from wrong separator", + "date_from wrong order", + ], + ) + def test_rejects_invalid_date_strings(self, date_from, date_to): + with raises(ValueError, match="YYYY-MM-DD format"): + GetMetrics(date_from=date_from, date_to=date_to) diff --git a/tests/Unit/Resources/Metrics/__init__.py b/tests/Unit/Resources/Metrics/__init__.py new file mode 100644 index 00000000..e69de29b From 5f2c08ee7268850d0fb5acceb38dedaac6fa5945 Mon Sep 17 00:00:00 2001 From: David Grayston Date: Mon, 30 Mar 2026 13:03:09 +0100 Subject: [PATCH 2/2] feat: Convert date strings to datetime --- .../Entities/Metrics/MetricsActiveSubscribers.py | 13 +++++++------ .../Entities/Metrics/MetricsAmountDatapoint.py | 5 +++-- .../Entities/Metrics/MetricsChargebacks.py | 13 +++++++------ .../Entities/Metrics/MetricsCheckoutConversion.py | 13 +++++++------ .../Metrics/MetricsCheckoutConversionDatapoint.py | 5 +++-- .../Entities/Metrics/MetricsCountDatapoint.py | 5 +++-- .../Metrics/MetricsMonthlyRecurringRevenue.py | 13 +++++++------ .../MetricsMonthlyRecurringRevenueChange.py | 13 +++++++------ paddle_billing/Entities/Metrics/MetricsRefunds.py | 13 +++++++------ paddle_billing/Entities/Metrics/MetricsRevenue.py | 13 +++++++------ .../Entities/Metrics/MetricsRevenueDatapoint.py | 5 +++-- .../_fixtures/response/active_subscribers.json | 14 +++++++------- .../Metrics/_fixtures/response/chargebacks.json | 14 +++++++------- .../_fixtures/response/checkout_conversion.json | 14 +++++++------- .../response/monthly_recurring_revenue.json | 14 +++++++------- .../response/monthly_recurring_revenue_change.json | 14 +++++++------- .../Metrics/_fixtures/response/refunds.json | 14 +++++++------- .../Metrics/_fixtures/response/revenue.json | 14 +++++++------- 18 files changed, 110 insertions(+), 99 deletions(-) diff --git a/paddle_billing/Entities/Metrics/MetricsActiveSubscribers.py b/paddle_billing/Entities/Metrics/MetricsActiveSubscribers.py index 36ff2820..d91af8c3 100644 --- a/paddle_billing/Entities/Metrics/MetricsActiveSubscribers.py +++ b/paddle_billing/Entities/Metrics/MetricsActiveSubscribers.py @@ -1,5 +1,6 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any from paddle_billing.Entities.Entity import Entity @@ -10,17 +11,17 @@ @dataclass class MetricsActiveSubscribers(Entity): timeseries: list[MetricsCountDatapoint] - starts_at: str - ends_at: str + starts_at: datetime + ends_at: datetime interval: MetricsInterval - updated_at: str + updated_at: datetime @staticmethod def from_dict(data: dict[str, Any]) -> MetricsActiveSubscribers: return MetricsActiveSubscribers( timeseries=[MetricsCountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], - starts_at=data["starts_at"], - ends_at=data["ends_at"], + starts_at=datetime.fromisoformat(data["starts_at"]), + ends_at=datetime.fromisoformat(data["ends_at"]), interval=MetricsInterval(data["interval"]), - updated_at=data["updated_at"], + updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Entities/Metrics/MetricsAmountDatapoint.py b/paddle_billing/Entities/Metrics/MetricsAmountDatapoint.py index 4d2a20ae..28f62d5d 100644 --- a/paddle_billing/Entities/Metrics/MetricsAmountDatapoint.py +++ b/paddle_billing/Entities/Metrics/MetricsAmountDatapoint.py @@ -1,16 +1,17 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any @dataclass class MetricsAmountDatapoint: - timestamp: str + timestamp: datetime amount: str @staticmethod def from_dict(data: dict[str, Any]) -> MetricsAmountDatapoint: return MetricsAmountDatapoint( - timestamp=data["timestamp"], + timestamp=datetime.fromisoformat(data["timestamp"]), amount=data["amount"], ) diff --git a/paddle_billing/Entities/Metrics/MetricsChargebacks.py b/paddle_billing/Entities/Metrics/MetricsChargebacks.py index 188b3d44..5144493f 100644 --- a/paddle_billing/Entities/Metrics/MetricsChargebacks.py +++ b/paddle_billing/Entities/Metrics/MetricsChargebacks.py @@ -1,5 +1,6 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any from paddle_billing.Entities.Entity import Entity @@ -10,17 +11,17 @@ @dataclass class MetricsChargebacks(Entity): timeseries: list[MetricsCountDatapoint] - starts_at: str - ends_at: str + starts_at: datetime + ends_at: datetime interval: MetricsInterval - updated_at: str + updated_at: datetime @staticmethod def from_dict(data: dict[str, Any]) -> MetricsChargebacks: return MetricsChargebacks( timeseries=[MetricsCountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], - starts_at=data["starts_at"], - ends_at=data["ends_at"], + starts_at=datetime.fromisoformat(data["starts_at"]), + ends_at=datetime.fromisoformat(data["ends_at"]), interval=MetricsInterval(data["interval"]), - updated_at=data["updated_at"], + updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Entities/Metrics/MetricsCheckoutConversion.py b/paddle_billing/Entities/Metrics/MetricsCheckoutConversion.py index d67f2165..d2a0de0c 100644 --- a/paddle_billing/Entities/Metrics/MetricsCheckoutConversion.py +++ b/paddle_billing/Entities/Metrics/MetricsCheckoutConversion.py @@ -1,5 +1,6 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any from paddle_billing.Entities.Entity import Entity @@ -10,17 +11,17 @@ @dataclass class MetricsCheckoutConversion(Entity): timeseries: list[MetricsCheckoutConversionDatapoint] - starts_at: str - ends_at: str + starts_at: datetime + ends_at: datetime interval: MetricsInterval - updated_at: str + updated_at: datetime @staticmethod def from_dict(data: dict[str, Any]) -> MetricsCheckoutConversion: return MetricsCheckoutConversion( timeseries=[MetricsCheckoutConversionDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], - starts_at=data["starts_at"], - ends_at=data["ends_at"], + starts_at=datetime.fromisoformat(data["starts_at"]), + ends_at=datetime.fromisoformat(data["ends_at"]), interval=MetricsInterval(data["interval"]), - updated_at=data["updated_at"], + updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Entities/Metrics/MetricsCheckoutConversionDatapoint.py b/paddle_billing/Entities/Metrics/MetricsCheckoutConversionDatapoint.py index 76711a27..10cfdd05 100644 --- a/paddle_billing/Entities/Metrics/MetricsCheckoutConversionDatapoint.py +++ b/paddle_billing/Entities/Metrics/MetricsCheckoutConversionDatapoint.py @@ -1,11 +1,12 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any @dataclass class MetricsCheckoutConversionDatapoint: - timestamp: str + timestamp: datetime count: int completed_count: int rate: str @@ -13,7 +14,7 @@ class MetricsCheckoutConversionDatapoint: @staticmethod def from_dict(data: dict[str, Any]) -> MetricsCheckoutConversionDatapoint: return MetricsCheckoutConversionDatapoint( - timestamp=data["timestamp"], + timestamp=datetime.fromisoformat(data["timestamp"]), count=data["count"], completed_count=data["completed_count"], rate=data["rate"], diff --git a/paddle_billing/Entities/Metrics/MetricsCountDatapoint.py b/paddle_billing/Entities/Metrics/MetricsCountDatapoint.py index d51559a4..edfe8303 100644 --- a/paddle_billing/Entities/Metrics/MetricsCountDatapoint.py +++ b/paddle_billing/Entities/Metrics/MetricsCountDatapoint.py @@ -1,16 +1,17 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any @dataclass class MetricsCountDatapoint: - timestamp: str + timestamp: datetime count: int @staticmethod def from_dict(data: dict[str, Any]) -> MetricsCountDatapoint: return MetricsCountDatapoint( - timestamp=data["timestamp"], + timestamp=datetime.fromisoformat(data["timestamp"]), count=data["count"], ) diff --git a/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenue.py b/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenue.py index be31650c..0101916a 100644 --- a/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenue.py +++ b/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenue.py @@ -1,5 +1,6 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any from paddle_billing.Entities.Entity import Entity @@ -12,18 +13,18 @@ class MetricsMonthlyRecurringRevenue(Entity): currency_code: CurrencyCode timeseries: list[MetricsAmountDatapoint] - starts_at: str - ends_at: str + starts_at: datetime + ends_at: datetime interval: MetricsInterval - updated_at: str + updated_at: datetime @staticmethod def from_dict(data: dict[str, Any]) -> MetricsMonthlyRecurringRevenue: return MetricsMonthlyRecurringRevenue( currency_code=CurrencyCode(data["currency_code"]), timeseries=[MetricsAmountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], - starts_at=data["starts_at"], - ends_at=data["ends_at"], + starts_at=datetime.fromisoformat(data["starts_at"]), + ends_at=datetime.fromisoformat(data["ends_at"]), interval=MetricsInterval(data["interval"]), - updated_at=data["updated_at"], + updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenueChange.py b/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenueChange.py index efef0248..ed308ca2 100644 --- a/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenueChange.py +++ b/paddle_billing/Entities/Metrics/MetricsMonthlyRecurringRevenueChange.py @@ -1,5 +1,6 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any from paddle_billing.Entities.Entity import Entity @@ -12,18 +13,18 @@ class MetricsMonthlyRecurringRevenueChange(Entity): currency_code: CurrencyCode timeseries: list[MetricsAmountDatapoint] - starts_at: str - ends_at: str + starts_at: datetime + ends_at: datetime interval: MetricsInterval - updated_at: str + updated_at: datetime @staticmethod def from_dict(data: dict[str, Any]) -> MetricsMonthlyRecurringRevenueChange: return MetricsMonthlyRecurringRevenueChange( currency_code=CurrencyCode(data["currency_code"]), timeseries=[MetricsAmountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], - starts_at=data["starts_at"], - ends_at=data["ends_at"], + starts_at=datetime.fromisoformat(data["starts_at"]), + ends_at=datetime.fromisoformat(data["ends_at"]), interval=MetricsInterval(data["interval"]), - updated_at=data["updated_at"], + updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Entities/Metrics/MetricsRefunds.py b/paddle_billing/Entities/Metrics/MetricsRefunds.py index 971f589d..deb544bf 100644 --- a/paddle_billing/Entities/Metrics/MetricsRefunds.py +++ b/paddle_billing/Entities/Metrics/MetricsRefunds.py @@ -1,5 +1,6 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any from paddle_billing.Entities.Entity import Entity @@ -12,18 +13,18 @@ class MetricsRefunds(Entity): currency_code: CurrencyCode timeseries: list[MetricsAmountDatapoint] - starts_at: str - ends_at: str + starts_at: datetime + ends_at: datetime interval: MetricsInterval - updated_at: str + updated_at: datetime @staticmethod def from_dict(data: dict[str, Any]) -> MetricsRefunds: return MetricsRefunds( currency_code=CurrencyCode(data["currency_code"]), timeseries=[MetricsAmountDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], - starts_at=data["starts_at"], - ends_at=data["ends_at"], + starts_at=datetime.fromisoformat(data["starts_at"]), + ends_at=datetime.fromisoformat(data["ends_at"]), interval=MetricsInterval(data["interval"]), - updated_at=data["updated_at"], + updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Entities/Metrics/MetricsRevenue.py b/paddle_billing/Entities/Metrics/MetricsRevenue.py index 93884f2e..311e4e87 100644 --- a/paddle_billing/Entities/Metrics/MetricsRevenue.py +++ b/paddle_billing/Entities/Metrics/MetricsRevenue.py @@ -1,5 +1,6 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any from paddle_billing.Entities.Entity import Entity @@ -12,18 +13,18 @@ class MetricsRevenue(Entity): currency_code: CurrencyCode timeseries: list[MetricsRevenueDatapoint] - starts_at: str - ends_at: str + starts_at: datetime + ends_at: datetime interval: MetricsInterval - updated_at: str + updated_at: datetime @staticmethod def from_dict(data: dict[str, Any]) -> MetricsRevenue: return MetricsRevenue( currency_code=CurrencyCode(data["currency_code"]), timeseries=[MetricsRevenueDatapoint.from_dict(dp) for dp in data.get("timeseries", [])], - starts_at=data["starts_at"], - ends_at=data["ends_at"], + starts_at=datetime.fromisoformat(data["starts_at"]), + ends_at=datetime.fromisoformat(data["ends_at"]), interval=MetricsInterval(data["interval"]), - updated_at=data["updated_at"], + updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Entities/Metrics/MetricsRevenueDatapoint.py b/paddle_billing/Entities/Metrics/MetricsRevenueDatapoint.py index b9f83100..317d4ca5 100644 --- a/paddle_billing/Entities/Metrics/MetricsRevenueDatapoint.py +++ b/paddle_billing/Entities/Metrics/MetricsRevenueDatapoint.py @@ -1,18 +1,19 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from typing import Any @dataclass class MetricsRevenueDatapoint: - timestamp: str + timestamp: datetime amount: str count: int @staticmethod def from_dict(data: dict[str, Any]) -> MetricsRevenueDatapoint: return MetricsRevenueDatapoint( - timestamp=data["timestamp"], + timestamp=datetime.fromisoformat(data["timestamp"]), amount=data["amount"], count=data["count"], ) diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/active_subscribers.json b/tests/Functional/Resources/Metrics/_fixtures/response/active_subscribers.json index 902cf50c..4d81b016 100644 --- a/tests/Functional/Resources/Metrics/_fixtures/response/active_subscribers.json +++ b/tests/Functional/Resources/Metrics/_fixtures/response/active_subscribers.json @@ -1,15 +1,15 @@ { "data": { "timeseries": [ - { "timestamp": "2025-09-01T00:00:00Z", "count": 1250 }, - { "timestamp": "2025-09-02T00:00:00Z", "count": 1267 }, - { "timestamp": "2025-09-03T00:00:00Z", "count": 1284 }, - { "timestamp": "2025-09-04T00:00:00Z", "count": 1291 } + { "timestamp": "2025-09-01T00:00:00.000000Z", "count": 1250 }, + { "timestamp": "2025-09-02T00:00:00.000000Z", "count": 1267 }, + { "timestamp": "2025-09-03T00:00:00.000000Z", "count": 1284 }, + { "timestamp": "2025-09-04T00:00:00.000000Z", "count": 1291 } ], - "starts_at": "2025-09-01T00:00:00Z", - "ends_at": "2025-09-05T00:00:00Z", + "starts_at": "2025-09-01T00:00:00.000000Z", + "ends_at": "2025-09-05T00:00:00.000000Z", "interval": "day", - "updated_at": "2025-09-04T20:30:00Z" + "updated_at": "2025-09-04T20:30:00.000000Z" }, "meta": { "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/chargebacks.json b/tests/Functional/Resources/Metrics/_fixtures/response/chargebacks.json index 00053153..21ef9060 100644 --- a/tests/Functional/Resources/Metrics/_fixtures/response/chargebacks.json +++ b/tests/Functional/Resources/Metrics/_fixtures/response/chargebacks.json @@ -1,15 +1,15 @@ { "data": { "timeseries": [ - { "timestamp": "2025-09-01T00:00:00Z", "count": 1 }, - { "timestamp": "2025-09-02T00:00:00Z", "count": 2 }, - { "timestamp": "2025-09-03T00:00:00Z", "count": 0 }, - { "timestamp": "2025-09-04T00:00:00Z", "count": 1 } + { "timestamp": "2025-09-01T00:00:00.000000Z", "count": 1 }, + { "timestamp": "2025-09-02T00:00:00.000000Z", "count": 2 }, + { "timestamp": "2025-09-03T00:00:00.000000Z", "count": 0 }, + { "timestamp": "2025-09-04T00:00:00.000000Z", "count": 1 } ], - "starts_at": "2025-09-01T00:00:00Z", - "ends_at": "2025-09-05T00:00:00Z", + "starts_at": "2025-09-01T00:00:00.000000Z", + "ends_at": "2025-09-05T00:00:00.000000Z", "interval": "day", - "updated_at": "2026-03-03T06:30:32.811Z" + "updated_at": "2026-03-03T06:30:32.811000Z" }, "meta": { "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/checkout_conversion.json b/tests/Functional/Resources/Metrics/_fixtures/response/checkout_conversion.json index 8c282289..4fcbe6ce 100644 --- a/tests/Functional/Resources/Metrics/_fixtures/response/checkout_conversion.json +++ b/tests/Functional/Resources/Metrics/_fixtures/response/checkout_conversion.json @@ -1,15 +1,15 @@ { "data": { "timeseries": [ - { "timestamp": "2025-09-01T00:00:00Z", "count": 151, "completed_count": 5, "rate": "0.033113" }, - { "timestamp": "2025-09-02T00:00:00Z", "count": 66, "completed_count": 11, "rate": "0.166667" }, - { "timestamp": "2025-09-03T00:00:00Z", "count": 139, "completed_count": 12, "rate": "0.086331" }, - { "timestamp": "2025-09-04T00:00:00Z", "count": 210, "completed_count": 28, "rate": "0.133333" } + { "timestamp": "2025-09-01T00:00:00.000000Z", "count": 151, "completed_count": 5, "rate": "0.033113" }, + { "timestamp": "2025-09-02T00:00:00.000000Z", "count": 66, "completed_count": 11, "rate": "0.166667" }, + { "timestamp": "2025-09-03T00:00:00.000000Z", "count": 139, "completed_count": 12, "rate": "0.086331" }, + { "timestamp": "2025-09-04T00:00:00.000000Z", "count": 210, "completed_count": 28, "rate": "0.133333" } ], - "starts_at": "2025-09-01T00:00:00Z", - "ends_at": "2025-09-05T00:00:00Z", + "starts_at": "2025-09-01T00:00:00.000000Z", + "ends_at": "2025-09-05T00:00:00.000000Z", "interval": "day", - "updated_at": "2025-09-04T20:30:00Z" + "updated_at": "2025-09-04T20:30:00.000000Z" }, "meta": { "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue.json b/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue.json index b309174c..f18adef0 100644 --- a/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue.json +++ b/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue.json @@ -2,15 +2,15 @@ "data": { "currency_code": "USD", "timeseries": [ - { "timestamp": "2025-09-01T00:00:00Z", "amount": "1286023068" }, - { "timestamp": "2025-09-02T00:00:00Z", "amount": "1345678901" }, - { "timestamp": "2025-09-03T00:00:00Z", "amount": "1398765432" }, - { "timestamp": "2025-09-04T00:00:00Z", "amount": "1420987654" } + { "timestamp": "2025-09-01T00:00:00.000000Z", "amount": "1286023068" }, + { "timestamp": "2025-09-02T00:00:00.000000Z", "amount": "1345678901" }, + { "timestamp": "2025-09-03T00:00:00.000000Z", "amount": "1398765432" }, + { "timestamp": "2025-09-04T00:00:00.000000Z", "amount": "1420987654" } ], - "starts_at": "2025-09-01T00:00:00Z", - "ends_at": "2025-09-05T00:00:00Z", + "starts_at": "2025-09-01T00:00:00.000000Z", + "ends_at": "2025-09-05T00:00:00.000000Z", "interval": "day", - "updated_at": "2025-09-04T20:30:00Z" + "updated_at": "2025-09-04T20:30:00.000000Z" }, "meta": { "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue_change.json b/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue_change.json index fa1ef803..c2605a36 100644 --- a/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue_change.json +++ b/tests/Functional/Resources/Metrics/_fixtures/response/monthly_recurring_revenue_change.json @@ -2,15 +2,15 @@ "data": { "currency_code": "USD", "timeseries": [ - { "timestamp": "2025-09-01T00:00:00Z", "amount": "125000" }, - { "timestamp": "2025-09-02T00:00:00Z", "amount": "-75000" }, - { "timestamp": "2025-09-03T00:00:00Z", "amount": "200000" }, - { "timestamp": "2025-09-04T00:00:00Z", "amount": "50000" } + { "timestamp": "2025-09-01T00:00:00.000000Z", "amount": "125000" }, + { "timestamp": "2025-09-02T00:00:00.000000Z", "amount": "-75000" }, + { "timestamp": "2025-09-03T00:00:00.000000Z", "amount": "200000" }, + { "timestamp": "2025-09-04T00:00:00.000000Z", "amount": "50000" } ], - "starts_at": "2025-09-01T00:00:00Z", - "ends_at": "2025-09-05T00:00:00Z", + "starts_at": "2025-09-01T00:00:00.000000Z", + "ends_at": "2025-09-05T00:00:00.000000Z", "interval": "day", - "updated_at": "2025-09-04T20:30:00Z" + "updated_at": "2025-09-04T20:30:00.000000Z" }, "meta": { "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/refunds.json b/tests/Functional/Resources/Metrics/_fixtures/response/refunds.json index 8661f552..cf35e37f 100644 --- a/tests/Functional/Resources/Metrics/_fixtures/response/refunds.json +++ b/tests/Functional/Resources/Metrics/_fixtures/response/refunds.json @@ -2,15 +2,15 @@ "data": { "currency_code": "USD", "timeseries": [ - { "timestamp": "2025-09-01T00:00:00Z", "amount": "10000" }, - { "timestamp": "2025-09-02T00:00:00Z", "amount": "0" }, - { "timestamp": "2025-09-03T00:00:00Z", "amount": "0" }, - { "timestamp": "2025-09-04T00:00:00Z", "amount": "0" } + { "timestamp": "2025-09-01T00:00:00.000000Z", "amount": "10000" }, + { "timestamp": "2025-09-02T00:00:00.000000Z", "amount": "0" }, + { "timestamp": "2025-09-03T00:00:00.000000Z", "amount": "0" }, + { "timestamp": "2025-09-04T00:00:00.000000Z", "amount": "0" } ], - "starts_at": "2025-09-01T00:00:00Z", - "ends_at": "2025-09-05T00:00:00Z", + "starts_at": "2025-09-01T00:00:00.000000Z", + "ends_at": "2025-09-05T00:00:00.000000Z", "interval": "day", - "updated_at": "2025-09-04T20:30:00Z" + "updated_at": "2025-09-04T20:30:00.000000Z" }, "meta": { "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8" diff --git a/tests/Functional/Resources/Metrics/_fixtures/response/revenue.json b/tests/Functional/Resources/Metrics/_fixtures/response/revenue.json index a6818819..31836a65 100644 --- a/tests/Functional/Resources/Metrics/_fixtures/response/revenue.json +++ b/tests/Functional/Resources/Metrics/_fixtures/response/revenue.json @@ -2,15 +2,15 @@ "data": { "currency_code": "USD", "timeseries": [ - { "timestamp": "2025-09-01T00:00:00Z", "amount": "1286023068", "count": 100 }, - { "timestamp": "2025-09-02T00:00:00Z", "amount": "1345678901", "count": 100 }, - { "timestamp": "2025-09-03T00:00:00Z", "amount": "1398765432", "count": 100 }, - { "timestamp": "2025-09-04T00:00:00Z", "amount": "1420987654", "count": 100 } + { "timestamp": "2025-09-01T00:00:00.000000Z", "amount": "1286023068", "count": 100 }, + { "timestamp": "2025-09-02T00:00:00.000000Z", "amount": "1345678901", "count": 100 }, + { "timestamp": "2025-09-03T00:00:00.000000Z", "amount": "1398765432", "count": 100 }, + { "timestamp": "2025-09-04T00:00:00.000000Z", "amount": "1420987654", "count": 100 } ], - "starts_at": "2025-09-01T00:00:00Z", - "ends_at": "2025-09-05T00:00:00Z", + "starts_at": "2025-09-01T00:00:00.000000Z", + "ends_at": "2025-09-05T00:00:00.000000Z", "interval": "day", - "updated_at": "2025-09-04T20:30:00Z" + "updated_at": "2025-09-04T20:30:00.000000Z" }, "meta": { "request_id": "b93d9c94-c28f-4e5d-af2e-044854d7afe8"