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.
Files changed (51) hide show
  1. {threecommon-0.7.0 → threecommon-0.7.1}/CHANGELOG.md +21 -0
  2. {threecommon-0.7.0 → threecommon-0.7.1}/PKG-INFO +1 -1
  3. {threecommon-0.7.0 → threecommon-0.7.1}/pyproject.toml +1 -1
  4. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/headers.py +6 -1
  5. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/http_client.py +4 -0
  6. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/features/service.py +4 -4
  7. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/invoices/service.py +4 -4
  8. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/prices/service.py +4 -6
  9. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/subscriptions/service.py +8 -8
  10. {threecommon-0.7.0 → threecommon-0.7.1}/.gitignore +0 -0
  11. {threecommon-0.7.0 → threecommon-0.7.1}/LICENSE +0 -0
  12. {threecommon-0.7.0 → threecommon-0.7.1}/README.md +0 -0
  13. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/__init__.py +0 -0
  14. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/__init__.py +0 -0
  15. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/parse.py +0 -0
  16. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/retry.py +0 -0
  17. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/telemetry.py +0 -0
  18. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_core/url.py +0 -0
  19. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_generated/__init__.py +0 -0
  20. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/_generated/models.py +0 -0
  21. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/api_version.py +0 -0
  22. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/client.py +0 -0
  23. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/config.py +0 -0
  24. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/contacts/__init__.py +0 -0
  25. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/contacts/service.py +0 -0
  26. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/contacts/types.py +0 -0
  27. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/entitlements/__init__.py +0 -0
  28. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/entitlements/service.py +0 -0
  29. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/entitlements/types.py +0 -0
  30. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/errors/__init__.py +0 -0
  31. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/errors/base.py +0 -0
  32. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/errors/classes.py +0 -0
  33. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/events/__init__.py +0 -0
  34. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/events/service.py +0 -0
  35. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/events/types.py +0 -0
  36. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/features/__init__.py +0 -0
  37. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/features/types.py +0 -0
  38. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/filters/__init__.py +0 -0
  39. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/filters/builder.py +0 -0
  40. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/filters/types.py +0 -0
  41. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/helpers.py +0 -0
  42. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/invoices/__init__.py +0 -0
  43. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/invoices/types.py +0 -0
  44. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/pagination/__init__.py +0 -0
  45. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/pagination/auto_paginator.py +0 -0
  46. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/prices/__init__.py +0 -0
  47. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/prices/types.py +0 -0
  48. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/py.typed +0 -0
  49. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/subscriptions/__init__.py +0 -0
  50. {threecommon-0.7.0 → threecommon-0.7.1}/src/threecommon/subscriptions/types.py +0 -0
  51. {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.0
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.0"
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", body={})
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", body={})
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", body={})
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", body={})
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"), body={})
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"), body={})
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"), body={})
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"), body={})
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", body={})
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", body={})
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", body={})
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"), body={})
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"), body={})
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"), body={})
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"), body={})
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"), body={})
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"), body={})
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"), body={})
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"), body={})
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