threecommon 0.1.0__py3-none-any.whl
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.
- threecommon/__init__.py +79 -0
- threecommon/_core/__init__.py +6 -0
- threecommon/_core/headers.py +41 -0
- threecommon/_core/http_client.py +424 -0
- threecommon/_core/parse.py +77 -0
- threecommon/_core/retry.py +80 -0
- threecommon/_core/telemetry.py +77 -0
- threecommon/_core/url.py +31 -0
- threecommon/_generated/__init__.py +8 -0
- threecommon/_generated/models.py +614 -0
- threecommon/api_version.py +15 -0
- threecommon/client.py +184 -0
- threecommon/config.py +140 -0
- threecommon/errors/__init__.py +35 -0
- threecommon/errors/base.py +81 -0
- threecommon/errors/classes.py +75 -0
- threecommon/events/__init__.py +28 -0
- threecommon/events/service.py +170 -0
- threecommon/events/types.py +124 -0
- threecommon/filters/__init__.py +51 -0
- threecommon/filters/builder.py +188 -0
- threecommon/filters/types.py +69 -0
- threecommon/helpers.py +19 -0
- threecommon/invoices/__init__.py +40 -0
- threecommon/invoices/service.py +266 -0
- threecommon/invoices/types.py +195 -0
- threecommon/pagination/__init__.py +12 -0
- threecommon/pagination/auto_paginator.py +98 -0
- threecommon/py.typed +0 -0
- threecommon/version.py +10 -0
- threecommon-0.1.0.dist-info/METADATA +399 -0
- threecommon-0.1.0.dist-info/RECORD +34 -0
- threecommon-0.1.0.dist-info/WHEEL +4 -0
- threecommon-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Sync and async invoices services.
|
|
2
|
+
|
|
3
|
+
Both services share the same wire shape and validation logic; the only
|
|
4
|
+
difference is which HTTP client they call.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
|
|
12
|
+
from threecommon._core.http_client import Request
|
|
13
|
+
from threecommon.errors.classes import ValidationError
|
|
14
|
+
from threecommon.invoices.types import (
|
|
15
|
+
CreateBody,
|
|
16
|
+
Invoice,
|
|
17
|
+
ListInvoicesResponse,
|
|
18
|
+
ListParams,
|
|
19
|
+
PaymentBody,
|
|
20
|
+
RetrieveParams,
|
|
21
|
+
UpdateBody,
|
|
22
|
+
VoidBody,
|
|
23
|
+
)
|
|
24
|
+
from threecommon.pagination import AsyncIter, Iter
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from threecommon._core.http_client import AsyncHTTPClient, HTTPClient
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _encode_list_params(params: ListParams | None) -> dict[str, str] | None:
|
|
31
|
+
if params is None:
|
|
32
|
+
return None
|
|
33
|
+
raw = params.model_dump(by_alias=True, exclude_none=True)
|
|
34
|
+
if not raw:
|
|
35
|
+
return None
|
|
36
|
+
return {k: str(v) for k, v in raw.items()}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _encode_retrieve_params(params: RetrieveParams | None) -> dict[str, str] | None:
|
|
40
|
+
if params is None or params.fields is None:
|
|
41
|
+
return None
|
|
42
|
+
return {"fields": params.fields}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _require_id(method: str, invoice_id: str) -> None:
|
|
46
|
+
if not invoice_id:
|
|
47
|
+
msg = f"invoices.{method}: id must be a non-empty string"
|
|
48
|
+
raise ValidationError(code="missing_id", message=msg)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _path_for(invoice_id: str) -> str:
|
|
52
|
+
return f"/invoices/{quote(invoice_id, safe='')}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _action_path(invoice_id: str, action: str) -> str:
|
|
56
|
+
return f"{_path_for(invoice_id)}/{action}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
# Sync
|
|
61
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class InvoicesService:
|
|
65
|
+
"""Sync invoices service — bound as ``client.invoices`` on [ThreeCommon]."""
|
|
66
|
+
|
|
67
|
+
__slots__ = ("_http",)
|
|
68
|
+
|
|
69
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
70
|
+
self._http = http
|
|
71
|
+
|
|
72
|
+
def list(self, params: ListParams | None = None) -> ListInvoicesResponse:
|
|
73
|
+
"""List the host's invoices (one page).
|
|
74
|
+
|
|
75
|
+
For full iteration use [list_auto_paginate][InvoicesService.list_auto_paginate].
|
|
76
|
+
"""
|
|
77
|
+
body = self._http.request(
|
|
78
|
+
Request(method="GET", path="/invoices", query=_encode_list_params(params))
|
|
79
|
+
)
|
|
80
|
+
return ListInvoicesResponse.model_validate(body)
|
|
81
|
+
|
|
82
|
+
def retrieve(self, invoice_id: str, params: RetrieveParams | None = None) -> Invoice:
|
|
83
|
+
"""Retrieve a single invoice by id."""
|
|
84
|
+
_require_id("retrieve", invoice_id)
|
|
85
|
+
body = self._http.request(
|
|
86
|
+
Request(
|
|
87
|
+
method="GET",
|
|
88
|
+
path=_path_for(invoice_id),
|
|
89
|
+
query=_encode_retrieve_params(params),
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return Invoice.model_validate(body["data"])
|
|
93
|
+
|
|
94
|
+
def create(self, body: CreateBody) -> Invoice:
|
|
95
|
+
"""Create a draft invoice. Totals are computed server-side from line items."""
|
|
96
|
+
if body is None:
|
|
97
|
+
raise ValidationError(
|
|
98
|
+
code="missing_body", message="invoices.create: body must be non-None"
|
|
99
|
+
)
|
|
100
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
101
|
+
response = self._http.request(Request(method="POST", path="/invoices", body=payload))
|
|
102
|
+
return Invoice.model_validate(response["data"])
|
|
103
|
+
|
|
104
|
+
def update(self, invoice_id: str, body: UpdateBody) -> Invoice:
|
|
105
|
+
"""Revise a draft invoice. Only legal while in draft."""
|
|
106
|
+
_require_id("update", invoice_id)
|
|
107
|
+
if body is None:
|
|
108
|
+
raise ValidationError(
|
|
109
|
+
code="missing_body", message="invoices.update: body must be non-None"
|
|
110
|
+
)
|
|
111
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
112
|
+
response = self._http.request(
|
|
113
|
+
Request(method="PATCH", path=_path_for(invoice_id), body=payload)
|
|
114
|
+
)
|
|
115
|
+
return Invoice.model_validate(response["data"])
|
|
116
|
+
|
|
117
|
+
def finalize(self, invoice_id: str) -> Invoice:
|
|
118
|
+
"""Finalize a draft invoice: assign a number, stamp ``issuedAt``, set status ``open``."""
|
|
119
|
+
_require_id("finalize", invoice_id)
|
|
120
|
+
response = self._http.request(
|
|
121
|
+
Request(method="POST", path=_action_path(invoice_id, "finalize"), body={})
|
|
122
|
+
)
|
|
123
|
+
return Invoice.model_validate(response["data"])
|
|
124
|
+
|
|
125
|
+
def void(self, invoice_id: str, body: VoidBody | None = None) -> Invoice:
|
|
126
|
+
"""Void an invoice. Permitted from ``draft`` or ``open``; paid invoices cannot be voided."""
|
|
127
|
+
_require_id("void", invoice_id)
|
|
128
|
+
payload = body.model_dump(by_alias=True, exclude_none=True) if body is not None else {}
|
|
129
|
+
response = self._http.request(
|
|
130
|
+
Request(method="POST", path=_action_path(invoice_id, "void"), body=payload)
|
|
131
|
+
)
|
|
132
|
+
return Invoice.model_validate(response["data"])
|
|
133
|
+
|
|
134
|
+
def record_payment(self, invoice_id: str, body: PaymentBody) -> Invoice:
|
|
135
|
+
"""Record a manual payment against an open invoice.
|
|
136
|
+
|
|
137
|
+
Cumulative payments meeting the total transition the invoice to ``paid``.
|
|
138
|
+
"""
|
|
139
|
+
_require_id("record_payment", invoice_id)
|
|
140
|
+
if body is None:
|
|
141
|
+
raise ValidationError(
|
|
142
|
+
code="missing_body",
|
|
143
|
+
message="invoices.record_payment: body must be non-None",
|
|
144
|
+
)
|
|
145
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
146
|
+
response = self._http.request(
|
|
147
|
+
Request(method="POST", path=_action_path(invoice_id, "payments"), body=payload)
|
|
148
|
+
)
|
|
149
|
+
return Invoice.model_validate(response["data"])
|
|
150
|
+
|
|
151
|
+
def list_auto_paginate(self, params: ListParams | None = None) -> Iter[Invoice]:
|
|
152
|
+
"""Iterate every invoice matching ``params``, paging automatically."""
|
|
153
|
+
start_page = params.page if params is not None and params.page is not None else 0
|
|
154
|
+
|
|
155
|
+
def fetch(page: int) -> tuple[list[Invoice], bool]:
|
|
156
|
+
page_params = (
|
|
157
|
+
params.model_copy(update={"page": page})
|
|
158
|
+
if params is not None
|
|
159
|
+
else ListParams(page=page)
|
|
160
|
+
)
|
|
161
|
+
body = self._http.request(
|
|
162
|
+
Request(method="GET", path="/invoices", query=_encode_list_params(page_params))
|
|
163
|
+
)
|
|
164
|
+
response = ListInvoicesResponse.model_validate(body)
|
|
165
|
+
return response.data, response.has_more
|
|
166
|
+
|
|
167
|
+
return Iter(fetch_page=fetch, start_page=start_page)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
# Async
|
|
172
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AsyncInvoicesService:
|
|
176
|
+
"""Async invoices service — bound as ``client.invoices`` on [AsyncThreeCommon]."""
|
|
177
|
+
|
|
178
|
+
__slots__ = ("_http",)
|
|
179
|
+
|
|
180
|
+
def __init__(self, http: AsyncHTTPClient) -> None:
|
|
181
|
+
self._http = http
|
|
182
|
+
|
|
183
|
+
async def list(self, params: ListParams | None = None) -> ListInvoicesResponse:
|
|
184
|
+
body = await self._http.request(
|
|
185
|
+
Request(method="GET", path="/invoices", query=_encode_list_params(params))
|
|
186
|
+
)
|
|
187
|
+
return ListInvoicesResponse.model_validate(body)
|
|
188
|
+
|
|
189
|
+
async def retrieve(self, invoice_id: str, params: RetrieveParams | None = None) -> Invoice:
|
|
190
|
+
_require_id("retrieve", invoice_id)
|
|
191
|
+
body = await self._http.request(
|
|
192
|
+
Request(
|
|
193
|
+
method="GET",
|
|
194
|
+
path=_path_for(invoice_id),
|
|
195
|
+
query=_encode_retrieve_params(params),
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
return Invoice.model_validate(body["data"])
|
|
199
|
+
|
|
200
|
+
async def create(self, body: CreateBody) -> Invoice:
|
|
201
|
+
if body is None:
|
|
202
|
+
raise ValidationError(
|
|
203
|
+
code="missing_body", message="invoices.create: body must be non-None"
|
|
204
|
+
)
|
|
205
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
206
|
+
response = await self._http.request(Request(method="POST", path="/invoices", body=payload))
|
|
207
|
+
return Invoice.model_validate(response["data"])
|
|
208
|
+
|
|
209
|
+
async def update(self, invoice_id: str, body: UpdateBody) -> Invoice:
|
|
210
|
+
_require_id("update", invoice_id)
|
|
211
|
+
if body is None:
|
|
212
|
+
raise ValidationError(
|
|
213
|
+
code="missing_body", message="invoices.update: body must be non-None"
|
|
214
|
+
)
|
|
215
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
216
|
+
response = await self._http.request(
|
|
217
|
+
Request(method="PATCH", path=_path_for(invoice_id), body=payload)
|
|
218
|
+
)
|
|
219
|
+
return Invoice.model_validate(response["data"])
|
|
220
|
+
|
|
221
|
+
async def finalize(self, invoice_id: str) -> Invoice:
|
|
222
|
+
_require_id("finalize", invoice_id)
|
|
223
|
+
response = await self._http.request(
|
|
224
|
+
Request(method="POST", path=_action_path(invoice_id, "finalize"), body={})
|
|
225
|
+
)
|
|
226
|
+
return Invoice.model_validate(response["data"])
|
|
227
|
+
|
|
228
|
+
async def void(self, invoice_id: str, body: VoidBody | None = None) -> Invoice:
|
|
229
|
+
_require_id("void", invoice_id)
|
|
230
|
+
payload = body.model_dump(by_alias=True, exclude_none=True) if body is not None else {}
|
|
231
|
+
response = await self._http.request(
|
|
232
|
+
Request(method="POST", path=_action_path(invoice_id, "void"), body=payload)
|
|
233
|
+
)
|
|
234
|
+
return Invoice.model_validate(response["data"])
|
|
235
|
+
|
|
236
|
+
async def record_payment(self, invoice_id: str, body: PaymentBody) -> Invoice:
|
|
237
|
+
_require_id("record_payment", invoice_id)
|
|
238
|
+
if body is None:
|
|
239
|
+
raise ValidationError(
|
|
240
|
+
code="missing_body",
|
|
241
|
+
message="invoices.record_payment: body must be non-None",
|
|
242
|
+
)
|
|
243
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
244
|
+
response = await self._http.request(
|
|
245
|
+
Request(method="POST", path=_action_path(invoice_id, "payments"), body=payload)
|
|
246
|
+
)
|
|
247
|
+
return Invoice.model_validate(response["data"])
|
|
248
|
+
|
|
249
|
+
def list_auto_paginate(self, params: ListParams | None = None) -> AsyncIter[Invoice]:
|
|
250
|
+
"""Async iterate every invoice matching ``params``."""
|
|
251
|
+
start_page = params.page if params is not None and params.page is not None else 0
|
|
252
|
+
http = self._http
|
|
253
|
+
|
|
254
|
+
async def fetch(page: int) -> tuple[list[Invoice], bool]:
|
|
255
|
+
page_params = (
|
|
256
|
+
params.model_copy(update={"page": page})
|
|
257
|
+
if params is not None
|
|
258
|
+
else ListParams(page=page)
|
|
259
|
+
)
|
|
260
|
+
body = await http.request(
|
|
261
|
+
Request(method="GET", path="/invoices", query=_encode_list_params(page_params))
|
|
262
|
+
)
|
|
263
|
+
response = ListInvoicesResponse.model_validate(body)
|
|
264
|
+
return response.data, response.has_more
|
|
265
|
+
|
|
266
|
+
return AsyncIter(fetch_page=fetch, start_page=start_page)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Public types for the invoices resource.
|
|
2
|
+
|
|
3
|
+
Hand-curated Pydantic models that mirror the wire shape (camelCase aliases
|
|
4
|
+
preserved). All response models use ``extra="ignore"`` so newer server-side
|
|
5
|
+
fields don't break older SDK versions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
#: Lifecycle status of an invoice. Future server-side values will arrive as
|
|
15
|
+
#: raw strings until the SDK is updated.
|
|
16
|
+
InvoiceStatus = Literal["draft", "open", "paid", "void"]
|
|
17
|
+
|
|
18
|
+
#: Invoice currency code; all line amounts must match.
|
|
19
|
+
InvoiceCurrency = Literal["USD", "CAD"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _BaseModel(BaseModel):
|
|
23
|
+
"""Shared config: accept snake_case or camelCase, ignore unknown fields."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(
|
|
26
|
+
populate_by_name=True,
|
|
27
|
+
extra="ignore",
|
|
28
|
+
str_strip_whitespace=False,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InvoiceLineItem(_BaseModel):
|
|
33
|
+
"""One line on an invoice."""
|
|
34
|
+
|
|
35
|
+
description: str
|
|
36
|
+
quantity: int
|
|
37
|
+
unit_amount: int = Field(serialization_alias="unitAmount", validation_alias="unitAmount")
|
|
38
|
+
product_id: str | None = Field(
|
|
39
|
+
default=None, serialization_alias="productId", validation_alias="productId"
|
|
40
|
+
)
|
|
41
|
+
tax_amount: int | None = Field(
|
|
42
|
+
default=None, serialization_alias="taxAmount", validation_alias="taxAmount"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InvoicePayment(_BaseModel):
|
|
47
|
+
"""One recorded payment against an invoice."""
|
|
48
|
+
|
|
49
|
+
id: str
|
|
50
|
+
amount: int
|
|
51
|
+
paid_at: str = Field(serialization_alias="paidAt", validation_alias="paidAt")
|
|
52
|
+
idempotency_key: str | None = Field(
|
|
53
|
+
default=None, serialization_alias="idempotencyKey", validation_alias="idempotencyKey"
|
|
54
|
+
)
|
|
55
|
+
note: str | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Invoice(_BaseModel):
|
|
59
|
+
"""One invoice as returned by the API.
|
|
60
|
+
|
|
61
|
+
Optional fields are populated only when the server returned them — list
|
|
62
|
+
responses with a ``fields`` filter omit unrequested values.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
id: str
|
|
66
|
+
host_id: str | None = Field(
|
|
67
|
+
default=None, serialization_alias="hostId", validation_alias="hostId"
|
|
68
|
+
)
|
|
69
|
+
customer_id: str | None = Field(
|
|
70
|
+
default=None, serialization_alias="customerId", validation_alias="customerId"
|
|
71
|
+
)
|
|
72
|
+
number: str | None = None
|
|
73
|
+
currency: InvoiceCurrency | None = None
|
|
74
|
+
line_items: list[InvoiceLineItem] | None = Field(
|
|
75
|
+
default=None, serialization_alias="lineItems", validation_alias="lineItems"
|
|
76
|
+
)
|
|
77
|
+
payments: list[InvoicePayment] | None = None
|
|
78
|
+
subtotal: int | None = None
|
|
79
|
+
tax_total: int | None = Field(
|
|
80
|
+
default=None, serialization_alias="taxTotal", validation_alias="taxTotal"
|
|
81
|
+
)
|
|
82
|
+
total: int | None = None
|
|
83
|
+
amount_paid: int | None = Field(
|
|
84
|
+
default=None, serialization_alias="amountPaid", validation_alias="amountPaid"
|
|
85
|
+
)
|
|
86
|
+
amount_due: int | None = Field(
|
|
87
|
+
default=None, serialization_alias="amountDue", validation_alias="amountDue"
|
|
88
|
+
)
|
|
89
|
+
status: InvoiceStatus | None = None
|
|
90
|
+
notes: str | None = None
|
|
91
|
+
issued_at: str | None = Field(
|
|
92
|
+
default=None, serialization_alias="issuedAt", validation_alias="issuedAt"
|
|
93
|
+
)
|
|
94
|
+
due_at: str | None = Field(default=None, serialization_alias="dueAt", validation_alias="dueAt")
|
|
95
|
+
paid_at: str | None = Field(
|
|
96
|
+
default=None, serialization_alias="paidAt", validation_alias="paidAt"
|
|
97
|
+
)
|
|
98
|
+
voided_at: str | None = Field(
|
|
99
|
+
default=None, serialization_alias="voidedAt", validation_alias="voidedAt"
|
|
100
|
+
)
|
|
101
|
+
subscription_id: str | None = Field(
|
|
102
|
+
default=None, serialization_alias="subscriptionId", validation_alias="subscriptionId"
|
|
103
|
+
)
|
|
104
|
+
quote_id: str | None = Field(
|
|
105
|
+
default=None, serialization_alias="quoteId", validation_alias="quoteId"
|
|
106
|
+
)
|
|
107
|
+
created_at: str | None = Field(
|
|
108
|
+
default=None, serialization_alias="createdAt", validation_alias="createdAt"
|
|
109
|
+
)
|
|
110
|
+
updated_at: str | None = Field(
|
|
111
|
+
default=None, serialization_alias="updatedAt", validation_alias="updatedAt"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ListInvoicesResponse(_BaseModel):
|
|
116
|
+
"""Successful response shape from ``GET /v1/invoices``."""
|
|
117
|
+
|
|
118
|
+
data: list[Invoice]
|
|
119
|
+
has_more: bool = Field(serialization_alias="hasMore", validation_alias="hasMore")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ListParams(_BaseModel):
|
|
123
|
+
"""Query parameters accepted by ``GET /v1/invoices``."""
|
|
124
|
+
|
|
125
|
+
page: int | None = None
|
|
126
|
+
page_size: int | None = Field(
|
|
127
|
+
default=None, serialization_alias="pageSize", validation_alias="pageSize"
|
|
128
|
+
)
|
|
129
|
+
status: InvoiceStatus | None = None
|
|
130
|
+
customer_id: str | None = Field(
|
|
131
|
+
default=None, serialization_alias="customerId", validation_alias="customerId"
|
|
132
|
+
)
|
|
133
|
+
issued_after: str | None = Field(
|
|
134
|
+
default=None, serialization_alias="issuedAfter", validation_alias="issuedAfter"
|
|
135
|
+
)
|
|
136
|
+
issued_before: str | None = Field(
|
|
137
|
+
default=None, serialization_alias="issuedBefore", validation_alias="issuedBefore"
|
|
138
|
+
)
|
|
139
|
+
fields: str | None = None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class RetrieveParams(_BaseModel):
|
|
143
|
+
"""Query parameters accepted by ``GET /v1/invoices/{id}``."""
|
|
144
|
+
|
|
145
|
+
fields: str | None = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class CreateBody(_BaseModel):
|
|
149
|
+
"""Body accepted by ``POST /v1/invoices``."""
|
|
150
|
+
|
|
151
|
+
customer_id: str = Field(serialization_alias="customerId", validation_alias="customerId")
|
|
152
|
+
currency: InvoiceCurrency
|
|
153
|
+
line_items: list[InvoiceLineItem] = Field(
|
|
154
|
+
serialization_alias="lineItems", validation_alias="lineItems"
|
|
155
|
+
)
|
|
156
|
+
notes: str | None = None
|
|
157
|
+
due_at: str | None = Field(default=None, serialization_alias="dueAt", validation_alias="dueAt")
|
|
158
|
+
subscription_id: str | None = Field(
|
|
159
|
+
default=None, serialization_alias="subscriptionId", validation_alias="subscriptionId"
|
|
160
|
+
)
|
|
161
|
+
quote_id: str | None = Field(
|
|
162
|
+
default=None, serialization_alias="quoteId", validation_alias="quoteId"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class UpdateBody(_BaseModel):
|
|
167
|
+
"""Body accepted by ``PATCH /v1/invoices/{id}``.
|
|
168
|
+
|
|
169
|
+
Only fields you provide are changed.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
customer_id: str | None = Field(
|
|
173
|
+
default=None, serialization_alias="customerId", validation_alias="customerId"
|
|
174
|
+
)
|
|
175
|
+
line_items: list[InvoiceLineItem] | None = Field(
|
|
176
|
+
default=None, serialization_alias="lineItems", validation_alias="lineItems"
|
|
177
|
+
)
|
|
178
|
+
notes: str | None = None
|
|
179
|
+
due_at: str | None = Field(default=None, serialization_alias="dueAt", validation_alias="dueAt")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class VoidBody(_BaseModel):
|
|
183
|
+
"""Body accepted by ``POST /v1/invoices/{id}/void``."""
|
|
184
|
+
|
|
185
|
+
reason: str | None = None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class PaymentBody(_BaseModel):
|
|
189
|
+
"""Body accepted by ``POST /v1/invoices/{id}/payments``."""
|
|
190
|
+
|
|
191
|
+
payment: int
|
|
192
|
+
idempotency_key: str | None = Field(
|
|
193
|
+
default=None, serialization_alias="idempotencyKey", validation_alias="idempotencyKey"
|
|
194
|
+
)
|
|
195
|
+
note: str | None = None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Pagination module — exports the sync and async auto-paginating iterators.
|
|
2
|
+
|
|
3
|
+
User usually receive these from resource methods (e.g.
|
|
4
|
+
``client.events.list_auto_paginate(...)``) rather than constructing them
|
|
5
|
+
directly. Importing the submodule directly is supported::
|
|
6
|
+
|
|
7
|
+
from threecommon.pagination import Iter, AsyncIter
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from threecommon.pagination.auto_paginator import AsyncIter, Iter
|
|
11
|
+
|
|
12
|
+
__all__ = ("AsyncIter", "Iter")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""auto-paginating iterators returned by every list endpoint.
|
|
2
|
+
|
|
3
|
+
Two flavors:
|
|
4
|
+
|
|
5
|
+
* [Iter][threecommon.pagination.Iter] — sync iterator. Use ``for ev in iter:``.
|
|
6
|
+
* [AsyncIter][threecommon.pagination.AsyncIter] — async iterator.
|
|
7
|
+
Use ``async for ev in iter:``.
|
|
8
|
+
|
|
9
|
+
Both walk pages lazily — one HTTP call per page, only when the user
|
|
10
|
+
drains the previous page's buffer.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
|
|
16
|
+
from typing import Generic, TypeVar
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
#: Synchronous page-fetch callback. Returns ``(page_items, has_more)``.
|
|
21
|
+
SyncFetchPage = Callable[[int], tuple[list[T], bool]]
|
|
22
|
+
|
|
23
|
+
#: Asynchronous page-fetch callback. Returns ``(page_items, has_more)``.
|
|
24
|
+
AsyncFetchPage = Callable[[int], Awaitable[tuple[list[T], bool]]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Iter(Generic[T]):
|
|
28
|
+
"""Sync auto-paginating iterator. ``for ev in iter:`` drives the pages."""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("_buffer", "_fetch_page", "_has_more", "_index", "_page")
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, fetch_page: SyncFetchPage[T], start_page: int = 0) -> None:
|
|
33
|
+
self._fetch_page = fetch_page
|
|
34
|
+
self._page = start_page
|
|
35
|
+
self._buffer: list[T] = []
|
|
36
|
+
self._index = 0
|
|
37
|
+
self._has_more = True
|
|
38
|
+
|
|
39
|
+
def __iter__(self) -> Iterator[T]:
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __next__(self) -> T:
|
|
43
|
+
if self._index < len(self._buffer):
|
|
44
|
+
value = self._buffer[self._index]
|
|
45
|
+
self._index += 1
|
|
46
|
+
return value
|
|
47
|
+
if not self._has_more:
|
|
48
|
+
raise StopIteration
|
|
49
|
+
|
|
50
|
+
data, has_more = self._fetch_page(self._page)
|
|
51
|
+
self._buffer = data
|
|
52
|
+
self._index = 0
|
|
53
|
+
self._has_more = has_more
|
|
54
|
+
self._page += 1
|
|
55
|
+
|
|
56
|
+
if not self._buffer:
|
|
57
|
+
raise StopIteration
|
|
58
|
+
|
|
59
|
+
value = self._buffer[self._index]
|
|
60
|
+
self._index += 1
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AsyncIter(Generic[T]):
|
|
65
|
+
"""Async auto-paginating iterator. ``async for ev in iter:`` drives the pages."""
|
|
66
|
+
|
|
67
|
+
__slots__ = ("_buffer", "_fetch_page", "_has_more", "_index", "_page")
|
|
68
|
+
|
|
69
|
+
def __init__(self, *, fetch_page: AsyncFetchPage[T], start_page: int = 0) -> None:
|
|
70
|
+
self._fetch_page = fetch_page
|
|
71
|
+
self._page = start_page
|
|
72
|
+
self._buffer: list[T] = []
|
|
73
|
+
self._index = 0
|
|
74
|
+
self._has_more = True
|
|
75
|
+
|
|
76
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
async def __anext__(self) -> T:
|
|
80
|
+
if self._index < len(self._buffer):
|
|
81
|
+
value = self._buffer[self._index]
|
|
82
|
+
self._index += 1
|
|
83
|
+
return value
|
|
84
|
+
if not self._has_more:
|
|
85
|
+
raise StopAsyncIteration
|
|
86
|
+
|
|
87
|
+
data, has_more = await self._fetch_page(self._page)
|
|
88
|
+
self._buffer = data
|
|
89
|
+
self._index = 0
|
|
90
|
+
self._has_more = has_more
|
|
91
|
+
self._page += 1
|
|
92
|
+
|
|
93
|
+
if not self._buffer:
|
|
94
|
+
raise StopAsyncIteration
|
|
95
|
+
|
|
96
|
+
value = self._buffer[self._index]
|
|
97
|
+
self._index += 1
|
|
98
|
+
return value
|
threecommon/py.typed
ADDED
|
File without changes
|
threecommon/version.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""SDK package version.
|
|
2
|
+
|
|
3
|
+
Replaced by the release pipeline at publish time (currently static; will
|
|
4
|
+
become dynamic via hatch-vcs once the path-prefixed-tag wiring lands — see
|
|
5
|
+
the comment in pyproject.toml).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.0.0.dev0"
|
|
9
|
+
|
|
10
|
+
VERSION = __version__
|