From cce43353dd6c249677df753b4192d69f71913810 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:22:33 +0000 Subject: [PATCH] feat(api)!: update SDKs to meteroid 0.1.0 --- api.md | 33 ++-- meteroid/api/__init__.py | 92 +++++++++++ meteroid/api/_pages.py | 233 ++++++++++++++++++++++++++++ meteroid/api/_pagination.py | 204 +++++++++++++++---------- meteroid/api/add_ons.py | 125 +++++++++------ meteroid/api/batch_jobs.py | 236 +++++++++++++++++----------- meteroid/api/client.py | 14 +- meteroid/api/common.py | 31 +++- meteroid/api/coupons.py | 121 +++++++++------ meteroid/api/credit_notes.py | 131 ++++++++++------ meteroid/api/custom_properties.py | 129 ++++++++++------ meteroid/api/customers.py | 123 +++++++++------ meteroid/api/features.py | 121 +++++++++------ meteroid/api/invoices.py | 129 ++++++++++------ meteroid/api/metrics.py | 122 +++++++++------ meteroid/api/plans.py | 246 ++++++++++++++++++------------ meteroid/api/product_families.py | 119 +++++++++------ meteroid/api/products.py | 121 +++++++++------ meteroid/api/subscriptions.py | 127 +++++++++------ meteroid/serialization.py | 6 +- 20 files changed, 1663 insertions(+), 800 deletions(-) create mode 100644 meteroid/api/_pages.py diff --git a/api.md b/api.md index 8af26d9..b92e499 100644 --- a/api.md +++ b/api.md @@ -6,6 +6,9 @@ resource; `AsyncMeteroid` has the same methods, to await, and `client.with_raw_r the same resources, returning the HTTP response. Every method also takes the keyword arguments `extra_headers`, `extra_query`, `extra_body`, `timeout` and `max_retries` for that call; `=...` marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/models). +A list method returns its first page, such as a `AddOnsListPage`: a subclass of the response +model (`AddOnListResponse`) with the paging members `items`, `has_next_page()`, `get_next_page()`, +`iter_pages()` and `body`, which iterates every item. The page classes are in `meteroid.api`. [Add ons](#add-ons) · [Batch jobs](#batch-jobs) · [Checkout sessions](#checkout-sessions) · [Connect](#connect) · [Coupons](#coupons) · [Credit notes](#credit-notes) · [Custom properties](#custom-properties) · [Customers](#customers) · [Entitlements](#entitlements) · [Events](#events) · [Features](#features) · [Invoices](#invoices) · [Metrics](#metrics) · [Oauth](#oauth) · [Oauth apps](#oauth-apps) · [Plans](#plans) · [Product families](#product-families) · [Products](#products) · [Subscriptions](#subscriptions) · [Usage](#usage) @@ -15,7 +18,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.add_ons.list(*, search=..., currency=..., include_archived=..., order_by=..., page=..., per_page=...) -> AddOnListResponse` | `GET /api/v1/addons` | [`AddOnListResponse`](meteroid/models/add_on_list_response.py) | +| `client.add_ons.list(*, search=..., currency=..., include_archived=..., order_by=..., page=..., per_page=...) -> AddOnsListPage` | `GET /api/v1/addons` | [`AddOnListResponse`](meteroid/models/add_on_list_response.py) pages of [`AddOn`](meteroid/models/add_on.py) | | `client.add_ons.create(*, name: str, price_id: PriceId, product_id: ProductId, description=..., max_instances_per_subscription=..., self_serviceable=...) -> AddOn` | `POST /api/v1/addons` | [`AddOn`](meteroid/models/add_on.py) | | `client.add_ons.retrieve(addon_id: str) -> AddOn` | `GET /api/v1/addons/{addon_id}` | [`AddOn`](meteroid/models/add_on.py) | | `client.add_ons.update(addon_id: str, *, description=..., max_instances_per_subscription=..., name=..., price_id=..., self_serviceable=...) -> AddOn` | `PATCH /api/v1/addons/{addon_id}` | [`AddOn`](meteroid/models/add_on.py) | @@ -30,9 +33,9 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.batch_jobs.list(*, job_type=..., status=..., page=..., per_page=...) -> BatchJobListResponse` | `GET /api/v1/batch-jobs` | [`BatchJobListResponse`](meteroid/models/batch_job_list_response.py) | +| `client.batch_jobs.list(*, job_type=..., status=..., page=..., per_page=...) -> BatchJobsListPage` | `GET /api/v1/batch-jobs` | [`BatchJobListResponse`](meteroid/models/batch_job_list_response.py) pages of [`BatchJobResponse`](meteroid/models/batch_job_response.py) | | `client.batch_jobs.retrieve(batch_job_id: str) -> BatchJobDetailResponse` | `GET /api/v1/batch-jobs/{batch_job_id}` | [`BatchJobDetailResponse`](meteroid/models/batch_job_detail_response.py) | -| `client.batch_jobs.list_failures(batch_job_id: str, *, chunk_id=..., limit=..., offset=...) -> BatchJobFailuresResponse` | `GET /api/v1/batch-jobs/{batch_job_id}/failures` | [`BatchJobFailuresResponse`](meteroid/models/batch_job_failures_response.py) | +| `client.batch_jobs.list_failures(batch_job_id: str, *, chunk_id=..., limit=..., offset=...) -> BatchJobsListFailuresPage` | `GET /api/v1/batch-jobs/{batch_job_id}/failures` | [`BatchJobFailuresResponse`](meteroid/models/batch_job_failures_response.py) pages of [`BatchJobItemFailureResponse`](meteroid/models/batch_job_item_failure_response.py) | ## Checkout sessions @@ -63,7 +66,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.coupons.list(*, search=..., filter=..., order_by=..., page=..., per_page=...) -> CouponListResponse` | `GET /api/v1/coupons` | [`CouponListResponse`](meteroid/models/coupon_list_response.py) | +| `client.coupons.list(*, search=..., filter=..., order_by=..., page=..., per_page=...) -> CouponsListPage` | `GET /api/v1/coupons` | [`CouponListResponse`](meteroid/models/coupon_list_response.py) pages of [`Coupon`](meteroid/models/coupon.py) | | `client.coupons.create(*, code: str, discount: CouponDiscount, description=..., expires_at=..., plan_ids=..., recurring_value=..., redemption_limit=..., reusable=...) -> Coupon` | `POST /api/v1/coupons` | [`Coupon`](meteroid/models/coupon.py) | | `client.coupons.retrieve(coupon_id: str) -> Coupon` | `GET /api/v1/coupons/{coupon_id}` | [`Coupon`](meteroid/models/coupon.py) | | `client.coupons.update(coupon_id: str, *, description=..., discount=..., plan_ids=...) -> Coupon` | `PATCH /api/v1/coupons/{coupon_id}` | [`Coupon`](meteroid/models/coupon.py) | @@ -78,7 +81,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.credit_notes.list(*, customer_id=..., invoice_id=..., status=..., search=..., order_by=..., page=..., per_page=...) -> CreditNoteListResponse` | `GET /api/v1/credit-notes` | [`CreditNoteListResponse`](meteroid/models/credit_note_list_response.py) | +| `client.credit_notes.list(*, customer_id=..., invoice_id=..., status=..., search=..., order_by=..., page=..., per_page=...) -> CreditNotesListPage` | `GET /api/v1/credit-notes` | [`CreditNoteListResponse`](meteroid/models/credit_note_list_response.py) pages of [`CreditNote`](meteroid/models/credit_note.py) | | `client.credit_notes.retrieve(credit_note_id: str) -> CreditNote` | `GET /api/v1/credit-notes/{credit_note_id}` | [`CreditNote`](meteroid/models/credit_note.py) | | `client.credit_notes.update_custom_properties(credit_note_id: str, *, custom_properties: t.Any) -> CreditNote` | `PATCH /api/v1/credit-notes/{credit_note_id}/custom-properties` | [`CreditNote`](meteroid/models/credit_note.py) | | `client.credit_notes.download(credit_note_id: str) -> bytes` | `GET /api/v1/credit-notes/{credit_note_id}/download` | bytes | @@ -90,7 +93,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.custom_properties.list_custom_property_definitions(*, entity_type=..., include_archived=..., page=..., per_page=...) -> CustomPropertyDefinitionListResponse` | `GET /api/v1/custom-property-definitions` | [`CustomPropertyDefinitionListResponse`](meteroid/models/custom_property_definition_list_response.py) | +| `client.custom_properties.list_custom_property_definitions(*, entity_type=..., include_archived=..., page=..., per_page=...) -> CustomPropertiesListCustomPropertyDefinitionsPage` | `GET /api/v1/custom-property-definitions` | [`CustomPropertyDefinitionListResponse`](meteroid/models/custom_property_definition_list_response.py) pages of [`CustomPropertyDefinition`](meteroid/models/custom_property_definition.py) | | `client.custom_properties.create_custom_property_definition(*, entity_type: CustomPropertyEntityType, key: str, name: str, property_type: CustomPropertyType, config=..., default_value=..., description=..., display_order=..., required=...) -> CustomPropertyDefinition` | `POST /api/v1/custom-property-definitions` | [`CustomPropertyDefinition`](meteroid/models/custom_property_definition.py) | | `client.custom_properties.retrieve_custom_property_definition(id: str) -> CustomPropertyDefinition` | `GET /api/v1/custom-property-definitions/{id}` | [`CustomPropertyDefinition`](meteroid/models/custom_property_definition.py) | | `client.custom_properties.update_custom_property_definition(id: str, *, config=..., default_value=..., description=..., display_order=..., name=..., required=...) -> CustomPropertyDefinition` | `PUT /api/v1/custom-property-definitions/{id}` | [`CustomPropertyDefinition`](meteroid/models/custom_property_definition.py) | @@ -102,7 +105,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.customers.list(*, order_by=..., page=..., per_page=..., search=..., archived=...) -> CustomerListResponse` | `GET /api/v1/customers` | [`CustomerListResponse`](meteroid/models/customer_list_response.py) | +| `client.customers.list(*, order_by=..., page=..., per_page=..., search=..., archived=...) -> CustomersListPage` | `GET /api/v1/customers` | [`CustomerListResponse`](meteroid/models/customer_list_response.py) pages of [`Customer`](meteroid/models/customer.py) | | `client.customers.create(*, currency: Currency, custom_taxes: list[CustomTaxRate], invoicing_emails: list[str], alias=..., billing_address=..., billing_email=..., buyer_reference=..., connected_account_id=..., custom_properties=..., customer_type=..., exemption_reason=..., first_name=..., invoicing_entity_id=..., invoicing_language=..., is_tax_exempt=..., last_name=..., legal_number=..., name=..., phone=..., preferred_locales=..., shipping_address=..., vat_number=...) -> Customer` | `POST /api/v1/customers` | [`Customer`](meteroid/models/customer.py) | | `client.customers.retrieve(id_or_alias: str) -> Customer` | `GET /api/v1/customers/{id_or_alias}` | [`Customer`](meteroid/models/customer.py) | | `client.customers.replace(id_or_alias: str, *, currency: Currency, custom_taxes: list[CustomTaxRate], invoicing_emails: list[str], invoicing_entity_id: InvoicingEntityId, alias=..., billing_address=..., billing_email=..., buyer_reference=..., custom_properties=..., customer_type=..., exemption_reason=..., first_name=..., invoicing_language=..., is_tax_exempt=..., last_name=..., legal_number=..., name=..., phone=..., preferred_locales=..., shipping_address=..., vat_number=...) -> Customer` | `PUT /api/v1/customers/{id_or_alias}` | [`Customer`](meteroid/models/customer.py) | @@ -136,7 +139,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.features.list(*, statuses=..., product_id=..., search=..., page=..., per_page=...) -> FeatureListResponse` | `GET /api/v1/features` | [`FeatureListResponse`](meteroid/models/feature_list_response.py) | +| `client.features.list(*, statuses=..., product_id=..., search=..., page=..., per_page=...) -> FeaturesListPage` | `GET /api/v1/features` | [`FeatureListResponse`](meteroid/models/feature_list_response.py) pages of [`Feature`](meteroid/models/feature.py) | | `client.features.create(*, code: str, feature_type: FeatureType, name: str, description=..., entitlement=..., product_id=...) -> Feature` | `POST /api/v1/features` | [`Feature`](meteroid/models/feature.py) | | `client.features.retrieve(id_or_code: str) -> Feature` | `GET /api/v1/features/{id_or_code}` | [`Feature`](meteroid/models/feature.py) | | `client.features.update(id_or_code: str, *, description=..., name=...) -> Feature` | `PATCH /api/v1/features/{id_or_code}` | [`Feature`](meteroid/models/feature.py) | @@ -149,7 +152,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.invoices.list(*, customer_id=..., subscription_id=..., statuses=..., einvoicing_status=..., order_by=..., page=..., per_page=...) -> InvoiceListResponse` | `GET /api/v1/invoices` | [`InvoiceListResponse`](meteroid/models/invoice_list_response.py) | +| `client.invoices.list(*, customer_id=..., subscription_id=..., statuses=..., einvoicing_status=..., order_by=..., page=..., per_page=...) -> InvoicesListPage` | `GET /api/v1/invoices` | [`InvoiceListResponse`](meteroid/models/invoice_list_response.py) pages of [`Invoice`](meteroid/models/invoice.py) | | `client.invoices.retrieve(invoice_id: str) -> Invoice` | `GET /api/v1/invoices/{invoice_id}` | [`Invoice`](meteroid/models/invoice.py) | | `client.invoices.update_custom_properties(invoice_id: str, *, custom_properties: t.Any) -> Invoice` | `PATCH /api/v1/invoices/{invoice_id}/custom-properties` | [`Invoice`](meteroid/models/invoice.py) | | `client.invoices.download(invoice_id: str) -> bytes` | `GET /api/v1/invoices/{invoice_id}/download` | bytes | @@ -162,7 +165,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.metrics.list(*, product_family_id=..., search=..., order_by=..., page=..., per_page=...) -> MetricListResponse` | `GET /api/v1/metrics` | [`MetricListResponse`](meteroid/models/metric_list_response.py) | +| `client.metrics.list(*, product_family_id=..., search=..., order_by=..., page=..., per_page=...) -> MetricsListPage` | `GET /api/v1/metrics` | [`MetricListResponse`](meteroid/models/metric_list_response.py) pages of [`MetricSummary`](meteroid/models/metric_summary.py) | | `client.metrics.create(*, aggregation_type: BillingMetricAggregateEnum, code: str, name: str, product_family_id: ProductFamilyId, aggregation_key=..., description=..., filters=..., product_id=..., segmentation_matrix=..., unit_conversion=..., usage_group_key=...) -> Metric` | `POST /api/v1/metrics` | [`Metric`](meteroid/models/metric.py) | | `client.metrics.retrieve(metric_id: str) -> Metric` | `GET /api/v1/metrics/{metric_id}` | [`Metric`](meteroid/models/metric.py) | | `client.metrics.update(metric_id: str, *, description=..., filters=..., name=..., segmentation_matrix=..., unit_conversion=...) -> Metric` | `PATCH /api/v1/metrics/{metric_id}` | [`Metric`](meteroid/models/metric.py) | @@ -199,7 +202,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | --- | --- | --- | | `client.plans.list_plan_version_entitlements(plan_version_id: str) -> ResolvedEntitlementListResponse` | `GET /api/v1/plan-versions/{plan_version_id}/entitlements` | [`ResolvedEntitlementListResponse`](meteroid/models/resolved_entitlement_list_response.py) | | `client.plans.create_plan_version_entitlement(plan_version_id: str, *, entitlements: list[EntitlementSpecRequest]) -> EntitlementListResponse` | `POST /api/v1/plan-versions/{plan_version_id}/entitlements` | [`EntitlementListResponse`](meteroid/models/entitlement_list_response.py) | -| `client.plans.list(*, product_family_id=..., search=..., status=..., plan_type=..., order_by=..., page=..., per_page=...) -> PlanListResponse` | `GET /api/v1/plans` | [`PlanListResponse`](meteroid/models/plan_list_response.py) | +| `client.plans.list(*, product_family_id=..., search=..., status=..., plan_type=..., order_by=..., page=..., per_page=...) -> PlansListPage` | `GET /api/v1/plans` | [`PlanListResponse`](meteroid/models/plan_list_response.py) pages of [`Plan`](meteroid/models/plan.py) | | `client.plans.create(*, components: list[PriceComponentInput], currency: str, name: str, plan_type: PlanTypeEnum, product_family_id: ProductFamilyId, status: PlanStatusEnum, add_ons=..., billing=..., description=..., entitlements=..., self_service_rank=..., tax_inclusive=..., trial=...) -> Plan` | `POST /api/v1/plans` | [`Plan`](meteroid/models/plan.py) | | `client.plans.update_version_minimum(plan_version_id: str, *, amount: str, scope: MinimumCommitmentScope) -> MinimumCommitment` | `PUT /api/v1/plans/versions/{plan_version_id}/minimum` | [`MinimumCommitment`](meteroid/models/minimum_commitment.py) | | `client.plans.delete_version_minimum(plan_version_id: str) -> None` | `DELETE /api/v1/plans/versions/{plan_version_id}/minimum` | nothing | @@ -209,7 +212,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | `client.plans.archive(plan_id: str) -> None` | `POST /api/v1/plans/{plan_id}/archive` | nothing | | `client.plans.publish(plan_id: str) -> Plan` | `POST /api/v1/plans/{plan_id}/publish` | [`Plan`](meteroid/models/plan.py) | | `client.plans.unarchive(plan_id: str) -> None` | `POST /api/v1/plans/{plan_id}/unarchive` | nothing | -| `client.plans.list_versions(plan_id: str, *, page=..., per_page=...) -> PlanVersionListResponse` | `GET /api/v1/plans/{plan_id}/versions` | [`PlanVersionListResponse`](meteroid/models/plan_version_list_response.py) | +| `client.plans.list_versions(plan_id: str, *, page=..., per_page=...) -> PlansListVersionsPage` | `GET /api/v1/plans/{plan_id}/versions` | [`PlanVersionListResponse`](meteroid/models/plan_version_list_response.py) pages of [`PlanVersionSummary`](meteroid/models/plan_version_summary.py) | ## Product families @@ -217,7 +220,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.product_families.list(*, order_by=..., page=..., per_page=..., search=...) -> ProductFamilyListResponse` | `GET /api/v1/product_families` | [`ProductFamilyListResponse`](meteroid/models/product_family_list_response.py) | +| `client.product_families.list(*, order_by=..., page=..., per_page=..., search=...) -> ProductFamiliesListPage` | `GET /api/v1/product_families` | [`ProductFamilyListResponse`](meteroid/models/product_family_list_response.py) pages of [`ProductFamily`](meteroid/models/product_family.py) | | `client.product_families.create(*, name: str) -> ProductFamily` | `POST /api/v1/product_families` | [`ProductFamily`](meteroid/models/product_family.py) | | `client.product_families.retrieve(id_or_alias: str) -> ProductFamily` | `GET /api/v1/product_families/{id_or_alias}` | [`ProductFamily`](meteroid/models/product_family.py) | @@ -227,7 +230,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.products.list(*, product_family_id=..., search=..., order_by=..., page=..., per_page=...) -> ProductListResponse` | `GET /api/v1/products` | [`ProductListResponse`](meteroid/models/product_list_response.py) | +| `client.products.list(*, product_family_id=..., search=..., order_by=..., page=..., per_page=...) -> ProductsListPage` | `GET /api/v1/products` | [`ProductListResponse`](meteroid/models/product_list_response.py) pages of [`Product`](meteroid/models/product.py) | | `client.products.create(*, fee_structure: ProductFeeStructure, name: str, product_family_id: ProductFamilyId, catalog=..., description=...) -> Product` | `POST /api/v1/products` | [`Product`](meteroid/models/product.py) | | `client.products.retrieve(product_id: str) -> Product` | `GET /api/v1/products/{product_id}` | [`Product`](meteroid/models/product.py) | | `client.products.update(product_id: str, *, description=..., fee_structure=..., name=...) -> Product` | `PATCH /api/v1/products/{product_id}` | [`Product`](meteroid/models/product.py) | @@ -242,7 +245,7 @@ marks optional keyword arguments. Models are in [`meteroid/models`](meteroid/mod | Method | Request | Returns | | --- | --- | --- | -| `client.subscriptions.list(*, customer_id=..., plan_id=..., statuses=..., order_by=..., page=..., per_page=...) -> SubscriptionListResponse` | `GET /api/v1/subscriptions` | [`SubscriptionListResponse`](meteroid/models/subscription_list_response.py) | +| `client.subscriptions.list(*, customer_id=..., plan_id=..., statuses=..., order_by=..., page=..., per_page=...) -> SubscriptionsListPage` | `GET /api/v1/subscriptions` | [`SubscriptionListResponse`](meteroid/models/subscription_list_response.py) pages of [`Subscription`](meteroid/models/subscription.py) | | `client.subscriptions.create(*, activation_condition: SubscriptionActivationConditionEnum, customer_id_or_alias: str, plan_id: PlanId, start_date: date, add_ons=..., auto_advance_invoices=..., backdate_invoices=..., billing_day_anchor=..., charge_automatically=..., coupon_codes=..., custom_properties=..., end_date=..., invoice_memo=..., net_terms=..., payment_methods_config=..., price_components=..., purchase_order=..., skip_past_invoices=..., trial_days=..., version=...) -> SubscriptionDetails` | `POST /api/v1/subscriptions` | [`SubscriptionDetails`](meteroid/models/subscription_details.py) | | `client.subscriptions.retrieve(subscription_id: str) -> SubscriptionDetails` | `GET /api/v1/subscriptions/{subscription_id}` | [`SubscriptionDetails`](meteroid/models/subscription_details.py) | | `client.subscriptions.update(subscription_id: str, *, auto_advance_invoices=..., charge_automatically=..., custom_properties=..., invoice_memo=..., net_terms=..., payment_methods_config=..., purchase_order=...) -> SubscriptionUpdateResponse` | `PATCH /api/v1/subscriptions/{subscription_id}` | [`SubscriptionUpdateResponse`](meteroid/models/subscription_update_response.py) | diff --git a/meteroid/api/__init__.py b/meteroid/api/__init__.py index 8f72447..0328548 100644 --- a/meteroid/api/__init__.py +++ b/meteroid/api/__init__.py @@ -38,6 +38,38 @@ from .client import AsyncMeteroid, Meteroid if t.TYPE_CHECKING: + from ._pages import ( + AddOnsListPage, + AsyncAddOnsListPage, + AsyncBatchJobsListFailuresPage, + AsyncBatchJobsListPage, + AsyncCouponsListPage, + AsyncCreditNotesListPage, + AsyncCustomersListPage, + AsyncCustomPropertiesListCustomPropertyDefinitionsPage, + AsyncFeaturesListPage, + AsyncInvoicesListPage, + AsyncMetricsListPage, + AsyncPlansListPage, + AsyncPlansListVersionsPage, + AsyncProductFamiliesListPage, + AsyncProductsListPage, + AsyncSubscriptionsListPage, + BatchJobsListFailuresPage, + BatchJobsListPage, + CouponsListPage, + CreditNotesListPage, + CustomersListPage, + CustomPropertiesListCustomPropertyDefinitionsPage, + FeaturesListPage, + InvoicesListPage, + MetricsListPage, + PlansListPage, + PlansListVersionsPage, + ProductFamiliesListPage, + ProductsListPage, + SubscriptionsListPage, + ) from .add_ons import AddOns, AsyncAddOns from .batch_jobs import AsyncBatchJobs, BatchJobs from .checkout_sessions import AsyncCheckoutSessions, CheckoutSessions @@ -101,6 +133,36 @@ "Subscriptions": "subscriptions", "AsyncUsage": "usage", "Usage": "usage", + "AsyncAddOnsListPage": "_pages", + "AddOnsListPage": "_pages", + "AsyncBatchJobsListPage": "_pages", + "BatchJobsListPage": "_pages", + "AsyncBatchJobsListFailuresPage": "_pages", + "BatchJobsListFailuresPage": "_pages", + "AsyncCouponsListPage": "_pages", + "CouponsListPage": "_pages", + "AsyncCreditNotesListPage": "_pages", + "CreditNotesListPage": "_pages", + "AsyncCustomPropertiesListCustomPropertyDefinitionsPage": "_pages", + "CustomPropertiesListCustomPropertyDefinitionsPage": "_pages", + "AsyncCustomersListPage": "_pages", + "CustomersListPage": "_pages", + "AsyncFeaturesListPage": "_pages", + "FeaturesListPage": "_pages", + "AsyncInvoicesListPage": "_pages", + "InvoicesListPage": "_pages", + "AsyncMetricsListPage": "_pages", + "MetricsListPage": "_pages", + "AsyncPlansListPage": "_pages", + "PlansListPage": "_pages", + "AsyncPlansListVersionsPage": "_pages", + "PlansListVersionsPage": "_pages", + "AsyncProductFamiliesListPage": "_pages", + "ProductFamiliesListPage": "_pages", + "AsyncProductsListPage": "_pages", + "ProductsListPage": "_pages", + "AsyncSubscriptionsListPage": "_pages", + "SubscriptionsListPage": "_pages", } @@ -185,4 +247,34 @@ def __dir__() -> list[str]: "Subscriptions", "AsyncUsage", "Usage", + "AsyncAddOnsListPage", + "AddOnsListPage", + "AsyncBatchJobsListPage", + "BatchJobsListPage", + "AsyncBatchJobsListFailuresPage", + "BatchJobsListFailuresPage", + "AsyncCouponsListPage", + "CouponsListPage", + "AsyncCreditNotesListPage", + "CreditNotesListPage", + "AsyncCustomPropertiesListCustomPropertyDefinitionsPage", + "CustomPropertiesListCustomPropertyDefinitionsPage", + "AsyncCustomersListPage", + "CustomersListPage", + "AsyncFeaturesListPage", + "FeaturesListPage", + "AsyncInvoicesListPage", + "InvoicesListPage", + "AsyncMetricsListPage", + "MetricsListPage", + "AsyncPlansListPage", + "PlansListPage", + "AsyncPlansListVersionsPage", + "PlansListVersionsPage", + "AsyncProductFamiliesListPage", + "ProductFamiliesListPage", + "AsyncProductsListPage", + "ProductsListPage", + "AsyncSubscriptionsListPage", + "SubscriptionsListPage", ] diff --git a/meteroid/api/_pages.py b/meteroid/api/_pages.py new file mode 100644 index 0000000..e717de6 --- /dev/null +++ b/meteroid/api/_pages.py @@ -0,0 +1,233 @@ +# this file is @generated +"""The pages of the list operations, each a subclass of the response model of its request.""" + +from __future__ import annotations + +from ..models import ( + AddOn, + AddOnListResponse, + BatchJobFailuresResponse, + BatchJobItemFailureResponse, + BatchJobListResponse, + BatchJobResponse, + Coupon, + CouponListResponse, + CreditNote, + CreditNoteListResponse, + Customer, + CustomerListResponse, + CustomPropertyDefinition, + CustomPropertyDefinitionListResponse, + Feature, + FeatureListResponse, + Invoice, + InvoiceListResponse, + MetricListResponse, + MetricSummary, + Plan, + PlanListResponse, + PlanVersionListResponse, + PlanVersionSummary, + Product, + ProductFamily, + ProductFamilyListResponse, + ProductListResponse, + Subscription, + SubscriptionListResponse, +) +from ._pagination import AsyncPage, SyncPage + +__all__ = [ + "AsyncAddOnsListPage", + "AddOnsListPage", + "AsyncBatchJobsListPage", + "BatchJobsListPage", + "AsyncBatchJobsListFailuresPage", + "BatchJobsListFailuresPage", + "AsyncCouponsListPage", + "CouponsListPage", + "AsyncCreditNotesListPage", + "CreditNotesListPage", + "AsyncCustomPropertiesListCustomPropertyDefinitionsPage", + "CustomPropertiesListCustomPropertyDefinitionsPage", + "AsyncCustomersListPage", + "CustomersListPage", + "AsyncFeaturesListPage", + "FeaturesListPage", + "AsyncInvoicesListPage", + "InvoicesListPage", + "AsyncMetricsListPage", + "MetricsListPage", + "AsyncPlansListPage", + "PlansListPage", + "AsyncPlansListVersionsPage", + "PlansListVersionsPage", + "AsyncProductFamiliesListPage", + "ProductFamiliesListPage", + "AsyncProductsListPage", + "ProductsListPage", + "AsyncSubscriptionsListPage", + "SubscriptionsListPage", +] + + +class AddOnsListPage(SyncPage[AddOn, AddOnListResponse], AddOnListResponse): + """A page of :class:`AddOn`, and the :class:`AddOnListResponse` response of its request.""" + + +class AsyncAddOnsListPage(AsyncPage[AddOn, AddOnListResponse], AddOnListResponse): + """A page of :class:`AddOn`, and the :class:`AddOnListResponse` response of its request.""" + + +class BatchJobsListPage( + SyncPage[BatchJobResponse, BatchJobListResponse], BatchJobListResponse +): + """A page of :class:`BatchJobResponse`, and the :class:`BatchJobListResponse` response of its request.""" + + +class AsyncBatchJobsListPage( + AsyncPage[BatchJobResponse, BatchJobListResponse], BatchJobListResponse +): + """A page of :class:`BatchJobResponse`, and the :class:`BatchJobListResponse` response of its request.""" + + +class BatchJobsListFailuresPage( + SyncPage[BatchJobItemFailureResponse, BatchJobFailuresResponse], + BatchJobFailuresResponse, +): + """A page of :class:`BatchJobItemFailureResponse`, and the :class:`BatchJobFailuresResponse` response of its request.""" + + +class AsyncBatchJobsListFailuresPage( + AsyncPage[BatchJobItemFailureResponse, BatchJobFailuresResponse], + BatchJobFailuresResponse, +): + """A page of :class:`BatchJobItemFailureResponse`, and the :class:`BatchJobFailuresResponse` response of its request.""" + + +class CouponsListPage(SyncPage[Coupon, CouponListResponse], CouponListResponse): + """A page of :class:`Coupon`, and the :class:`CouponListResponse` response of its request.""" + + +class AsyncCouponsListPage(AsyncPage[Coupon, CouponListResponse], CouponListResponse): + """A page of :class:`Coupon`, and the :class:`CouponListResponse` response of its request.""" + + +class CreditNotesListPage( + SyncPage[CreditNote, CreditNoteListResponse], CreditNoteListResponse +): + """A page of :class:`CreditNote`, and the :class:`CreditNoteListResponse` response of its request.""" + + +class AsyncCreditNotesListPage( + AsyncPage[CreditNote, CreditNoteListResponse], CreditNoteListResponse +): + """A page of :class:`CreditNote`, and the :class:`CreditNoteListResponse` response of its request.""" + + +class CustomPropertiesListCustomPropertyDefinitionsPage( + SyncPage[CustomPropertyDefinition, CustomPropertyDefinitionListResponse], + CustomPropertyDefinitionListResponse, +): + """A page of :class:`CustomPropertyDefinition`, and the :class:`CustomPropertyDefinitionListResponse` response of its request.""" + + +class AsyncCustomPropertiesListCustomPropertyDefinitionsPage( + AsyncPage[CustomPropertyDefinition, CustomPropertyDefinitionListResponse], + CustomPropertyDefinitionListResponse, +): + """A page of :class:`CustomPropertyDefinition`, and the :class:`CustomPropertyDefinitionListResponse` response of its request.""" + + +class CustomersListPage(SyncPage[Customer, CustomerListResponse], CustomerListResponse): + """A page of :class:`Customer`, and the :class:`CustomerListResponse` response of its request.""" + + +class AsyncCustomersListPage( + AsyncPage[Customer, CustomerListResponse], CustomerListResponse +): + """A page of :class:`Customer`, and the :class:`CustomerListResponse` response of its request.""" + + +class FeaturesListPage(SyncPage[Feature, FeatureListResponse], FeatureListResponse): + """A page of :class:`Feature`, and the :class:`FeatureListResponse` response of its request.""" + + +class AsyncFeaturesListPage( + AsyncPage[Feature, FeatureListResponse], FeatureListResponse +): + """A page of :class:`Feature`, and the :class:`FeatureListResponse` response of its request.""" + + +class InvoicesListPage(SyncPage[Invoice, InvoiceListResponse], InvoiceListResponse): + """A page of :class:`Invoice`, and the :class:`InvoiceListResponse` response of its request.""" + + +class AsyncInvoicesListPage( + AsyncPage[Invoice, InvoiceListResponse], InvoiceListResponse +): + """A page of :class:`Invoice`, and the :class:`InvoiceListResponse` response of its request.""" + + +class MetricsListPage(SyncPage[MetricSummary, MetricListResponse], MetricListResponse): + """A page of :class:`MetricSummary`, and the :class:`MetricListResponse` response of its request.""" + + +class AsyncMetricsListPage( + AsyncPage[MetricSummary, MetricListResponse], MetricListResponse +): + """A page of :class:`MetricSummary`, and the :class:`MetricListResponse` response of its request.""" + + +class PlansListPage(SyncPage[Plan, PlanListResponse], PlanListResponse): + """A page of :class:`Plan`, and the :class:`PlanListResponse` response of its request.""" + + +class AsyncPlansListPage(AsyncPage[Plan, PlanListResponse], PlanListResponse): + """A page of :class:`Plan`, and the :class:`PlanListResponse` response of its request.""" + + +class PlansListVersionsPage( + SyncPage[PlanVersionSummary, PlanVersionListResponse], PlanVersionListResponse +): + """A page of :class:`PlanVersionSummary`, and the :class:`PlanVersionListResponse` response of its request.""" + + +class AsyncPlansListVersionsPage( + AsyncPage[PlanVersionSummary, PlanVersionListResponse], PlanVersionListResponse +): + """A page of :class:`PlanVersionSummary`, and the :class:`PlanVersionListResponse` response of its request.""" + + +class ProductFamiliesListPage( + SyncPage[ProductFamily, ProductFamilyListResponse], ProductFamilyListResponse +): + """A page of :class:`ProductFamily`, and the :class:`ProductFamilyListResponse` response of its request.""" + + +class AsyncProductFamiliesListPage( + AsyncPage[ProductFamily, ProductFamilyListResponse], ProductFamilyListResponse +): + """A page of :class:`ProductFamily`, and the :class:`ProductFamilyListResponse` response of its request.""" + + +class ProductsListPage(SyncPage[Product, ProductListResponse], ProductListResponse): + """A page of :class:`Product`, and the :class:`ProductListResponse` response of its request.""" + + +class AsyncProductsListPage( + AsyncPage[Product, ProductListResponse], ProductListResponse +): + """A page of :class:`Product`, and the :class:`ProductListResponse` response of its request.""" + + +class SubscriptionsListPage( + SyncPage[Subscription, SubscriptionListResponse], SubscriptionListResponse +): + """A page of :class:`Subscription`, and the :class:`SubscriptionListResponse` response of its request.""" + + +class AsyncSubscriptionsListPage( + AsyncPage[Subscription, SubscriptionListResponse], SubscriptionListResponse +): + """A page of :class:`Subscription`, and the :class:`SubscriptionListResponse` response of its request.""" diff --git a/meteroid/api/_pagination.py b/meteroid/api/_pagination.py index debd1a0..743ada5 100644 --- a/meteroid/api/_pagination.py +++ b/meteroid/api/_pagination.py @@ -1,9 +1,10 @@ # this file is @generated by Perseid; edit the shared runtime template. """Pages of the paginated list operations, and iterators over all their items. -``client.items.list()`` returns the first page. Iterating it, with ``for`` -(``async for`` on the async client), walks every item of every page, fetching -the next ones on demand; :meth:`SyncPage.iter_pages` walks the pages. +``client.items.list()`` returns the first page, which is also the response of its +request: its properties are read on the page. Iterating it, with ``for`` (``async for`` +on the async client), walks every item of every page, fetching the next ones on +demand; :meth:`SyncPage.iter_pages` walks the pages. """ from __future__ import annotations @@ -11,7 +12,7 @@ import dataclasses import typing as t -from ..serialization import Unset +from ..serialization import BaseModel, Unset __all__ = [ "AsyncPage", @@ -21,16 +22,20 @@ "PagePaging", "Paging", "SyncPage", - "async_first_page", - "first_page", "step", ] _Body = t.TypeVar("_Body") _Item = t.TypeVar("_Item") +_M = t.TypeVar("_M", bound=BaseModel) _P = t.TypeVar("_P") _V = t.TypeVar("_V") _W = t.TypeVar("_W") +_S = t.TypeVar("_S", bound="SyncPage[t.Any, t.Any]") +_A = t.TypeVar("_A", bound="AsyncPage[t.Any, t.Any]") + +#: The paging members: a response property of the same name is only read through ``body``. +_MEMBERS = frozenset({"body", "get_next_page", "has_next_page", "items", "iter_pages"}) def step(value: _V | Unset | None, get: t.Callable[[_V], _W]) -> _W | None: @@ -127,83 +132,148 @@ def next( return offset -class _Page(t.Generic[_Item, _Body]): - body: _Body - """The decoded response of this page.""" - items: list[_Item] - """The items of this page.""" +class _Page(BaseModel, t.Generic[_Item, _M]): + """The paging members shared by sync and async pages.""" + + # For type checkers only: at runtime, they would type the response properties of their name. + if t.TYPE_CHECKING: + body: _M + """The response of this page, as decoded, with the properties a paging member shadows.""" + items: list[_Item] + """The items of this page.""" - def __init__(self, body: _Body, items: list[_Item], has_next: bool) -> None: + def _set(self, body: _M, items: list[_Item]) -> None: + # The response's properties, but those named like a paging member, which they would hide. + self.__dict__.update((k, v) for k, v in vars(body).items() if k not in _MEMBERS) self.body = body self.items = items - self._has_next = has_next - def has_next_page(self) -> bool: - """Whether another page follows this one.""" - return self._has_next + def _field_value(self, name: str) -> t.Any: + return ( + getattr(self.body, name) if name in _MEMBERS else super()._field_value(name) + ) - def __repr__(self) -> str: - return f"<{type(self).__name__} items={self.items!r} has_next_page={self._has_next}>" + def _field_values(self) -> tuple[t.Any, ...]: + return tuple( + self._field_value(f.name) for f in dataclasses.fields(self) if f.compare + ) + def __eq__(self, other: object) -> bool: + if type(other) is not type(self) or not isinstance(other, _Page): + return NotImplemented + return self._field_values() == other._field_values() -class SyncPage(_Page[_Item, _Body]): - """One page of a list operation. Iterating it yields the items of this page and the next.""" + def __repr__(self) -> str: + fields = ( + f"{f.name}={self._field_value(f.name)!r}" + for f in dataclasses.fields(self) + if f.repr + ) + return f"{type(self).__name__}({', '.join(fields)})" + + +class SyncPage(_Page[_Item, _M]): + """One page of a list operation, and the response of its request. + + A list method returns a subclass of the response model and of this class, such as + ``WidgetsListPage`` for ``client.widgets.list()``: the response's properties are read + on the page. Iterating it yields the items of this page and of the next ones. + """ + + if t.TYPE_CHECKING: + _next_page: t.Callable[[], SyncPage[_Item, _M]] | None + + @classmethod + def _first( + cls: type[_S], + fetch: t.Callable[[_P | None], _M], + param: _P | None, + paging: Paging[_M, _Item, _P], + ) -> _S: + """Fetches the page of ``param`` (the first page when ``None``).""" + param = paging.first(param) + body = fetch(param) + items = paging.items_of(body) + after = paging.next(param, body, items) + page = cls.__new__(cls) + page._set(body, items) + page._next_page = ( + None if after is None else lambda: cls._first(fetch, after, paging) + ) + return page - def __init__( - self, - body: _Body, - items: list[_Item], - next_page: t.Callable[[], SyncPage[_Item, _Body]] | None, - ) -> None: - super().__init__(body, items, next_page is not None) - self._next_page = next_page + def has_next_page(self) -> bool: + """Whether another page follows this one.""" + return self._next_page is not None - def get_next_page(self) -> SyncPage[_Item, _Body]: + def get_next_page(self: _S) -> _S: """Fetches the next page. Raises :class:`RuntimeError` after the last one.""" if self._next_page is None: raise RuntimeError("no next page: check has_next_page() first") - return self._next_page() + return t.cast(_S, self._next_page()) - def iter_pages(self) -> t.Iterator[SyncPage[_Item, _Body]]: + def iter_pages(self: _S) -> t.Iterator[_S]: """This page, then each following one.""" page = self while True: yield page - if page._next_page is None: + if not page.has_next_page(): return - page = page._next_page() + page = page.get_next_page() def __iter__(self) -> t.Iterator[_Item]: for page in self.iter_pages(): yield from page.items -class AsyncPage(_Page[_Item, _Body]): - """One page of a list operation. ``async for`` yields the items of this page and the next.""" +class AsyncPage(_Page[_Item, _M]): + """One page of a list operation, and the response of its request, for asyncio. + + A list method returns a subclass of the response model and of this class, such as + ``AsyncWidgetsListPage`` for ``client.widgets.list()``: the response's properties are + read on the page. ``async for`` yields the items of this page and of the next ones. + """ + + if t.TYPE_CHECKING: + _next_page: t.Callable[[], t.Awaitable[AsyncPage[_Item, _M]]] | None + + @classmethod + async def _first( + cls: type[_A], + fetch: t.Callable[[_P | None], t.Awaitable[_M]], + param: _P | None, + paging: Paging[_M, _Item, _P], + ) -> _A: + """Fetches the page of ``param`` (the first page when ``None``).""" + param = paging.first(param) + body = await fetch(param) + items = paging.items_of(body) + after = paging.next(param, body, items) + page = cls.__new__(cls) + page._set(body, items) + page._next_page = ( + None if after is None else lambda: cls._first(fetch, after, paging) + ) + return page - def __init__( - self, - body: _Body, - items: list[_Item], - next_page: t.Callable[[], t.Awaitable[AsyncPage[_Item, _Body]]] | None, - ) -> None: - super().__init__(body, items, next_page is not None) - self._next_page = next_page + def has_next_page(self) -> bool: + """Whether another page follows this one.""" + return self._next_page is not None - async def get_next_page(self) -> AsyncPage[_Item, _Body]: + async def get_next_page(self: _A) -> _A: """Fetches the next page. Raises :class:`RuntimeError` after the last one.""" if self._next_page is None: raise RuntimeError("no next page: check has_next_page() first") - return await self._next_page() + return t.cast(_A, await self._next_page()) - async def iter_pages(self) -> t.AsyncIterator[AsyncPage[_Item, _Body]]: + async def iter_pages(self: _A) -> t.AsyncIterator[_A]: """This page, then each following one.""" page = self while True: yield page - if page._next_page is None: + if not page.has_next_page(): return - page = await page._next_page() + page = await page.get_next_page() async def __aiter__(self) -> t.AsyncIterator[_Item]: async for page in self.iter_pages(): @@ -211,45 +281,13 @@ async def __aiter__(self) -> t.AsyncIterator[_Item]: yield item -def first_page( - fetch: t.Callable[[_P | None], _Body], - param: _P | None, - paging: Paging[_Body, _Item, _P], -) -> SyncPage[_Item, _Body]: - """Fetches the page of ``param`` (the first page when ``None``).""" - param = paging.first(param) - body = fetch(param) - items = paging.items_of(body) - after = paging.next(param, body, items) - if after is None: - return SyncPage(body, items, None) - return SyncPage(body, items, lambda: first_page(fetch, after, paging)) - - -async def async_first_page( - fetch: t.Callable[[_P | None], t.Awaitable[_Body]], - param: _P | None, - paging: Paging[_Body, _Item, _P], -) -> AsyncPage[_Item, _Body]: - """Fetches the page of ``param`` (the first page when ``None``).""" - param = paging.first(param) - body = await fetch(param) - items = paging.items_of(body) - after = paging.next(param, body, items) - if after is None: - return AsyncPage(body, items, None) - return AsyncPage(body, items, lambda: async_first_page(fetch, after, paging)) - - -class AsyncPaginator(t.Generic[_Item, _Body]): +class AsyncPaginator(t.Generic[_Item, _A]): """The first page of an async list call: ``await`` it for the page, or ``async for`` items.""" - def __init__( - self, first_page: t.Callable[[], t.Awaitable[AsyncPage[_Item, _Body]]] - ) -> None: + def __init__(self, first_page: t.Callable[[], t.Awaitable[_A]]) -> None: self._first_page = first_page - def __await__(self) -> t.Generator[t.Any, None, AsyncPage[_Item, _Body]]: + def __await__(self) -> t.Generator[t.Any, None, _A]: return self._first_page().__await__() async def __aiter__(self) -> t.AsyncIterator[_Item]: diff --git a/meteroid/api/add_ons.py b/meteroid/api/add_ons.py index 73686e5..e35da4b 100644 --- a/meteroid/api/add_ons.py +++ b/meteroid/api/add_ons.py @@ -20,6 +20,15 @@ UpdateAddOnRequest, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AddOnsListPage, + AsyncAddOnsListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -39,7 +48,7 @@ def with_raw_response(self) -> AsyncAddOnsWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncAddOnsWithRawResponse(self) - async def list( + def list( self, *, search: str | None = None, @@ -53,38 +62,49 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> AddOnListResponse: + ) -> AsyncPaginator[AddOn, AsyncAddOnsListPage]: """List add-ons :param include_archived: Include archived add-ons in the results (default: false) :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/addons", - query_params=serialize_query_params( - { - "search": search, - "currency": currency, - "include_archived": include_archived, - "order_by": order_by, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> AddOnListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/addons", + query_params=serialize_query_params( + { + "search": search, + "currency": currency, + "include_archived": include_archived, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, AddOnListResponse) + + paging = PagePaging[AddOnListResponse, AddOn]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, AddOnListResponse) + return AsyncPaginator(lambda: AsyncAddOnsListPage._first(fetch, page, paging)) async def create( self, @@ -372,38 +392,49 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> AddOnListResponse: + ) -> AddOnsListPage: """List add-ons :param include_archived: Include archived add-ons in the results (default: false) :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/addons", - query_params=serialize_query_params( - { - "search": search, - "currency": currency, - "include_archived": include_archived, - "order_by": order_by, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> AddOnListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/addons", + query_params=serialize_query_params( + { + "search": search, + "currency": currency, + "include_archived": include_archived, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, AddOnListResponse) + + paging = PagePaging[AddOnListResponse, AddOn]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, AddOnListResponse) + return AddOnsListPage._first(fetch, page, paging) def create( self, diff --git a/meteroid/api/batch_jobs.py b/meteroid/api/batch_jobs.py index 48504cc..6f24ea3 100644 --- a/meteroid/api/batch_jobs.py +++ b/meteroid/api/batch_jobs.py @@ -11,11 +11,25 @@ BatchJobChunkId, BatchJobDetailResponse, BatchJobFailuresResponse, + BatchJobItemFailureResponse, BatchJobListResponse, + BatchJobResponse, BatchJobStatus, BatchJobType, ) from ..serialization import UNSET, Unset +from ._pages import ( + AsyncBatchJobsListFailuresPage, + AsyncBatchJobsListPage, + BatchJobsListFailuresPage, + BatchJobsListPage, +) +from ._pagination import ( + AsyncPaginator, + OffsetPaging, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -35,7 +49,7 @@ def with_raw_response(self) -> AsyncBatchJobsWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncBatchJobsWithRawResponse(self) - async def list( + def list( self, *, job_type: BatchJobType | None = None, @@ -47,34 +61,47 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> BatchJobListResponse: + ) -> AsyncPaginator[BatchJobResponse, AsyncBatchJobsListPage]: """List batch jobs with optional filtering by type and status. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/batch-jobs", - query_params=serialize_query_params( - { - "job_type": job_type, - "status": status, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> BatchJobListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/batch-jobs", + query_params=serialize_query_params( + { + "job_type": job_type, + "status": status, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, BatchJobListResponse) + + paging = PagePaging[BatchJobListResponse, BatchJobResponse]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, + ) + return AsyncPaginator( + lambda: AsyncBatchJobsListPage._first(fetch, page, paging) ) - return decode_response(response, BatchJobListResponse) async def retrieve( self, @@ -108,7 +135,7 @@ async def retrieve( ) return decode_response(response, BatchJobDetailResponse) - async def list_failures( + def list_failures( self, batch_job_id: str, *, @@ -120,35 +147,45 @@ async def list_failures( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> BatchJobFailuresResponse: + ) -> AsyncPaginator[BatchJobItemFailureResponse, AsyncBatchJobsListFailuresPage]: """List batch job failures Retrieve paginated failures for a batch job.""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/batch-jobs/{batch_job_id}/failures", - path_params={ - "batch_job_id": batch_job_id, - }, - query_params=serialize_query_params( - { - "chunk_id": chunk_id, - "limit": limit, - "offset": offset, + + async def fetch(page_param: int | None) -> BatchJobFailuresResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/batch-jobs/{batch_job_id}/failures", + path_params={ + "batch_job_id": batch_job_id, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + query_params=serialize_query_params( + { + "chunk_id": chunk_id, + "limit": limit, + "offset": page_param, + }, + ), + error_types={ + "default": _models.RestErrorResponse, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, BatchJobFailuresResponse) + + paging = OffsetPaging[BatchJobFailuresResponse, BatchJobItemFailureResponse]( + items=lambda body: body.data, + total=lambda body: body.total_count, + ) + return AsyncPaginator( + lambda: AsyncBatchJobsListFailuresPage._first(fetch, offset, paging) ) - return decode_response(response, BatchJobFailuresResponse) class AsyncBatchJobsWithRawResponse: @@ -180,34 +217,45 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> BatchJobListResponse: + ) -> BatchJobsListPage: """List batch jobs with optional filtering by type and status. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/batch-jobs", - query_params=serialize_query_params( - { - "job_type": job_type, - "status": status, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> BatchJobListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/batch-jobs", + query_params=serialize_query_params( + { + "job_type": job_type, + "status": status, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, BatchJobListResponse) + + paging = PagePaging[BatchJobListResponse, BatchJobResponse]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, BatchJobListResponse) + return BatchJobsListPage._first(fetch, page, paging) def retrieve( self, @@ -253,35 +301,43 @@ def list_failures( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> BatchJobFailuresResponse: + ) -> BatchJobsListFailuresPage: """List batch job failures Retrieve paginated failures for a batch job.""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/batch-jobs/{batch_job_id}/failures", - path_params={ - "batch_job_id": batch_job_id, - }, - query_params=serialize_query_params( - { - "chunk_id": chunk_id, - "limit": limit, - "offset": offset, + + def fetch(page_param: int | None) -> BatchJobFailuresResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/batch-jobs/{batch_job_id}/failures", + path_params={ + "batch_job_id": batch_job_id, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + query_params=serialize_query_params( + { + "chunk_id": chunk_id, + "limit": limit, + "offset": page_param, + }, + ), + error_types={ + "default": _models.RestErrorResponse, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, BatchJobFailuresResponse) + + paging = OffsetPaging[BatchJobFailuresResponse, BatchJobItemFailureResponse]( + items=lambda body: body.data, + total=lambda body: body.total_count, ) - return decode_response(response, BatchJobFailuresResponse) + return BatchJobsListFailuresPage._first(fetch, offset, paging) class BatchJobsWithRawResponse: diff --git a/meteroid/api/client.py b/meteroid/api/client.py index af7fc69..4021d48 100644 --- a/meteroid/api/client.py +++ b/meteroid/api/client.py @@ -9,7 +9,7 @@ import httpx -from ..serialization import UNSET, MeteroidError, Unset +from ..serialization import UNSET, Unset from ._auth import SecurityScheme, TokenProvider from .common import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, Configuration, Timeout from .middleware import AsyncMiddleware, SyncMiddleware @@ -144,6 +144,8 @@ "MeteroidWithRawResponse", ] +DEFAULT_BASE_URL = "https://api.meteroid.com" + SECURITY_SCHEMES: dict[str, SecurityScheme] = { "bearer_auth": SecurityScheme("bearer"), @@ -160,11 +162,7 @@ def _configuration( token_provider: TokenProvider | None, ) -> Configuration: """The configuration of a client: its arguments, else the environment.""" - base_url = base_url or os.environ.get("METEROID_BASE_URL") - if not base_url: - raise MeteroidError( - "pass base_url to the client or set the METEROID_BASE_URL environment variable" - ) + base_url = base_url or os.environ.get("METEROID_BASE_URL") or DEFAULT_BASE_URL return Configuration( base_path=str(base_url).rstrip("/"), bearer_access_token=api_key @@ -214,7 +212,7 @@ def __init__( """Creates a client. :param api_key: The token of the API, read from ``METEROID_API_KEY`` when ``None``. - :param base_url: Defaults to ``METEROID_BASE_URL``; one of them is required. + :param base_url: Defaults to ``METEROID_BASE_URL``, else ``https://api.meteroid.com``. :param timeout: Of each attempt, in seconds (60 by default); ``None`` waits. :param max_retries: Retries of a failed idempotent request (2 by default). :param default_headers: Sent with every request. @@ -608,7 +606,7 @@ def __init__( """Creates a client. :param api_key: The token of the API, read from ``METEROID_API_KEY`` when ``None``. - :param base_url: Defaults to ``METEROID_BASE_URL``; one of them is required. + :param base_url: Defaults to ``METEROID_BASE_URL``, else ``https://api.meteroid.com``. :param timeout: Of each attempt, in seconds (60 by default); ``None`` waits. :param max_retries: Retries of a failed idempotent request (2 by default). :param default_headers: Sent with every request. diff --git a/meteroid/api/common.py b/meteroid/api/common.py index 2c29cdd..a879185 100644 --- a/meteroid/api/common.py +++ b/meteroid/api/common.py @@ -1,14 +1,15 @@ # this file is @generated by Perseid; edit the shared runtime template. """Shared HTTP plumbing for the generated API resources. -Authentication, an automatic ``idempotency-key`` on POST, retries with jittered -exponential backoff, per-request timeouts and headers, and typed errors. +Authentication, an automatic ``idempotency-key`` on POST when the API deduplicates +by it, retries with jittered exponential backoff, per-request timeouts and headers, +and typed errors. Retries: a request is retried on connection errors, timeouts, 408, 429 and 5xx responses, honouring ``Retry-After`` (or ``retry-after-ms``) up to a minute, but only when replaying it is safe: for GET, HEAD, OPTIONS, TRACE, PUT and DELETE, and for any request carrying an ``idempotency-key``, which every POST gets -automatically. A timed-out request can therefore take up to +automatically when the API deduplicates by it. A timed-out request can therefore take up to ``timeout * (1 + max_retries)`` plus the backoff delays before :class:`APITimeoutError` is raised. """ @@ -90,6 +91,8 @@ DEFAULT_TIMEOUT: float = 60 DEFAULT_MAX_RETRIES = 2 +AUTO_IDEMPOTENCY_KEY: bool = json.loads("false") +"""Whether the API deduplicates POSTs by ``idempotency-key``, so that each gets one to be retried.""" INITIAL_RETRY_DELAY = 0.5 """Seconds before the first retry, doubling for each next one up to :data:`MAX_RETRY_DELAY`.""" MAX_RETRY_DELAY = 8.0 @@ -364,6 +367,8 @@ class ApiRequest: timeout: Timeout | Unset = UNSET max_retries: int | None = None stream: bool = False + replayable: bool = False + """Whether a POST is safe to retry without an ``idempotency-key``.""" def _with_extra_body( @@ -535,7 +540,11 @@ def _build( cookies.append(previous) headers["cookie"] = "; ".join(cookies) method = spec.method.upper() - if method == "POST" and "idempotency-key" not in headers: + if ( + AUTO_IDEMPOTENCY_KEY + and method == "POST" + and "idempotency-key" not in headers + ): headers["idempotency-key"] = f"auto_{uuid.uuid4()}" if spec.stream: headers["accept"] = "text/event-stream" @@ -591,7 +600,11 @@ def _build( files=files or None, timeout=timeout, ) - safe = method in _REPLAYABLE_METHODS or "idempotency-key" in headers + safe = ( + spec.replayable + or method in _REPLAYABLE_METHODS + or "idempotency-key" in headers + ) return request, safe and replayable(content, files) def _needs_token(self, spec: ApiRequest) -> bool: @@ -609,8 +622,14 @@ def _token_request(self, scheme: SecurityScheme) -> tuple[str, ApiRequest]: assert oauth is not None url = token_url(self._cfg.base_path, scheme.token_url) form, headers = oauth.request_parts(scheme.scope) + # Another token is as good as the first: the request is safe to retry. return url, ApiRequest( - "POST", url, form_body=form, security=(), extra_headers=headers + "POST", + url, + form_body=form, + security=(), + extra_headers=headers, + replayable=True, ) def _rejected_token( diff --git a/meteroid/api/coupons.py b/meteroid/api/coupons.py index fa63b2a..a49055e 100644 --- a/meteroid/api/coupons.py +++ b/meteroid/api/coupons.py @@ -18,6 +18,15 @@ UpdateCouponRequest, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncCouponsListPage, + CouponsListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -37,7 +46,7 @@ def with_raw_response(self) -> AsyncCouponsWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncCouponsWithRawResponse(self) - async def list( + def list( self, *, search: str | None = None, @@ -50,36 +59,47 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> CouponListResponse: + ) -> AsyncPaginator[Coupon, AsyncCouponsListPage]: """List coupons :param order_by: Sort order. Format: `column.direction`. Allowed columns: `code`, `created_at`, `expires_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/coupons", - query_params=serialize_query_params( - { - "search": search, - "filter": filter, - "order_by": order_by, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> CouponListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/coupons", + query_params=serialize_query_params( + { + "search": search, + "filter": filter, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, CouponListResponse) + + paging = PagePaging[CouponListResponse, Coupon]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, CouponListResponse) + return AsyncPaginator(lambda: AsyncCouponsListPage._first(fetch, page, paging)) async def create( self, @@ -351,36 +371,47 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> CouponListResponse: + ) -> CouponsListPage: """List coupons :param order_by: Sort order. Format: `column.direction`. Allowed columns: `code`, `created_at`, `expires_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/coupons", - query_params=serialize_query_params( - { - "search": search, - "filter": filter, - "order_by": order_by, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> CouponListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/coupons", + query_params=serialize_query_params( + { + "search": search, + "filter": filter, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, CouponListResponse) + + paging = PagePaging[CouponListResponse, Coupon]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, CouponListResponse) + return CouponsListPage._first(fetch, page, paging) def create( self, diff --git a/meteroid/api/credit_notes.py b/meteroid/api/credit_notes.py index 31456b8..98ad631 100644 --- a/meteroid/api/credit_notes.py +++ b/meteroid/api/credit_notes.py @@ -15,6 +15,15 @@ InvoiceId, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncCreditNotesListPage, + CreditNotesListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -34,7 +43,7 @@ def with_raw_response(self) -> AsyncCreditNotesWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncCreditNotesWithRawResponse(self) - async def list( + def list( self, *, customer_id: CustomerId | None = None, @@ -49,7 +58,7 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> CreditNoteListResponse: + ) -> AsyncPaginator[CreditNote, AsyncCreditNotesListPage]: """List credit notes List a tenant's credit notes, optionally filtered by customer, invoice or status. @@ -60,32 +69,45 @@ async def list( :param order_by: Sort order. Format: `column.direction`. Allowed columns: `created_at`, `credit_note_number`, `total`, `status`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/credit-notes", - query_params=serialize_query_params( - { - "customer_id": customer_id, - "invoice_id": invoice_id, - "status": status, - "search": search, - "order_by": order_by, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> CreditNoteListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/credit-notes", + query_params=serialize_query_params( + { + "customer_id": customer_id, + "invoice_id": invoice_id, + "status": status, + "search": search, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, CreditNoteListResponse) + + paging = PagePaging[CreditNoteListResponse, CreditNote]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, + ) + return AsyncPaginator( + lambda: AsyncCreditNotesListPage._first(fetch, page, paging) ) - return decode_response(response, CreditNoteListResponse) async def retrieve( self, @@ -261,7 +283,7 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> CreditNoteListResponse: + ) -> CreditNotesListPage: """List credit notes List a tenant's credit notes, optionally filtered by customer, invoice or status. @@ -272,32 +294,43 @@ def list( :param order_by: Sort order. Format: `column.direction`. Allowed columns: `created_at`, `credit_note_number`, `total`, `status`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/credit-notes", - query_params=serialize_query_params( - { - "customer_id": customer_id, - "invoice_id": invoice_id, - "status": status, - "search": search, - "order_by": order_by, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> CreditNoteListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/credit-notes", + query_params=serialize_query_params( + { + "customer_id": customer_id, + "invoice_id": invoice_id, + "status": status, + "search": search, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, CreditNoteListResponse) + + paging = PagePaging[CreditNoteListResponse, CreditNote]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, CreditNoteListResponse) + return CreditNotesListPage._first(fetch, page, paging) def retrieve( self, diff --git a/meteroid/api/custom_properties.py b/meteroid/api/custom_properties.py index 35c5c35..eebb50f 100644 --- a/meteroid/api/custom_properties.py +++ b/meteroid/api/custom_properties.py @@ -18,6 +18,15 @@ PropertyConfig, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncCustomPropertiesListCustomPropertyDefinitionsPage, + CustomPropertiesListCustomPropertyDefinitionsPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -37,7 +46,7 @@ def with_raw_response(self) -> AsyncCustomPropertiesWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncCustomPropertiesWithRawResponse(self) - async def list_custom_property_definitions( + def list_custom_property_definitions( self, *, entity_type: CustomPropertyEntityType | None = None, @@ -49,36 +58,55 @@ async def list_custom_property_definitions( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> CustomPropertyDefinitionListResponse: + ) -> AsyncPaginator[ + CustomPropertyDefinition, AsyncCustomPropertiesListCustomPropertyDefinitionsPage + ]: """List custom property definitions :param entity_type: Filter to a single entity type. :param include_archived: Include archived (soft-deleted) definitions. Defaults to false. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/custom-property-definitions", - query_params=serialize_query_params( - { - "entity_type": entity_type, - "include_archived": include_archived, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> CustomPropertyDefinitionListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/custom-property-definitions", + query_params=serialize_query_params( + { + "entity_type": entity_type, + "include_archived": include_archived, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) + ) + return decode_response(response, CustomPropertyDefinitionListResponse) + + paging = PagePaging[ + CustomPropertyDefinitionListResponse, CustomPropertyDefinition + ]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, + ) + return AsyncPaginator( + lambda: AsyncCustomPropertiesListCustomPropertyDefinitionsPage._first( + fetch, page, paging ) ) - return decode_response(response, CustomPropertyDefinitionListResponse) async def create_custom_property_definition( self, @@ -283,36 +311,51 @@ def list_custom_property_definitions( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> CustomPropertyDefinitionListResponse: + ) -> CustomPropertiesListCustomPropertyDefinitionsPage: """List custom property definitions :param entity_type: Filter to a single entity type. :param include_archived: Include archived (soft-deleted) definitions. Defaults to false. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/custom-property-definitions", - query_params=serialize_query_params( - { - "entity_type": entity_type, - "include_archived": include_archived, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> CustomPropertyDefinitionListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/custom-property-definitions", + query_params=serialize_query_params( + { + "entity_type": entity_type, + "include_archived": include_archived, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, CustomPropertyDefinitionListResponse) + + paging = PagePaging[ + CustomPropertyDefinitionListResponse, CustomPropertyDefinition + ]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, + ) + return CustomPropertiesListCustomPropertyDefinitionsPage._first( + fetch, page, paging ) - return decode_response(response, CustomPropertyDefinitionListResponse) def create_custom_property_definition( self, diff --git a/meteroid/api/customers.py b/meteroid/api/customers.py index c60bd67..32efa80 100644 --- a/meteroid/api/customers.py +++ b/meteroid/api/customers.py @@ -27,6 +27,15 @@ ShippingAddress, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncCustomersListPage, + CustomersListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -46,7 +55,7 @@ def with_raw_response(self) -> AsyncCustomersWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncCustomersWithRawResponse(self) - async def list( + def list( self, *, order_by: str | None = None, @@ -59,36 +68,49 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> CustomerListResponse: + ) -> AsyncPaginator[Customer, AsyncCustomersListPage]: """List customers with optional pagination and search filtering. :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `email`, `alias`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/customers", - query_params=serialize_query_params( - { - "order_by": order_by, - "page": page, - "per_page": per_page, - "search": search, - "archived": archived, + + async def fetch(page_param: int | None) -> CustomerListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/customers", + query_params=serialize_query_params( + { + "order_by": order_by, + "page": page_param, + "per_page": per_page, + "search": search, + "archived": archived, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, CustomerListResponse) + + paging = PagePaging[CustomerListResponse, Customer]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, + ) + return AsyncPaginator( + lambda: AsyncCustomersListPage._first(fetch, page, paging) ) - return decode_response(response, CustomerListResponse) async def create( self, @@ -558,36 +580,47 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> CustomerListResponse: + ) -> CustomersListPage: """List customers with optional pagination and search filtering. :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `email`, `alias`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/customers", - query_params=serialize_query_params( - { - "order_by": order_by, - "page": page, - "per_page": per_page, - "search": search, - "archived": archived, + + def fetch(page_param: int | None) -> CustomerListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/customers", + query_params=serialize_query_params( + { + "order_by": order_by, + "page": page_param, + "per_page": per_page, + "search": search, + "archived": archived, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, CustomerListResponse) + + paging = PagePaging[CustomerListResponse, Customer]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, CustomerListResponse) + return CustomersListPage._first(fetch, page, paging) def create( self, diff --git a/meteroid/api/features.py b/meteroid/api/features.py index 6c9c1d6..b746231 100644 --- a/meteroid/api/features.py +++ b/meteroid/api/features.py @@ -18,6 +18,15 @@ UpdateFeatureRequest, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncFeaturesListPage, + FeaturesListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -37,7 +46,7 @@ def with_raw_response(self) -> AsyncFeaturesWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncFeaturesWithRawResponse(self) - async def list( + def list( self, *, statuses: builtins.list[FeatureStatus] | None = None, @@ -50,7 +59,7 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> FeatureListResponse: + ) -> AsyncPaginator[Feature, AsyncFeaturesListPage]: """List features :param statuses: Filter by feature status. Repeat the param to select multiple, omit to return all. @@ -58,30 +67,41 @@ async def list( :param search: Search by feature name. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/features", - query_params=serialize_query_params( - { - "statuses": statuses, - "product_id": product_id, - "search": search, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> FeatureListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/features", + query_params=serialize_query_params( + { + "statuses": statuses, + "product_id": product_id, + "search": search, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, FeatureListResponse) + + paging = PagePaging[FeatureListResponse, Feature]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, FeatureListResponse) + return AsyncPaginator(lambda: AsyncFeaturesListPage._first(fetch, page, paging)) async def create( self, @@ -296,7 +316,7 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> FeatureListResponse: + ) -> FeaturesListPage: """List features :param statuses: Filter by feature status. Repeat the param to select multiple, omit to return all. @@ -304,30 +324,41 @@ def list( :param search: Search by feature name. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/features", - query_params=serialize_query_params( - { - "statuses": statuses, - "product_id": product_id, - "search": search, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> FeatureListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/features", + query_params=serialize_query_params( + { + "statuses": statuses, + "product_id": product_id, + "search": search, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, FeatureListResponse) + + paging = PagePaging[FeatureListResponse, Feature]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, FeatureListResponse) + return FeaturesListPage._first(fetch, page, paging) def create( self, diff --git a/meteroid/api/invoices.py b/meteroid/api/invoices.py index 1ffe4e3..23079c5 100644 --- a/meteroid/api/invoices.py +++ b/meteroid/api/invoices.py @@ -16,6 +16,15 @@ SubscriptionId, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncInvoicesListPage, + InvoicesListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -35,7 +44,7 @@ def with_raw_response(self) -> AsyncInvoicesWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncInvoicesWithRawResponse(self) - async def list( + def list( self, *, customer_id: str | None = None, @@ -50,7 +59,7 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> InvoiceListResponse: + ) -> AsyncPaginator[Invoice, AsyncInvoicesListPage]: """List invoices with optional filtering by customer, subscription, or status. :param customer_id: Filter by customer ID or alias @@ -58,32 +67,43 @@ async def list( :param order_by: Sort order. Format: `column.direction`. Allowed columns: `invoice_number`, `customer_name`, `amount`, `invoice_date`, `status`, `payment_status`. Direction: `asc` or `desc`. Default: `invoice_date.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/invoices", - query_params=serialize_query_params( - { - "customer_id": customer_id, - "subscription_id": subscription_id, - "statuses": statuses, - "einvoicing_status": einvoicing_status, - "order_by": order_by, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> InvoiceListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/invoices", + query_params=serialize_query_params( + { + "customer_id": customer_id, + "subscription_id": subscription_id, + "statuses": statuses, + "einvoicing_status": einvoicing_status, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, InvoiceListResponse) + + paging = PagePaging[InvoiceListResponse, Invoice]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, InvoiceListResponse) + return AsyncPaginator(lambda: AsyncInvoicesListPage._first(fetch, page, paging)) async def retrieve( self, @@ -297,7 +317,7 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> InvoiceListResponse: + ) -> InvoicesListPage: """List invoices with optional filtering by customer, subscription, or status. :param customer_id: Filter by customer ID or alias @@ -305,32 +325,43 @@ def list( :param order_by: Sort order. Format: `column.direction`. Allowed columns: `invoice_number`, `customer_name`, `amount`, `invoice_date`, `status`, `payment_status`. Direction: `asc` or `desc`. Default: `invoice_date.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/invoices", - query_params=serialize_query_params( - { - "customer_id": customer_id, - "subscription_id": subscription_id, - "statuses": statuses, - "einvoicing_status": einvoicing_status, - "order_by": order_by, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> InvoiceListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/invoices", + query_params=serialize_query_params( + { + "customer_id": customer_id, + "subscription_id": subscription_id, + "statuses": statuses, + "einvoicing_status": einvoicing_status, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, InvoiceListResponse) + + paging = PagePaging[InvoiceListResponse, Invoice]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, InvoiceListResponse) + return InvoicesListPage._first(fetch, page, paging) def retrieve( self, diff --git a/meteroid/api/metrics.py b/meteroid/api/metrics.py index 65dfb14..9f9991e 100644 --- a/meteroid/api/metrics.py +++ b/meteroid/api/metrics.py @@ -15,12 +15,22 @@ MetricFilter, MetricListResponse, MetricSegmentationMatrix, + MetricSummary, ProductFamilyId, ProductId, UnitConversion, UpdateMetricRequest, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncMetricsListPage, + MetricsListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -40,7 +50,7 @@ def with_raw_response(self) -> AsyncMetricsWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncMetricsWithRawResponse(self) - async def list( + def list( self, *, product_family_id: ProductFamilyId | None = None, @@ -53,37 +63,48 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> MetricListResponse: + ) -> AsyncPaginator[MetricSummary, AsyncMetricsListPage]: """List billable metrics :param search: Search by metric name or code :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `code`, `created_at`. Direction: `asc` or `desc`. Default: `name.asc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/metrics", - query_params=serialize_query_params( - { - "product_family_id": product_family_id, - "search": search, - "order_by": order_by, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> MetricListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/metrics", + query_params=serialize_query_params( + { + "product_family_id": product_family_id, + "search": search, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, MetricListResponse) + + paging = PagePaging[MetricListResponse, MetricSummary]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, MetricListResponse) + return AsyncPaginator(lambda: AsyncMetricsListPage._first(fetch, page, paging)) async def create( self, @@ -314,37 +335,48 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> MetricListResponse: + ) -> MetricsListPage: """List billable metrics :param search: Search by metric name or code :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `code`, `created_at`. Direction: `asc` or `desc`. Default: `name.asc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/metrics", - query_params=serialize_query_params( - { - "product_family_id": product_family_id, - "search": search, - "order_by": order_by, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> MetricListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/metrics", + query_params=serialize_query_params( + { + "product_family_id": product_family_id, + "search": search, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, MetricListResponse) + + paging = PagePaging[MetricListResponse, MetricSummary]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, MetricListResponse) + return MetricsListPage._first(fetch, page, paging) def create( self, diff --git a/meteroid/api/plans.py b/meteroid/api/plans.py index e67b6a4..e63a85e 100644 --- a/meteroid/api/plans.py +++ b/meteroid/api/plans.py @@ -25,6 +25,7 @@ PlanTypeEnum, PlanTypeEnumLiteral, PlanVersionListResponse, + PlanVersionSummary, PriceComponentInput, ProductFamilyId, ReplacePlanRequest, @@ -32,6 +33,17 @@ TrialConfig, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncPlansListPage, + AsyncPlansListVersionsPage, + PlansListPage, + PlansListVersionsPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -120,7 +132,7 @@ async def create_plan_version_entitlement( ) return decode_response(response, EntitlementListResponse) - async def list( + def list( self, *, product_family_id: ProductFamilyId | None = None, @@ -135,7 +147,7 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> PlanListResponse: + ) -> AsyncPaginator[Plan, AsyncPlansListPage]: """List plans :param search: Search by plan name @@ -144,32 +156,43 @@ async def list( :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `status`, `plan_type`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/plans", - query_params=serialize_query_params( - { - "product_family_id": product_family_id, - "search": search, - "status": status, - "plan_type": plan_type, - "order_by": order_by, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> PlanListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/plans", + query_params=serialize_query_params( + { + "product_family_id": product_family_id, + "search": search, + "status": status, + "plan_type": plan_type, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, PlanListResponse) + + paging = PagePaging[PlanListResponse, Plan]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, PlanListResponse) + return AsyncPaginator(lambda: AsyncPlansListPage._first(fetch, page, paging)) async def create( self, @@ -543,7 +566,7 @@ async def unarchive( ) ) - async def list_versions( + def list_versions( self, plan_id: str, *, @@ -554,35 +577,48 @@ async def list_versions( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> PlanVersionListResponse: + ) -> AsyncPaginator[PlanVersionSummary, AsyncPlansListVersionsPage]: """List plan versions :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/plans/{plan_id}/versions", - path_params={ - "plan_id": plan_id, - }, - query_params=serialize_query_params( - { - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> PlanVersionListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/plans/{plan_id}/versions", + path_params={ + "plan_id": plan_id, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + query_params=serialize_query_params( + { + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, PlanVersionListResponse) + + paging = PagePaging[PlanVersionListResponse, PlanVersionSummary]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, + ) + return AsyncPaginator( + lambda: AsyncPlansListVersionsPage._first(fetch, page, paging) ) - return decode_response(response, PlanVersionListResponse) class AsyncPlansWithRawResponse: @@ -704,7 +740,7 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> PlanListResponse: + ) -> PlansListPage: """List plans :param search: Search by plan name @@ -713,32 +749,43 @@ def list( :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `status`, `plan_type`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/plans", - query_params=serialize_query_params( - { - "product_family_id": product_family_id, - "search": search, - "status": status, - "plan_type": plan_type, - "order_by": order_by, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> PlanListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/plans", + query_params=serialize_query_params( + { + "product_family_id": product_family_id, + "search": search, + "status": status, + "plan_type": plan_type, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, PlanListResponse) + + paging = PagePaging[PlanListResponse, Plan]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, PlanListResponse) + return PlansListPage._first(fetch, page, paging) def create( self, @@ -1123,35 +1170,46 @@ def list_versions( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> PlanVersionListResponse: + ) -> PlansListVersionsPage: """List plan versions :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/plans/{plan_id}/versions", - path_params={ - "plan_id": plan_id, - }, - query_params=serialize_query_params( - { - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> PlanVersionListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/plans/{plan_id}/versions", + path_params={ + "plan_id": plan_id, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + query_params=serialize_query_params( + { + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, PlanVersionListResponse) + + paging = PagePaging[PlanVersionListResponse, PlanVersionSummary]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, PlanVersionListResponse) + return PlansListVersionsPage._first(fetch, page, paging) class PlansWithRawResponse: diff --git a/meteroid/api/product_families.py b/meteroid/api/product_families.py index d4c3db9..422e6a6 100644 --- a/meteroid/api/product_families.py +++ b/meteroid/api/product_families.py @@ -12,6 +12,15 @@ ProductFamilyListResponse, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncProductFamiliesListPage, + ProductFamiliesListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -31,7 +40,7 @@ def with_raw_response(self) -> AsyncProductFamiliesWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncProductFamiliesWithRawResponse(self) - async def list( + def list( self, *, order_by: str | None = None, @@ -43,35 +52,48 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> ProductFamilyListResponse: + ) -> AsyncPaginator[ProductFamily, AsyncProductFamiliesListPage]: """List product families :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/product_families", - query_params=serialize_query_params( - { - "order_by": order_by, - "page": page, - "per_page": per_page, - "search": search, + + async def fetch(page_param: int | None) -> ProductFamilyListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/product_families", + query_params=serialize_query_params( + { + "order_by": order_by, + "page": page_param, + "per_page": per_page, + "search": search, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, ProductFamilyListResponse) + + paging = PagePaging[ProductFamilyListResponse, ProductFamily]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, + ) + return AsyncPaginator( + lambda: AsyncProductFamiliesListPage._first(fetch, page, paging) ) - return decode_response(response, ProductFamilyListResponse) async def create( self, @@ -168,35 +190,46 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> ProductFamilyListResponse: + ) -> ProductFamiliesListPage: """List product families :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/product_families", - query_params=serialize_query_params( - { - "order_by": order_by, - "page": page, - "per_page": per_page, - "search": search, + + def fetch(page_param: int | None) -> ProductFamilyListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/product_families", + query_params=serialize_query_params( + { + "order_by": order_by, + "page": page_param, + "per_page": per_page, + "search": search, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, ProductFamilyListResponse) + + paging = PagePaging[ProductFamilyListResponse, ProductFamily]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, ProductFamilyListResponse) + return ProductFamiliesListPage._first(fetch, page, paging) def create( self, diff --git a/meteroid/api/products.py b/meteroid/api/products.py index bfb2f3c..38adea5 100644 --- a/meteroid/api/products.py +++ b/meteroid/api/products.py @@ -20,6 +20,15 @@ UpdateProductRequest, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncProductsListPage, + ProductsListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -39,7 +48,7 @@ def with_raw_response(self) -> AsyncProductsWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncProductsWithRawResponse(self) - async def list( + def list( self, *, product_family_id: ProductFamilyId | None = None, @@ -52,36 +61,47 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> ProductListResponse: + ) -> AsyncPaginator[Product, AsyncProductsListPage]: """List products :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `created_at`. Direction: `asc` or `desc`. Default: `name.asc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/products", - query_params=serialize_query_params( - { - "product_family_id": product_family_id, - "search": search, - "order_by": order_by, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> ProductListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/products", + query_params=serialize_query_params( + { + "product_family_id": product_family_id, + "search": search, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, ProductListResponse) + + paging = PagePaging[ProductListResponse, Product]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, ProductListResponse) + return AsyncPaginator(lambda: AsyncProductsListPage._first(fetch, page, paging)) async def create( self, @@ -370,36 +390,47 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> ProductListResponse: + ) -> ProductsListPage: """List products :param order_by: Sort order. Format: `column.direction`. Allowed columns: `name`, `created_at`. Direction: `asc` or `desc`. Default: `name.asc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/products", - query_params=serialize_query_params( - { - "product_family_id": product_family_id, - "search": search, - "order_by": order_by, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> ProductListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/products", + query_params=serialize_query_params( + { + "product_family_id": product_family_id, + "search": search, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, ProductListResponse) + + paging = PagePaging[ProductListResponse, Product]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, ProductListResponse) + return ProductsListPage._first(fetch, page, paging) def create( self, diff --git a/meteroid/api/subscriptions.py b/meteroid/api/subscriptions.py index 67595b4..68ab93f 100644 --- a/meteroid/api/subscriptions.py +++ b/meteroid/api/subscriptions.py @@ -27,6 +27,15 @@ SubscriptionUpdateResponse, ) from ..serialization import UNSET, Unset, to_json_value +from ._pages import ( + AsyncSubscriptionsListPage, + SubscriptionsListPage, +) +from ._pagination import ( + AsyncPaginator, + PagePaging, + step, +) from ._response import async_to_raw_response_wrapper, to_raw_response_wrapper from .common import ( ApiBaseAsync, @@ -46,7 +55,7 @@ def with_raw_response(self) -> AsyncSubscriptionsWithRawResponse: """These methods, returning an :class:`APIResponse` with the status and headers.""" return AsyncSubscriptionsWithRawResponse(self) - async def list( + def list( self, *, customer_id: str | None = None, @@ -60,38 +69,51 @@ async def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> SubscriptionListResponse: + ) -> AsyncPaginator[Subscription, AsyncSubscriptionsListPage]: """List subscriptions with optional filtering by customer or plan. :param customer_id: Filter by customer ID or alias :param order_by: Sort order. Format: `column.direction`. Allowed columns: `customer_name`, `plan_name`, `mrr_cents`, `billing_start_date`, `end_date`, `status`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = await self._request( - ApiRequest( - method="get", - path="/api/v1/subscriptions", - query_params=serialize_query_params( - { - "customer_id": customer_id, - "plan_id": plan_id, - "statuses": statuses, - "order_by": order_by, - "page": page, - "per_page": per_page, + + async def fetch(page_param: int | None) -> SubscriptionListResponse: + response = await self._request( + ApiRequest( + method="get", + path="/api/v1/subscriptions", + query_params=serialize_query_params( + { + "customer_id": customer_id, + "plan_id": plan_id, + "statuses": statuses, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, SubscriptionListResponse) + + paging = PagePaging[SubscriptionListResponse, Subscription]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, + ) + return AsyncPaginator( + lambda: AsyncSubscriptionsListPage._first(fetch, page, paging) ) - return decode_response(response, SubscriptionListResponse) async def create( self, @@ -406,38 +428,49 @@ def list( extra_body: t.Mapping[str, object] | None = None, timeout: Timeout | Unset = UNSET, max_retries: int | None = None, - ) -> SubscriptionListResponse: + ) -> SubscriptionsListPage: """List subscriptions with optional filtering by customer or plan. :param customer_id: Filter by customer ID or alias :param order_by: Sort order. Format: `column.direction`. Allowed columns: `customer_name`, `plan_name`, `mrr_cents`, `billing_start_date`, `end_date`, `status`, `created_at`. Direction: `asc` or `desc`. Default: `created_at.desc`. :param page: Page number (0-indexed) :param per_page: Number of items per page""" - response = self._request( - ApiRequest( - method="get", - path="/api/v1/subscriptions", - query_params=serialize_query_params( - { - "customer_id": customer_id, - "plan_id": plan_id, - "statuses": statuses, - "order_by": order_by, - "page": page, - "per_page": per_page, + + def fetch(page_param: int | None) -> SubscriptionListResponse: + response = self._request( + ApiRequest( + method="get", + path="/api/v1/subscriptions", + query_params=serialize_query_params( + { + "customer_id": customer_id, + "plan_id": plan_id, + "statuses": statuses, + "order_by": order_by, + "page": page_param, + "per_page": per_page, + }, + ), + error_types={ + "default": _models.RestErrorResponse, }, - ), - error_types={ - "default": _models.RestErrorResponse, - }, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - max_retries=max_retries, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + max_retries=max_retries, + ) ) + return decode_response(response, SubscriptionListResponse) + + paging = PagePaging[SubscriptionListResponse, Subscription]( + items=lambda body: body.data, + total_pages=lambda body: step( + body.pagination_meta, lambda v: v.total_pages + ), + first_page=0, ) - return decode_response(response, SubscriptionListResponse) + return SubscriptionsListPage._first(fetch, page, paging) def create( self, diff --git a/meteroid/serialization.py b/meteroid/serialization.py index 403fb5d..90983c8 100644 --- a/meteroid/serialization.py +++ b/meteroid/serialization.py @@ -690,6 +690,10 @@ def _with_extra(self, out: dict[str, t.Any]) -> dict[str, t.Any]: out.setdefault(key, value) return out + def _field_value(self, name: str) -> t.Any: + """The value of the field ``name``, which a page reads from its body when it shadows it.""" + return getattr(self, name) + def _fields_to_dict(self, skip: t.Container[str] = ()) -> dict[str, t.Any]: out: dict[str, t.Any] = {} hints = _type_hints(type(self)) @@ -697,7 +701,7 @@ def _fields_to_dict(self, skip: t.Container[str] = ()) -> dict[str, t.Any]: for field in dataclasses.fields(self): if field.name in skip: continue - value = getattr(self, field.name) + value = self._field_value(field.name) # `None` leaves out an optional field defaulting to `None` rather than UNSET, # unless it was received as `null`. if value is UNSET or (