ergon-framework-python 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- ergon/__init__.py +1 -1
- ergon/connector/__init__.py +20 -0
- ergon/connector/ergon_platform/__init__.py +78 -11
- ergon/connector/ergon_platform/async_connector.py +3 -221
- ergon/connector/ergon_platform/channels/__init__.py +54 -0
- ergon/connector/ergon_platform/channels/adapters.py +230 -0
- ergon/connector/ergon_platform/channels/async_connector.py +327 -0
- ergon/connector/ergon_platform/channels/connector.py +294 -0
- ergon/connector/ergon_platform/channels/models.py +460 -0
- ergon/connector/ergon_platform/channels/services/__init__.py +39 -0
- ergon/connector/ergon_platform/channels/services/activity.py +300 -0
- ergon/connector/ergon_platform/channels/services/addresses.py +325 -0
- ergon/connector/ergon_platform/channels/services/attachments.py +283 -0
- ergon/connector/ergon_platform/channels/services/messages.py +127 -0
- ergon/connector/ergon_platform/channels/services/records.py +75 -0
- ergon/connector/ergon_platform/channels/services/service.py +34 -0
- ergon/connector/ergon_platform/connector.py +3 -198
- ergon/connector/ergon_platform/models.py +1 -73
- ergon/connector/ergon_platform/utils.py +39 -248
- ergon/connector/ergon_platform/workflows/__init__.py +52 -0
- ergon/connector/ergon_platform/{_operations.py → workflows/_operations.py} +8 -3
- ergon/connector/ergon_platform/workflows/async_connector.py +224 -0
- ergon/connector/ergon_platform/workflows/connector.py +201 -0
- ergon/connector/ergon_platform/workflows/models.py +90 -0
- ergon/connector/ergon_platform/workflows/utils.py +250 -0
- ergon/connector/nylas/async_connector.py +6 -0
- ergon/connector/nylas/async_service.py +3 -0
- ergon/connector/nylas/connector.py +6 -0
- ergon/connector/nylas/models.py +8 -0
- ergon/connector/nylas/service.py +9 -0
- ergon/connector/nylas/utils.py +2 -4
- ergon/task/mixins/consumer.py +0 -2
- {ergon_framework_python-0.2.0.dist-info → ergon_framework_python-0.2.2.dist-info}/METADATA +3 -3
- {ergon_framework_python-0.2.0.dist-info → ergon_framework_python-0.2.2.dist-info}/RECORD +38 -21
- {ergon_framework_python-0.2.0.dist-info → ergon_framework_python-0.2.2.dist-info}/WHEEL +0 -0
- {ergon_framework_python-0.2.0.dist-info → ergon_framework_python-0.2.2.dist-info}/entry_points.txt +0 -0
- {ergon_framework_python-0.2.0.dist-info → ergon_framework_python-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {ergon_framework_python-0.2.0.dist-info → ergon_framework_python-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from ....transaction import Transaction
|
|
6
|
+
from ..adapters import ActivityAdapter
|
|
7
|
+
from ..models import ErgonPlatformChannelsConsumerConfig
|
|
8
|
+
from .records import SdkRecord
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChannelsActivityService:
|
|
12
|
+
"""Activity history, inbox claims, and lease settlement."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: Any) -> None:
|
|
15
|
+
self.client = client
|
|
16
|
+
|
|
17
|
+
def fetch_activity_events(
|
|
18
|
+
self,
|
|
19
|
+
company_id: Optional[str] = None,
|
|
20
|
+
limit: Optional[int] = None,
|
|
21
|
+
offset: int = 0,
|
|
22
|
+
**params: Any,
|
|
23
|
+
) -> List[Transaction]:
|
|
24
|
+
"""Fetch the activity events."""
|
|
25
|
+
response = self.client.channels.company_activity(
|
|
26
|
+
company_id=company_id,
|
|
27
|
+
**self._page_query(params, limit=limit, offset=offset),
|
|
28
|
+
)
|
|
29
|
+
events = SdkRecord.items(
|
|
30
|
+
response,
|
|
31
|
+
keys=["items", "data", "results"],
|
|
32
|
+
)
|
|
33
|
+
return [ActivityAdapter.to_transaction(event, source="activity") for event in events]
|
|
34
|
+
|
|
35
|
+
def fetch_inbox_events(
|
|
36
|
+
self,
|
|
37
|
+
config_id: str,
|
|
38
|
+
address_id: Optional[str] = None,
|
|
39
|
+
limit: Optional[int] = None,
|
|
40
|
+
offset: int = 0,
|
|
41
|
+
**params: Any,
|
|
42
|
+
) -> List[Transaction]:
|
|
43
|
+
"""Fetch the inbox events."""
|
|
44
|
+
response = self.client.channels.configs.activity(
|
|
45
|
+
config_id,
|
|
46
|
+
**self._page_query(
|
|
47
|
+
params,
|
|
48
|
+
limit=limit,
|
|
49
|
+
offset=offset,
|
|
50
|
+
address_id=address_id,
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
events = SdkRecord.items(
|
|
54
|
+
response,
|
|
55
|
+
keys=["items", "data", "results"],
|
|
56
|
+
)
|
|
57
|
+
return [
|
|
58
|
+
ActivityAdapter.to_transaction(
|
|
59
|
+
event,
|
|
60
|
+
source="config_activity",
|
|
61
|
+
)
|
|
62
|
+
for event in events
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def claim_inbox_transactions(
|
|
66
|
+
self,
|
|
67
|
+
config_id: str,
|
|
68
|
+
address_id: str,
|
|
69
|
+
config: ErgonPlatformChannelsConsumerConfig,
|
|
70
|
+
limit: int,
|
|
71
|
+
seen_ids: Optional[set[str]] = None,
|
|
72
|
+
) -> List[Transaction]:
|
|
73
|
+
"""Claim a batch and release every lease if pagination/filtering fails."""
|
|
74
|
+
leased: List[Transaction] = []
|
|
75
|
+
try:
|
|
76
|
+
return self._claim_inbox_transactions(
|
|
77
|
+
config_id=config_id,
|
|
78
|
+
address_id=address_id,
|
|
79
|
+
config=config,
|
|
80
|
+
limit=limit,
|
|
81
|
+
seen_ids=seen_ids,
|
|
82
|
+
leased=leased,
|
|
83
|
+
)
|
|
84
|
+
except Exception:
|
|
85
|
+
self.release_claims(
|
|
86
|
+
config_id,
|
|
87
|
+
leased,
|
|
88
|
+
delay_seconds=config.nack_delay_seconds,
|
|
89
|
+
)
|
|
90
|
+
if seen_ids is not None:
|
|
91
|
+
for transaction in leased:
|
|
92
|
+
if transaction.id:
|
|
93
|
+
seen_ids.discard(transaction.id)
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
def _claim_inbox_transactions(
|
|
97
|
+
self,
|
|
98
|
+
config_id: str,
|
|
99
|
+
address_id: str,
|
|
100
|
+
config: ErgonPlatformChannelsConsumerConfig,
|
|
101
|
+
limit: int,
|
|
102
|
+
seen_ids: Optional[set[str]],
|
|
103
|
+
leased: List[Transaction],
|
|
104
|
+
) -> List[Transaction]:
|
|
105
|
+
"""Claim the inbox transactions."""
|
|
106
|
+
subscription_id = config.resolved_subscription_id(
|
|
107
|
+
config_id,
|
|
108
|
+
address_id,
|
|
109
|
+
)
|
|
110
|
+
cursor: Optional[str] = None
|
|
111
|
+
accepted: List[Transaction] = []
|
|
112
|
+
activity_filter = config.effective_activity_filter()
|
|
113
|
+
|
|
114
|
+
claim = self._activity_claim()
|
|
115
|
+
|
|
116
|
+
while len(accepted) < limit:
|
|
117
|
+
response = claim(
|
|
118
|
+
config_id,
|
|
119
|
+
subscription_id=subscription_id,
|
|
120
|
+
address_id=address_id,
|
|
121
|
+
consumer_id=config.consumer_id,
|
|
122
|
+
limit=min(
|
|
123
|
+
config.claim_page_size,
|
|
124
|
+
max(1, limit - len(accepted)),
|
|
125
|
+
),
|
|
126
|
+
visibility_timeout_seconds=config.visibility_timeout_seconds,
|
|
127
|
+
cursor=cursor,
|
|
128
|
+
idempotency_key=str(uuid4()),
|
|
129
|
+
)
|
|
130
|
+
items = SdkRecord.items(response, keys=["items"])
|
|
131
|
+
if not items:
|
|
132
|
+
break
|
|
133
|
+
for item in items:
|
|
134
|
+
transaction = ActivityAdapter.claimed_transaction(item)
|
|
135
|
+
leased.append(transaction)
|
|
136
|
+
matches = ActivityAdapter.belongs_to_address(
|
|
137
|
+
transaction,
|
|
138
|
+
address_id,
|
|
139
|
+
)
|
|
140
|
+
matches = matches and activity_filter.matches(transaction)
|
|
141
|
+
duplicate = bool(seen_ids is not None and transaction.id and transaction.id in seen_ids)
|
|
142
|
+
if not matches or duplicate:
|
|
143
|
+
self.ack_inbox_event(config_id, transaction)
|
|
144
|
+
leased.remove(transaction)
|
|
145
|
+
continue
|
|
146
|
+
accepted.append(transaction)
|
|
147
|
+
if seen_ids is not None and transaction.id:
|
|
148
|
+
seen_ids.add(transaction.id)
|
|
149
|
+
if len(accepted) >= limit:
|
|
150
|
+
break
|
|
151
|
+
cursor_value = SdkRecord(response).get("next_cursor")
|
|
152
|
+
cursor = str(cursor_value) if cursor_value else None
|
|
153
|
+
if cursor is None:
|
|
154
|
+
break
|
|
155
|
+
return accepted
|
|
156
|
+
|
|
157
|
+
def _activity_claim(self) -> Callable[..., Any]:
|
|
158
|
+
"""Return the SDK claim operation after validating its availability."""
|
|
159
|
+
claim = getattr(
|
|
160
|
+
self.client.channels.configs,
|
|
161
|
+
"activity_claim",
|
|
162
|
+
None,
|
|
163
|
+
)
|
|
164
|
+
if not callable(claim):
|
|
165
|
+
raise RuntimeError("ergon-platform-sdk>=0.2.0 is required: channels.configs.activity_claim is unavailable")
|
|
166
|
+
return claim
|
|
167
|
+
|
|
168
|
+
def get_activity_count(
|
|
169
|
+
self,
|
|
170
|
+
company_id: Optional[str] = None,
|
|
171
|
+
**params: Any,
|
|
172
|
+
) -> int:
|
|
173
|
+
"""Get the activity count."""
|
|
174
|
+
query = {**params, "limit": 1, "page": 1}
|
|
175
|
+
response = self.client.channels.company_activity(
|
|
176
|
+
company_id=company_id,
|
|
177
|
+
**query,
|
|
178
|
+
)
|
|
179
|
+
return SdkRecord.total(response)
|
|
180
|
+
|
|
181
|
+
def get_inbox_events_count(
|
|
182
|
+
self,
|
|
183
|
+
config_id: str,
|
|
184
|
+
address_id: Optional[str] = None,
|
|
185
|
+
**params: Any,
|
|
186
|
+
) -> int:
|
|
187
|
+
"""Get the inbox events count."""
|
|
188
|
+
query: Dict[str, Any] = {**params, "limit": 1, "page": 1}
|
|
189
|
+
if address_id:
|
|
190
|
+
query["address_id"] = address_id
|
|
191
|
+
response = self.client.channels.configs.activity(config_id, **query)
|
|
192
|
+
return SdkRecord.total(response)
|
|
193
|
+
|
|
194
|
+
def get_activity_event(
|
|
195
|
+
self,
|
|
196
|
+
event_id: str,
|
|
197
|
+
company_id: Optional[str] = None,
|
|
198
|
+
**params: Any,
|
|
199
|
+
) -> Transaction:
|
|
200
|
+
"""Get the activity event."""
|
|
201
|
+
event = self.client.channels.company_activity_event(
|
|
202
|
+
company_id=company_id,
|
|
203
|
+
event_id=event_id,
|
|
204
|
+
**params,
|
|
205
|
+
)
|
|
206
|
+
return ActivityAdapter.to_transaction(event, source="activity")
|
|
207
|
+
|
|
208
|
+
def get_inbox_event(
|
|
209
|
+
self,
|
|
210
|
+
config_id: str,
|
|
211
|
+
event_id: str,
|
|
212
|
+
**params: Any,
|
|
213
|
+
) -> Transaction:
|
|
214
|
+
"""Get the inbox event."""
|
|
215
|
+
event = self.client.channels.configs.activity_event(
|
|
216
|
+
config_id,
|
|
217
|
+
event_id,
|
|
218
|
+
**params,
|
|
219
|
+
)
|
|
220
|
+
return ActivityAdapter.to_transaction(
|
|
221
|
+
event,
|
|
222
|
+
source="config_activity",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def finalize_fetched_transactions(
|
|
227
|
+
transactions: List[Transaction],
|
|
228
|
+
config: ErgonPlatformChannelsConsumerConfig,
|
|
229
|
+
seen_ids: Optional[set[str]] = None,
|
|
230
|
+
) -> List[Transaction]:
|
|
231
|
+
"""Finalize the fetched transactions."""
|
|
232
|
+
return ActivityAdapter.finalize_fetch(
|
|
233
|
+
transactions,
|
|
234
|
+
config,
|
|
235
|
+
seen_ids=seen_ids,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def ack_inbox_event(
|
|
239
|
+
self,
|
|
240
|
+
config_id: str,
|
|
241
|
+
transaction: Transaction,
|
|
242
|
+
) -> Any:
|
|
243
|
+
"""Ack the inbox event."""
|
|
244
|
+
delivery = ActivityAdapter.delivery(transaction)
|
|
245
|
+
return self.client.channels.configs.activity_ack(
|
|
246
|
+
config_id,
|
|
247
|
+
transaction.id,
|
|
248
|
+
subscription_id=delivery["subscription_id"],
|
|
249
|
+
lease_token=delivery["lease_token"],
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def nack_inbox_event(
|
|
253
|
+
self,
|
|
254
|
+
config_id: str,
|
|
255
|
+
transaction: Transaction,
|
|
256
|
+
requeue: bool = True,
|
|
257
|
+
delay_seconds: int = 0,
|
|
258
|
+
) -> Any:
|
|
259
|
+
"""Nack the inbox event."""
|
|
260
|
+
delivery = ActivityAdapter.delivery(transaction)
|
|
261
|
+
return self.client.channels.configs.activity_nack(
|
|
262
|
+
config_id,
|
|
263
|
+
transaction.id,
|
|
264
|
+
subscription_id=delivery["subscription_id"],
|
|
265
|
+
lease_token=delivery["lease_token"],
|
|
266
|
+
requeue=requeue,
|
|
267
|
+
delay_seconds=delay_seconds,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def release_claims(
|
|
271
|
+
self,
|
|
272
|
+
config_id: str,
|
|
273
|
+
transactions: List[Transaction],
|
|
274
|
+
delay_seconds: int,
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Best-effort requeue of a batch that cannot be handed to the task."""
|
|
277
|
+
for transaction in transactions:
|
|
278
|
+
with suppress(Exception):
|
|
279
|
+
self.nack_inbox_event(
|
|
280
|
+
config_id,
|
|
281
|
+
transaction,
|
|
282
|
+
requeue=True,
|
|
283
|
+
delay_seconds=delay_seconds,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _page_query(
|
|
288
|
+
params: Dict[str, Any],
|
|
289
|
+
limit: Optional[int],
|
|
290
|
+
offset: int,
|
|
291
|
+
**extra: Any,
|
|
292
|
+
) -> Dict[str, Any]:
|
|
293
|
+
"""Page query."""
|
|
294
|
+
query = {key: value for key, value in extra.items() if value is not None}
|
|
295
|
+
query.update(params)
|
|
296
|
+
if limit is not None:
|
|
297
|
+
query.setdefault("limit", limit)
|
|
298
|
+
page_size = limit or 50
|
|
299
|
+
query.setdefault("page", (offset // page_size) + 1)
|
|
300
|
+
return query
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from ..models import (
|
|
4
|
+
AddressDirection,
|
|
5
|
+
ErgonPlatformChannelsConsumerConfig,
|
|
6
|
+
ResolvedInboxAddress,
|
|
7
|
+
)
|
|
8
|
+
from .records import SdkRecord
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChannelsAddressService:
|
|
12
|
+
"""Resolve consumer and sender addresses through Channels."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: Any) -> None:
|
|
15
|
+
self._client = client
|
|
16
|
+
self._by_email: Dict[str, ResolvedInboxAddress] = {}
|
|
17
|
+
self._by_id: Dict[str, ResolvedInboxAddress] = {}
|
|
18
|
+
|
|
19
|
+
def resolve_consumer_inbox(
|
|
20
|
+
self,
|
|
21
|
+
config: ErgonPlatformChannelsConsumerConfig,
|
|
22
|
+
) -> ResolvedInboxAddress:
|
|
23
|
+
"""Resolve the consumer inbox address."""
|
|
24
|
+
if config.config_id:
|
|
25
|
+
return self.resolve_in_config(config.config_id, config.address)
|
|
26
|
+
return self.resolve_email(config.address)
|
|
27
|
+
|
|
28
|
+
def resolve_sender_inbox(
|
|
29
|
+
self,
|
|
30
|
+
address: Optional[str],
|
|
31
|
+
address_id: Optional[str],
|
|
32
|
+
direction: Optional[AddressDirection] = None,
|
|
33
|
+
config_id: Optional[str] = None,
|
|
34
|
+
) -> ResolvedInboxAddress:
|
|
35
|
+
"""Resolve the sender inbox address."""
|
|
36
|
+
if address_id:
|
|
37
|
+
try:
|
|
38
|
+
return self.resolve_id(address_id)
|
|
39
|
+
except ValueError:
|
|
40
|
+
return ResolvedInboxAddress(
|
|
41
|
+
address=address or address_id,
|
|
42
|
+
address_id=address_id,
|
|
43
|
+
config_id=config_id or "",
|
|
44
|
+
direction=direction or "both",
|
|
45
|
+
)
|
|
46
|
+
if address and config_id:
|
|
47
|
+
try:
|
|
48
|
+
return self.resolve_in_config(
|
|
49
|
+
config_id,
|
|
50
|
+
address,
|
|
51
|
+
direction_override=direction,
|
|
52
|
+
)
|
|
53
|
+
except ValueError:
|
|
54
|
+
pass
|
|
55
|
+
if address:
|
|
56
|
+
return self.resolve_email(address)
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"address is required to send a channel message "
|
|
59
|
+
"(set producer_config.address, consumer_config.address, "
|
|
60
|
+
"or address_id in a dict payload)"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def resolve_email(self, address_email: str) -> ResolvedInboxAddress:
|
|
64
|
+
"""Resolve the email address."""
|
|
65
|
+
cached = self._by_email.get(address_email)
|
|
66
|
+
if cached:
|
|
67
|
+
return cached
|
|
68
|
+
self._load_granted_addresses()
|
|
69
|
+
resolved = self._by_email.get(address_email)
|
|
70
|
+
if not resolved:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Channel address {address_email!r} not found via "
|
|
73
|
+
"channels.addresses (check the API key scope or the address "
|
|
74
|
+
"spelling). Set consumer_config.config_id (console URL UUID) "
|
|
75
|
+
"with the inbox email so the connector can resolve the "
|
|
76
|
+
"address inside that channel config."
|
|
77
|
+
)
|
|
78
|
+
return resolved
|
|
79
|
+
|
|
80
|
+
def resolve_id(self, address_id: str) -> ResolvedInboxAddress:
|
|
81
|
+
"""Resolve the address id."""
|
|
82
|
+
cached = self._by_id.get(address_id)
|
|
83
|
+
if cached:
|
|
84
|
+
return cached
|
|
85
|
+
self._load_granted_addresses()
|
|
86
|
+
resolved = self._by_id.get(address_id)
|
|
87
|
+
if not resolved:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Channel address id {address_id!r} not found via "
|
|
90
|
+
"channels.addresses (check the API key scope or the address id)."
|
|
91
|
+
)
|
|
92
|
+
return resolved
|
|
93
|
+
|
|
94
|
+
def resolve_in_config(
|
|
95
|
+
self,
|
|
96
|
+
config_id: str,
|
|
97
|
+
address_email: str,
|
|
98
|
+
direction_override: Optional[AddressDirection] = None,
|
|
99
|
+
address_id: Optional[str] = None,
|
|
100
|
+
) -> ResolvedInboxAddress:
|
|
101
|
+
"""Resolve the address in the config."""
|
|
102
|
+
if address_id:
|
|
103
|
+
return self._remember(
|
|
104
|
+
ResolvedInboxAddress(
|
|
105
|
+
address=address_email,
|
|
106
|
+
address_id=address_id,
|
|
107
|
+
config_id=config_id,
|
|
108
|
+
direction=direction_override or "both",
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
cached = self._by_email.get(address_email)
|
|
113
|
+
if cached and cached.config_id == config_id:
|
|
114
|
+
return cached
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
page = self._client.channels.configs.addresses(config_id).list(limit=100)
|
|
118
|
+
for entry in SdkRecord.items(
|
|
119
|
+
page,
|
|
120
|
+
keys=["items", "data", "results"],
|
|
121
|
+
):
|
|
122
|
+
parsed = self._parse_config_entry(
|
|
123
|
+
entry,
|
|
124
|
+
config_id=config_id,
|
|
125
|
+
)
|
|
126
|
+
if parsed and parsed.address == address_email:
|
|
127
|
+
return self._remember(parsed)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
return self._remember(
|
|
132
|
+
self._from_config_activity(
|
|
133
|
+
config_id,
|
|
134
|
+
address_email,
|
|
135
|
+
direction_override=direction_override,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _remember(
|
|
140
|
+
self,
|
|
141
|
+
resolved: ResolvedInboxAddress,
|
|
142
|
+
) -> ResolvedInboxAddress:
|
|
143
|
+
"""Remember the resolved address."""
|
|
144
|
+
resolved = self._with_config_capabilities(resolved)
|
|
145
|
+
self._by_email[resolved.address] = resolved
|
|
146
|
+
self._by_id[resolved.address_id] = resolved
|
|
147
|
+
return resolved
|
|
148
|
+
|
|
149
|
+
def _with_config_capabilities(
|
|
150
|
+
self,
|
|
151
|
+
inbox: ResolvedInboxAddress,
|
|
152
|
+
) -> ResolvedInboxAddress:
|
|
153
|
+
"""With the config capabilities."""
|
|
154
|
+
if not inbox.config_id:
|
|
155
|
+
return inbox
|
|
156
|
+
try:
|
|
157
|
+
verified = self._client.channels.configs.verify(inbox.config_id)
|
|
158
|
+
except Exception:
|
|
159
|
+
return inbox
|
|
160
|
+
record = SdkRecord(verified)
|
|
161
|
+
capabilities = record.get("capabilities") or {}
|
|
162
|
+
if not isinstance(capabilities, dict):
|
|
163
|
+
capabilities = {}
|
|
164
|
+
inbound_enabled = record.get("inbound_enabled")
|
|
165
|
+
return inbox.model_copy(
|
|
166
|
+
update={
|
|
167
|
+
"config_name": record.get("name"),
|
|
168
|
+
"config_can_send": capabilities.get("can_send"),
|
|
169
|
+
"config_can_receive": (inbound_enabled if inbound_enabled is not None else None),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _from_config_activity(
|
|
174
|
+
self,
|
|
175
|
+
config_id: str,
|
|
176
|
+
address_email: str,
|
|
177
|
+
direction_override: Optional[AddressDirection] = None,
|
|
178
|
+
) -> ResolvedInboxAddress:
|
|
179
|
+
"""From the config activity."""
|
|
180
|
+
normalized = address_email.strip().lower()
|
|
181
|
+
created_response = self._client.channels.configs.activity(
|
|
182
|
+
config_id,
|
|
183
|
+
limit=50,
|
|
184
|
+
page=1,
|
|
185
|
+
event_type="channels.address.created",
|
|
186
|
+
)
|
|
187
|
+
for item in SdkRecord.items(
|
|
188
|
+
created_response,
|
|
189
|
+
keys=["items", "data", "results"],
|
|
190
|
+
):
|
|
191
|
+
resolved = self._match_activity_item(
|
|
192
|
+
config_id,
|
|
193
|
+
address_email,
|
|
194
|
+
normalized,
|
|
195
|
+
item,
|
|
196
|
+
direction_override=direction_override,
|
|
197
|
+
)
|
|
198
|
+
if resolved is not None:
|
|
199
|
+
return resolved
|
|
200
|
+
|
|
201
|
+
limit = 50
|
|
202
|
+
for page in range(1, 6):
|
|
203
|
+
response = self._client.channels.configs.activity(
|
|
204
|
+
config_id,
|
|
205
|
+
limit=limit,
|
|
206
|
+
page=page,
|
|
207
|
+
)
|
|
208
|
+
items = SdkRecord.items(
|
|
209
|
+
response,
|
|
210
|
+
keys=["items", "data", "results"],
|
|
211
|
+
)
|
|
212
|
+
if not items:
|
|
213
|
+
break
|
|
214
|
+
for item in items:
|
|
215
|
+
resolved = self._match_activity_item(
|
|
216
|
+
config_id,
|
|
217
|
+
address_email,
|
|
218
|
+
normalized,
|
|
219
|
+
item,
|
|
220
|
+
direction_override=direction_override,
|
|
221
|
+
)
|
|
222
|
+
if resolved is not None:
|
|
223
|
+
return resolved
|
|
224
|
+
if len(items) < limit:
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f"Channel address {address_email!r} not found under config "
|
|
229
|
+
f"{config_id!r}. Check the inbox email and CHANNELS_CONFIG_ID "
|
|
230
|
+
"from the console URL."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _match_activity_item(
|
|
234
|
+
self,
|
|
235
|
+
config_id: str,
|
|
236
|
+
address_email: str,
|
|
237
|
+
normalized_email: str,
|
|
238
|
+
item: Any,
|
|
239
|
+
direction_override: Optional[AddressDirection] = None,
|
|
240
|
+
) -> Optional[ResolvedInboxAddress]:
|
|
241
|
+
"""Match the activity item."""
|
|
242
|
+
record = SdkRecord(item)
|
|
243
|
+
summary = str(record.get("summary") or "").strip().lower()
|
|
244
|
+
event_type = str(record.get("event_type") or "")
|
|
245
|
+
if event_type != "channels.address.created" and summary != normalized_email:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
event_id = record.get("id")
|
|
249
|
+
if not event_id:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
detail = self._client.channels.configs.activity_event(
|
|
253
|
+
config_id,
|
|
254
|
+
str(event_id),
|
|
255
|
+
)
|
|
256
|
+
payload = SdkRecord(detail).get("payload") or {}
|
|
257
|
+
if not isinstance(payload, dict):
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
payload_address = str(payload.get("address") or summary or "").strip().lower()
|
|
261
|
+
if payload_address and payload_address != normalized_email:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
address_id = payload.get("channel_address_id") or payload.get("address_id") or payload.get("id")
|
|
265
|
+
if not address_id and summary == normalized_email and "address.created" in event_type:
|
|
266
|
+
address_id = payload.get("channel_address_id")
|
|
267
|
+
if not address_id:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
direction = direction_override or ResolvedInboxAddress.parse_direction(payload.get("direction"))
|
|
271
|
+
return ResolvedInboxAddress(
|
|
272
|
+
address=address_email,
|
|
273
|
+
address_id=str(address_id),
|
|
274
|
+
config_id=config_id,
|
|
275
|
+
direction=direction,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _load_granted_addresses(self) -> None:
|
|
279
|
+
"""Load the granted addresses."""
|
|
280
|
+
if self._by_email:
|
|
281
|
+
return
|
|
282
|
+
for entry in SdkRecord.items(
|
|
283
|
+
self._client.channels.addresses(),
|
|
284
|
+
keys=["items", "data", "results"],
|
|
285
|
+
):
|
|
286
|
+
parsed = self._parse_granted_entry(entry)
|
|
287
|
+
if parsed is not None:
|
|
288
|
+
self._remember(parsed)
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def _parse_config_entry(
|
|
292
|
+
cls,
|
|
293
|
+
entry: Any,
|
|
294
|
+
config_id: str,
|
|
295
|
+
) -> Optional[ResolvedInboxAddress]:
|
|
296
|
+
"""Parse the config entry."""
|
|
297
|
+
record = SdkRecord(entry)
|
|
298
|
+
entry_email = record.get("address")
|
|
299
|
+
entry_id = record.get("id") or record.get("channel_address_id")
|
|
300
|
+
if not entry_email or not entry_id:
|
|
301
|
+
return None
|
|
302
|
+
return ResolvedInboxAddress(
|
|
303
|
+
address=str(entry_email),
|
|
304
|
+
address_id=str(entry_id),
|
|
305
|
+
config_id=config_id,
|
|
306
|
+
direction=ResolvedInboxAddress.parse_direction(record.get("direction")),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def _parse_granted_entry(
|
|
311
|
+
entry: Any,
|
|
312
|
+
) -> Optional[ResolvedInboxAddress]:
|
|
313
|
+
"""Parse the granted entry."""
|
|
314
|
+
record = SdkRecord(entry)
|
|
315
|
+
entry_email = record.get("address")
|
|
316
|
+
entry_id = record.get("id")
|
|
317
|
+
entry_config = record.get("channel_config_id")
|
|
318
|
+
if not entry_email or not entry_id or not entry_config:
|
|
319
|
+
return None
|
|
320
|
+
return ResolvedInboxAddress(
|
|
321
|
+
address=str(entry_email),
|
|
322
|
+
address_id=str(entry_id),
|
|
323
|
+
config_id=str(entry_config),
|
|
324
|
+
direction=ResolvedInboxAddress.parse_direction(record.get("direction")),
|
|
325
|
+
)
|