lago-python-client 1.46.0__tar.gz → 1.47.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/PKG-INFO +1 -1
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/client.py +4 -1
- lago_python_client-1.47.0/lago_python_client/observability.py +67 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/services/rate_limit.py +110 -1
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/services/request.py +10 -1
- lago_python_client-1.47.0/lago_python_client/version.py +1 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client.egg-info/PKG-INFO +1 -1
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client.egg-info/SOURCES.txt +2 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_rate_limit.py +283 -1
- lago_python_client-1.46.0/lago_python_client/version.py +0 -1
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/LICENSE +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/README.md +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/activity_logs/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/activity_logs/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/add_ons/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/add_ons/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/api_logs/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/api_logs/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/base_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/base_model.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/billable_metrics/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/billable_metrics/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/billing_entities/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/billing_entities/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/coupons/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/coupons/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/credit_notes/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/credit_notes/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/applied_coupons_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/credit_notes_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/invoices_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/payment_methods_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/payment_requests_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/payments_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/subscriptions_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/wallet_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/wallets/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/wallets/alert_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/wallets/metadata_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/wallets_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/events/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/events/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/exceptions.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/fees/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/fees/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/functools_ext.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/gross_revenues/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/gross_revenues/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/invoice_collections/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/invoice_collections/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/invoiced_usages/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/invoiced_usages/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/invoices/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/invoices/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/mixins.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/activity_log.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/add_on.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/alert.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/api_log.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/applied_coupon.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/billable_metric.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/billing_entity.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/billing_period.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/charge.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/coupon.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/credit.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/credit_note.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/customer.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/customer_projected_usage.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/customer_usage.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/error_detail.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/event.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/fee.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/fixed_charge.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/gross_revenue.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/invoice.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/invoice_collection.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/invoice_custom_section.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/invoice_item.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/invoiced_usage.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/lifetime_usage.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/minimum_commitment.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/mrr.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/organization.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/overdue_balance.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/payment.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/payment_method.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/payment_receipt.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/payment_request.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/plan.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/subscription.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/tax.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/usage.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/usage_threshold.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/wallet.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/wallet_transaction.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/models/webhook_endpoint.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/mrrs/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/mrrs/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/organizations/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/organizations/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/overdue_balances/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/overdue_balances/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/payment_receipts/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/payment_receipts/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/payment_requests/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/payment_requests/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/payments/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/payments/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/plans/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/plans/charges_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/plans/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/plans/fixed_charges_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/py.typed +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/services/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/services/json.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/services/response.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/subscriptions/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/subscriptions/charges_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/subscriptions/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/subscriptions/fixed_charges_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/taxes/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/taxes/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/usages/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/usages/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/wallets/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/wallets/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/webhook_endpoints/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/webhook_endpoints/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/webhooks/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/webhooks/clients.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client.egg-info/dependency_links.txt +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client.egg-info/requires.txt +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client.egg-info/top_level.txt +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/pyproject.toml +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/setup.cfg +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_activity_logs_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_add_on_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_api_logs_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_applied_coupon_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_billable_metric_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_billing_entity_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_coupon_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_credit_note_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_applied_coupons_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_credit_notes_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_invoices_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_payment_methods_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_payment_requests_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_payments_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_sbscriptions_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_wallet_alert_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_wallet_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_wallet_metadata_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_customer_wallets_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_event_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_fee_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_functools_ext.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_gross_revenue_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_invoice_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_invoice_collection_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_invoiced_usage_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_json_services.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_mrr_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_organization_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_overdue_balance_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_payment_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_payment_receipt_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_payment_request_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_plan_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_request_services.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_response_services.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_subscription_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_tax_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_usage_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_wallet_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_wallet_transaction_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_webhook_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/test_webhook_endpoint_client.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/utils/__init__.py +0 -0
- {lago_python_client-1.46.0 → lago_python_client-1.47.0}/tests/utils/mixin.py +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import Optional
|
|
1
2
|
from urllib.parse import urljoin
|
|
2
3
|
|
|
3
4
|
from .activity_logs.clients import ActivityLogClient
|
|
@@ -32,7 +33,7 @@ from .payments.clients import PaymentClient
|
|
|
32
33
|
from .plans.charges_client import PlanChargesClient
|
|
33
34
|
from .plans.clients import PlanClient
|
|
34
35
|
from .plans.fixed_charges_client import PlanFixedChargesClient
|
|
35
|
-
from .services.rate_limit import RateLimitRetryConfig
|
|
36
|
+
from .services.rate_limit import RateLimitCallback, RateLimitRetryConfig
|
|
36
37
|
from .subscriptions.charges_client import SubscriptionChargesClient
|
|
37
38
|
from .subscriptions.clients import SubscriptionClient
|
|
38
39
|
from .subscriptions.fixed_charges_client import SubscriptionFixedChargesClient
|
|
@@ -61,6 +62,7 @@ class Client:
|
|
|
61
62
|
ingest_api_url: str = "",
|
|
62
63
|
max_retries: int = 3,
|
|
63
64
|
retry_on_rate_limit: bool = True,
|
|
65
|
+
on_rate_limit_info: Optional[RateLimitCallback] = None,
|
|
64
66
|
) -> None:
|
|
65
67
|
self.api_key: str = api_key
|
|
66
68
|
self.api_url: str = api_url
|
|
@@ -69,6 +71,7 @@ class Client:
|
|
|
69
71
|
self.rate_limit_retry_config: RateLimitRetryConfig = RateLimitRetryConfig(
|
|
70
72
|
max_retries=max_retries,
|
|
71
73
|
retry_on_rate_limit=retry_on_rate_limit,
|
|
74
|
+
on_rate_limit_info=on_rate_limit_info,
|
|
72
75
|
)
|
|
73
76
|
|
|
74
77
|
@property
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional observability helpers for Lago rate limits.
|
|
3
|
+
|
|
4
|
+
Provides ready-to-use ``on_rate_limit_info`` callbacks so callers can opt into
|
|
5
|
+
rate limit observability without writing one themselves.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from lago_python_client import Client
|
|
9
|
+
from lago_python_client.observability import LoggingRateLimitObserver
|
|
10
|
+
|
|
11
|
+
client = Client(
|
|
12
|
+
api_key="...",
|
|
13
|
+
on_rate_limit_info=LoggingRateLimitObserver(),
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Optional, Sequence
|
|
19
|
+
|
|
20
|
+
from .services.rate_limit import RateLimitInfo
|
|
21
|
+
|
|
22
|
+
DEFAULT_THRESHOLDS: Sequence[float] = (0.80, 0.90, 0.95)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LoggingRateLimitObserver:
|
|
26
|
+
"""
|
|
27
|
+
Emits a log line each time rate limit usage crosses a configured threshold.
|
|
28
|
+
|
|
29
|
+
Stateless across calls: every response that lands above any threshold logs.
|
|
30
|
+
If you want one-shot crossings, wrap it with your own state.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
thresholds: Usage fractions (0.0 - 1.0) that should produce a log line.
|
|
34
|
+
Defaults to 80%, 90%, 95%.
|
|
35
|
+
logger: Logger to emit on. Defaults to ``lago_python_client.rate_limit``.
|
|
36
|
+
level: Log level used when a threshold is crossed. Defaults to WARNING.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
thresholds: Sequence[float] = DEFAULT_THRESHOLDS,
|
|
42
|
+
logger: Optional[logging.Logger] = None,
|
|
43
|
+
level: int = logging.WARNING,
|
|
44
|
+
) -> None:
|
|
45
|
+
# Sort descending so we report the highest matching threshold first.
|
|
46
|
+
self.thresholds = tuple(sorted(thresholds, reverse=True))
|
|
47
|
+
self.logger = logger or logging.getLogger("lago_python_client.rate_limit")
|
|
48
|
+
self.level = level
|
|
49
|
+
|
|
50
|
+
def __call__(self, info: RateLimitInfo) -> None:
|
|
51
|
+
pct = info.usage_pct
|
|
52
|
+
if pct is None:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
for threshold in self.thresholds:
|
|
56
|
+
if pct >= threshold:
|
|
57
|
+
self.logger.log(
|
|
58
|
+
self.level,
|
|
59
|
+
"Lago rate limit at %.0f%% (limit=%s, remaining=%s, reset=%ss, %s %s)",
|
|
60
|
+
pct * 100,
|
|
61
|
+
info.limit,
|
|
62
|
+
info.remaining,
|
|
63
|
+
info.reset,
|
|
64
|
+
info.method,
|
|
65
|
+
info.url,
|
|
66
|
+
)
|
|
67
|
+
return
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/services/rate_limit.py
RENAMED
|
@@ -1,11 +1,87 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import time
|
|
2
|
-
from
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Callable, Optional
|
|
3
5
|
|
|
4
6
|
if TYPE_CHECKING:
|
|
5
7
|
from httpx import Response
|
|
6
8
|
|
|
7
9
|
from ..exceptions import LagoRateLimitError
|
|
8
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RateLimitInfo:
|
|
16
|
+
"""
|
|
17
|
+
Parsed rate limit headers from a Lago API response.
|
|
18
|
+
|
|
19
|
+
Provided to ``on_rate_limit_info`` callbacks after every successful request
|
|
20
|
+
so callers can build their own observability (metrics, logs, alerts).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
limit: Optional[int]
|
|
24
|
+
remaining: Optional[int]
|
|
25
|
+
reset: Optional[int]
|
|
26
|
+
url: str
|
|
27
|
+
method: str
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def usage_pct(self) -> Optional[float]:
|
|
31
|
+
"""
|
|
32
|
+
Fraction of the rate limit currently used, between 0.0 and 1.0.
|
|
33
|
+
|
|
34
|
+
Returns None when the headers aren't usable (missing, zero limit, etc.).
|
|
35
|
+
"""
|
|
36
|
+
if self.limit is None or self.remaining is None or self.limit <= 0:
|
|
37
|
+
return None
|
|
38
|
+
return 1.0 - (self.remaining / self.limit)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
RateLimitCallback = Callable[[RateLimitInfo], None]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_rate_limit_info(
|
|
45
|
+
response: "Response",
|
|
46
|
+
method: str,
|
|
47
|
+
url: str,
|
|
48
|
+
) -> Optional[RateLimitInfo]:
|
|
49
|
+
"""
|
|
50
|
+
Parse ``x-ratelimit-*`` headers from a response into a RateLimitInfo.
|
|
51
|
+
|
|
52
|
+
Returns ``None`` when no rate limit headers are present (for example, on a
|
|
53
|
+
self-hosted Lago instance that doesn't enforce limits) so callers can skip
|
|
54
|
+
emission entirely.
|
|
55
|
+
"""
|
|
56
|
+
headers = response.headers
|
|
57
|
+
limit: Optional[int] = None
|
|
58
|
+
remaining: Optional[int] = None
|
|
59
|
+
reset: Optional[int] = None
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
limit = int(headers["x-ratelimit-limit"])
|
|
63
|
+
except (KeyError, ValueError, TypeError):
|
|
64
|
+
pass
|
|
65
|
+
try:
|
|
66
|
+
remaining = int(headers["x-ratelimit-remaining"])
|
|
67
|
+
except (KeyError, ValueError, TypeError):
|
|
68
|
+
pass
|
|
69
|
+
try:
|
|
70
|
+
reset = int(headers["x-ratelimit-reset"])
|
|
71
|
+
except (KeyError, ValueError, TypeError):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
if limit is None and remaining is None and reset is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
return RateLimitInfo(
|
|
78
|
+
limit=limit,
|
|
79
|
+
remaining=remaining,
|
|
80
|
+
reset=reset,
|
|
81
|
+
url=url,
|
|
82
|
+
method=method,
|
|
83
|
+
)
|
|
84
|
+
|
|
9
85
|
|
|
10
86
|
class RateLimitRetryConfig:
|
|
11
87
|
"""Configuration for rate limit retry behavior."""
|
|
@@ -17,6 +93,7 @@ class RateLimitRetryConfig:
|
|
|
17
93
|
base_backoff_seconds: float = 1.0,
|
|
18
94
|
backoff_multiplier: float = 2.0,
|
|
19
95
|
max_retry_delay: float = 20.0,
|
|
96
|
+
on_rate_limit_info: Optional[RateLimitCallback] = None,
|
|
20
97
|
):
|
|
21
98
|
"""
|
|
22
99
|
Initialize rate limit retry configuration.
|
|
@@ -27,12 +104,18 @@ class RateLimitRetryConfig:
|
|
|
27
104
|
base_backoff_seconds: Initial backoff duration in seconds for exponential backoff.
|
|
28
105
|
backoff_multiplier: Multiplier for exponential backoff between retries.
|
|
29
106
|
max_retry_delay: Maximum delay in seconds before a retry (default: 20).
|
|
107
|
+
on_rate_limit_info: Optional callback invoked after every successful
|
|
108
|
+
response with parsed rate limit headers. Use this to build
|
|
109
|
+
observability around the rate limit (warn at 80%, emit metrics,
|
|
110
|
+
etc.). Exceptions raised from the callback are logged and
|
|
111
|
+
swallowed so they never break the request flow.
|
|
30
112
|
"""
|
|
31
113
|
self.max_retries = max_retries
|
|
32
114
|
self.retry_on_rate_limit = retry_on_rate_limit
|
|
33
115
|
self.base_backoff_seconds = base_backoff_seconds
|
|
34
116
|
self.backoff_multiplier = backoff_multiplier
|
|
35
117
|
self.max_retry_delay = max_retry_delay
|
|
118
|
+
self.on_rate_limit_info = on_rate_limit_info
|
|
36
119
|
|
|
37
120
|
def calculate_backoff(self, retry_attempt: int) -> float:
|
|
38
121
|
"""
|
|
@@ -47,6 +130,32 @@ class RateLimitRetryConfig:
|
|
|
47
130
|
return self.base_backoff_seconds * (self.backoff_multiplier**retry_attempt)
|
|
48
131
|
|
|
49
132
|
|
|
133
|
+
def emit_rate_limit_info(
|
|
134
|
+
response: "Response",
|
|
135
|
+
config: "RateLimitRetryConfig",
|
|
136
|
+
method: str,
|
|
137
|
+
url: str,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Invoke the ``on_rate_limit_info`` callback if configured.
|
|
141
|
+
|
|
142
|
+
Parses rate limit headers off the response and hands them to the user's
|
|
143
|
+
callback. Any exception raised from the callback is logged and swallowed so
|
|
144
|
+
a buggy observer never breaks the underlying request.
|
|
145
|
+
"""
|
|
146
|
+
if config.on_rate_limit_info is None:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
info = parse_rate_limit_info(response, method=method, url=url)
|
|
150
|
+
if info is None:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
config.on_rate_limit_info(info)
|
|
155
|
+
except Exception:
|
|
156
|
+
logger.exception("on_rate_limit_info callback raised; suppressing")
|
|
157
|
+
|
|
158
|
+
|
|
50
159
|
def handle_rate_limit_response(
|
|
51
160
|
response: "Response",
|
|
52
161
|
config: RateLimitRetryConfig,
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/services/request.py
RENAMED
|
@@ -12,6 +12,7 @@ import httpx
|
|
|
12
12
|
from ..version import LAGO_VERSION
|
|
13
13
|
from .rate_limit import (
|
|
14
14
|
RateLimitRetryConfig,
|
|
15
|
+
emit_rate_limit_info,
|
|
15
16
|
handle_rate_limit_response,
|
|
16
17
|
is_rate_limit_response,
|
|
17
18
|
wait_for_retry,
|
|
@@ -92,12 +93,20 @@ def _create_retry_wrapper(
|
|
|
92
93
|
if rate_limit_retry_config is None:
|
|
93
94
|
rate_limit_retry_config = RateLimitRetryConfig()
|
|
94
95
|
|
|
96
|
+
method_name = http_method.__name__.upper()
|
|
97
|
+
|
|
95
98
|
retry_attempt = 0
|
|
96
99
|
while True:
|
|
97
100
|
response = http_method(url, **kwargs)
|
|
98
101
|
|
|
99
|
-
# If not rate limited, return the response
|
|
102
|
+
# If not rate limited, emit observability info and return the response
|
|
100
103
|
if not is_rate_limit_response(response):
|
|
104
|
+
emit_rate_limit_info(
|
|
105
|
+
response,
|
|
106
|
+
rate_limit_retry_config,
|
|
107
|
+
method=method_name,
|
|
108
|
+
url=url,
|
|
109
|
+
)
|
|
101
110
|
return response
|
|
102
111
|
|
|
103
112
|
# Handle rate limit - may raise LagoRateLimitError or return wait duration
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
LAGO_VERSION = "1.47.0"
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client.egg-info/SOURCES.txt
RENAMED
|
@@ -9,6 +9,7 @@ setup.cfg
|
|
|
9
9
|
./lago_python_client/exceptions.py
|
|
10
10
|
./lago_python_client/functools_ext.py
|
|
11
11
|
./lago_python_client/mixins.py
|
|
12
|
+
./lago_python_client/observability.py
|
|
12
13
|
./lago_python_client/py.typed
|
|
13
14
|
./lago_python_client/version.py
|
|
14
15
|
./lago_python_client/activity_logs/__init__.py
|
|
@@ -185,6 +186,7 @@ lago_python_client/client.py
|
|
|
185
186
|
lago_python_client/exceptions.py
|
|
186
187
|
lago_python_client/functools_ext.py
|
|
187
188
|
lago_python_client/mixins.py
|
|
189
|
+
lago_python_client/observability.py
|
|
188
190
|
lago_python_client/py.typed
|
|
189
191
|
lago_python_client/version.py
|
|
190
192
|
lago_python_client.egg-info/PKG-INFO
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -5,7 +6,12 @@ from pytest_httpx import HTTPXMock
|
|
|
5
6
|
|
|
6
7
|
from lago_python_client.client import Client
|
|
7
8
|
from lago_python_client.exceptions import LagoApiError, LagoRateLimitError
|
|
8
|
-
from lago_python_client.
|
|
9
|
+
from lago_python_client.observability import LoggingRateLimitObserver
|
|
10
|
+
from lago_python_client.services.rate_limit import (
|
|
11
|
+
RateLimitInfo,
|
|
12
|
+
RateLimitRetryConfig,
|
|
13
|
+
parse_rate_limit_info,
|
|
14
|
+
)
|
|
9
15
|
|
|
10
16
|
ENDPOINT = "https://api.getlago.com/api/v1/api_logs"
|
|
11
17
|
|
|
@@ -318,3 +324,279 @@ class TestRateLimitResponses:
|
|
|
318
324
|
error = exc_info.value
|
|
319
325
|
assert isinstance(error, LagoApiError)
|
|
320
326
|
assert not isinstance(error, LagoRateLimitError)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TestRateLimitInfo:
|
|
330
|
+
def test_usage_pct_basic(self):
|
|
331
|
+
info = RateLimitInfo(
|
|
332
|
+
limit=100,
|
|
333
|
+
remaining=20,
|
|
334
|
+
reset=30,
|
|
335
|
+
url="u",
|
|
336
|
+
method="GET",
|
|
337
|
+
)
|
|
338
|
+
assert info.usage_pct == pytest.approx(0.80)
|
|
339
|
+
|
|
340
|
+
def test_usage_pct_full(self):
|
|
341
|
+
info = RateLimitInfo(
|
|
342
|
+
limit=100,
|
|
343
|
+
remaining=0,
|
|
344
|
+
reset=30,
|
|
345
|
+
url="u",
|
|
346
|
+
method="GET",
|
|
347
|
+
)
|
|
348
|
+
assert info.usage_pct == pytest.approx(1.0)
|
|
349
|
+
|
|
350
|
+
def test_usage_pct_none_when_limit_missing(self):
|
|
351
|
+
info = RateLimitInfo(
|
|
352
|
+
limit=None,
|
|
353
|
+
remaining=20,
|
|
354
|
+
reset=30,
|
|
355
|
+
url="u",
|
|
356
|
+
method="GET",
|
|
357
|
+
)
|
|
358
|
+
assert info.usage_pct is None
|
|
359
|
+
|
|
360
|
+
def test_usage_pct_none_when_remaining_missing(self):
|
|
361
|
+
info = RateLimitInfo(
|
|
362
|
+
limit=100,
|
|
363
|
+
remaining=None,
|
|
364
|
+
reset=30,
|
|
365
|
+
url="u",
|
|
366
|
+
method="GET",
|
|
367
|
+
)
|
|
368
|
+
assert info.usage_pct is None
|
|
369
|
+
|
|
370
|
+
def test_usage_pct_none_when_limit_zero(self):
|
|
371
|
+
info = RateLimitInfo(
|
|
372
|
+
limit=0,
|
|
373
|
+
remaining=0,
|
|
374
|
+
reset=30,
|
|
375
|
+
url="u",
|
|
376
|
+
method="GET",
|
|
377
|
+
)
|
|
378
|
+
assert info.usage_pct is None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class TestParseRateLimitInfo:
|
|
382
|
+
def _make_response(self, headers):
|
|
383
|
+
class _R:
|
|
384
|
+
def __init__(self, h):
|
|
385
|
+
self.headers = h
|
|
386
|
+
|
|
387
|
+
return _R(headers)
|
|
388
|
+
|
|
389
|
+
def test_parse_full_headers(self):
|
|
390
|
+
resp = self._make_response(
|
|
391
|
+
{
|
|
392
|
+
"x-ratelimit-limit": "100",
|
|
393
|
+
"x-ratelimit-remaining": "42",
|
|
394
|
+
"x-ratelimit-reset": "5",
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
info = parse_rate_limit_info(resp, method="GET", url="https://x")
|
|
399
|
+
|
|
400
|
+
assert info is not None
|
|
401
|
+
assert info.limit == 100
|
|
402
|
+
assert info.remaining == 42
|
|
403
|
+
assert info.reset == 5
|
|
404
|
+
assert info.method == "GET"
|
|
405
|
+
assert info.url == "https://x"
|
|
406
|
+
|
|
407
|
+
def test_parse_returns_none_when_headers_absent(self):
|
|
408
|
+
resp = self._make_response({})
|
|
409
|
+
assert parse_rate_limit_info(resp, method="GET", url="https://x") is None
|
|
410
|
+
|
|
411
|
+
def test_parse_partial_headers(self):
|
|
412
|
+
resp = self._make_response({"x-ratelimit-limit": "100"})
|
|
413
|
+
|
|
414
|
+
info = parse_rate_limit_info(resp, method="GET", url="https://x")
|
|
415
|
+
|
|
416
|
+
assert info is not None
|
|
417
|
+
assert info.limit == 100
|
|
418
|
+
assert info.remaining is None
|
|
419
|
+
assert info.reset is None
|
|
420
|
+
|
|
421
|
+
def test_parse_invalid_headers(self):
|
|
422
|
+
resp = self._make_response(
|
|
423
|
+
{
|
|
424
|
+
"x-ratelimit-limit": "not-a-number",
|
|
425
|
+
"x-ratelimit-remaining": "nope",
|
|
426
|
+
"x-ratelimit-reset": "bad",
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# All fields fail to parse -> all None -> treated as no rate limit info
|
|
431
|
+
assert parse_rate_limit_info(resp, method="GET", url="https://x") is None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class TestRateLimitInfoCallback:
|
|
435
|
+
def test_callback_fires_on_success(self, httpx_mock: HTTPXMock):
|
|
436
|
+
"""Callback receives rate limit info after a 2xx response."""
|
|
437
|
+
captured = []
|
|
438
|
+
|
|
439
|
+
client = Client(api_key="test-key", on_rate_limit_info=captured.append)
|
|
440
|
+
request_id = "test-id"
|
|
441
|
+
|
|
442
|
+
httpx_mock.add_response(
|
|
443
|
+
method="GET",
|
|
444
|
+
url=ENDPOINT + f"/{request_id}",
|
|
445
|
+
status_code=200,
|
|
446
|
+
headers={
|
|
447
|
+
"x-ratelimit-limit": "100",
|
|
448
|
+
"x-ratelimit-remaining": "20",
|
|
449
|
+
"x-ratelimit-reset": "5",
|
|
450
|
+
},
|
|
451
|
+
content=mock_response("fixtures/api_log.json"),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
client.api_logs.find(request_id)
|
|
455
|
+
|
|
456
|
+
assert len(captured) == 1
|
|
457
|
+
info = captured[0]
|
|
458
|
+
assert info.limit == 100
|
|
459
|
+
assert info.remaining == 20
|
|
460
|
+
assert info.reset == 5
|
|
461
|
+
assert info.usage_pct == pytest.approx(0.80)
|
|
462
|
+
assert info.method == "GET"
|
|
463
|
+
|
|
464
|
+
def test_callback_not_called_when_headers_absent(self, httpx_mock: HTTPXMock):
|
|
465
|
+
"""Self-hosted-style response with no rate limit headers must not call back."""
|
|
466
|
+
captured = []
|
|
467
|
+
|
|
468
|
+
client = Client(api_key="test-key", on_rate_limit_info=captured.append)
|
|
469
|
+
request_id = "test-id"
|
|
470
|
+
|
|
471
|
+
httpx_mock.add_response(
|
|
472
|
+
method="GET",
|
|
473
|
+
url=ENDPOINT + f"/{request_id}",
|
|
474
|
+
status_code=200,
|
|
475
|
+
content=mock_response("fixtures/api_log.json"),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
client.api_logs.find(request_id)
|
|
479
|
+
|
|
480
|
+
assert captured == []
|
|
481
|
+
|
|
482
|
+
def test_callback_exception_is_swallowed(self, httpx_mock: HTTPXMock, caplog):
|
|
483
|
+
"""A buggy callback must not break the request."""
|
|
484
|
+
|
|
485
|
+
def boom(info):
|
|
486
|
+
raise RuntimeError("intentional")
|
|
487
|
+
|
|
488
|
+
client = Client(api_key="test-key", on_rate_limit_info=boom)
|
|
489
|
+
request_id = "test-id"
|
|
490
|
+
|
|
491
|
+
httpx_mock.add_response(
|
|
492
|
+
method="GET",
|
|
493
|
+
url=ENDPOINT + f"/{request_id}",
|
|
494
|
+
status_code=200,
|
|
495
|
+
headers={
|
|
496
|
+
"x-ratelimit-limit": "100",
|
|
497
|
+
"x-ratelimit-remaining": "1",
|
|
498
|
+
"x-ratelimit-reset": "5",
|
|
499
|
+
},
|
|
500
|
+
content=mock_response("fixtures/api_log.json"),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
with caplog.at_level(logging.ERROR, logger="lago_python_client.services.rate_limit"):
|
|
504
|
+
result = client.api_logs.find(request_id)
|
|
505
|
+
|
|
506
|
+
assert result is not None
|
|
507
|
+
assert any("on_rate_limit_info callback raised" in rec.message for rec in caplog.records)
|
|
508
|
+
|
|
509
|
+
def test_callback_fires_only_once_after_429_retry_resolves(
|
|
510
|
+
self,
|
|
511
|
+
httpx_mock: HTTPXMock,
|
|
512
|
+
monkeypatch,
|
|
513
|
+
):
|
|
514
|
+
"""After a 429 retry sequence resolves to 2xx, callback fires once for the success."""
|
|
515
|
+
monkeypatch.setattr("lago_python_client.services.rate_limit.time.sleep", lambda _: None)
|
|
516
|
+
|
|
517
|
+
captured = []
|
|
518
|
+
client = Client(api_key="test-key", on_rate_limit_info=captured.append)
|
|
519
|
+
request_id = "test-id"
|
|
520
|
+
|
|
521
|
+
httpx_mock.add_response(
|
|
522
|
+
method="GET",
|
|
523
|
+
url=ENDPOINT + f"/{request_id}",
|
|
524
|
+
status_code=429,
|
|
525
|
+
headers={"x-ratelimit-reset": "1"},
|
|
526
|
+
)
|
|
527
|
+
httpx_mock.add_response(
|
|
528
|
+
method="GET",
|
|
529
|
+
url=ENDPOINT + f"/{request_id}",
|
|
530
|
+
status_code=200,
|
|
531
|
+
headers={
|
|
532
|
+
"x-ratelimit-limit": "100",
|
|
533
|
+
"x-ratelimit-remaining": "50",
|
|
534
|
+
"x-ratelimit-reset": "5",
|
|
535
|
+
},
|
|
536
|
+
content=mock_response("fixtures/api_log.json"),
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
client.api_logs.find(request_id)
|
|
540
|
+
|
|
541
|
+
assert len(captured) == 1
|
|
542
|
+
assert captured[0].remaining == 50
|
|
543
|
+
|
|
544
|
+
def test_callback_not_called_when_429_exhausts(
|
|
545
|
+
self,
|
|
546
|
+
httpx_mock: HTTPXMock,
|
|
547
|
+
monkeypatch,
|
|
548
|
+
):
|
|
549
|
+
"""Exhausted 429 retries raise without ever invoking the success callback."""
|
|
550
|
+
monkeypatch.setattr("lago_python_client.services.rate_limit.time.sleep", lambda _: None)
|
|
551
|
+
|
|
552
|
+
captured = []
|
|
553
|
+
client = Client(api_key="test-key", on_rate_limit_info=captured.append)
|
|
554
|
+
request_id = "test-id"
|
|
555
|
+
|
|
556
|
+
for _ in range(4):
|
|
557
|
+
httpx_mock.add_response(
|
|
558
|
+
method="GET",
|
|
559
|
+
url=ENDPOINT + f"/{request_id}",
|
|
560
|
+
status_code=429,
|
|
561
|
+
headers={"x-ratelimit-reset": "1"},
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
with pytest.raises(LagoRateLimitError):
|
|
565
|
+
client.api_logs.find(request_id)
|
|
566
|
+
|
|
567
|
+
assert captured == []
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class TestLoggingRateLimitObserver:
|
|
571
|
+
def _info(self, *, limit, remaining):
|
|
572
|
+
return RateLimitInfo(
|
|
573
|
+
limit=limit,
|
|
574
|
+
remaining=remaining,
|
|
575
|
+
reset=10,
|
|
576
|
+
url="https://x",
|
|
577
|
+
method="GET",
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
def test_logs_above_threshold(self, caplog):
|
|
581
|
+
observer = LoggingRateLimitObserver(thresholds=(0.80, 0.90, 0.95))
|
|
582
|
+
|
|
583
|
+
with caplog.at_level(logging.WARNING, logger="lago_python_client.rate_limit"):
|
|
584
|
+
observer(self._info(limit=100, remaining=4)) # 96% used
|
|
585
|
+
|
|
586
|
+
assert any("96%" in rec.getMessage() for rec in caplog.records)
|
|
587
|
+
|
|
588
|
+
def test_silent_below_threshold(self, caplog):
|
|
589
|
+
observer = LoggingRateLimitObserver(thresholds=(0.80,))
|
|
590
|
+
|
|
591
|
+
with caplog.at_level(logging.WARNING, logger="lago_python_client.rate_limit"):
|
|
592
|
+
observer(self._info(limit=100, remaining=50)) # 50% used
|
|
593
|
+
|
|
594
|
+
assert caplog.records == []
|
|
595
|
+
|
|
596
|
+
def test_silent_when_usage_pct_unavailable(self, caplog):
|
|
597
|
+
observer = LoggingRateLimitObserver()
|
|
598
|
+
|
|
599
|
+
with caplog.at_level(logging.WARNING, logger="lago_python_client.rate_limit"):
|
|
600
|
+
observer(self._info(limit=None, remaining=None))
|
|
601
|
+
|
|
602
|
+
assert caplog.records == []
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
LAGO_VERSION = "1.46.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/activity_logs/__init__.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/activity_logs/clients.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/add_ons/__init__.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/add_ons/clients.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/api_logs/__init__.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/api_logs/clients.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/coupons/__init__.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/coupons/clients.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/credit_notes/__init__.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/credit_notes/clients.py
RENAMED
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{lago_python_client-1.46.0 → lago_python_client-1.47.0}/lago_python_client/customers/clients.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|