ergon-framework-python 0.2.1__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/task/mixins/consumer.py +0 -2
- {ergon_framework_python-0.2.1.dist-info → ergon_framework_python-0.2.2.dist-info}/METADATA +3 -3
- {ergon_framework_python-0.2.1.dist-info → ergon_framework_python-0.2.2.dist-info}/RECORD +32 -15
- {ergon_framework_python-0.2.1.dist-info → ergon_framework_python-0.2.2.dist-info}/WHEEL +0 -0
- {ergon_framework_python-0.2.1.dist-info → ergon_framework_python-0.2.2.dist-info}/entry_points.txt +0 -0
- {ergon_framework_python-0.2.1.dist-info → ergon_framework_python-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {ergon_framework_python-0.2.1.dist-info → ergon_framework_python-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from ...connector import AsyncConnector
|
|
7
|
+
from ...transaction import Transaction
|
|
8
|
+
from .._client import create_ergon_client
|
|
9
|
+
from ..models import ErgonPlatformClient
|
|
10
|
+
from .models import (
|
|
11
|
+
DEFAULT_CHANNEL,
|
|
12
|
+
ErgonPlatformChannelsConfig,
|
|
13
|
+
ErgonPlatformChannelsConsumerConfig,
|
|
14
|
+
ErgonPlatformChannelsProducerConfig,
|
|
15
|
+
InboxAttachmentFile,
|
|
16
|
+
ResolvedInboxAddress,
|
|
17
|
+
SendMessageAttachment,
|
|
18
|
+
SendMessageInput,
|
|
19
|
+
SendMessagePayload,
|
|
20
|
+
)
|
|
21
|
+
from .services import ErgonPlatformChannelsService
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AsyncErgonPlatformChannelsConnector(AsyncConnector):
|
|
27
|
+
"""Async connector for one Ergon Platform channels inbox."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
client: ErgonPlatformClient,
|
|
32
|
+
channels_config: Optional[ErgonPlatformChannelsConfig] = None,
|
|
33
|
+
consumer_config: Optional[ErgonPlatformChannelsConsumerConfig] = None,
|
|
34
|
+
producer_config: Optional[ErgonPlatformChannelsProducerConfig] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
if channels_config is not None:
|
|
37
|
+
consumer_config = consumer_config or channels_config.to_consumer_config()
|
|
38
|
+
producer_config = producer_config or channels_config.to_producer_config()
|
|
39
|
+
self._consumer_config = consumer_config
|
|
40
|
+
self._producer_config = producer_config or ErgonPlatformChannelsProducerConfig()
|
|
41
|
+
self.client = create_ergon_client(client)
|
|
42
|
+
self.service = ErgonPlatformChannelsService(
|
|
43
|
+
client,
|
|
44
|
+
self.client,
|
|
45
|
+
download_client=self._attachment_download_client(client),
|
|
46
|
+
)
|
|
47
|
+
self._seen_event_ids: set[str] = set()
|
|
48
|
+
|
|
49
|
+
async def fetch_transactions_async(
|
|
50
|
+
self,
|
|
51
|
+
batch_size: Optional[int] = None,
|
|
52
|
+
*args,
|
|
53
|
+
**kwargs,
|
|
54
|
+
) -> List[Transaction]:
|
|
55
|
+
config = self._require_consumer_config("fetch transactions")
|
|
56
|
+
inbox = await self._resolve_inbox_async(config)
|
|
57
|
+
inbox.ensure_can_receive()
|
|
58
|
+
limit = batch_size or config.batch_size
|
|
59
|
+
if kwargs:
|
|
60
|
+
raise TypeError(f"Unsupported inbox claim options: {', '.join(sorted(kwargs))}")
|
|
61
|
+
transactions = await asyncio.to_thread(
|
|
62
|
+
lambda: self.service.activity.claim_inbox_transactions(
|
|
63
|
+
config_id=inbox.config_id,
|
|
64
|
+
address_id=inbox.address_id,
|
|
65
|
+
config=config,
|
|
66
|
+
limit=limit,
|
|
67
|
+
seen_ids=self._seen_event_ids if config.deduplicate_fetched_events else None,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
return await self._hydrate_fetched_transactions_async(config, inbox.config_id, transactions)
|
|
72
|
+
except Exception:
|
|
73
|
+
await asyncio.to_thread(
|
|
74
|
+
lambda: self.service.activity.release_claims(
|
|
75
|
+
inbox.config_id,
|
|
76
|
+
transactions,
|
|
77
|
+
delay_seconds=config.nack_delay_seconds,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
for transaction in transactions:
|
|
81
|
+
if transaction.id:
|
|
82
|
+
self._seen_event_ids.discard(transaction.id)
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
async def _hydrate_fetched_transactions_async(
|
|
86
|
+
self,
|
|
87
|
+
config: ErgonPlatformChannelsConsumerConfig,
|
|
88
|
+
config_id: str,
|
|
89
|
+
transactions: List[Transaction],
|
|
90
|
+
) -> List[Transaction]:
|
|
91
|
+
if not config.download_attachments:
|
|
92
|
+
return transactions
|
|
93
|
+
return await asyncio.to_thread(
|
|
94
|
+
lambda: self.service.attachments.hydrate_inbox_transactions(
|
|
95
|
+
config_id,
|
|
96
|
+
transactions,
|
|
97
|
+
failure_policy=config.attachment_failure_policy,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def fetch_transaction_by_id_async(self, transaction_id: str, *args, **kwargs) -> Transaction:
|
|
102
|
+
config = self._require_consumer_config("fetch a transaction by id")
|
|
103
|
+
inbox = await self._resolve_inbox_async(config)
|
|
104
|
+
inbox.ensure_can_receive()
|
|
105
|
+
transaction = await asyncio.to_thread(
|
|
106
|
+
lambda: self.service.activity.get_inbox_event(
|
|
107
|
+
inbox.config_id,
|
|
108
|
+
transaction_id,
|
|
109
|
+
**kwargs,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
hydrated = await self._hydrate_fetched_transactions_async(config, inbox.config_id, [transaction])
|
|
113
|
+
return hydrated[0]
|
|
114
|
+
|
|
115
|
+
async def get_transactions_count_async(self, *args, **kwargs) -> int:
|
|
116
|
+
config = self._require_consumer_config("count transactions")
|
|
117
|
+
inbox = await self._resolve_inbox_async(config)
|
|
118
|
+
inbox.ensure_can_receive()
|
|
119
|
+
return await asyncio.to_thread(
|
|
120
|
+
lambda: self.service.activity.get_inbox_events_count(
|
|
121
|
+
inbox.config_id,
|
|
122
|
+
address_id=inbox.address_id,
|
|
123
|
+
**kwargs,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def dispatch_transactions_async(
|
|
128
|
+
self,
|
|
129
|
+
transactions: List[Transaction],
|
|
130
|
+
*args,
|
|
131
|
+
**kwargs,
|
|
132
|
+
) -> List[str]:
|
|
133
|
+
sent_ids: List[str] = []
|
|
134
|
+
for transaction in transactions:
|
|
135
|
+
result = await self._send_from_payload(transaction.payload)
|
|
136
|
+
sent_ids.append(self.service.messages.send_response_id(result))
|
|
137
|
+
return sent_ids
|
|
138
|
+
|
|
139
|
+
async def send_message(self, payload: SendMessagePayload) -> Any:
|
|
140
|
+
return await self._send_from_payload(payload)
|
|
141
|
+
|
|
142
|
+
async def send_email_async(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
to: Union[str, List[str]],
|
|
146
|
+
subject: str,
|
|
147
|
+
text: Optional[str] = None,
|
|
148
|
+
html: Optional[str] = None,
|
|
149
|
+
cc: Optional[List[str]] = None,
|
|
150
|
+
bcc: Optional[List[str]] = None,
|
|
151
|
+
reply_to: Optional[str] = None,
|
|
152
|
+
in_reply_to: Optional[str] = None,
|
|
153
|
+
attachments: Optional[List[SendMessageAttachment]] = None,
|
|
154
|
+
) -> str:
|
|
155
|
+
"""Send one email and return the platform log/message id."""
|
|
156
|
+
payload = SendMessageInput(
|
|
157
|
+
to=self.service.messages.normalize_recipients(to),
|
|
158
|
+
subject=subject,
|
|
159
|
+
text=text,
|
|
160
|
+
html=html,
|
|
161
|
+
cc=cc,
|
|
162
|
+
bcc=bcc,
|
|
163
|
+
reply_to=reply_to,
|
|
164
|
+
in_reply_to=in_reply_to,
|
|
165
|
+
attachments=attachments,
|
|
166
|
+
)
|
|
167
|
+
result = await self._send_from_payload(payload)
|
|
168
|
+
return self.service.messages.send_response_id(result)
|
|
169
|
+
|
|
170
|
+
async def list_thread_messages(self, thread_id: str, **params: Any) -> List[Transaction]:
|
|
171
|
+
return await asyncio.to_thread(lambda: self.service.messages.fetch_thread_messages(thread_id, **params))
|
|
172
|
+
|
|
173
|
+
async def list_company_activity(self, **params: Any) -> List[Transaction]:
|
|
174
|
+
return await asyncio.to_thread(lambda: self.service.activity.fetch_activity_events(**params))
|
|
175
|
+
|
|
176
|
+
async def resolve_inbox_async(self) -> ResolvedInboxAddress:
|
|
177
|
+
"""Resolve ``address`` (+ optional ``config_id``) to platform routing metadata."""
|
|
178
|
+
config = self._require_consumer_config("resolve inbox")
|
|
179
|
+
return await self._resolve_inbox_async(config)
|
|
180
|
+
|
|
181
|
+
async def resolve_address_info(self, *, address: Optional[str] = None) -> Dict[str, Any]:
|
|
182
|
+
config = self._require_consumer_config("resolve address info")
|
|
183
|
+
if address and address != config.address:
|
|
184
|
+
config = config.model_copy(update={"address": address})
|
|
185
|
+
inbox = await self._resolve_inbox_async(config)
|
|
186
|
+
return inbox.as_info_dict()
|
|
187
|
+
|
|
188
|
+
async def ack_transaction(
|
|
189
|
+
self,
|
|
190
|
+
transaction: Transaction,
|
|
191
|
+
allow_attachment_failures: bool = False,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Acknowledge a processed inbox event on the platform."""
|
|
194
|
+
config = self._require_consumer_config("ack a transaction")
|
|
195
|
+
inbox = await self._resolve_inbox_async(config)
|
|
196
|
+
failures = (transaction.metadata or {}).get("attachment_failures")
|
|
197
|
+
if failures and not allow_attachment_failures:
|
|
198
|
+
raise ValueError(
|
|
199
|
+
"Transaction has failed attachment downloads; nack it or pass "
|
|
200
|
+
"allow_attachment_failures=True to acknowledge explicitly"
|
|
201
|
+
)
|
|
202
|
+
await asyncio.to_thread(
|
|
203
|
+
lambda: self.service.activity.ack_inbox_event(
|
|
204
|
+
inbox.config_id,
|
|
205
|
+
transaction,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
if transaction.id:
|
|
209
|
+
self._seen_event_ids.add(transaction.id)
|
|
210
|
+
|
|
211
|
+
async def nack_transaction(
|
|
212
|
+
self,
|
|
213
|
+
transaction: Transaction,
|
|
214
|
+
requeue: bool = True,
|
|
215
|
+
delay_seconds: Optional[int] = None,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Requeue or fail an inbox event on the platform."""
|
|
218
|
+
config = self._require_consumer_config("nack a transaction")
|
|
219
|
+
inbox = await self._resolve_inbox_async(config)
|
|
220
|
+
delay = config.nack_delay_seconds if delay_seconds is None else delay_seconds
|
|
221
|
+
await asyncio.to_thread(
|
|
222
|
+
lambda: self.service.activity.nack_inbox_event(
|
|
223
|
+
inbox.config_id,
|
|
224
|
+
transaction,
|
|
225
|
+
requeue=requeue,
|
|
226
|
+
delay_seconds=delay,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if requeue and transaction.id:
|
|
230
|
+
self._seen_event_ids.discard(transaction.id)
|
|
231
|
+
|
|
232
|
+
async def download_attachments(
|
|
233
|
+
self,
|
|
234
|
+
transaction: Transaction,
|
|
235
|
+
dest: Optional[Union[str, Path]] = None,
|
|
236
|
+
) -> List[InboxAttachmentFile]:
|
|
237
|
+
"""Download inbound attachments from a fetched inbox event.
|
|
238
|
+
|
|
239
|
+
Attachment ids and filenames come from the transaction payload. Pass
|
|
240
|
+
``dest`` to write ``{dest}/{event_id}/{filename}`` and fill ``path``.
|
|
241
|
+
"""
|
|
242
|
+
config = self._require_consumer_config("download attachments")
|
|
243
|
+
inbox = await self._resolve_inbox_async(config)
|
|
244
|
+
inbox.ensure_can_receive()
|
|
245
|
+
return await asyncio.to_thread(
|
|
246
|
+
lambda: self.service.attachments.download_inbox_attachments(
|
|
247
|
+
inbox.config_id,
|
|
248
|
+
transaction,
|
|
249
|
+
dest=dest,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
async def close(self) -> None:
|
|
254
|
+
await asyncio.to_thread(self.service.close)
|
|
255
|
+
await asyncio.to_thread(self.client.close)
|
|
256
|
+
|
|
257
|
+
def _attachment_download_client(self, config: ErgonPlatformClient) -> Any:
|
|
258
|
+
timeout = 20.0
|
|
259
|
+
if self._consumer_config is not None:
|
|
260
|
+
timeout = self._consumer_config.attachment_download_timeout
|
|
261
|
+
download_config = config.model_copy(update={"timeout": timeout, "max_retries": 0})
|
|
262
|
+
return create_ergon_client(download_config)
|
|
263
|
+
|
|
264
|
+
def _require_consumer_config(self, action: str) -> ErgonPlatformChannelsConsumerConfig:
|
|
265
|
+
if self._consumer_config is None:
|
|
266
|
+
raise ValueError(f"AsyncErgonPlatformChannelsConnector requires a consumer_config to {action}")
|
|
267
|
+
return self._consumer_config
|
|
268
|
+
|
|
269
|
+
async def _resolve_inbox_async(
|
|
270
|
+
self,
|
|
271
|
+
config: ErgonPlatformChannelsConsumerConfig,
|
|
272
|
+
) -> ResolvedInboxAddress:
|
|
273
|
+
return await asyncio.to_thread(lambda: self._resolve_inbox(config))
|
|
274
|
+
|
|
275
|
+
def _resolve_inbox(self, config: ErgonPlatformChannelsConsumerConfig) -> ResolvedInboxAddress:
|
|
276
|
+
return self.service.addresses.resolve_consumer_inbox(config)
|
|
277
|
+
|
|
278
|
+
def _resolve_send_inbox(
|
|
279
|
+
self,
|
|
280
|
+
*,
|
|
281
|
+
address_id: Optional[str],
|
|
282
|
+
address: Optional[str],
|
|
283
|
+
) -> ResolvedInboxAddress:
|
|
284
|
+
return self.service.addresses.resolve_sender_inbox(
|
|
285
|
+
address=address,
|
|
286
|
+
address_id=address_id,
|
|
287
|
+
config_id=(
|
|
288
|
+
self._producer_config.config_id or (self._consumer_config.config_id if self._consumer_config else None)
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def _default_inbox_address(self) -> Optional[str]:
|
|
293
|
+
if self._consumer_config and self._consumer_config.address:
|
|
294
|
+
return self._consumer_config.address
|
|
295
|
+
return self._producer_config.address
|
|
296
|
+
|
|
297
|
+
async def _send_from_payload(self, payload: SendMessagePayload) -> Any:
|
|
298
|
+
parts = self.service.messages.normalize_send_payload(payload)
|
|
299
|
+
top = parts["top"]
|
|
300
|
+
config: Dict[str, Any] = parts["config"]
|
|
301
|
+
producer = self._producer_config
|
|
302
|
+
|
|
303
|
+
send_address = producer.address or self._default_inbox_address()
|
|
304
|
+
inbox = await asyncio.to_thread(
|
|
305
|
+
lambda: self._resolve_send_inbox(
|
|
306
|
+
address_id=top.get("address_id"),
|
|
307
|
+
address=send_address if top.get("address_id") is None else None,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
inbox.ensure_can_send()
|
|
311
|
+
|
|
312
|
+
channel = top.get("channel") or DEFAULT_CHANNEL
|
|
313
|
+
resource_id = top.get("resource_id")
|
|
314
|
+
service_name = top.get("service_name") or producer.service_name
|
|
315
|
+
|
|
316
|
+
if producer.default_reply_to and not config.get("reply_to"):
|
|
317
|
+
config["reply_to"] = producer.default_reply_to
|
|
318
|
+
|
|
319
|
+
return await asyncio.to_thread(
|
|
320
|
+
lambda: self.service.messages.send_message(
|
|
321
|
+
address_id=inbox.address_id,
|
|
322
|
+
channel=channel,
|
|
323
|
+
config=config,
|
|
324
|
+
resource_id=resource_id,
|
|
325
|
+
service_name=service_name,
|
|
326
|
+
)
|
|
327
|
+
)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from ...connector import Connector
|
|
6
|
+
from ...transaction import Transaction
|
|
7
|
+
from .._client import create_ergon_client
|
|
8
|
+
from ..models import ErgonPlatformClient
|
|
9
|
+
from .models import (
|
|
10
|
+
DEFAULT_CHANNEL,
|
|
11
|
+
ErgonPlatformChannelsConfig,
|
|
12
|
+
ErgonPlatformChannelsConsumerConfig,
|
|
13
|
+
ErgonPlatformChannelsProducerConfig,
|
|
14
|
+
InboxAttachmentFile,
|
|
15
|
+
ResolvedInboxAddress,
|
|
16
|
+
SendMessageAttachment,
|
|
17
|
+
SendMessageInput,
|
|
18
|
+
SendMessagePayload,
|
|
19
|
+
)
|
|
20
|
+
from .services import ErgonPlatformChannelsService
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ErgonPlatformChannelsConnector(Connector):
|
|
26
|
+
"""Sync connector for one Ergon Platform channels inbox."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
client: ErgonPlatformClient,
|
|
31
|
+
channels_config: Optional[ErgonPlatformChannelsConfig] = None,
|
|
32
|
+
consumer_config: Optional[ErgonPlatformChannelsConsumerConfig] = None,
|
|
33
|
+
producer_config: Optional[ErgonPlatformChannelsProducerConfig] = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
if channels_config is not None:
|
|
36
|
+
consumer_config = consumer_config or channels_config.to_consumer_config()
|
|
37
|
+
producer_config = producer_config or channels_config.to_producer_config()
|
|
38
|
+
self._consumer_config = consumer_config
|
|
39
|
+
self._producer_config = producer_config or ErgonPlatformChannelsProducerConfig()
|
|
40
|
+
self.client = create_ergon_client(client)
|
|
41
|
+
self.service = ErgonPlatformChannelsService(
|
|
42
|
+
client,
|
|
43
|
+
self.client,
|
|
44
|
+
download_client=self._attachment_download_client(client),
|
|
45
|
+
)
|
|
46
|
+
self._seen_event_ids: set[str] = set()
|
|
47
|
+
|
|
48
|
+
def fetch_transactions(
|
|
49
|
+
self,
|
|
50
|
+
batch_size: Optional[int] = None,
|
|
51
|
+
*args,
|
|
52
|
+
**kwargs,
|
|
53
|
+
) -> List[Transaction]:
|
|
54
|
+
config = self._require_consumer_config("fetch transactions")
|
|
55
|
+
inbox = self._resolve_inbox(config)
|
|
56
|
+
inbox.ensure_can_receive()
|
|
57
|
+
limit = batch_size or config.batch_size
|
|
58
|
+
if kwargs:
|
|
59
|
+
raise TypeError(f"Unsupported inbox claim options: {', '.join(sorted(kwargs))}")
|
|
60
|
+
transactions = self.service.activity.claim_inbox_transactions(
|
|
61
|
+
config_id=inbox.config_id,
|
|
62
|
+
address_id=inbox.address_id,
|
|
63
|
+
config=config,
|
|
64
|
+
limit=limit,
|
|
65
|
+
seen_ids=self._seen_event_ids if config.deduplicate_fetched_events else None,
|
|
66
|
+
)
|
|
67
|
+
try:
|
|
68
|
+
return self._hydrate_fetched_transactions(config, inbox.config_id, transactions)
|
|
69
|
+
except Exception:
|
|
70
|
+
self.service.activity.release_claims(
|
|
71
|
+
inbox.config_id,
|
|
72
|
+
transactions,
|
|
73
|
+
delay_seconds=config.nack_delay_seconds,
|
|
74
|
+
)
|
|
75
|
+
for transaction in transactions:
|
|
76
|
+
if transaction.id:
|
|
77
|
+
self._seen_event_ids.discard(transaction.id)
|
|
78
|
+
raise
|
|
79
|
+
|
|
80
|
+
def _hydrate_fetched_transactions(
|
|
81
|
+
self,
|
|
82
|
+
config: ErgonPlatformChannelsConsumerConfig,
|
|
83
|
+
config_id: str,
|
|
84
|
+
transactions: List[Transaction],
|
|
85
|
+
) -> List[Transaction]:
|
|
86
|
+
if not config.download_attachments:
|
|
87
|
+
return transactions
|
|
88
|
+
return self.service.attachments.hydrate_inbox_transactions(
|
|
89
|
+
config_id,
|
|
90
|
+
transactions,
|
|
91
|
+
failure_policy=config.attachment_failure_policy,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def fetch_transaction_by_id(self, transaction_id: str, *args, **kwargs) -> Transaction:
|
|
95
|
+
config = self._require_consumer_config("fetch a transaction by id")
|
|
96
|
+
inbox = self._resolve_inbox(config)
|
|
97
|
+
inbox.ensure_can_receive()
|
|
98
|
+
transaction = self.service.activity.get_inbox_event(
|
|
99
|
+
inbox.config_id,
|
|
100
|
+
transaction_id,
|
|
101
|
+
**kwargs,
|
|
102
|
+
)
|
|
103
|
+
hydrated = self._hydrate_fetched_transactions(config, inbox.config_id, [transaction])
|
|
104
|
+
return hydrated[0]
|
|
105
|
+
|
|
106
|
+
def get_transactions_count(self, *args, **kwargs) -> int:
|
|
107
|
+
config = self._require_consumer_config("count transactions")
|
|
108
|
+
inbox = self._resolve_inbox(config)
|
|
109
|
+
inbox.ensure_can_receive()
|
|
110
|
+
return self.service.activity.get_inbox_events_count(
|
|
111
|
+
inbox.config_id,
|
|
112
|
+
address_id=inbox.address_id,
|
|
113
|
+
**kwargs,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def dispatch_transactions(self, transactions: List[Transaction], *args, **kwargs) -> List[str]:
|
|
117
|
+
sent_ids: List[str] = []
|
|
118
|
+
for transaction in transactions:
|
|
119
|
+
result = self._send_from_payload(transaction.payload)
|
|
120
|
+
sent_ids.append(self.service.messages.send_response_id(result))
|
|
121
|
+
return sent_ids
|
|
122
|
+
|
|
123
|
+
def send_message(self, payload: SendMessagePayload) -> Any:
|
|
124
|
+
return self._send_from_payload(payload)
|
|
125
|
+
|
|
126
|
+
def send_email(
|
|
127
|
+
self,
|
|
128
|
+
to: Union[str, List[str]],
|
|
129
|
+
subject: str,
|
|
130
|
+
text: Optional[str] = None,
|
|
131
|
+
html: Optional[str] = None,
|
|
132
|
+
cc: Optional[List[str]] = None,
|
|
133
|
+
bcc: Optional[List[str]] = None,
|
|
134
|
+
reply_to: Optional[str] = None,
|
|
135
|
+
in_reply_to: Optional[str] = None,
|
|
136
|
+
attachments: Optional[List[SendMessageAttachment]] = None,
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Send one email and return the platform log/message id."""
|
|
139
|
+
payload = SendMessageInput(
|
|
140
|
+
to=self.service.messages.normalize_recipients(to),
|
|
141
|
+
subject=subject,
|
|
142
|
+
text=text,
|
|
143
|
+
html=html,
|
|
144
|
+
cc=cc,
|
|
145
|
+
bcc=bcc,
|
|
146
|
+
reply_to=reply_to,
|
|
147
|
+
in_reply_to=in_reply_to,
|
|
148
|
+
attachments=attachments,
|
|
149
|
+
)
|
|
150
|
+
result = self._send_from_payload(payload)
|
|
151
|
+
return self.service.messages.send_response_id(result)
|
|
152
|
+
|
|
153
|
+
def list_thread_messages(self, thread_id: str, **params: Any) -> List[Transaction]:
|
|
154
|
+
"""Read messages of a specific thread (outside the normal consumer loop)."""
|
|
155
|
+
return self.service.messages.fetch_thread_messages(thread_id, **params)
|
|
156
|
+
|
|
157
|
+
def list_company_activity(self, **params: Any) -> List[Transaction]:
|
|
158
|
+
"""Read the company-wide channels activity feed (admin/reporting)."""
|
|
159
|
+
return self.service.activity.fetch_activity_events(**params)
|
|
160
|
+
|
|
161
|
+
def resolve_inbox(self) -> ResolvedInboxAddress:
|
|
162
|
+
"""Resolve ``address`` (+ optional ``config_id``) to platform routing metadata."""
|
|
163
|
+
config = self._require_consumer_config("resolve inbox")
|
|
164
|
+
return self._resolve_inbox(config)
|
|
165
|
+
|
|
166
|
+
def resolve_address_info(self, *, address: Optional[str] = None) -> Dict[str, Any]:
|
|
167
|
+
"""Return resolved inbox metadata including receive/send capabilities."""
|
|
168
|
+
config = self._require_consumer_config("resolve address info")
|
|
169
|
+
if address and address != config.address:
|
|
170
|
+
config = config.model_copy(update={"address": address})
|
|
171
|
+
return self._resolve_inbox(config).as_info_dict()
|
|
172
|
+
|
|
173
|
+
def ack_transaction(
|
|
174
|
+
self,
|
|
175
|
+
transaction: Transaction,
|
|
176
|
+
*,
|
|
177
|
+
allow_attachment_failures: bool = False,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Acknowledge a processed inbox event on the platform."""
|
|
180
|
+
config = self._require_consumer_config("ack a transaction")
|
|
181
|
+
inbox = self._resolve_inbox(config)
|
|
182
|
+
failures = (transaction.metadata or {}).get("attachment_failures")
|
|
183
|
+
if failures and not allow_attachment_failures:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"Transaction has failed attachment downloads; nack it or pass "
|
|
186
|
+
"allow_attachment_failures=True to acknowledge explicitly"
|
|
187
|
+
)
|
|
188
|
+
self.service.activity.ack_inbox_event(inbox.config_id, transaction)
|
|
189
|
+
if transaction.id:
|
|
190
|
+
self._seen_event_ids.add(transaction.id)
|
|
191
|
+
|
|
192
|
+
def nack_transaction(
|
|
193
|
+
self,
|
|
194
|
+
transaction: Transaction,
|
|
195
|
+
requeue: bool = True,
|
|
196
|
+
delay_seconds: Optional[int] = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Requeue or fail an inbox event on the platform."""
|
|
199
|
+
config = self._require_consumer_config("nack a transaction")
|
|
200
|
+
inbox = self._resolve_inbox(config)
|
|
201
|
+
delay = config.nack_delay_seconds if delay_seconds is None else delay_seconds
|
|
202
|
+
self.service.activity.nack_inbox_event(
|
|
203
|
+
inbox.config_id,
|
|
204
|
+
transaction,
|
|
205
|
+
requeue=requeue,
|
|
206
|
+
delay_seconds=delay,
|
|
207
|
+
)
|
|
208
|
+
if requeue and transaction.id:
|
|
209
|
+
self._seen_event_ids.discard(transaction.id)
|
|
210
|
+
|
|
211
|
+
def download_attachments(
|
|
212
|
+
self,
|
|
213
|
+
transaction: Transaction,
|
|
214
|
+
dest: Optional[Union[str, Path]] = None,
|
|
215
|
+
) -> List[InboxAttachmentFile]:
|
|
216
|
+
"""Download inbound attachments from a fetched inbox event.
|
|
217
|
+
|
|
218
|
+
Attachment ids and filenames come from the transaction payload. Pass
|
|
219
|
+
``dest`` to write ``{dest}/{event_id}/{filename}`` and fill ``path``.
|
|
220
|
+
"""
|
|
221
|
+
config = self._require_consumer_config("download attachments")
|
|
222
|
+
inbox = self._resolve_inbox(config)
|
|
223
|
+
inbox.ensure_can_receive()
|
|
224
|
+
return self.service.attachments.download_inbox_attachments(
|
|
225
|
+
inbox.config_id,
|
|
226
|
+
transaction,
|
|
227
|
+
dest=dest,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def close(self) -> None:
|
|
231
|
+
self.service.close()
|
|
232
|
+
self.client.close()
|
|
233
|
+
|
|
234
|
+
def _attachment_download_client(self, config: ErgonPlatformClient) -> Any:
|
|
235
|
+
timeout = 20.0
|
|
236
|
+
if self._consumer_config is not None:
|
|
237
|
+
timeout = self._consumer_config.attachment_download_timeout
|
|
238
|
+
download_config = config.model_copy(update={"timeout": timeout, "max_retries": 0})
|
|
239
|
+
return create_ergon_client(download_config)
|
|
240
|
+
|
|
241
|
+
def _require_consumer_config(self, action: str) -> ErgonPlatformChannelsConsumerConfig:
|
|
242
|
+
if self._consumer_config is None:
|
|
243
|
+
raise ValueError(f"ErgonPlatformChannelsConnector requires a consumer_config to {action}")
|
|
244
|
+
return self._consumer_config
|
|
245
|
+
|
|
246
|
+
def _resolve_inbox(self, config: ErgonPlatformChannelsConsumerConfig) -> ResolvedInboxAddress:
|
|
247
|
+
return self.service.addresses.resolve_consumer_inbox(config)
|
|
248
|
+
|
|
249
|
+
def _resolve_send_inbox(
|
|
250
|
+
self,
|
|
251
|
+
*,
|
|
252
|
+
address_id: Optional[str],
|
|
253
|
+
address: Optional[str],
|
|
254
|
+
) -> ResolvedInboxAddress:
|
|
255
|
+
return self.service.addresses.resolve_sender_inbox(
|
|
256
|
+
address=address,
|
|
257
|
+
address_id=address_id,
|
|
258
|
+
config_id=(
|
|
259
|
+
self._producer_config.config_id or (self._consumer_config.config_id if self._consumer_config else None)
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _default_inbox_address(self) -> Optional[str]:
|
|
264
|
+
if self._consumer_config and self._consumer_config.address:
|
|
265
|
+
return self._consumer_config.address
|
|
266
|
+
return self._producer_config.address
|
|
267
|
+
|
|
268
|
+
def _send_from_payload(self, payload: SendMessagePayload) -> Any:
|
|
269
|
+
parts = self.service.messages.normalize_send_payload(payload)
|
|
270
|
+
top = parts["top"]
|
|
271
|
+
config: Dict[str, Any] = parts["config"]
|
|
272
|
+
producer = self._producer_config
|
|
273
|
+
|
|
274
|
+
send_address = producer.address or self._default_inbox_address()
|
|
275
|
+
inbox = self._resolve_send_inbox(
|
|
276
|
+
address_id=top.get("address_id"),
|
|
277
|
+
address=send_address if top.get("address_id") is None else None,
|
|
278
|
+
)
|
|
279
|
+
inbox.ensure_can_send()
|
|
280
|
+
|
|
281
|
+
channel = top.get("channel") or DEFAULT_CHANNEL
|
|
282
|
+
resource_id = top.get("resource_id")
|
|
283
|
+
service_name = top.get("service_name") or producer.service_name
|
|
284
|
+
|
|
285
|
+
if producer.default_reply_to and not config.get("reply_to"):
|
|
286
|
+
config["reply_to"] = producer.default_reply_to
|
|
287
|
+
|
|
288
|
+
return self.service.messages.send_message(
|
|
289
|
+
address_id=inbox.address_id,
|
|
290
|
+
channel=channel,
|
|
291
|
+
config=config,
|
|
292
|
+
resource_id=resource_id,
|
|
293
|
+
service_name=service_name,
|
|
294
|
+
)
|