threecommon 0.3.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.
- {threecommon-0.3.0 → threecommon-0.5.0}/CHANGELOG.md +31 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/PKG-INFO +1 -1
- {threecommon-0.3.0 → threecommon-0.5.0}/pyproject.toml +1 -1
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/client.py +17 -0
- threecommon-0.5.0/src/threecommon/contacts/__init__.py +58 -0
- threecommon-0.5.0/src/threecommon/contacts/service.py +331 -0
- threecommon-0.5.0/src/threecommon/contacts/types.py +310 -0
- threecommon-0.5.0/src/threecommon/entitlements/__init__.py +37 -0
- threecommon-0.5.0/src/threecommon/entitlements/service.py +243 -0
- threecommon-0.5.0/src/threecommon/entitlements/types.py +142 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/.gitignore +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/LICENSE +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/README.md +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_core/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_core/headers.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_core/http_client.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_core/parse.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_core/retry.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_core/telemetry.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_core/url.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_generated/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/_generated/models.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/api_version.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/config.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/errors/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/errors/base.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/errors/classes.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/events/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/events/service.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/events/types.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/filters/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/filters/builder.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/filters/types.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/helpers.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/invoices/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/invoices/service.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/invoices/types.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/pagination/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/pagination/auto_paginator.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/py.typed +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/subscriptions/__init__.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/subscriptions/service.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/subscriptions/types.py +0 -0
- {threecommon-0.3.0 → threecommon-0.5.0}/src/threecommon/version.py +0 -0
|
@@ -5,6 +5,37 @@ 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
|
+
|
|
21
|
+
## 0.4.0
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Contacts resource. The new `client.contacts` surface covers the full
|
|
26
|
+
contact lifecycle: `list`, `count`, `retrieve`, `create`, `update`
|
|
27
|
+
(with optional `merge_with` + `resolution` for absorbing a second
|
|
28
|
+
contact during an email change), `delete`, `bulk_upsert`,
|
|
29
|
+
`list_activity`, and both `list_auto_paginate` +
|
|
30
|
+
`list_activity_auto_paginate` iterators. Both sync and async surfaces.
|
|
31
|
+
- New public types on `threecommon.contacts`: `Contact`,
|
|
32
|
+
`ContactWithOrderDetails`, `ContactActivity`, `ContactProperty`,
|
|
33
|
+
`ContactUpdate`, `CreateBody`, `UpdateBody`, `BulkUpsertBody`,
|
|
34
|
+
`BulkUpsertItem`, `ListParams`, `ActivityListParams`, plus result
|
|
35
|
+
envelopes `ListContactsResponse`, `ListActivityResponse`, `CountResult`,
|
|
36
|
+
`BulkUpsertResult`, `DeleteResult`, and the lifecycle / merge / activity
|
|
37
|
+
literal unions.
|
|
38
|
+
|
|
8
39
|
## 0.3.0
|
|
9
40
|
|
|
10
41
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threecommon
|
|
3
|
-
Version: 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.
|
|
13
|
+
version = "0.5.0"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"pydantic>=2.7,<3.0",
|
|
@@ -18,6 +18,8 @@ from threecommon._core.http_client import (
|
|
|
18
18
|
from threecommon._core.retry import RetryPolicy
|
|
19
19
|
from threecommon._core.telemetry import Telemetry
|
|
20
20
|
from threecommon.config import RetryDelay, resolve_config
|
|
21
|
+
from threecommon.contacts.service import AsyncContactsService, ContactsService
|
|
22
|
+
from threecommon.entitlements.service import AsyncEntitlementsService, EntitlementsService
|
|
21
23
|
from threecommon.events.service import AsyncEventsService, EventsService
|
|
22
24
|
from threecommon.invoices.service import AsyncInvoicesService, InvoicesService
|
|
23
25
|
from threecommon.subscriptions.service import AsyncSubscriptionsService, SubscriptionsService
|
|
@@ -55,6 +57,15 @@ class ThreeCommon:
|
|
|
55
57
|
subscriptions: SubscriptionsService
|
|
56
58
|
"""Subscriptions resource — list, retrieve, create, update, activate, cancel, bill, renew."""
|
|
57
59
|
|
|
60
|
+
contacts: ContactsService
|
|
61
|
+
"""Contacts resource — ``list``, ``count``, ``retrieve``, ``create``,
|
|
62
|
+
``update``, ``delete``, ``bulk_upsert``, ``list_activity``, plus
|
|
63
|
+
auto-paginators."""
|
|
64
|
+
|
|
65
|
+
entitlements: EntitlementsService
|
|
66
|
+
"""Entitlements resource — ``list``, ``retrieve``, ``lookup``, ``grant``,
|
|
67
|
+
``consume``, plus ``list_auto_paginate``."""
|
|
68
|
+
|
|
58
69
|
_http: HTTPClient
|
|
59
70
|
_telemetry: Telemetry
|
|
60
71
|
|
|
@@ -98,6 +109,8 @@ class ThreeCommon:
|
|
|
98
109
|
self.events = EventsService(self._http)
|
|
99
110
|
self.invoices = InvoicesService(self._http)
|
|
100
111
|
self.subscriptions = SubscriptionsService(self._http)
|
|
112
|
+
self.contacts = ContactsService(self._http)
|
|
113
|
+
self.entitlements = EntitlementsService(self._http)
|
|
101
114
|
|
|
102
115
|
def close(self) -> None:
|
|
103
116
|
"""Close the underlying httpx client (no-op if you supplied your own)."""
|
|
@@ -126,6 +139,8 @@ class AsyncThreeCommon:
|
|
|
126
139
|
events: AsyncEventsService
|
|
127
140
|
invoices: AsyncInvoicesService
|
|
128
141
|
subscriptions: AsyncSubscriptionsService
|
|
142
|
+
contacts: AsyncContactsService
|
|
143
|
+
entitlements: AsyncEntitlementsService
|
|
129
144
|
|
|
130
145
|
_http: AsyncHTTPClient
|
|
131
146
|
_telemetry: Telemetry
|
|
@@ -170,6 +185,8 @@ class AsyncThreeCommon:
|
|
|
170
185
|
self.events = AsyncEventsService(self._http)
|
|
171
186
|
self.invoices = AsyncInvoicesService(self._http)
|
|
172
187
|
self.subscriptions = AsyncSubscriptionsService(self._http)
|
|
188
|
+
self.contacts = AsyncContactsService(self._http)
|
|
189
|
+
self.entitlements = AsyncEntitlementsService(self._http)
|
|
173
190
|
|
|
174
191
|
async def aclose(self) -> None:
|
|
175
192
|
"""Close the underlying async httpx client."""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Contacts resource — sync and async clients plus public types.
|
|
2
|
+
|
|
3
|
+
Most callers reach this module through
|
|
4
|
+
[ThreeCommon.contacts][threecommon.ThreeCommon] /
|
|
5
|
+
[AsyncThreeCommon.contacts][threecommon.AsyncThreeCommon]; importing the
|
|
6
|
+
service classes directly is supported for advanced wiring.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from threecommon.contacts.service import AsyncContactsService, ContactsService
|
|
10
|
+
from threecommon.contacts.types import (
|
|
11
|
+
ActivityListParams,
|
|
12
|
+
BulkUpsertBody,
|
|
13
|
+
BulkUpsertItem,
|
|
14
|
+
BulkUpsertResult,
|
|
15
|
+
CompactContactStatus,
|
|
16
|
+
Contact,
|
|
17
|
+
ContactActivity,
|
|
18
|
+
ContactActivityType,
|
|
19
|
+
ContactMergeResolution,
|
|
20
|
+
ContactProperty,
|
|
21
|
+
ContactQuickFilter,
|
|
22
|
+
ContactStatus,
|
|
23
|
+
ContactUpdate,
|
|
24
|
+
ContactWithOrderDetails,
|
|
25
|
+
CountResult,
|
|
26
|
+
CreateBody,
|
|
27
|
+
DeleteResult,
|
|
28
|
+
ListActivityResponse,
|
|
29
|
+
ListContactsResponse,
|
|
30
|
+
ListParams,
|
|
31
|
+
UpdateBody,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = (
|
|
35
|
+
"ActivityListParams",
|
|
36
|
+
"AsyncContactsService",
|
|
37
|
+
"BulkUpsertBody",
|
|
38
|
+
"BulkUpsertItem",
|
|
39
|
+
"BulkUpsertResult",
|
|
40
|
+
"CompactContactStatus",
|
|
41
|
+
"Contact",
|
|
42
|
+
"ContactActivity",
|
|
43
|
+
"ContactActivityType",
|
|
44
|
+
"ContactMergeResolution",
|
|
45
|
+
"ContactProperty",
|
|
46
|
+
"ContactQuickFilter",
|
|
47
|
+
"ContactStatus",
|
|
48
|
+
"ContactUpdate",
|
|
49
|
+
"ContactWithOrderDetails",
|
|
50
|
+
"ContactsService",
|
|
51
|
+
"CountResult",
|
|
52
|
+
"CreateBody",
|
|
53
|
+
"DeleteResult",
|
|
54
|
+
"ListActivityResponse",
|
|
55
|
+
"ListContactsResponse",
|
|
56
|
+
"ListParams",
|
|
57
|
+
"UpdateBody",
|
|
58
|
+
)
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Sync and async contacts 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.contacts.types import (
|
|
14
|
+
ActivityListParams,
|
|
15
|
+
BulkUpsertBody,
|
|
16
|
+
BulkUpsertResult,
|
|
17
|
+
Contact,
|
|
18
|
+
ContactActivity,
|
|
19
|
+
ContactWithOrderDetails,
|
|
20
|
+
CountResult,
|
|
21
|
+
CreateBody,
|
|
22
|
+
DeleteResult,
|
|
23
|
+
ListActivityResponse,
|
|
24
|
+
ListContactsResponse,
|
|
25
|
+
ListParams,
|
|
26
|
+
UpdateBody,
|
|
27
|
+
)
|
|
28
|
+
from threecommon.errors.classes import ValidationError
|
|
29
|
+
from threecommon.pagination import AsyncIter, Iter
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from threecommon._core.http_client import AsyncHTTPClient, HTTPClient
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _encode_list_params(params: ListParams | None) -> dict[str, str] | None:
|
|
36
|
+
if params is None:
|
|
37
|
+
return None
|
|
38
|
+
raw = params.model_dump(by_alias=True, exclude_none=True)
|
|
39
|
+
if not raw:
|
|
40
|
+
return None
|
|
41
|
+
return {k: str(v) for k, v in raw.items()}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _encode_activity_params(params: ActivityListParams | None) -> dict[str, str] | None:
|
|
45
|
+
if params is None:
|
|
46
|
+
return None
|
|
47
|
+
raw = params.model_dump(by_alias=True, exclude_none=True)
|
|
48
|
+
if not raw:
|
|
49
|
+
return None
|
|
50
|
+
return {k: str(v) for k, v in raw.items()}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _require_id(method: str, contact_id: str) -> None:
|
|
54
|
+
if not contact_id:
|
|
55
|
+
msg = f"contacts.{method}: id must be a non-empty string"
|
|
56
|
+
raise ValidationError(code="missing_id", message=msg)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _path_for(contact_id: str) -> str:
|
|
60
|
+
return f"/contacts/{quote(contact_id, safe='')}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _activity_path(contact_id: str) -> str:
|
|
64
|
+
return f"{_path_for(contact_id)}/activity"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------
|
|
68
|
+
# Sync
|
|
69
|
+
# ---------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ContactsService:
|
|
73
|
+
"""Sync contacts service — bound as ``client.contacts`` on [ThreeCommon]."""
|
|
74
|
+
|
|
75
|
+
__slots__ = ("_http",)
|
|
76
|
+
|
|
77
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
78
|
+
self._http = http
|
|
79
|
+
|
|
80
|
+
def list(self, params: ListParams | None = None) -> ListContactsResponse:
|
|
81
|
+
"""List the host's contacts (one page).
|
|
82
|
+
|
|
83
|
+
For full iteration use [list_auto_paginate][ContactsService.list_auto_paginate].
|
|
84
|
+
"""
|
|
85
|
+
body = self._http.request(
|
|
86
|
+
Request(method="GET", path="/contacts", query=_encode_list_params(params))
|
|
87
|
+
)
|
|
88
|
+
return ListContactsResponse.model_validate(body)
|
|
89
|
+
|
|
90
|
+
def count(self) -> CountResult:
|
|
91
|
+
"""Return the total contact count for the host."""
|
|
92
|
+
body = self._http.request(Request(method="GET", path="/contacts/count"))
|
|
93
|
+
return CountResult.model_validate(body["data"])
|
|
94
|
+
|
|
95
|
+
def retrieve(self, contact_id: str) -> Contact:
|
|
96
|
+
"""Retrieve a single contact by id."""
|
|
97
|
+
_require_id("retrieve", contact_id)
|
|
98
|
+
body = self._http.request(Request(method="GET", path=_path_for(contact_id)))
|
|
99
|
+
return Contact.model_validate(body["data"])
|
|
100
|
+
|
|
101
|
+
def create(self, body: CreateBody) -> Contact:
|
|
102
|
+
"""Create a new contact. Raises ``ConflictError`` on duplicate email."""
|
|
103
|
+
if body is None:
|
|
104
|
+
raise ValidationError(
|
|
105
|
+
code="missing_body", message="contacts.create: body must be non-None"
|
|
106
|
+
)
|
|
107
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
108
|
+
response = self._http.request(Request(method="POST", path="/contacts", body=payload))
|
|
109
|
+
return Contact.model_validate(response["data"])
|
|
110
|
+
|
|
111
|
+
def update(self, contact_id: str, body: UpdateBody) -> ContactWithOrderDetails:
|
|
112
|
+
"""Update a contact. Returns the richer order-details projection."""
|
|
113
|
+
_require_id("update", contact_id)
|
|
114
|
+
if body is None:
|
|
115
|
+
raise ValidationError(
|
|
116
|
+
code="missing_body", message="contacts.update: body must be non-None"
|
|
117
|
+
)
|
|
118
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
119
|
+
response = self._http.request(
|
|
120
|
+
Request(method="PATCH", path=_path_for(contact_id), body=payload)
|
|
121
|
+
)
|
|
122
|
+
return ContactWithOrderDetails.model_validate(response["data"])
|
|
123
|
+
|
|
124
|
+
def delete(self, contact_id: str) -> DeleteResult:
|
|
125
|
+
"""Permanently remove a contact. Echoes the removed contact's id."""
|
|
126
|
+
_require_id("delete", contact_id)
|
|
127
|
+
response = self._http.request(Request(method="DELETE", path=_path_for(contact_id)))
|
|
128
|
+
return DeleteResult.model_validate(response["data"])
|
|
129
|
+
|
|
130
|
+
def bulk_upsert(self, body: BulkUpsertBody) -> BulkUpsertResult:
|
|
131
|
+
"""Bulk-upsert up to 10,000 contacts in one round-trip.
|
|
132
|
+
|
|
133
|
+
Deduplicated server-side by email; existing rows are updated rather
|
|
134
|
+
than rejected.
|
|
135
|
+
"""
|
|
136
|
+
if body is None:
|
|
137
|
+
raise ValidationError(
|
|
138
|
+
code="missing_body", message="contacts.bulk_upsert: body must be non-None"
|
|
139
|
+
)
|
|
140
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
141
|
+
response = self._http.request(Request(method="POST", path="/contacts/bulk", body=payload))
|
|
142
|
+
return BulkUpsertResult.model_validate(response["data"])
|
|
143
|
+
|
|
144
|
+
def list_activity(
|
|
145
|
+
self, contact_id: str, params: ActivityListParams | None = None
|
|
146
|
+
) -> ListActivityResponse:
|
|
147
|
+
"""Paginated activity log for a contact."""
|
|
148
|
+
_require_id("list_activity", contact_id)
|
|
149
|
+
body = self._http.request(
|
|
150
|
+
Request(
|
|
151
|
+
method="GET",
|
|
152
|
+
path=_activity_path(contact_id),
|
|
153
|
+
query=_encode_activity_params(params),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
return ListActivityResponse.model_validate(body)
|
|
157
|
+
|
|
158
|
+
def list_auto_paginate(self, params: ListParams | None = None) -> Iter[Contact]:
|
|
159
|
+
"""Iterate every contact matching ``params``, paging automatically."""
|
|
160
|
+
start_page = (
|
|
161
|
+
params.page_number if params is not None and params.page_number is not None else 0
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def fetch(page: int) -> tuple[list[Contact], bool]:
|
|
165
|
+
page_params = (
|
|
166
|
+
params.model_copy(update={"page_number": page})
|
|
167
|
+
if params is not None
|
|
168
|
+
else ListParams(page_number=page)
|
|
169
|
+
)
|
|
170
|
+
body = self._http.request(
|
|
171
|
+
Request(method="GET", path="/contacts", query=_encode_list_params(page_params))
|
|
172
|
+
)
|
|
173
|
+
response = ListContactsResponse.model_validate(body)
|
|
174
|
+
return response.data, response.has_more
|
|
175
|
+
|
|
176
|
+
return Iter(fetch_page=fetch, start_page=start_page)
|
|
177
|
+
|
|
178
|
+
def list_activity_auto_paginate(
|
|
179
|
+
self, contact_id: str, params: ActivityListParams | None = None
|
|
180
|
+
) -> Iter[ContactActivity]:
|
|
181
|
+
"""Iterate every activity record for a contact, paging automatically."""
|
|
182
|
+
_require_id("list_activity_auto_paginate", contact_id)
|
|
183
|
+
start_page = (
|
|
184
|
+
params.page_number if params is not None and params.page_number is not None else 0
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def fetch(page: int) -> tuple[list[ContactActivity], bool]:
|
|
188
|
+
page_params = (
|
|
189
|
+
params.model_copy(update={"page_number": page})
|
|
190
|
+
if params is not None
|
|
191
|
+
else ActivityListParams(page_number=page)
|
|
192
|
+
)
|
|
193
|
+
body = self._http.request(
|
|
194
|
+
Request(
|
|
195
|
+
method="GET",
|
|
196
|
+
path=_activity_path(contact_id),
|
|
197
|
+
query=_encode_activity_params(page_params),
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
response = ListActivityResponse.model_validate(body)
|
|
201
|
+
return response.data, response.has_more
|
|
202
|
+
|
|
203
|
+
return Iter(fetch_page=fetch, start_page=start_page)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------
|
|
207
|
+
# Async
|
|
208
|
+
# ---------------
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class AsyncContactsService:
|
|
212
|
+
"""Async contacts service — bound as ``client.contacts`` on [AsyncThreeCommon]."""
|
|
213
|
+
|
|
214
|
+
__slots__ = ("_http",)
|
|
215
|
+
|
|
216
|
+
def __init__(self, http: AsyncHTTPClient) -> None:
|
|
217
|
+
self._http = http
|
|
218
|
+
|
|
219
|
+
async def list(self, params: ListParams | None = None) -> ListContactsResponse:
|
|
220
|
+
body = await self._http.request(
|
|
221
|
+
Request(method="GET", path="/contacts", query=_encode_list_params(params))
|
|
222
|
+
)
|
|
223
|
+
return ListContactsResponse.model_validate(body)
|
|
224
|
+
|
|
225
|
+
async def count(self) -> CountResult:
|
|
226
|
+
body = await self._http.request(Request(method="GET", path="/contacts/count"))
|
|
227
|
+
return CountResult.model_validate(body["data"])
|
|
228
|
+
|
|
229
|
+
async def retrieve(self, contact_id: str) -> Contact:
|
|
230
|
+
_require_id("retrieve", contact_id)
|
|
231
|
+
body = await self._http.request(Request(method="GET", path=_path_for(contact_id)))
|
|
232
|
+
return Contact.model_validate(body["data"])
|
|
233
|
+
|
|
234
|
+
async def create(self, body: CreateBody) -> Contact:
|
|
235
|
+
if body is None:
|
|
236
|
+
raise ValidationError(
|
|
237
|
+
code="missing_body", message="contacts.create: body must be non-None"
|
|
238
|
+
)
|
|
239
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
240
|
+
response = await self._http.request(Request(method="POST", path="/contacts", body=payload))
|
|
241
|
+
return Contact.model_validate(response["data"])
|
|
242
|
+
|
|
243
|
+
async def update(self, contact_id: str, body: UpdateBody) -> ContactWithOrderDetails:
|
|
244
|
+
_require_id("update", contact_id)
|
|
245
|
+
if body is None:
|
|
246
|
+
raise ValidationError(
|
|
247
|
+
code="missing_body", message="contacts.update: body must be non-None"
|
|
248
|
+
)
|
|
249
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
250
|
+
response = await self._http.request(
|
|
251
|
+
Request(method="PATCH", path=_path_for(contact_id), body=payload)
|
|
252
|
+
)
|
|
253
|
+
return ContactWithOrderDetails.model_validate(response["data"])
|
|
254
|
+
|
|
255
|
+
async def delete(self, contact_id: str) -> DeleteResult:
|
|
256
|
+
_require_id("delete", contact_id)
|
|
257
|
+
response = await self._http.request(Request(method="DELETE", path=_path_for(contact_id)))
|
|
258
|
+
return DeleteResult.model_validate(response["data"])
|
|
259
|
+
|
|
260
|
+
async def bulk_upsert(self, body: BulkUpsertBody) -> BulkUpsertResult:
|
|
261
|
+
if body is None:
|
|
262
|
+
raise ValidationError(
|
|
263
|
+
code="missing_body", message="contacts.bulk_upsert: body must be non-None"
|
|
264
|
+
)
|
|
265
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
266
|
+
response = await self._http.request(
|
|
267
|
+
Request(method="POST", path="/contacts/bulk", body=payload)
|
|
268
|
+
)
|
|
269
|
+
return BulkUpsertResult.model_validate(response["data"])
|
|
270
|
+
|
|
271
|
+
async def list_activity(
|
|
272
|
+
self, contact_id: str, params: ActivityListParams | None = None
|
|
273
|
+
) -> ListActivityResponse:
|
|
274
|
+
_require_id("list_activity", contact_id)
|
|
275
|
+
body = await self._http.request(
|
|
276
|
+
Request(
|
|
277
|
+
method="GET",
|
|
278
|
+
path=_activity_path(contact_id),
|
|
279
|
+
query=_encode_activity_params(params),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return ListActivityResponse.model_validate(body)
|
|
283
|
+
|
|
284
|
+
def list_auto_paginate(self, params: ListParams | None = None) -> AsyncIter[Contact]:
|
|
285
|
+
"""Async iterate every contact matching ``params``."""
|
|
286
|
+
start_page = (
|
|
287
|
+
params.page_number if params is not None and params.page_number is not None else 0
|
|
288
|
+
)
|
|
289
|
+
http = self._http
|
|
290
|
+
|
|
291
|
+
async def fetch(page: int) -> tuple[list[Contact], bool]:
|
|
292
|
+
page_params = (
|
|
293
|
+
params.model_copy(update={"page_number": page})
|
|
294
|
+
if params is not None
|
|
295
|
+
else ListParams(page_number=page)
|
|
296
|
+
)
|
|
297
|
+
body = await http.request(
|
|
298
|
+
Request(method="GET", path="/contacts", query=_encode_list_params(page_params))
|
|
299
|
+
)
|
|
300
|
+
response = ListContactsResponse.model_validate(body)
|
|
301
|
+
return response.data, response.has_more
|
|
302
|
+
|
|
303
|
+
return AsyncIter(fetch_page=fetch, start_page=start_page)
|
|
304
|
+
|
|
305
|
+
def list_activity_auto_paginate(
|
|
306
|
+
self, contact_id: str, params: ActivityListParams | None = None
|
|
307
|
+
) -> AsyncIter[ContactActivity]:
|
|
308
|
+
"""Async iterate every activity record for a contact."""
|
|
309
|
+
_require_id("list_activity_auto_paginate", contact_id)
|
|
310
|
+
start_page = (
|
|
311
|
+
params.page_number if params is not None and params.page_number is not None else 0
|
|
312
|
+
)
|
|
313
|
+
http = self._http
|
|
314
|
+
|
|
315
|
+
async def fetch(page: int) -> tuple[list[ContactActivity], bool]:
|
|
316
|
+
page_params = (
|
|
317
|
+
params.model_copy(update={"page_number": page})
|
|
318
|
+
if params is not None
|
|
319
|
+
else ActivityListParams(page_number=page)
|
|
320
|
+
)
|
|
321
|
+
body = await http.request(
|
|
322
|
+
Request(
|
|
323
|
+
method="GET",
|
|
324
|
+
path=_activity_path(contact_id),
|
|
325
|
+
query=_encode_activity_params(page_params),
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
response = ListActivityResponse.model_validate(body)
|
|
329
|
+
return response.data, response.has_more
|
|
330
|
+
|
|
331
|
+
return AsyncIter(fetch_page=fetch, start_page=start_page)
|