threecommon 0.7.0__tar.gz → 0.7.1__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.
- {threecommon-0.7.0 → threecommon-0.7.1}/CHANGELOG.md +21 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/PKG-INFO +1 -1
- {threecommon-0.7.0 → threecommon-0.7.1}/pyproject.toml +1 -1
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/headers.py +6 -1
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/http_client.py +4 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/features/service.py +4 -4
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/invoices/service.py +4 -4
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/prices/service.py +4 -6
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/subscriptions/service.py +8 -8
- {threecommon-0.7.0 → threecommon-0.7.1}/.gitignore +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/LICENSE +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/README.md +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/parse.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/retry.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/telemetry.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/url.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_generated/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_generated/models.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/api_version.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/client.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/config.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/contacts/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/contacts/service.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/contacts/types.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/entitlements/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/entitlements/service.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/entitlements/types.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/errors/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/errors/base.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/errors/classes.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/events/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/events/service.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/events/types.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/features/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/features/types.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/filters/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/filters/builder.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/filters/types.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/helpers.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/invoices/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/invoices/types.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/pagination/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/pagination/auto_paginator.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/prices/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/prices/types.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/py.typed +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/subscriptions/__init__.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/subscriptions/types.py +0 -0
- {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/version.py +0 -0
|
@@ -5,6 +5,27 @@ versions follow [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## 0.7.1
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Requests without a body no longer send `Content-Type: application/json`.
|
|
13
|
+
`DELETE` (e.g. `invoices.delete_draft`, `contacts.delete`) and the
|
|
14
|
+
action-style POST methods previously advertised a JSON body, which servers
|
|
15
|
+
enforcing `Content-Type` against an empty body reject with HTTP 400.
|
|
16
|
+
`build_headers` now sets `Content-Type` only when a body is present. Applies
|
|
17
|
+
to both the sync and async clients.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The action-style POST methods (`invoices.finalize`, `invoices.auto_charge`,
|
|
22
|
+
`features.archive`, `features.unarchive`, `prices.archive`,
|
|
23
|
+
`prices.unarchive`, `subscriptions.activate`, `subscriptions.mark_unpaid`,
|
|
24
|
+
`subscriptions.bill`, `subscriptions.renew`) no longer send an empty `{}`
|
|
25
|
+
request body; these endpoints take no body per the API spec. `void`,
|
|
26
|
+
`cancel`, and `cancel_immediately`, which accept an optional body, are
|
|
27
|
+
unchanged.
|
|
28
|
+
|
|
8
29
|
## 0.7.0
|
|
9
30
|
|
|
10
31
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threecommon
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Official Python client for the 3Common Public API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/3-Common/sdk/tree/main/sdk-python
|
|
6
6
|
Project-URL: Issues, https://github.com/3-Common/sdk/issues
|
|
@@ -10,7 +10,7 @@ license = { text = "MIT" }
|
|
|
10
10
|
authors = [{ name = "3Common", email = "support@3common.com" }]
|
|
11
11
|
keywords = ["3common", "sdk", "api-client", "events", "invoices"]
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
|
-
version = "0.7.
|
|
13
|
+
version = "0.7.1"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"pydantic>=2.7,<3.0",
|
|
@@ -25,6 +25,7 @@ def build_headers(
|
|
|
25
25
|
user_agent_extra: str | None = None,
|
|
26
26
|
telemetry_header: str | None = None,
|
|
27
27
|
idempotency_key: str | None = None,
|
|
28
|
+
has_body: bool = True,
|
|
28
29
|
) -> dict[str, str]:
|
|
29
30
|
"""Return a fresh header dict populated with every header the SDK sends."""
|
|
30
31
|
headers: dict[str, str] = {
|
|
@@ -32,8 +33,12 @@ def build_headers(
|
|
|
32
33
|
"Threecommon-Version": api_version,
|
|
33
34
|
"User-Agent": f"ThreeCommonPython/{sdk_version} ({user_agent_suffix(user_agent_extra)})",
|
|
34
35
|
"Accept": "application/json",
|
|
35
|
-
"Content-Type": "application/json",
|
|
36
36
|
}
|
|
37
|
+
# Bodyless requests (DELETE, action-style POSTs like finalize/auto_charge)
|
|
38
|
+
# must not advertise a JSON body: a server enforcing Content-Type against
|
|
39
|
+
# an empty body rejects them with a 400.
|
|
40
|
+
if has_body:
|
|
41
|
+
headers["Content-Type"] = "application/json"
|
|
37
42
|
if telemetry_header:
|
|
38
43
|
headers["Threecommon-Client-Telemetry"] = telemetry_header
|
|
39
44
|
if idempotency_key:
|
|
@@ -172,6 +172,7 @@ def _build_request_headers(
|
|
|
172
172
|
telemetry: Telemetry,
|
|
173
173
|
idempotency_key: str | None,
|
|
174
174
|
user_agent_extra: str | None,
|
|
175
|
+
has_body: bool,
|
|
175
176
|
) -> dict[str, str]:
|
|
176
177
|
return build_headers(
|
|
177
178
|
api_key=api_key,
|
|
@@ -180,6 +181,7 @@ def _build_request_headers(
|
|
|
180
181
|
user_agent_extra=user_agent_extra,
|
|
181
182
|
telemetry_header=telemetry.header_value(sdk_version=VERSION, api_version=api_version),
|
|
182
183
|
idempotency_key=idempotency_key,
|
|
184
|
+
has_body=has_body,
|
|
183
185
|
)
|
|
184
186
|
|
|
185
187
|
|
|
@@ -245,6 +247,7 @@ class HTTPClient:
|
|
|
245
247
|
telemetry=self._opts.telemetry,
|
|
246
248
|
idempotency_key=resolved.idempotency_key,
|
|
247
249
|
user_agent_extra=self._opts.user_agent_extra,
|
|
250
|
+
has_body=resolved.body is not None,
|
|
248
251
|
)
|
|
249
252
|
|
|
250
253
|
start = time.monotonic()
|
|
@@ -352,6 +355,7 @@ class AsyncHTTPClient:
|
|
|
352
355
|
telemetry=self._opts.telemetry,
|
|
353
356
|
idempotency_key=resolved.idempotency_key,
|
|
354
357
|
user_agent_extra=self._opts.user_agent_extra,
|
|
358
|
+
has_body=resolved.body is not None,
|
|
355
359
|
)
|
|
356
360
|
|
|
357
361
|
start = time.monotonic()
|
|
@@ -133,7 +133,7 @@ class FeaturesService:
|
|
|
133
133
|
"""Soft-archive a feature. Idempotent."""
|
|
134
134
|
_require_id("archive", feature_id)
|
|
135
135
|
response = self._http.request(
|
|
136
|
-
Request(method="POST", path=f"{_path_for(feature_id)}/archive"
|
|
136
|
+
Request(method="POST", path=f"{_path_for(feature_id)}/archive")
|
|
137
137
|
)
|
|
138
138
|
return Feature.model_validate(response["data"])
|
|
139
139
|
|
|
@@ -141,7 +141,7 @@ class FeaturesService:
|
|
|
141
141
|
"""Reactivate a previously archived feature. Idempotent."""
|
|
142
142
|
_require_id("unarchive", feature_id)
|
|
143
143
|
response = self._http.request(
|
|
144
|
-
Request(method="POST", path=f"{_path_for(feature_id)}/unarchive"
|
|
144
|
+
Request(method="POST", path=f"{_path_for(feature_id)}/unarchive")
|
|
145
145
|
)
|
|
146
146
|
return Feature.model_validate(response["data"])
|
|
147
147
|
|
|
@@ -220,14 +220,14 @@ class AsyncFeaturesService:
|
|
|
220
220
|
async def archive(self, feature_id: str) -> Feature:
|
|
221
221
|
_require_id("archive", feature_id)
|
|
222
222
|
response = await self._http.request(
|
|
223
|
-
Request(method="POST", path=f"{_path_for(feature_id)}/archive"
|
|
223
|
+
Request(method="POST", path=f"{_path_for(feature_id)}/archive")
|
|
224
224
|
)
|
|
225
225
|
return Feature.model_validate(response["data"])
|
|
226
226
|
|
|
227
227
|
async def unarchive(self, feature_id: str) -> Feature:
|
|
228
228
|
_require_id("unarchive", feature_id)
|
|
229
229
|
response = await self._http.request(
|
|
230
|
-
Request(method="POST", path=f"{_path_for(feature_id)}/unarchive"
|
|
230
|
+
Request(method="POST", path=f"{_path_for(feature_id)}/unarchive")
|
|
231
231
|
)
|
|
232
232
|
return Feature.model_validate(response["data"])
|
|
233
233
|
|
|
@@ -138,7 +138,7 @@ class InvoicesService:
|
|
|
138
138
|
"""Finalize a draft invoice: assign a number, stamp ``issuedAt``, set status ``open``."""
|
|
139
139
|
_require_id("finalize", invoice_id)
|
|
140
140
|
response = self._http.request(
|
|
141
|
-
Request(method="POST", path=_action_path(invoice_id, "finalize")
|
|
141
|
+
Request(method="POST", path=_action_path(invoice_id, "finalize"))
|
|
142
142
|
)
|
|
143
143
|
return Invoice.model_validate(response["data"])
|
|
144
144
|
|
|
@@ -177,7 +177,7 @@ class InvoicesService:
|
|
|
177
177
|
"""
|
|
178
178
|
_require_id("auto_charge", invoice_id)
|
|
179
179
|
response = self._http.request(
|
|
180
|
-
Request(method="POST", path=_action_path(invoice_id, "auto_charge")
|
|
180
|
+
Request(method="POST", path=_action_path(invoice_id, "auto_charge"))
|
|
181
181
|
)
|
|
182
182
|
return _build_auto_charge_result(response)
|
|
183
183
|
|
|
@@ -283,7 +283,7 @@ class AsyncInvoicesService:
|
|
|
283
283
|
async def finalize(self, invoice_id: str) -> Invoice:
|
|
284
284
|
_require_id("finalize", invoice_id)
|
|
285
285
|
response = await self._http.request(
|
|
286
|
-
Request(method="POST", path=_action_path(invoice_id, "finalize")
|
|
286
|
+
Request(method="POST", path=_action_path(invoice_id, "finalize"))
|
|
287
287
|
)
|
|
288
288
|
return Invoice.model_validate(response["data"])
|
|
289
289
|
|
|
@@ -311,7 +311,7 @@ class AsyncInvoicesService:
|
|
|
311
311
|
async def auto_charge(self, invoice_id: str) -> AutoChargeResult:
|
|
312
312
|
_require_id("auto_charge", invoice_id)
|
|
313
313
|
response = await self._http.request(
|
|
314
|
-
Request(method="POST", path=_action_path(invoice_id, "auto_charge")
|
|
314
|
+
Request(method="POST", path=_action_path(invoice_id, "auto_charge"))
|
|
315
315
|
)
|
|
316
316
|
return _build_auto_charge_result(response)
|
|
317
317
|
|
|
@@ -114,16 +114,14 @@ class PricesService:
|
|
|
114
114
|
def archive(self, price_id: str) -> Price:
|
|
115
115
|
"""Soft-archive a price. Idempotent."""
|
|
116
116
|
_require_id("archive", price_id)
|
|
117
|
-
response = self._http.request(
|
|
118
|
-
Request(method="POST", path=f"{_path_for(price_id)}/archive", body={})
|
|
119
|
-
)
|
|
117
|
+
response = self._http.request(Request(method="POST", path=f"{_path_for(price_id)}/archive"))
|
|
120
118
|
return Price.model_validate(response["data"])
|
|
121
119
|
|
|
122
120
|
def unarchive(self, price_id: str) -> Price:
|
|
123
121
|
"""Reactivate a previously archived price. Idempotent."""
|
|
124
122
|
_require_id("unarchive", price_id)
|
|
125
123
|
response = self._http.request(
|
|
126
|
-
Request(method="POST", path=f"{_path_for(price_id)}/unarchive"
|
|
124
|
+
Request(method="POST", path=f"{_path_for(price_id)}/unarchive")
|
|
127
125
|
)
|
|
128
126
|
return Price.model_validate(response["data"])
|
|
129
127
|
|
|
@@ -196,14 +194,14 @@ class AsyncPricesService:
|
|
|
196
194
|
async def archive(self, price_id: str) -> Price:
|
|
197
195
|
_require_id("archive", price_id)
|
|
198
196
|
response = await self._http.request(
|
|
199
|
-
Request(method="POST", path=f"{_path_for(price_id)}/archive"
|
|
197
|
+
Request(method="POST", path=f"{_path_for(price_id)}/archive")
|
|
200
198
|
)
|
|
201
199
|
return Price.model_validate(response["data"])
|
|
202
200
|
|
|
203
201
|
async def unarchive(self, price_id: str) -> Price:
|
|
204
202
|
_require_id("unarchive", price_id)
|
|
205
203
|
response = await self._http.request(
|
|
206
|
-
Request(method="POST", path=f"{_path_for(price_id)}/unarchive"
|
|
204
|
+
Request(method="POST", path=f"{_path_for(price_id)}/unarchive")
|
|
207
205
|
)
|
|
208
206
|
return Price.model_validate(response["data"])
|
|
209
207
|
|
|
@@ -146,7 +146,7 @@ class SubscriptionsService:
|
|
|
146
146
|
"""Transition an incomplete or trialing subscription to ``active``."""
|
|
147
147
|
_require_id("activate", subscription_id)
|
|
148
148
|
response = self._http.request(
|
|
149
|
-
Request(method="POST", path=_action_path(subscription_id, "activate")
|
|
149
|
+
Request(method="POST", path=_action_path(subscription_id, "activate"))
|
|
150
150
|
)
|
|
151
151
|
return Subscription.model_validate(response["data"])
|
|
152
152
|
|
|
@@ -178,7 +178,7 @@ class SubscriptionsService:
|
|
|
178
178
|
"""Admin override — mark a subscription ``unpaid`` (terminal)."""
|
|
179
179
|
_require_id("mark_unpaid", subscription_id)
|
|
180
180
|
response = self._http.request(
|
|
181
|
-
Request(method="POST", path=_action_path(subscription_id, "mark-unpaid")
|
|
181
|
+
Request(method="POST", path=_action_path(subscription_id, "mark-unpaid"))
|
|
182
182
|
)
|
|
183
183
|
return Subscription.model_validate(response["data"])
|
|
184
184
|
|
|
@@ -186,7 +186,7 @@ class SubscriptionsService:
|
|
|
186
186
|
"""Generate a draft invoice for the current period without advancing it."""
|
|
187
187
|
_require_id("bill", subscription_id)
|
|
188
188
|
response = self._http.request(
|
|
189
|
-
Request(method="POST", path=_action_path(subscription_id, "bill")
|
|
189
|
+
Request(method="POST", path=_action_path(subscription_id, "bill"))
|
|
190
190
|
)
|
|
191
191
|
return _build_bill_result(response)
|
|
192
192
|
|
|
@@ -194,7 +194,7 @@ class SubscriptionsService:
|
|
|
194
194
|
"""Advance the subscription to its next billing period and generate an invoice."""
|
|
195
195
|
_require_id("renew", subscription_id)
|
|
196
196
|
response = self._http.request(
|
|
197
|
-
Request(method="POST", path=_action_path(subscription_id, "renew")
|
|
197
|
+
Request(method="POST", path=_action_path(subscription_id, "renew"))
|
|
198
198
|
)
|
|
199
199
|
return _build_renew_result(response)
|
|
200
200
|
|
|
@@ -293,7 +293,7 @@ class AsyncSubscriptionsService:
|
|
|
293
293
|
async def activate(self, subscription_id: str) -> Subscription:
|
|
294
294
|
_require_id("activate", subscription_id)
|
|
295
295
|
response = await self._http.request(
|
|
296
|
-
Request(method="POST", path=_action_path(subscription_id, "activate")
|
|
296
|
+
Request(method="POST", path=_action_path(subscription_id, "activate"))
|
|
297
297
|
)
|
|
298
298
|
return Subscription.model_validate(response["data"])
|
|
299
299
|
|
|
@@ -322,21 +322,21 @@ class AsyncSubscriptionsService:
|
|
|
322
322
|
async def mark_unpaid(self, subscription_id: str) -> Subscription:
|
|
323
323
|
_require_id("mark_unpaid", subscription_id)
|
|
324
324
|
response = await self._http.request(
|
|
325
|
-
Request(method="POST", path=_action_path(subscription_id, "mark-unpaid")
|
|
325
|
+
Request(method="POST", path=_action_path(subscription_id, "mark-unpaid"))
|
|
326
326
|
)
|
|
327
327
|
return Subscription.model_validate(response["data"])
|
|
328
328
|
|
|
329
329
|
async def bill(self, subscription_id: str) -> BillSubscriptionResult:
|
|
330
330
|
_require_id("bill", subscription_id)
|
|
331
331
|
response = await self._http.request(
|
|
332
|
-
Request(method="POST", path=_action_path(subscription_id, "bill")
|
|
332
|
+
Request(method="POST", path=_action_path(subscription_id, "bill"))
|
|
333
333
|
)
|
|
334
334
|
return _build_bill_result(response)
|
|
335
335
|
|
|
336
336
|
async def renew(self, subscription_id: str) -> RenewSubscriptionResult:
|
|
337
337
|
_require_id("renew", subscription_id)
|
|
338
338
|
response = await self._http.request(
|
|
339
|
-
Request(method="POST", path=_action_path(subscription_id, "renew")
|
|
339
|
+
Request(method="POST", path=_action_path(subscription_id, "renew"))
|
|
340
340
|
)
|
|
341
341
|
return _build_renew_result(response)
|
|
342
342
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|