aamp-sdk 0.1.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.
- aamp_sdk-0.1.0/PKG-INFO +120 -0
- aamp_sdk-0.1.0/README.md +101 -0
- aamp_sdk-0.1.0/aamp_sdk/__init__.py +45 -0
- aamp_sdk-0.1.0/aamp_sdk/client.py +371 -0
- aamp_sdk-0.1.0/aamp_sdk/events.py +49 -0
- aamp_sdk-0.1.0/aamp_sdk/jmap_push.py +570 -0
- aamp_sdk-0.1.0/aamp_sdk/protocol.py +357 -0
- aamp_sdk-0.1.0/aamp_sdk/smtp.py +470 -0
- aamp_sdk-0.1.0/aamp_sdk.egg-info/PKG-INFO +120 -0
- aamp_sdk-0.1.0/aamp_sdk.egg-info/SOURCES.txt +16 -0
- aamp_sdk-0.1.0/aamp_sdk.egg-info/dependency_links.txt +1 -0
- aamp_sdk-0.1.0/aamp_sdk.egg-info/requires.txt +1 -0
- aamp_sdk-0.1.0/aamp_sdk.egg-info/top_level.txt +1 -0
- aamp_sdk-0.1.0/pyproject.toml +27 -0
- aamp_sdk-0.1.0/setup.cfg +4 -0
- aamp_sdk-0.1.0/tests/test_e2e.py +462 -0
- aamp_sdk-0.1.0/tests/test_protocol.py +57 -0
- aamp_sdk-0.1.0/tests/test_real_service.py +144 -0
aamp_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aamp-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Portable Python SDK for AAMP
|
|
5
|
+
Author: AAMP
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Communications :: Email
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: websocket-client>=1.8.0
|
|
19
|
+
|
|
20
|
+
# aamp-sdk
|
|
21
|
+
|
|
22
|
+
Python SDK for AAMP.
|
|
23
|
+
|
|
24
|
+
This SDK now includes the same core runtime shape as the Node.js SDK:
|
|
25
|
+
|
|
26
|
+
- AAMP discovery and mailbox registration
|
|
27
|
+
- directory query and profile updates
|
|
28
|
+
- realtime stream create / append / get / close
|
|
29
|
+
- AAMP header builders and parsers
|
|
30
|
+
- SMTP sending for `task.dispatch`, `task.result`, `task.cancel`, `task.help_needed`, `task.stream.opened`, and `card.*`
|
|
31
|
+
- JMAP WebSocket push reception with polling fallback
|
|
32
|
+
- attachment blob download
|
|
33
|
+
- recent mailbox reconciliation as a safety net
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install aamp-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from aamp_sdk import AampClient
|
|
45
|
+
|
|
46
|
+
client = AampClient.from_mailbox_identity(
|
|
47
|
+
email="agent@example.com",
|
|
48
|
+
smtp_password="<smtp-password>",
|
|
49
|
+
base_url="https://meshmail.ai",
|
|
50
|
+
reject_unauthorized=False,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def on_dispatch(task: dict) -> None:
|
|
54
|
+
client.send_result(
|
|
55
|
+
to=task["from"],
|
|
56
|
+
task_id=task["taskId"],
|
|
57
|
+
status="completed",
|
|
58
|
+
output="done",
|
|
59
|
+
in_reply_to=task["messageId"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
client.on("task.dispatch", on_dispatch)
|
|
63
|
+
client.connect()
|
|
64
|
+
|
|
65
|
+
task_id, message_id = client.send_task(
|
|
66
|
+
to="dispatcher@example.com",
|
|
67
|
+
title="Prepare a summary",
|
|
68
|
+
body_text="Summarize the latest rollout status.",
|
|
69
|
+
priority="high",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
stream = client.create_stream(task_id=task_id, peer_email="dispatcher@example.com")
|
|
73
|
+
client.send_stream_opened(
|
|
74
|
+
to="dispatcher@example.com",
|
|
75
|
+
task_id=task_id,
|
|
76
|
+
stream_id=stream["streamId"],
|
|
77
|
+
in_reply_to=message_id,
|
|
78
|
+
)
|
|
79
|
+
client.append_stream_event(
|
|
80
|
+
stream_id=stream["streamId"],
|
|
81
|
+
event_type="status",
|
|
82
|
+
payload={"stage": "running"},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
client.send_result(
|
|
86
|
+
to="dispatcher@example.com",
|
|
87
|
+
task_id=task_id,
|
|
88
|
+
status="completed",
|
|
89
|
+
output="done",
|
|
90
|
+
in_reply_to=message_id,
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Parse AAMP headers
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from aamp_sdk import parse_aamp_headers
|
|
98
|
+
|
|
99
|
+
message = parse_aamp_headers(
|
|
100
|
+
{
|
|
101
|
+
"from": "dispatcher@example.com",
|
|
102
|
+
"to": "agent@example.com",
|
|
103
|
+
"subject": "[AAMP Task] Review patch",
|
|
104
|
+
"messageId": "<msg-1@example.com>",
|
|
105
|
+
"bodyText": "Please review the patch.",
|
|
106
|
+
"headers": {
|
|
107
|
+
"X-AAMP-Intent": "task.dispatch",
|
|
108
|
+
"X-AAMP-TaskId": "task-123",
|
|
109
|
+
"X-AAMP-Priority": "high",
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Run tests
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cd packages/sdk-python
|
|
119
|
+
python -m unittest discover -s tests
|
|
120
|
+
```
|
aamp_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# aamp-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for AAMP.
|
|
4
|
+
|
|
5
|
+
This SDK now includes the same core runtime shape as the Node.js SDK:
|
|
6
|
+
|
|
7
|
+
- AAMP discovery and mailbox registration
|
|
8
|
+
- directory query and profile updates
|
|
9
|
+
- realtime stream create / append / get / close
|
|
10
|
+
- AAMP header builders and parsers
|
|
11
|
+
- SMTP sending for `task.dispatch`, `task.result`, `task.cancel`, `task.help_needed`, `task.stream.opened`, and `card.*`
|
|
12
|
+
- JMAP WebSocket push reception with polling fallback
|
|
13
|
+
- attachment blob download
|
|
14
|
+
- recent mailbox reconciliation as a safety net
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python -m pip install aamp-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from aamp_sdk import AampClient
|
|
26
|
+
|
|
27
|
+
client = AampClient.from_mailbox_identity(
|
|
28
|
+
email="agent@example.com",
|
|
29
|
+
smtp_password="<smtp-password>",
|
|
30
|
+
base_url="https://meshmail.ai",
|
|
31
|
+
reject_unauthorized=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def on_dispatch(task: dict) -> None:
|
|
35
|
+
client.send_result(
|
|
36
|
+
to=task["from"],
|
|
37
|
+
task_id=task["taskId"],
|
|
38
|
+
status="completed",
|
|
39
|
+
output="done",
|
|
40
|
+
in_reply_to=task["messageId"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
client.on("task.dispatch", on_dispatch)
|
|
44
|
+
client.connect()
|
|
45
|
+
|
|
46
|
+
task_id, message_id = client.send_task(
|
|
47
|
+
to="dispatcher@example.com",
|
|
48
|
+
title="Prepare a summary",
|
|
49
|
+
body_text="Summarize the latest rollout status.",
|
|
50
|
+
priority="high",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
stream = client.create_stream(task_id=task_id, peer_email="dispatcher@example.com")
|
|
54
|
+
client.send_stream_opened(
|
|
55
|
+
to="dispatcher@example.com",
|
|
56
|
+
task_id=task_id,
|
|
57
|
+
stream_id=stream["streamId"],
|
|
58
|
+
in_reply_to=message_id,
|
|
59
|
+
)
|
|
60
|
+
client.append_stream_event(
|
|
61
|
+
stream_id=stream["streamId"],
|
|
62
|
+
event_type="status",
|
|
63
|
+
payload={"stage": "running"},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
client.send_result(
|
|
67
|
+
to="dispatcher@example.com",
|
|
68
|
+
task_id=task_id,
|
|
69
|
+
status="completed",
|
|
70
|
+
output="done",
|
|
71
|
+
in_reply_to=message_id,
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Parse AAMP headers
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from aamp_sdk import parse_aamp_headers
|
|
79
|
+
|
|
80
|
+
message = parse_aamp_headers(
|
|
81
|
+
{
|
|
82
|
+
"from": "dispatcher@example.com",
|
|
83
|
+
"to": "agent@example.com",
|
|
84
|
+
"subject": "[AAMP Task] Review patch",
|
|
85
|
+
"messageId": "<msg-1@example.com>",
|
|
86
|
+
"bodyText": "Please review the patch.",
|
|
87
|
+
"headers": {
|
|
88
|
+
"X-AAMP-Intent": "task.dispatch",
|
|
89
|
+
"X-AAMP-TaskId": "task-123",
|
|
90
|
+
"X-AAMP-Priority": "high",
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Run tests
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd packages/sdk-python
|
|
100
|
+
python -m unittest discover -s tests
|
|
101
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Python SDK for portable AAMP integrations."""
|
|
2
|
+
|
|
3
|
+
from .client import AampClient
|
|
4
|
+
from .events import TinyEmitter
|
|
5
|
+
from .jmap_push import JmapPushClient
|
|
6
|
+
from .protocol import (
|
|
7
|
+
AAMP_HEADER,
|
|
8
|
+
AAMP_PROTOCOL_VERSION,
|
|
9
|
+
build_ack_headers,
|
|
10
|
+
build_cancel_headers,
|
|
11
|
+
build_card_query_headers,
|
|
12
|
+
build_card_response_headers,
|
|
13
|
+
build_dispatch_headers,
|
|
14
|
+
build_help_headers,
|
|
15
|
+
build_result_headers,
|
|
16
|
+
build_stream_opened_headers,
|
|
17
|
+
normalize_headers,
|
|
18
|
+
parse_aamp_headers,
|
|
19
|
+
parse_dispatch_context_header,
|
|
20
|
+
serialize_dispatch_context_header,
|
|
21
|
+
)
|
|
22
|
+
from .smtp import Attachment, SmtpSender, derive_mailbox_service_defaults
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AampClient",
|
|
26
|
+
"AAMP_HEADER",
|
|
27
|
+
"AAMP_PROTOCOL_VERSION",
|
|
28
|
+
"Attachment",
|
|
29
|
+
"JmapPushClient",
|
|
30
|
+
"SmtpSender",
|
|
31
|
+
"TinyEmitter",
|
|
32
|
+
"build_ack_headers",
|
|
33
|
+
"build_cancel_headers",
|
|
34
|
+
"build_card_query_headers",
|
|
35
|
+
"build_card_response_headers",
|
|
36
|
+
"build_dispatch_headers",
|
|
37
|
+
"build_help_headers",
|
|
38
|
+
"build_result_headers",
|
|
39
|
+
"build_stream_opened_headers",
|
|
40
|
+
"derive_mailbox_service_defaults",
|
|
41
|
+
"normalize_headers",
|
|
42
|
+
"parse_aamp_headers",
|
|
43
|
+
"parse_dispatch_context_header",
|
|
44
|
+
"serialize_dispatch_context_header",
|
|
45
|
+
]
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Portable Python client for AAMP service APIs and message sending."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import ssl
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import urlencode, urljoin
|
|
10
|
+
from urllib.request import Request, urlopen
|
|
11
|
+
|
|
12
|
+
from .events import TinyEmitter
|
|
13
|
+
from .jmap_push import JmapPushClient
|
|
14
|
+
from .smtp import SmtpSender, derive_mailbox_service_defaults
|
|
15
|
+
|
|
16
|
+
DEFAULT_HTTP_TIMEOUT_SECS = 30
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ssl_context(reject_unauthorized: bool) -> ssl.SSLContext:
|
|
20
|
+
return ssl.create_default_context() if reject_unauthorized else ssl._create_unverified_context()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AampClient(TinyEmitter):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
email: str,
|
|
28
|
+
mailbox_token: str,
|
|
29
|
+
base_url: str,
|
|
30
|
+
smtp_password: str,
|
|
31
|
+
http_send_base_url: str | None = None,
|
|
32
|
+
smtp_host: str | None = None,
|
|
33
|
+
smtp_port: int = 587,
|
|
34
|
+
reconnect_interval: float = 5.0,
|
|
35
|
+
reject_unauthorized: bool = True,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.email = email
|
|
39
|
+
self.mailbox_token = mailbox_token
|
|
40
|
+
self.base_url = base_url
|
|
41
|
+
self.reject_unauthorized = reject_unauthorized
|
|
42
|
+
|
|
43
|
+
derived = derive_mailbox_service_defaults(email, base_url)
|
|
44
|
+
self.smtp_sender = SmtpSender(
|
|
45
|
+
host=smtp_host or str(derived["smtp_host"]),
|
|
46
|
+
port=smtp_port,
|
|
47
|
+
user=email,
|
|
48
|
+
password=smtp_password,
|
|
49
|
+
http_base_url=http_send_base_url or base_url,
|
|
50
|
+
auth_token=mailbox_token,
|
|
51
|
+
reject_unauthorized=reject_unauthorized,
|
|
52
|
+
)
|
|
53
|
+
decoded = base64.b64decode(mailbox_token.encode("ascii")).decode("utf-8")
|
|
54
|
+
_mailbox_email, _sep, password = decoded.partition(":")
|
|
55
|
+
if not _sep or not password:
|
|
56
|
+
raise RuntimeError("Invalid mailboxToken format: expected base64(email:password)")
|
|
57
|
+
self.jmap_client = JmapPushClient(
|
|
58
|
+
email=email,
|
|
59
|
+
password=password,
|
|
60
|
+
jmap_url=base_url,
|
|
61
|
+
reconnect_interval=reconnect_interval,
|
|
62
|
+
reject_unauthorized=reject_unauthorized,
|
|
63
|
+
)
|
|
64
|
+
for event_name in [
|
|
65
|
+
"task.dispatch",
|
|
66
|
+
"task.cancel",
|
|
67
|
+
"task.result",
|
|
68
|
+
"task.help_needed",
|
|
69
|
+
"task.ack",
|
|
70
|
+
"task.stream.opened",
|
|
71
|
+
"card.query",
|
|
72
|
+
"card.response",
|
|
73
|
+
"reply",
|
|
74
|
+
"connected",
|
|
75
|
+
"disconnected",
|
|
76
|
+
"error",
|
|
77
|
+
]:
|
|
78
|
+
self.jmap_client.on(event_name, self._forward_event(event_name))
|
|
79
|
+
self.jmap_client.on("_autoAck", self._handle_auto_ack)
|
|
80
|
+
|
|
81
|
+
def _forward_event(self, event_name: str) -> Any:
|
|
82
|
+
def handler(*args: Any) -> None:
|
|
83
|
+
self.emit(event_name, *args)
|
|
84
|
+
|
|
85
|
+
return handler
|
|
86
|
+
|
|
87
|
+
def _handle_auto_ack(self, payload: dict[str, Any]) -> None:
|
|
88
|
+
try:
|
|
89
|
+
self.smtp_sender.send_ack(
|
|
90
|
+
to=str(payload["to"]),
|
|
91
|
+
task_id=str(payload["taskId"]),
|
|
92
|
+
in_reply_to=str(payload["messageId"]),
|
|
93
|
+
)
|
|
94
|
+
except Exception as err:
|
|
95
|
+
self.emit("error", RuntimeError(f"[AAMP] Failed to send ACK for task {payload.get('taskId')}: {err}"))
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_mailbox_identity(
|
|
99
|
+
cls,
|
|
100
|
+
*,
|
|
101
|
+
email: str,
|
|
102
|
+
smtp_password: str,
|
|
103
|
+
base_url: str | None = None,
|
|
104
|
+
smtp_port: int = 587,
|
|
105
|
+
reconnect_interval: float = 5.0,
|
|
106
|
+
reject_unauthorized: bool = True,
|
|
107
|
+
) -> "AampClient":
|
|
108
|
+
derived = derive_mailbox_service_defaults(email, base_url)
|
|
109
|
+
token = base64.b64encode(f"{email}:{smtp_password}".encode("utf-8")).decode("ascii")
|
|
110
|
+
return cls(
|
|
111
|
+
email=email,
|
|
112
|
+
mailbox_token=token,
|
|
113
|
+
base_url=str(derived["http_base_url"] or f"https://{email.split('@', 1)[1]}"),
|
|
114
|
+
smtp_password=smtp_password,
|
|
115
|
+
smtp_host=str(derived["smtp_host"]),
|
|
116
|
+
smtp_port=smtp_port,
|
|
117
|
+
reconnect_interval=reconnect_interval,
|
|
118
|
+
reject_unauthorized=reject_unauthorized,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _request_json(
|
|
123
|
+
url: str,
|
|
124
|
+
*,
|
|
125
|
+
method: str = "GET",
|
|
126
|
+
body: Any | None = None,
|
|
127
|
+
headers: dict[str, str] | None = None,
|
|
128
|
+
reject_unauthorized: bool = True,
|
|
129
|
+
) -> Any:
|
|
130
|
+
data = None
|
|
131
|
+
request_headers = {"Accept": "application/json", **(headers or {})}
|
|
132
|
+
if body is not None:
|
|
133
|
+
request_headers["Content-Type"] = "application/json"
|
|
134
|
+
data = json.dumps(body).encode("utf-8")
|
|
135
|
+
request = Request(url, method=method, data=data, headers=request_headers)
|
|
136
|
+
with urlopen(
|
|
137
|
+
request,
|
|
138
|
+
context=_ssl_context(reject_unauthorized),
|
|
139
|
+
timeout=DEFAULT_HTTP_TIMEOUT_SECS,
|
|
140
|
+
) as response:
|
|
141
|
+
return json.loads(response.read().decode("utf-8"))
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def discover_aamp_service(cls, aamp_host: str, *, reject_unauthorized: bool = True) -> dict[str, Any]:
|
|
145
|
+
base = aamp_host.rstrip("/")
|
|
146
|
+
discovery = cls._request_json(
|
|
147
|
+
f"{base}/.well-known/aamp",
|
|
148
|
+
reject_unauthorized=reject_unauthorized,
|
|
149
|
+
)
|
|
150
|
+
if not discovery.get("api", {}).get("url"):
|
|
151
|
+
raise RuntimeError("AAMP discovery did not return api.url")
|
|
152
|
+
return discovery
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def _call_discovered_api(
|
|
156
|
+
cls,
|
|
157
|
+
base: str,
|
|
158
|
+
*,
|
|
159
|
+
action: str,
|
|
160
|
+
method: str = "GET",
|
|
161
|
+
query: dict[str, Any] | None = None,
|
|
162
|
+
body: Any | None = None,
|
|
163
|
+
auth_token: str | None = None,
|
|
164
|
+
reject_unauthorized: bool = True,
|
|
165
|
+
) -> Any:
|
|
166
|
+
discovery = cls.discover_aamp_service(base, reject_unauthorized=reject_unauthorized)
|
|
167
|
+
api_url = urljoin(f"{base.rstrip('/')}/", discovery["api"]["url"])
|
|
168
|
+
params = {"action": action}
|
|
169
|
+
for key, value in (query or {}).items():
|
|
170
|
+
if value is not None:
|
|
171
|
+
params[key] = str(value).lower() if isinstance(value, bool) else str(value)
|
|
172
|
+
url = f"{api_url}?{urlencode(params)}"
|
|
173
|
+
headers = {"Authorization": f"Basic {auth_token}"} if auth_token else None
|
|
174
|
+
return cls._request_json(
|
|
175
|
+
url,
|
|
176
|
+
method=method,
|
|
177
|
+
body=body,
|
|
178
|
+
headers=headers,
|
|
179
|
+
reject_unauthorized=reject_unauthorized,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def register_mailbox(
|
|
184
|
+
cls,
|
|
185
|
+
*,
|
|
186
|
+
aamp_host: str,
|
|
187
|
+
slug: str,
|
|
188
|
+
description: str | None = None,
|
|
189
|
+
reject_unauthorized: bool = True,
|
|
190
|
+
) -> dict[str, str]:
|
|
191
|
+
base = aamp_host.rstrip("/")
|
|
192
|
+
registration = cls._call_discovered_api(
|
|
193
|
+
base,
|
|
194
|
+
action="aamp.mailbox.register",
|
|
195
|
+
method="POST",
|
|
196
|
+
body={"slug": slug, "description": description},
|
|
197
|
+
reject_unauthorized=reject_unauthorized,
|
|
198
|
+
)
|
|
199
|
+
code = registration.get("registrationCode")
|
|
200
|
+
if not code:
|
|
201
|
+
raise RuntimeError("Mailbox registration succeeded but no registrationCode was returned")
|
|
202
|
+
|
|
203
|
+
credentials = cls._call_discovered_api(
|
|
204
|
+
base,
|
|
205
|
+
action="aamp.mailbox.credentials",
|
|
206
|
+
query={"code": code},
|
|
207
|
+
reject_unauthorized=reject_unauthorized,
|
|
208
|
+
)
|
|
209
|
+
email = credentials.get("email")
|
|
210
|
+
mailbox_token = credentials.get("mailbox", {}).get("token")
|
|
211
|
+
smtp_password = credentials.get("smtp", {}).get("password")
|
|
212
|
+
if not email or not mailbox_token or not smtp_password:
|
|
213
|
+
raise RuntimeError("Mailbox credential exchange returned an incomplete identity payload")
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"email": email,
|
|
217
|
+
"mailboxToken": mailbox_token,
|
|
218
|
+
"smtpPassword": smtp_password,
|
|
219
|
+
"baseUrl": base,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def send_task(self, **kwargs: Any) -> tuple[str, str]:
|
|
223
|
+
return self.smtp_sender.send_task(**kwargs)
|
|
224
|
+
|
|
225
|
+
def connect(self) -> None:
|
|
226
|
+
self.jmap_client.start()
|
|
227
|
+
|
|
228
|
+
def disconnect(self) -> None:
|
|
229
|
+
self.jmap_client.stop()
|
|
230
|
+
|
|
231
|
+
def is_connected(self) -> bool:
|
|
232
|
+
return self.jmap_client.is_connected()
|
|
233
|
+
|
|
234
|
+
def is_using_polling_fallback(self) -> bool:
|
|
235
|
+
return self.jmap_client.is_using_polling_fallback()
|
|
236
|
+
|
|
237
|
+
def send_result(self, **kwargs: Any) -> None:
|
|
238
|
+
self.smtp_sender.send_result(**kwargs)
|
|
239
|
+
|
|
240
|
+
def send_help(self, **kwargs: Any) -> None:
|
|
241
|
+
self.smtp_sender.send_help(**kwargs)
|
|
242
|
+
|
|
243
|
+
def send_cancel(self, **kwargs: Any) -> None:
|
|
244
|
+
self.smtp_sender.send_cancel(**kwargs)
|
|
245
|
+
|
|
246
|
+
def send_stream_opened(self, **kwargs: Any) -> None:
|
|
247
|
+
self.smtp_sender.send_stream_opened(**kwargs)
|
|
248
|
+
|
|
249
|
+
def send_card_query(self, **kwargs: Any) -> tuple[str, str]:
|
|
250
|
+
return self.smtp_sender.send_card_query(**kwargs)
|
|
251
|
+
|
|
252
|
+
def send_card_response(self, **kwargs: Any) -> None:
|
|
253
|
+
self.smtp_sender.send_card_response(**kwargs)
|
|
254
|
+
|
|
255
|
+
def download_blob(self, blob_id: str, filename: str | None = None) -> bytes:
|
|
256
|
+
return self.jmap_client.download_blob(blob_id, filename)
|
|
257
|
+
|
|
258
|
+
def reconcile_recent_emails(self, limit: int = 20, *, include_historical: bool = False) -> int:
|
|
259
|
+
return self.jmap_client.reconcile_recent_emails(limit, include_historical=include_historical)
|
|
260
|
+
|
|
261
|
+
def update_directory_profile(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
summary: str | None = None,
|
|
265
|
+
card_text: str | None = None,
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
response = self._call_discovered_api(
|
|
268
|
+
self.base_url,
|
|
269
|
+
action="aamp.directory.upsert",
|
|
270
|
+
method="POST",
|
|
271
|
+
auth_token=self.mailbox_token,
|
|
272
|
+
body={"summary": summary, "cardText": card_text},
|
|
273
|
+
reject_unauthorized=self.reject_unauthorized,
|
|
274
|
+
)
|
|
275
|
+
return dict(response.get("profile", {}))
|
|
276
|
+
|
|
277
|
+
def list_directory(
|
|
278
|
+
self,
|
|
279
|
+
*,
|
|
280
|
+
scope: str | None = None,
|
|
281
|
+
include_self: bool | None = None,
|
|
282
|
+
limit: int | None = None,
|
|
283
|
+
) -> list[dict[str, Any]]:
|
|
284
|
+
response = self._call_discovered_api(
|
|
285
|
+
self.base_url,
|
|
286
|
+
action="aamp.directory.list",
|
|
287
|
+
auth_token=self.mailbox_token,
|
|
288
|
+
query={"scope": scope, "includeSelf": include_self, "limit": limit},
|
|
289
|
+
reject_unauthorized=self.reject_unauthorized,
|
|
290
|
+
)
|
|
291
|
+
return list(response.get("agents", []))
|
|
292
|
+
|
|
293
|
+
def search_directory(
|
|
294
|
+
self,
|
|
295
|
+
*,
|
|
296
|
+
query: str,
|
|
297
|
+
scope: str | None = None,
|
|
298
|
+
include_self: bool | None = None,
|
|
299
|
+
limit: int | None = None,
|
|
300
|
+
) -> list[dict[str, Any]]:
|
|
301
|
+
response = self._call_discovered_api(
|
|
302
|
+
self.base_url,
|
|
303
|
+
action="aamp.directory.search",
|
|
304
|
+
auth_token=self.mailbox_token,
|
|
305
|
+
query={
|
|
306
|
+
"q": query,
|
|
307
|
+
"scope": scope,
|
|
308
|
+
"includeSelf": include_self,
|
|
309
|
+
"limit": limit,
|
|
310
|
+
},
|
|
311
|
+
reject_unauthorized=self.reject_unauthorized,
|
|
312
|
+
)
|
|
313
|
+
return list(response.get("agents", []))
|
|
314
|
+
|
|
315
|
+
def _resolve_stream_capability(self) -> dict[str, Any]:
|
|
316
|
+
discovery = self.discover_aamp_service(
|
|
317
|
+
self.base_url,
|
|
318
|
+
reject_unauthorized=self.reject_unauthorized,
|
|
319
|
+
)
|
|
320
|
+
stream = discovery.get("capabilities", {}).get("stream")
|
|
321
|
+
if not stream or not stream.get("transport"):
|
|
322
|
+
raise RuntimeError("AAMP stream capability is not available on this service")
|
|
323
|
+
return dict(stream)
|
|
324
|
+
|
|
325
|
+
def create_stream(self, *, task_id: str, peer_email: str) -> dict[str, Any]:
|
|
326
|
+
stream = self._resolve_stream_capability()
|
|
327
|
+
return self._call_discovered_api(
|
|
328
|
+
self.base_url,
|
|
329
|
+
action=stream.get("createAction", "aamp.stream.create"),
|
|
330
|
+
method="POST",
|
|
331
|
+
auth_token=self.mailbox_token,
|
|
332
|
+
body={"taskId": task_id, "peerEmail": peer_email},
|
|
333
|
+
reject_unauthorized=self.reject_unauthorized,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def append_stream_event(self, *, stream_id: str, event_type: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
337
|
+
stream = self._resolve_stream_capability()
|
|
338
|
+
return self._call_discovered_api(
|
|
339
|
+
self.base_url,
|
|
340
|
+
action=stream.get("appendAction", "aamp.stream.append"),
|
|
341
|
+
method="POST",
|
|
342
|
+
auth_token=self.mailbox_token,
|
|
343
|
+
body={"streamId": stream_id, "type": event_type, "payload": payload},
|
|
344
|
+
reject_unauthorized=self.reject_unauthorized,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def close_stream(self, *, stream_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
348
|
+
stream = self._resolve_stream_capability()
|
|
349
|
+
return self._call_discovered_api(
|
|
350
|
+
self.base_url,
|
|
351
|
+
action=stream.get("closeAction", "aamp.stream.close"),
|
|
352
|
+
method="POST",
|
|
353
|
+
auth_token=self.mailbox_token,
|
|
354
|
+
body={"streamId": stream_id, "payload": payload or {}},
|
|
355
|
+
reject_unauthorized=self.reject_unauthorized,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def get_task_stream(
|
|
359
|
+
self,
|
|
360
|
+
*,
|
|
361
|
+
task_id: str | None = None,
|
|
362
|
+
stream_id: str | None = None,
|
|
363
|
+
) -> dict[str, Any]:
|
|
364
|
+
stream = self._resolve_stream_capability()
|
|
365
|
+
return self._call_discovered_api(
|
|
366
|
+
self.base_url,
|
|
367
|
+
action=stream.get("getAction", "aamp.stream.get"),
|
|
368
|
+
auth_token=self.mailbox_token,
|
|
369
|
+
query={"taskId": task_id, "streamId": stream_id},
|
|
370
|
+
reject_unauthorized=self.reject_unauthorized,
|
|
371
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""A tiny event emitter used by the Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Listener = Callable[..., Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TinyEmitter:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._listeners: dict[str, list[Listener]] = defaultdict(list)
|
|
16
|
+
self._once_wrappers: dict[tuple[str, Listener], Listener] = {}
|
|
17
|
+
self._lock = threading.RLock()
|
|
18
|
+
|
|
19
|
+
def on(self, event: str, listener: Listener) -> "TinyEmitter":
|
|
20
|
+
with self._lock:
|
|
21
|
+
self._listeners[event].append(listener)
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def once(self, event: str, listener: Listener) -> "TinyEmitter":
|
|
25
|
+
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
26
|
+
self.off(event, listener)
|
|
27
|
+
return listener(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
with self._lock:
|
|
30
|
+
self._once_wrappers[(event, listener)] = wrapped
|
|
31
|
+
self._listeners[event].append(wrapped)
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def off(self, event: str, listener: Listener) -> "TinyEmitter":
|
|
35
|
+
with self._lock:
|
|
36
|
+
wrapped = self._once_wrappers.pop((event, listener), None)
|
|
37
|
+
target = wrapped or listener
|
|
38
|
+
bucket = self._listeners.get(event, [])
|
|
39
|
+
self._listeners[event] = [item for item in bucket if item is not target]
|
|
40
|
+
if not self._listeners[event]:
|
|
41
|
+
self._listeners.pop(event, None)
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def emit(self, event: str, *args: Any, **kwargs: Any) -> bool:
|
|
45
|
+
with self._lock:
|
|
46
|
+
listeners = list(self._listeners.get(event, []))
|
|
47
|
+
for listener in listeners:
|
|
48
|
+
listener(*args, **kwargs)
|
|
49
|
+
return bool(listeners)
|