threecommon 0.4.0__tar.gz → 0.5.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.
Files changed (45) hide show
  1. {threecommon-0.4.0 → threecommon-0.5.0}/CHANGELOG.md +13 -0
  2. {threecommon-0.4.0 → threecommon-0.5.0}/PKG-INFO +1 -1
  3. {threecommon-0.4.0 → threecommon-0.5.0}/pyproject.toml +1 -1
  4. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/client.py +8 -0
  5. threecommon-0.5.0/src/threecommon/entitlements/__init__.py +37 -0
  6. threecommon-0.5.0/src/threecommon/entitlements/service.py +243 -0
  7. threecommon-0.5.0/src/threecommon/entitlements/types.py +142 -0
  8. {threecommon-0.4.0 → threecommon-0.5.0}/.gitignore +0 -0
  9. {threecommon-0.4.0 → threecommon-0.5.0}/LICENSE +0 -0
  10. {threecommon-0.4.0 → threecommon-0.5.0}/README.md +0 -0
  11. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/__init__.py +0 -0
  12. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_core/__init__.py +0 -0
  13. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_core/headers.py +0 -0
  14. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_core/http_client.py +0 -0
  15. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_core/parse.py +0 -0
  16. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_core/retry.py +0 -0
  17. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_core/telemetry.py +0 -0
  18. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_core/url.py +0 -0
  19. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_generated/__init__.py +0 -0
  20. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/_generated/models.py +0 -0
  21. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/api_version.py +0 -0
  22. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/config.py +0 -0
  23. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/contacts/__init__.py +0 -0
  24. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/contacts/service.py +0 -0
  25. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/contacts/types.py +0 -0
  26. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/errors/__init__.py +0 -0
  27. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/errors/base.py +0 -0
  28. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/errors/classes.py +0 -0
  29. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/events/__init__.py +0 -0
  30. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/events/service.py +0 -0
  31. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/events/types.py +0 -0
  32. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/filters/__init__.py +0 -0
  33. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/filters/builder.py +0 -0
  34. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/filters/types.py +0 -0
  35. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/helpers.py +0 -0
  36. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/invoices/__init__.py +0 -0
  37. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/invoices/service.py +0 -0
  38. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/invoices/types.py +0 -0
  39. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/pagination/__init__.py +0 -0
  40. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/pagination/auto_paginator.py +0 -0
  41. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/py.typed +0 -0
  42. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/subscriptions/__init__.py +0 -0
  43. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/subscriptions/service.py +0 -0
  44. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/subscriptions/types.py +0 -0
  45. {threecommon-0.4.0 → threecommon-0.5.0}/src/threecommon/version.py +0 -0
@@ -5,6 +5,19 @@ versions follow [SemVer](https://semver.org/spec/v2.0.0.html).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## 0.5.0
9
+
10
+ ### Added
11
+
12
+ - Entitlements resource. The new `client.entitlements` surface covers balance
13
+ lookups and grant management: `list`, `retrieve`, `lookup` (by contact +
14
+ feature), `grant` (manual top-up, idempotent on `grant_id`), `consume`
15
+ (debit balance), and `list_auto_paginate`. Both sync and async surfaces.
16
+ - New public types on `threecommon.entitlements`: `Entitlement`,
17
+ `EntitlementGrant`, `EntitlementGrantSource`, `GrantBody`, `ConsumeBody`,
18
+ `ListParams`, `RetrieveParams`, `LookupParams`, and the
19
+ `ListEntitlementsResponse` envelope.
20
+
8
21
  ## 0.4.0
9
22
 
10
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threecommon
3
- Version: 0.4.0
3
+ Version: 0.5.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.4.0"
13
+ version = "0.5.0"
14
14
  dependencies = [
15
15
  "httpx>=0.27,<1.0",
16
16
  "pydantic>=2.7,<3.0",
@@ -19,6 +19,7 @@ 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
23
24
  from threecommon.invoices.service import AsyncInvoicesService, InvoicesService
24
25
  from threecommon.subscriptions.service import AsyncSubscriptionsService, SubscriptionsService
@@ -61,6 +62,10 @@ class ThreeCommon:
61
62
  ``update``, ``delete``, ``bulk_upsert``, ``list_activity``, plus
62
63
  auto-paginators."""
63
64
 
65
+ entitlements: EntitlementsService
66
+ """Entitlements resource — ``list``, ``retrieve``, ``lookup``, ``grant``,
67
+ ``consume``, plus ``list_auto_paginate``."""
68
+
64
69
  _http: HTTPClient
65
70
  _telemetry: Telemetry
66
71
 
@@ -105,6 +110,7 @@ class ThreeCommon:
105
110
  self.invoices = InvoicesService(self._http)
106
111
  self.subscriptions = SubscriptionsService(self._http)
107
112
  self.contacts = ContactsService(self._http)
113
+ self.entitlements = EntitlementsService(self._http)
108
114
 
109
115
  def close(self) -> None:
110
116
  """Close the underlying httpx client (no-op if you supplied your own)."""
@@ -134,6 +140,7 @@ class AsyncThreeCommon:
134
140
  invoices: AsyncInvoicesService
135
141
  subscriptions: AsyncSubscriptionsService
136
142
  contacts: AsyncContactsService
143
+ entitlements: AsyncEntitlementsService
137
144
 
138
145
  _http: AsyncHTTPClient
139
146
  _telemetry: Telemetry
@@ -179,6 +186,7 @@ class AsyncThreeCommon:
179
186
  self.invoices = AsyncInvoicesService(self._http)
180
187
  self.subscriptions = AsyncSubscriptionsService(self._http)
181
188
  self.contacts = AsyncContactsService(self._http)
189
+ self.entitlements = AsyncEntitlementsService(self._http)
182
190
 
183
191
  async def aclose(self) -> None:
184
192
  """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
File without changes
File without changes
File without changes