threecommon 0.4.0__tar.gz → 0.7.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.
- {threecommon-0.4.0 → threecommon-0.7.0}/CHANGELOG.md +41 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/PKG-INFO +1 -1
- {threecommon-0.4.0 → threecommon-0.7.0}/pyproject.toml +1 -1
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/client.py +24 -0
- threecommon-0.7.0/src/threecommon/entitlements/__init__.py +37 -0
- threecommon-0.7.0/src/threecommon/entitlements/service.py +243 -0
- threecommon-0.7.0/src/threecommon/entitlements/types.py +142 -0
- threecommon-0.7.0/src/threecommon/features/__init__.py +42 -0
- threecommon-0.7.0/src/threecommon/features/service.py +251 -0
- threecommon-0.7.0/src/threecommon/features/types.py +175 -0
- threecommon-0.7.0/src/threecommon/prices/__init__.py +46 -0
- threecommon-0.7.0/src/threecommon/prices/service.py +227 -0
- threecommon-0.7.0/src/threecommon/prices/types.py +186 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/.gitignore +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/LICENSE +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/README.md +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_core/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_core/headers.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_core/http_client.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_core/parse.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_core/retry.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_core/telemetry.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_core/url.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_generated/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/_generated/models.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/api_version.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/config.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/contacts/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/contacts/service.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/contacts/types.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/errors/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/errors/base.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/errors/classes.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/events/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/events/service.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/events/types.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/filters/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/filters/builder.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/filters/types.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/helpers.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/invoices/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/invoices/service.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/invoices/types.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/pagination/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/pagination/auto_paginator.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/py.typed +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/subscriptions/__init__.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/subscriptions/service.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/subscriptions/types.py +0 -0
- {threecommon-0.4.0 → threecommon-0.7.0}/src/threecommon/version.py +0 -0
|
@@ -5,6 +5,47 @@ versions follow [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## 0.7.0
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Features resource. The new `client.features` surface covers the feature
|
|
13
|
+
catalog: `list`, `resolve` (resolve a feature's live value for a customer),
|
|
14
|
+
`retrieve`, `create`, `update`, `archive`, `unarchive`, and a
|
|
15
|
+
`list_auto_paginate` iterator. Both sync and async surfaces.
|
|
16
|
+
- New public types on `threecommon.features`: `Feature`, `FeatureType`,
|
|
17
|
+
`ResolvedFeature`, the `ResolvedFeatureValue` discriminated union and its
|
|
18
|
+
`ResolvedFeatureBoolean`/`ResolvedFeatureQuantity`/`ResolvedFeatureEnum`/
|
|
19
|
+
`ResolvedFeatureDuration` members, `CreateBody`, `UpdateBody`, `ListParams`,
|
|
20
|
+
`RetrieveParams`, `ResolveParams`, and the `ListFeaturesResponse` envelope.
|
|
21
|
+
|
|
22
|
+
## 0.6.0
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Prices resource. The new `client.prices` surface covers the price catalog:
|
|
27
|
+
`list`, `retrieve`, `create`, `update`, `archive`, `unarchive`, and a
|
|
28
|
+
`list_auto_paginate` iterator. Both sync and async surfaces.
|
|
29
|
+
- New public types on `threecommon.prices`: `Price`, `PriceRecurring`,
|
|
30
|
+
`PriceFeature` (the boolean/quantity/enum/duration grant union) and its
|
|
31
|
+
`PriceFeatureBoolean`/`PriceFeatureQuantity`/`PriceFeatureEnum`/
|
|
32
|
+
`PriceFeatureDuration` members, `CreateBody`, `UpdateBody`, `ListParams`,
|
|
33
|
+
`RetrieveParams`, the `ListPricesResponse` envelope, and the
|
|
34
|
+
`PriceType`/`PriceCurrency`/`PriceInterval` literal unions.
|
|
35
|
+
|
|
36
|
+
## 0.5.0
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- Entitlements resource. The new `client.entitlements` surface covers balance
|
|
41
|
+
lookups and grant management: `list`, `retrieve`, `lookup` (by contact +
|
|
42
|
+
feature), `grant` (manual top-up, idempotent on `grant_id`), `consume`
|
|
43
|
+
(debit balance), and `list_auto_paginate`. Both sync and async surfaces.
|
|
44
|
+
- New public types on `threecommon.entitlements`: `Entitlement`,
|
|
45
|
+
`EntitlementGrant`, `EntitlementGrantSource`, `GrantBody`, `ConsumeBody`,
|
|
46
|
+
`ListParams`, `RetrieveParams`, `LookupParams`, and the
|
|
47
|
+
`ListEntitlementsResponse` envelope.
|
|
48
|
+
|
|
8
49
|
## 0.4.0
|
|
9
50
|
|
|
10
51
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threecommon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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.
|
|
13
|
+
version = "0.7.0"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"pydantic>=2.7,<3.0",
|
|
@@ -19,8 +19,11 @@ from threecommon._core.retry import RetryPolicy
|
|
|
19
19
|
from threecommon._core.telemetry import Telemetry
|
|
20
20
|
from threecommon.config import RetryDelay, resolve_config
|
|
21
21
|
from threecommon.contacts.service import AsyncContactsService, ContactsService
|
|
22
|
+
from threecommon.entitlements.service import AsyncEntitlementsService, EntitlementsService
|
|
22
23
|
from threecommon.events.service import AsyncEventsService, EventsService
|
|
24
|
+
from threecommon.features.service import AsyncFeaturesService, FeaturesService
|
|
23
25
|
from threecommon.invoices.service import AsyncInvoicesService, InvoicesService
|
|
26
|
+
from threecommon.prices.service import AsyncPricesService, PricesService
|
|
24
27
|
from threecommon.subscriptions.service import AsyncSubscriptionsService, SubscriptionsService
|
|
25
28
|
|
|
26
29
|
if TYPE_CHECKING: # pragma: no cover
|
|
@@ -61,6 +64,18 @@ class ThreeCommon:
|
|
|
61
64
|
``update``, ``delete``, ``bulk_upsert``, ``list_activity``, plus
|
|
62
65
|
auto-paginators."""
|
|
63
66
|
|
|
67
|
+
entitlements: EntitlementsService
|
|
68
|
+
"""Entitlements resource — ``list``, ``retrieve``, ``lookup``, ``grant``,
|
|
69
|
+
``consume``, plus ``list_auto_paginate``."""
|
|
70
|
+
|
|
71
|
+
prices: PricesService
|
|
72
|
+
"""Prices resource — ``list``, ``retrieve``, ``create``, ``update``,
|
|
73
|
+
``archive``, ``unarchive``, plus ``list_auto_paginate``."""
|
|
74
|
+
|
|
75
|
+
features: FeaturesService
|
|
76
|
+
"""Features resource — ``list``, ``resolve``, ``retrieve``, ``create``,
|
|
77
|
+
``update``, ``archive``, ``unarchive``, plus ``list_auto_paginate``."""
|
|
78
|
+
|
|
64
79
|
_http: HTTPClient
|
|
65
80
|
_telemetry: Telemetry
|
|
66
81
|
|
|
@@ -105,6 +120,9 @@ class ThreeCommon:
|
|
|
105
120
|
self.invoices = InvoicesService(self._http)
|
|
106
121
|
self.subscriptions = SubscriptionsService(self._http)
|
|
107
122
|
self.contacts = ContactsService(self._http)
|
|
123
|
+
self.entitlements = EntitlementsService(self._http)
|
|
124
|
+
self.prices = PricesService(self._http)
|
|
125
|
+
self.features = FeaturesService(self._http)
|
|
108
126
|
|
|
109
127
|
def close(self) -> None:
|
|
110
128
|
"""Close the underlying httpx client (no-op if you supplied your own)."""
|
|
@@ -134,6 +152,9 @@ class AsyncThreeCommon:
|
|
|
134
152
|
invoices: AsyncInvoicesService
|
|
135
153
|
subscriptions: AsyncSubscriptionsService
|
|
136
154
|
contacts: AsyncContactsService
|
|
155
|
+
entitlements: AsyncEntitlementsService
|
|
156
|
+
prices: AsyncPricesService
|
|
157
|
+
features: AsyncFeaturesService
|
|
137
158
|
|
|
138
159
|
_http: AsyncHTTPClient
|
|
139
160
|
_telemetry: Telemetry
|
|
@@ -179,6 +200,9 @@ class AsyncThreeCommon:
|
|
|
179
200
|
self.invoices = AsyncInvoicesService(self._http)
|
|
180
201
|
self.subscriptions = AsyncSubscriptionsService(self._http)
|
|
181
202
|
self.contacts = AsyncContactsService(self._http)
|
|
203
|
+
self.entitlements = AsyncEntitlementsService(self._http)
|
|
204
|
+
self.prices = AsyncPricesService(self._http)
|
|
205
|
+
self.features = AsyncFeaturesService(self._http)
|
|
182
206
|
|
|
183
207
|
async def aclose(self) -> None:
|
|
184
208
|
"""Close the underlying async httpx client."""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Entitlements resource — sync and async clients plus public types.
|
|
2
|
+
|
|
3
|
+
Most callers reach this module through
|
|
4
|
+
[ThreeCommon.entitlements][threecommon.ThreeCommon] /
|
|
5
|
+
[AsyncThreeCommon.entitlements][threecommon.AsyncThreeCommon]; importing the
|
|
6
|
+
service classes directly is supported for advanced wiring.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from threecommon.entitlements.service import (
|
|
10
|
+
AsyncEntitlementsService,
|
|
11
|
+
EntitlementsService,
|
|
12
|
+
)
|
|
13
|
+
from threecommon.entitlements.types import (
|
|
14
|
+
ConsumeBody,
|
|
15
|
+
Entitlement,
|
|
16
|
+
EntitlementGrant,
|
|
17
|
+
EntitlementGrantSource,
|
|
18
|
+
GrantBody,
|
|
19
|
+
ListEntitlementsResponse,
|
|
20
|
+
ListParams,
|
|
21
|
+
LookupParams,
|
|
22
|
+
RetrieveParams,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = (
|
|
26
|
+
"AsyncEntitlementsService",
|
|
27
|
+
"ConsumeBody",
|
|
28
|
+
"Entitlement",
|
|
29
|
+
"EntitlementGrant",
|
|
30
|
+
"EntitlementGrantSource",
|
|
31
|
+
"EntitlementsService",
|
|
32
|
+
"GrantBody",
|
|
33
|
+
"ListEntitlementsResponse",
|
|
34
|
+
"ListParams",
|
|
35
|
+
"LookupParams",
|
|
36
|
+
"RetrieveParams",
|
|
37
|
+
)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Sync and async entitlements 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.entitlements.types import (
|
|
14
|
+
ConsumeBody,
|
|
15
|
+
Entitlement,
|
|
16
|
+
GrantBody,
|
|
17
|
+
ListEntitlementsResponse,
|
|
18
|
+
ListParams,
|
|
19
|
+
LookupParams,
|
|
20
|
+
RetrieveParams,
|
|
21
|
+
)
|
|
22
|
+
from threecommon.errors.classes import ValidationError
|
|
23
|
+
from threecommon.pagination import AsyncIter, Iter
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from threecommon._core.http_client import AsyncHTTPClient, HTTPClient
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _encode_list_params(params: ListParams | None) -> dict[str, str] | None:
|
|
30
|
+
if params is None:
|
|
31
|
+
return None
|
|
32
|
+
raw = params.model_dump(by_alias=True, exclude_none=True)
|
|
33
|
+
if not raw:
|
|
34
|
+
return None
|
|
35
|
+
return {k: str(v) for k, v in raw.items()}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _encode_retrieve_params(params: RetrieveParams | None) -> dict[str, str] | None:
|
|
39
|
+
if params is None or params.fields is None:
|
|
40
|
+
return None
|
|
41
|
+
return {"fields": params.fields}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _encode_lookup_params(params: LookupParams) -> dict[str, str]:
|
|
45
|
+
raw = params.model_dump(by_alias=True, exclude_none=True)
|
|
46
|
+
return {k: str(v) for k, v in raw.items()}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_id(method: str, entitlement_id: str) -> None:
|
|
50
|
+
if not entitlement_id:
|
|
51
|
+
msg = f"entitlements.{method}: id must be a non-empty string"
|
|
52
|
+
raise ValidationError(code="missing_id", message=msg)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _require_lookup_params(params: LookupParams) -> None:
|
|
56
|
+
if not params.contact_id:
|
|
57
|
+
raise ValidationError(
|
|
58
|
+
code="missing_contact_id",
|
|
59
|
+
message="entitlements.lookup: contact_id must be a non-empty string",
|
|
60
|
+
)
|
|
61
|
+
if not params.feature_key:
|
|
62
|
+
raise ValidationError(
|
|
63
|
+
code="missing_feature_key",
|
|
64
|
+
message="entitlements.lookup: feature_key must be a non-empty string",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _path_for(entitlement_id: str) -> str:
|
|
69
|
+
return f"/entitlements/{quote(entitlement_id, safe='')}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
# Sync
|
|
74
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class EntitlementsService:
|
|
78
|
+
"""Sync entitlements service — bound as ``client.entitlements`` on [ThreeCommon]."""
|
|
79
|
+
|
|
80
|
+
__slots__ = ("_http",)
|
|
81
|
+
|
|
82
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
83
|
+
self._http = http
|
|
84
|
+
|
|
85
|
+
def list(self, params: ListParams | None = None) -> ListEntitlementsResponse:
|
|
86
|
+
"""List the host's entitlement balance records (one page).
|
|
87
|
+
|
|
88
|
+
For full iteration use
|
|
89
|
+
[list_auto_paginate][EntitlementsService.list_auto_paginate].
|
|
90
|
+
"""
|
|
91
|
+
body = self._http.request(
|
|
92
|
+
Request(method="GET", path="/entitlements", query=_encode_list_params(params))
|
|
93
|
+
)
|
|
94
|
+
return ListEntitlementsResponse.model_validate(body)
|
|
95
|
+
|
|
96
|
+
def retrieve(self, entitlement_id: str, params: RetrieveParams | None = None) -> Entitlement:
|
|
97
|
+
"""Retrieve a single entitlement record by id, including grant history."""
|
|
98
|
+
_require_id("retrieve", entitlement_id)
|
|
99
|
+
body = self._http.request(
|
|
100
|
+
Request(
|
|
101
|
+
method="GET",
|
|
102
|
+
path=_path_for(entitlement_id),
|
|
103
|
+
query=_encode_retrieve_params(params),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
return Entitlement.model_validate(body["data"])
|
|
107
|
+
|
|
108
|
+
def lookup(self, params: LookupParams) -> Entitlement:
|
|
109
|
+
"""Look up the unique entitlement for a ``(contact_id, feature_key)`` pair.
|
|
110
|
+
|
|
111
|
+
Raises [NotFoundError][threecommon.NotFoundError] if no record exists yet.
|
|
112
|
+
"""
|
|
113
|
+
_require_lookup_params(params)
|
|
114
|
+
body = self._http.request(
|
|
115
|
+
Request(method="GET", path="/entitlements/lookup", query=_encode_lookup_params(params))
|
|
116
|
+
)
|
|
117
|
+
return Entitlement.model_validate(body["data"])
|
|
118
|
+
|
|
119
|
+
def grant(self, body: GrantBody) -> Entitlement:
|
|
120
|
+
"""Add a manual entitlement grant. Idempotent on ``grant_id``."""
|
|
121
|
+
if body is None:
|
|
122
|
+
raise ValidationError(
|
|
123
|
+
code="missing_body", message="entitlements.grant: body must be non-None"
|
|
124
|
+
)
|
|
125
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
126
|
+
response = self._http.request(
|
|
127
|
+
Request(method="POST", path="/entitlements/grants", body=payload)
|
|
128
|
+
)
|
|
129
|
+
return Entitlement.model_validate(response["data"])
|
|
130
|
+
|
|
131
|
+
def consume(self, body: ConsumeBody) -> Entitlement:
|
|
132
|
+
"""Debit units from a customer's entitlement balance.
|
|
133
|
+
|
|
134
|
+
Raises [ConflictError][threecommon.ConflictError] on insufficient balance.
|
|
135
|
+
"""
|
|
136
|
+
if body is None:
|
|
137
|
+
raise ValidationError(
|
|
138
|
+
code="missing_body", message="entitlements.consume: body must be non-None"
|
|
139
|
+
)
|
|
140
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
141
|
+
response = self._http.request(
|
|
142
|
+
Request(method="POST", path="/entitlements/consume", body=payload)
|
|
143
|
+
)
|
|
144
|
+
return Entitlement.model_validate(response["data"])
|
|
145
|
+
|
|
146
|
+
def list_auto_paginate(self, params: ListParams | None = None) -> Iter[Entitlement]:
|
|
147
|
+
"""Iterate every entitlement matching ``params``, paging automatically."""
|
|
148
|
+
start_page = params.page if params is not None and params.page is not None else 0
|
|
149
|
+
|
|
150
|
+
def fetch(page: int) -> tuple[list[Entitlement], bool]:
|
|
151
|
+
page_params = (
|
|
152
|
+
params.model_copy(update={"page": page})
|
|
153
|
+
if params is not None
|
|
154
|
+
else ListParams(page=page)
|
|
155
|
+
)
|
|
156
|
+
body = self._http.request(
|
|
157
|
+
Request(method="GET", path="/entitlements", query=_encode_list_params(page_params))
|
|
158
|
+
)
|
|
159
|
+
response = ListEntitlementsResponse.model_validate(body)
|
|
160
|
+
return response.data, response.has_more
|
|
161
|
+
|
|
162
|
+
return Iter(fetch_page=fetch, start_page=start_page)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
# Async
|
|
167
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class AsyncEntitlementsService:
|
|
171
|
+
"""Async entitlements service — bound as ``client.entitlements`` on [AsyncThreeCommon]."""
|
|
172
|
+
|
|
173
|
+
__slots__ = ("_http",)
|
|
174
|
+
|
|
175
|
+
def __init__(self, http: AsyncHTTPClient) -> None:
|
|
176
|
+
self._http = http
|
|
177
|
+
|
|
178
|
+
async def list(self, params: ListParams | None = None) -> ListEntitlementsResponse:
|
|
179
|
+
body = await self._http.request(
|
|
180
|
+
Request(method="GET", path="/entitlements", query=_encode_list_params(params))
|
|
181
|
+
)
|
|
182
|
+
return ListEntitlementsResponse.model_validate(body)
|
|
183
|
+
|
|
184
|
+
async def retrieve(
|
|
185
|
+
self, entitlement_id: str, params: RetrieveParams | None = None
|
|
186
|
+
) -> Entitlement:
|
|
187
|
+
_require_id("retrieve", entitlement_id)
|
|
188
|
+
body = await self._http.request(
|
|
189
|
+
Request(
|
|
190
|
+
method="GET",
|
|
191
|
+
path=_path_for(entitlement_id),
|
|
192
|
+
query=_encode_retrieve_params(params),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
return Entitlement.model_validate(body["data"])
|
|
196
|
+
|
|
197
|
+
async def lookup(self, params: LookupParams) -> Entitlement:
|
|
198
|
+
_require_lookup_params(params)
|
|
199
|
+
body = await self._http.request(
|
|
200
|
+
Request(method="GET", path="/entitlements/lookup", query=_encode_lookup_params(params))
|
|
201
|
+
)
|
|
202
|
+
return Entitlement.model_validate(body["data"])
|
|
203
|
+
|
|
204
|
+
async def grant(self, body: GrantBody) -> Entitlement:
|
|
205
|
+
if body is None:
|
|
206
|
+
raise ValidationError(
|
|
207
|
+
code="missing_body", message="entitlements.grant: body must be non-None"
|
|
208
|
+
)
|
|
209
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
210
|
+
response = await self._http.request(
|
|
211
|
+
Request(method="POST", path="/entitlements/grants", body=payload)
|
|
212
|
+
)
|
|
213
|
+
return Entitlement.model_validate(response["data"])
|
|
214
|
+
|
|
215
|
+
async def consume(self, body: ConsumeBody) -> Entitlement:
|
|
216
|
+
if body is None:
|
|
217
|
+
raise ValidationError(
|
|
218
|
+
code="missing_body", message="entitlements.consume: body must be non-None"
|
|
219
|
+
)
|
|
220
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
221
|
+
response = await self._http.request(
|
|
222
|
+
Request(method="POST", path="/entitlements/consume", body=payload)
|
|
223
|
+
)
|
|
224
|
+
return Entitlement.model_validate(response["data"])
|
|
225
|
+
|
|
226
|
+
def list_auto_paginate(self, params: ListParams | None = None) -> AsyncIter[Entitlement]:
|
|
227
|
+
"""Async iterate every entitlement matching ``params``."""
|
|
228
|
+
start_page = params.page if params is not None and params.page is not None else 0
|
|
229
|
+
http = self._http
|
|
230
|
+
|
|
231
|
+
async def fetch(page: int) -> tuple[list[Entitlement], bool]:
|
|
232
|
+
page_params = (
|
|
233
|
+
params.model_copy(update={"page": page})
|
|
234
|
+
if params is not None
|
|
235
|
+
else ListParams(page=page)
|
|
236
|
+
)
|
|
237
|
+
body = await http.request(
|
|
238
|
+
Request(method="GET", path="/entitlements", query=_encode_list_params(page_params))
|
|
239
|
+
)
|
|
240
|
+
response = ListEntitlementsResponse.model_validate(body)
|
|
241
|
+
return response.data, response.has_more
|
|
242
|
+
|
|
243
|
+
return AsyncIter(fetch_page=fetch, start_page=start_page)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Public types for the entitlements 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
|
+
#: Source of an entitlement grant.
|
|
15
|
+
#:
|
|
16
|
+
#: * ``subscription_recurring`` — cycle grant from a subscription renewal.
|
|
17
|
+
#: * ``one_time_addon`` — top-up purchase (consumed first by ``consume``).
|
|
18
|
+
#: * ``manual`` — admin-applied grant.
|
|
19
|
+
EntitlementGrantSource = Literal[
|
|
20
|
+
"subscription_recurring",
|
|
21
|
+
"one_time_addon",
|
|
22
|
+
"manual",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _BaseModel(BaseModel):
|
|
27
|
+
"""Shared config: accept snake_case or camelCase, ignore unknown fields."""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(
|
|
30
|
+
populate_by_name=True,
|
|
31
|
+
extra="ignore",
|
|
32
|
+
str_strip_whitespace=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EntitlementGrant(_BaseModel):
|
|
37
|
+
"""One grant in an entitlement's grant history."""
|
|
38
|
+
|
|
39
|
+
id: str
|
|
40
|
+
source: EntitlementGrantSource
|
|
41
|
+
source_id: str | None = Field(
|
|
42
|
+
default=None, serialization_alias="sourceId", validation_alias="sourceId"
|
|
43
|
+
)
|
|
44
|
+
price_id: str | None = Field(
|
|
45
|
+
default=None, serialization_alias="priceId", validation_alias="priceId"
|
|
46
|
+
)
|
|
47
|
+
amount: int
|
|
48
|
+
remaining: int
|
|
49
|
+
added_at: str = Field(serialization_alias="addedAt", validation_alias="addedAt")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Entitlement(_BaseModel):
|
|
53
|
+
"""One entitlement balance record as returned by the API.
|
|
54
|
+
|
|
55
|
+
Optional fields are populated only when the server returned them — list
|
|
56
|
+
responses with a ``fields`` filter omit unrequested values.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
id: str
|
|
60
|
+
host_id: str | None = Field(
|
|
61
|
+
default=None, serialization_alias="hostId", validation_alias="hostId"
|
|
62
|
+
)
|
|
63
|
+
contact_id: str | None = Field(
|
|
64
|
+
default=None, serialization_alias="contactId", validation_alias="contactId"
|
|
65
|
+
)
|
|
66
|
+
feature_key: str | None = Field(
|
|
67
|
+
default=None, serialization_alias="featureKey", validation_alias="featureKey"
|
|
68
|
+
)
|
|
69
|
+
balance: int | None = None
|
|
70
|
+
grants: list[EntitlementGrant] | None = None
|
|
71
|
+
total_granted: int | None = Field(
|
|
72
|
+
default=None, serialization_alias="totalGranted", validation_alias="totalGranted"
|
|
73
|
+
)
|
|
74
|
+
total_consumed: int | None = Field(
|
|
75
|
+
default=None, serialization_alias="totalConsumed", validation_alias="totalConsumed"
|
|
76
|
+
)
|
|
77
|
+
metadata: dict[str, str] | None = None
|
|
78
|
+
created_at: str | None = Field(
|
|
79
|
+
default=None, serialization_alias="createdAt", validation_alias="createdAt"
|
|
80
|
+
)
|
|
81
|
+
updated_at: str | None = Field(
|
|
82
|
+
default=None, serialization_alias="updatedAt", validation_alias="updatedAt"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ListEntitlementsResponse(_BaseModel):
|
|
87
|
+
"""Successful response shape from ``GET /v1/entitlements``."""
|
|
88
|
+
|
|
89
|
+
data: list[Entitlement]
|
|
90
|
+
has_more: bool = Field(serialization_alias="hasMore", validation_alias="hasMore")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ListParams(_BaseModel):
|
|
94
|
+
"""Query parameters accepted by ``GET /v1/entitlements``."""
|
|
95
|
+
|
|
96
|
+
page: int | None = None
|
|
97
|
+
page_size: int | None = Field(
|
|
98
|
+
default=None, serialization_alias="pageSize", validation_alias="pageSize"
|
|
99
|
+
)
|
|
100
|
+
contact_id: str | None = Field(
|
|
101
|
+
default=None, serialization_alias="contactId", validation_alias="contactId"
|
|
102
|
+
)
|
|
103
|
+
feature_key: str | None = Field(
|
|
104
|
+
default=None, serialization_alias="featureKey", validation_alias="featureKey"
|
|
105
|
+
)
|
|
106
|
+
min_balance: int | None = Field(
|
|
107
|
+
default=None, serialization_alias="minBalance", validation_alias="minBalance"
|
|
108
|
+
)
|
|
109
|
+
fields: str | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class RetrieveParams(_BaseModel):
|
|
113
|
+
"""Query parameters accepted by ``GET /v1/entitlements/{id}``."""
|
|
114
|
+
|
|
115
|
+
fields: str | None = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class LookupParams(_BaseModel):
|
|
119
|
+
"""Query parameters accepted by ``GET /v1/entitlements/lookup``."""
|
|
120
|
+
|
|
121
|
+
contact_id: str = Field(serialization_alias="contactId", validation_alias="contactId")
|
|
122
|
+
feature_key: str = Field(serialization_alias="featureKey", validation_alias="featureKey")
|
|
123
|
+
fields: str | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class GrantBody(_BaseModel):
|
|
127
|
+
"""Body accepted by ``POST /v1/entitlements/grants``."""
|
|
128
|
+
|
|
129
|
+
contact_id: str = Field(serialization_alias="contactId", validation_alias="contactId")
|
|
130
|
+
feature_key: str = Field(serialization_alias="featureKey", validation_alias="featureKey")
|
|
131
|
+
amount: int
|
|
132
|
+
grant_id: str = Field(serialization_alias="grantId", validation_alias="grantId")
|
|
133
|
+
metadata: dict[str, str] | None = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ConsumeBody(_BaseModel):
|
|
137
|
+
"""Body accepted by ``POST /v1/entitlements/consume``."""
|
|
138
|
+
|
|
139
|
+
contact_id: str = Field(serialization_alias="contactId", validation_alias="contactId")
|
|
140
|
+
feature_key: str = Field(serialization_alias="featureKey", validation_alias="featureKey")
|
|
141
|
+
amount: int
|
|
142
|
+
reason: str | None = None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Features resource — sync and async clients plus public types.
|
|
2
|
+
|
|
3
|
+
Most callers reach this module through
|
|
4
|
+
[ThreeCommon.features][threecommon.ThreeCommon] /
|
|
5
|
+
[AsyncThreeCommon.features][threecommon.AsyncThreeCommon]; importing the service
|
|
6
|
+
classes directly is supported for advanced wiring.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from threecommon.features.service import AsyncFeaturesService, FeaturesService
|
|
10
|
+
from threecommon.features.types import (
|
|
11
|
+
CreateBody,
|
|
12
|
+
Feature,
|
|
13
|
+
FeatureType,
|
|
14
|
+
ListFeaturesResponse,
|
|
15
|
+
ListParams,
|
|
16
|
+
ResolvedFeature,
|
|
17
|
+
ResolvedFeatureBoolean,
|
|
18
|
+
ResolvedFeatureDuration,
|
|
19
|
+
ResolvedFeatureEnum,
|
|
20
|
+
ResolvedFeatureQuantity,
|
|
21
|
+
ResolveParams,
|
|
22
|
+
RetrieveParams,
|
|
23
|
+
UpdateBody,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = (
|
|
27
|
+
"AsyncFeaturesService",
|
|
28
|
+
"CreateBody",
|
|
29
|
+
"Feature",
|
|
30
|
+
"FeatureType",
|
|
31
|
+
"FeaturesService",
|
|
32
|
+
"ListFeaturesResponse",
|
|
33
|
+
"ListParams",
|
|
34
|
+
"ResolveParams",
|
|
35
|
+
"ResolvedFeature",
|
|
36
|
+
"ResolvedFeatureBoolean",
|
|
37
|
+
"ResolvedFeatureDuration",
|
|
38
|
+
"ResolvedFeatureEnum",
|
|
39
|
+
"ResolvedFeatureQuantity",
|
|
40
|
+
"RetrieveParams",
|
|
41
|
+
"UpdateBody",
|
|
42
|
+
)
|