marona 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.
marona-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blessing Nyuwani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
marona-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.4
2
+ Name: marona
3
+ Version: 0.1.0
4
+ Summary: Official Python client SDK for Marona-compatible AI runtimes.
5
+ Author: Blessing Nyuwani
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://www.marona.ai
8
+ Project-URL: Repository, https://github.com/BlessingNyuwani/edge-node-service
9
+ Project-URL: Documentation, https://hub.marona.ai
10
+ Keywords: marona,sdk,mcp,agents,ai,runtime
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: httpx<1.0,>=0.28
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
26
+ Requires-Dist: build<2.0,>=1.2; extra == "dev"
27
+ Requires-Dist: twine<7.0,>=6.0; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # marona
31
+
32
+ Official Python client SDK for Marona-compatible AI runtimes.
33
+
34
+ Marona provides a simple interface for messaging, discovering apps, managing
35
+ permissions, connecting service accounts, receiving inbound events, and using
36
+ tools from any compatible runtime.
37
+
38
+ You can use it with Marona Cloud or with your own self-hosted Marona-compatible
39
+ runtime.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install marona
45
+ ```
46
+
47
+ ## Send a message
48
+
49
+ ```python
50
+ import asyncio
51
+
52
+ from marona import Marona
53
+
54
+
55
+ async def main() -> None:
56
+ client = Marona(
57
+ base_url="https://edge.marona.ai",
58
+ api_key="mrn_live_...",
59
+ )
60
+ response = await client.message(
61
+ "Create a PowerPoint about AI introduction",
62
+ interface="api",
63
+ conversation_id="demo",
64
+ )
65
+ print(response.reply)
66
+ for artifact in response.artifacts:
67
+ print(artifact.get("filename"))
68
+
69
+
70
+ asyncio.run(main())
71
+ ```
72
+
73
+ ## Runtime targets
74
+
75
+ Marona Cloud:
76
+
77
+ ```python
78
+ from marona import Marona
79
+
80
+ client = Marona(
81
+ base_url="https://edge.marona.ai",
82
+ api_key="mrn_live_...",
83
+ )
84
+ ```
85
+
86
+ Self-hosted:
87
+
88
+ ```python
89
+ from marona import Marona
90
+
91
+ client = Marona(
92
+ base_url="https://my-company-edge.example.com",
93
+ api_key="mrn_live_...",
94
+ )
95
+ ```
96
+
97
+ ## List available apps
98
+
99
+ ```python
100
+ import asyncio
101
+
102
+ from marona import Marona
103
+
104
+
105
+ async def main() -> None:
106
+ client = Marona("https://edge.marona.ai")
107
+ apps = await client.list_apps(limit=20, search="slides")
108
+
109
+ for app in apps.apps:
110
+ print(app["slug"], app["name"])
111
+
112
+
113
+ asyncio.run(main())
114
+ ```
115
+
116
+ ## Pair an interface with WhatsApp
117
+
118
+ ```python
119
+ import asyncio
120
+
121
+ from marona import Marona
122
+
123
+
124
+ async def main() -> None:
125
+ client = Marona("https://edge.marona.ai")
126
+ pairing = await client.start_pairing(interface="web", device_name="My web app")
127
+
128
+ print(pairing.display_code)
129
+ print(pairing.whatsapp_url)
130
+
131
+
132
+ asyncio.run(main())
133
+ ```
134
+
135
+ After the user sends the pairing code from WhatsApp, poll for completion:
136
+
137
+ ```python
138
+ status = await client.pairing_status(pairing.pairing_id)
139
+
140
+ if status.identity:
141
+ identity_token = status.identity.access_token
142
+ ```
143
+
144
+ Use the identity token on future requests:
145
+
146
+ ```python
147
+ response = await client.message(
148
+ "What is on my calendar today?",
149
+ identity_token=identity_token,
150
+ )
151
+ ```
152
+
153
+ ## Connect an app account
154
+
155
+ Some apps need a user service connection, such as Gmail, Google Calendar, or
156
+ Google Slides.
157
+
158
+ ```python
159
+ import asyncio
160
+
161
+ from marona import Marona
162
+
163
+
164
+ async def main() -> None:
165
+ client = Marona("https://edge.marona.ai")
166
+ identity_token = "mrn_identity_token_from_pairing"
167
+ connection = await client.connect_app(
168
+ "slides",
169
+ provider="google",
170
+ identity_token=identity_token,
171
+ return_url="https://example.com/connected",
172
+ )
173
+
174
+ print(connection.authorization_url)
175
+
176
+
177
+ asyncio.run(main())
178
+ ```
179
+
180
+ Open `authorization_url` in a browser so the user can authorize the provider.
181
+
182
+ ## Attach files
183
+
184
+ ```python
185
+ import asyncio
186
+
187
+ from marona import Marona, RuntimeAttachment
188
+
189
+
190
+ async def main() -> None:
191
+ identity_token = "mrn_identity_token_from_pairing"
192
+ attachment = RuntimeAttachment.from_bytes(
193
+ b"hello",
194
+ filename="note.txt",
195
+ mime_type="text/plain",
196
+ kind="document",
197
+ )
198
+
199
+ client = Marona("https://edge.marona.ai")
200
+ response = await client.message(
201
+ "Summarize this file",
202
+ attachments=[attachment],
203
+ identity_token=identity_token,
204
+ )
205
+ print(response.reply)
206
+
207
+
208
+ asyncio.run(main())
209
+ ```
210
+
211
+ ## Advanced
212
+
213
+ `EdgeRuntimeClient` is kept as a compatibility class for existing integrations.
214
+ New integrations should import `Marona`.
marona-0.1.0/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # marona
2
+
3
+ Official Python client SDK for Marona-compatible AI runtimes.
4
+
5
+ Marona provides a simple interface for messaging, discovering apps, managing
6
+ permissions, connecting service accounts, receiving inbound events, and using
7
+ tools from any compatible runtime.
8
+
9
+ You can use it with Marona Cloud or with your own self-hosted Marona-compatible
10
+ runtime.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install marona
16
+ ```
17
+
18
+ ## Send a message
19
+
20
+ ```python
21
+ import asyncio
22
+
23
+ from marona import Marona
24
+
25
+
26
+ async def main() -> None:
27
+ client = Marona(
28
+ base_url="https://edge.marona.ai",
29
+ api_key="mrn_live_...",
30
+ )
31
+ response = await client.message(
32
+ "Create a PowerPoint about AI introduction",
33
+ interface="api",
34
+ conversation_id="demo",
35
+ )
36
+ print(response.reply)
37
+ for artifact in response.artifacts:
38
+ print(artifact.get("filename"))
39
+
40
+
41
+ asyncio.run(main())
42
+ ```
43
+
44
+ ## Runtime targets
45
+
46
+ Marona Cloud:
47
+
48
+ ```python
49
+ from marona import Marona
50
+
51
+ client = Marona(
52
+ base_url="https://edge.marona.ai",
53
+ api_key="mrn_live_...",
54
+ )
55
+ ```
56
+
57
+ Self-hosted:
58
+
59
+ ```python
60
+ from marona import Marona
61
+
62
+ client = Marona(
63
+ base_url="https://my-company-edge.example.com",
64
+ api_key="mrn_live_...",
65
+ )
66
+ ```
67
+
68
+ ## List available apps
69
+
70
+ ```python
71
+ import asyncio
72
+
73
+ from marona import Marona
74
+
75
+
76
+ async def main() -> None:
77
+ client = Marona("https://edge.marona.ai")
78
+ apps = await client.list_apps(limit=20, search="slides")
79
+
80
+ for app in apps.apps:
81
+ print(app["slug"], app["name"])
82
+
83
+
84
+ asyncio.run(main())
85
+ ```
86
+
87
+ ## Pair an interface with WhatsApp
88
+
89
+ ```python
90
+ import asyncio
91
+
92
+ from marona import Marona
93
+
94
+
95
+ async def main() -> None:
96
+ client = Marona("https://edge.marona.ai")
97
+ pairing = await client.start_pairing(interface="web", device_name="My web app")
98
+
99
+ print(pairing.display_code)
100
+ print(pairing.whatsapp_url)
101
+
102
+
103
+ asyncio.run(main())
104
+ ```
105
+
106
+ After the user sends the pairing code from WhatsApp, poll for completion:
107
+
108
+ ```python
109
+ status = await client.pairing_status(pairing.pairing_id)
110
+
111
+ if status.identity:
112
+ identity_token = status.identity.access_token
113
+ ```
114
+
115
+ Use the identity token on future requests:
116
+
117
+ ```python
118
+ response = await client.message(
119
+ "What is on my calendar today?",
120
+ identity_token=identity_token,
121
+ )
122
+ ```
123
+
124
+ ## Connect an app account
125
+
126
+ Some apps need a user service connection, such as Gmail, Google Calendar, or
127
+ Google Slides.
128
+
129
+ ```python
130
+ import asyncio
131
+
132
+ from marona import Marona
133
+
134
+
135
+ async def main() -> None:
136
+ client = Marona("https://edge.marona.ai")
137
+ identity_token = "mrn_identity_token_from_pairing"
138
+ connection = await client.connect_app(
139
+ "slides",
140
+ provider="google",
141
+ identity_token=identity_token,
142
+ return_url="https://example.com/connected",
143
+ )
144
+
145
+ print(connection.authorization_url)
146
+
147
+
148
+ asyncio.run(main())
149
+ ```
150
+
151
+ Open `authorization_url` in a browser so the user can authorize the provider.
152
+
153
+ ## Attach files
154
+
155
+ ```python
156
+ import asyncio
157
+
158
+ from marona import Marona, RuntimeAttachment
159
+
160
+
161
+ async def main() -> None:
162
+ identity_token = "mrn_identity_token_from_pairing"
163
+ attachment = RuntimeAttachment.from_bytes(
164
+ b"hello",
165
+ filename="note.txt",
166
+ mime_type="text/plain",
167
+ kind="document",
168
+ )
169
+
170
+ client = Marona("https://edge.marona.ai")
171
+ response = await client.message(
172
+ "Summarize this file",
173
+ attachments=[attachment],
174
+ identity_token=identity_token,
175
+ )
176
+ print(response.reply)
177
+
178
+
179
+ asyncio.run(main())
180
+ ```
181
+
182
+ ## Advanced
183
+
184
+ `EdgeRuntimeClient` is kept as a compatibility class for existing integrations.
185
+ New integrations should import `Marona`.
@@ -0,0 +1,39 @@
1
+ from .business import MaronaBusinessApiError, MaronaBusinessClient
2
+ from .client import EdgeRuntimeClient, EdgeRuntimeError, Marona
3
+ from .models import (
4
+ IdentityAuthenticated,
5
+ IdentityPairingStarted,
6
+ IdentityPairingStatus,
7
+ RealtimeSessionCreated,
8
+ RealtimeSessionRequest,
9
+ RuntimeApp,
10
+ RuntimeAppsPage,
11
+ RuntimeAttachment,
12
+ RuntimeInboundEvent,
13
+ RuntimeMessageRequest,
14
+ RuntimeMessageResponse,
15
+ ServiceConnectionAction,
16
+ )
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ __all__ = [
21
+ "EdgeRuntimeClient",
22
+ "EdgeRuntimeError",
23
+ "Marona",
24
+ "__version__",
25
+ "MaronaBusinessApiError",
26
+ "MaronaBusinessClient",
27
+ "IdentityAuthenticated",
28
+ "IdentityPairingStarted",
29
+ "IdentityPairingStatus",
30
+ "RealtimeSessionCreated",
31
+ "RealtimeSessionRequest",
32
+ "RuntimeApp",
33
+ "RuntimeAppsPage",
34
+ "RuntimeAttachment",
35
+ "RuntimeInboundEvent",
36
+ "RuntimeMessageRequest",
37
+ "RuntimeMessageResponse",
38
+ "ServiceConnectionAction",
39
+ ]
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+
8
+ class MaronaBusinessApiError(RuntimeError):
9
+ pass
10
+
11
+
12
+ class MaronaBusinessClient:
13
+ def __init__(
14
+ self,
15
+ base_url: str,
16
+ *,
17
+ token: str = "",
18
+ app_id: str = "",
19
+ timeout: float = 30,
20
+ client: httpx.AsyncClient | None = None,
21
+ ) -> None:
22
+ self.base_url = base_url.rstrip("/")
23
+ self.token = token
24
+ self.app_id = app_id
25
+ self.timeout = timeout
26
+ self._client = client
27
+
28
+ async def upload_media(
29
+ self,
30
+ *,
31
+ media_type: str,
32
+ file_name: str,
33
+ mime_type: str,
34
+ data: bytes,
35
+ purpose: str = "generated_reply",
36
+ ) -> dict[str, Any]:
37
+ self._require_token()
38
+ files = {"file": (file_name, data, mime_type)}
39
+ form = {
40
+ "type": media_type,
41
+ "purpose": purpose,
42
+ **({"app_id": self.app_id} if self.app_id else {}),
43
+ }
44
+ response = await self._request(
45
+ "POST",
46
+ "/media",
47
+ timeout=60,
48
+ data=form,
49
+ files=files,
50
+ )
51
+ payload = response.json()
52
+ media = payload.get("media") if isinstance(payload, dict) else None
53
+ if not isinstance(media, dict) or not media.get("media_id"):
54
+ raise MaronaBusinessApiError("Marona Business API media upload did not return media_id.")
55
+ return self._normalize_media_response_url(media)
56
+
57
+ async def send_reply(
58
+ self,
59
+ conversation_id: str,
60
+ *,
61
+ message_type: str = "text",
62
+ body: str = "",
63
+ media_id: str | None = None,
64
+ caption: str | None = None,
65
+ ) -> dict[str, Any]:
66
+ self._require_token()
67
+ clean_type = (message_type or "text").strip().lower()
68
+ if clean_type == "text":
69
+ payload: dict[str, Any] = {
70
+ "conversation_id": conversation_id,
71
+ "type": "text",
72
+ "text": {"body": body},
73
+ }
74
+ elif clean_type in {"image", "audio", "video", "document"}:
75
+ if not media_id:
76
+ raise MaronaBusinessApiError("media_id is required for media replies.")
77
+ payload = {
78
+ "conversation_id": conversation_id,
79
+ "type": clean_type,
80
+ clean_type: {
81
+ "media_id": media_id,
82
+ "caption": caption if caption is not None else body,
83
+ },
84
+ }
85
+ else:
86
+ raise MaronaBusinessApiError(f"Unsupported reply type: {clean_type}")
87
+ if self.app_id:
88
+ payload["app_id"] = self.app_id
89
+ response = await self._request("POST", "/messages", json=payload, timeout=20)
90
+ return response.json()
91
+
92
+ async def sync_external_conversation_message(self, payload: dict[str, Any]) -> dict[str, Any]:
93
+ self._require_token()
94
+ if self.app_id:
95
+ payload = {**payload, "app_id": self.app_id}
96
+ response = await self._request(
97
+ "POST",
98
+ "/external/conversations/sync",
99
+ json=payload,
100
+ timeout=30,
101
+ )
102
+ return response.json()
103
+
104
+ async def get_memory_context(
105
+ self,
106
+ *,
107
+ conversation_id: str | None = None,
108
+ external_conversation_id: str | None = None,
109
+ external_conversation_key: str | None = None,
110
+ limit: int = 12,
111
+ ) -> dict[str, Any]:
112
+ self._require_token()
113
+ payload = {
114
+ "app_id": self.app_id or None,
115
+ "conversation_id": conversation_id,
116
+ "external_conversation_id": external_conversation_id,
117
+ "external_conversation_key": external_conversation_key,
118
+ "limit": max(4, min(int(limit or 12), 24)),
119
+ }
120
+ response = await self._request("POST", "/memory/context", json=payload, timeout=20)
121
+ return response.json()
122
+
123
+ async def update_external_message_status(self, payload: dict[str, Any]) -> dict[str, Any]:
124
+ self._require_token()
125
+ if self.app_id:
126
+ payload = {**payload, "app_id": self.app_id}
127
+ response = await self._request(
128
+ "PATCH",
129
+ "/external/messages/status",
130
+ json=payload,
131
+ timeout=20,
132
+ )
133
+ return response.json()
134
+
135
+ async def accept_call(self, call_id: str) -> dict[str, Any]:
136
+ self._require_token()
137
+ clean_call_id = (call_id or "").strip()
138
+ if not clean_call_id:
139
+ raise MaronaBusinessApiError("call_id is required.")
140
+ response = await self._request("POST", f"/calls/{clean_call_id}/accept", timeout=30)
141
+ return response.json()
142
+
143
+ async def health(self) -> dict[str, Any]:
144
+ response = await self._request("GET", "/health", auth=False, timeout=10)
145
+ return response.json()
146
+
147
+ async def _request(self, method: str, path: str, *, timeout: float | None = None, auth: bool = True, **kwargs: Any) -> httpx.Response:
148
+ headers = {**(self._headers() if auth else {}), **kwargs.pop("headers", {})}
149
+ client = self._client
150
+ close_after = False
151
+ if client is None:
152
+ client = httpx.AsyncClient(timeout=timeout or self.timeout)
153
+ close_after = True
154
+ try:
155
+ response = await client.request(
156
+ method,
157
+ f"{self.base_url}{path}",
158
+ headers=headers or None,
159
+ **kwargs,
160
+ )
161
+ if response.status_code >= 400:
162
+ raise MaronaBusinessApiError(
163
+ f"Marona Business API request failed with {response.status_code}: {response.text[:500]}"
164
+ )
165
+ return response
166
+ finally:
167
+ if close_after:
168
+ await client.aclose()
169
+
170
+ def _headers(self) -> dict[str, str]:
171
+ headers = {"Authorization": f"Bearer {self.token}"}
172
+ if self.app_id:
173
+ headers["X-Marona-App-Id"] = self.app_id
174
+ return headers
175
+
176
+ def _require_token(self) -> None:
177
+ if not self.token:
178
+ raise MaronaBusinessApiError("MARONA_BUSINESS_API_TOKEN is not configured.")
179
+
180
+ def _normalize_media_response_url(self, media: dict[str, Any]) -> dict[str, Any]:
181
+ url = str(media.get("url") or "").strip()
182
+ if url.startswith("http://") and self.base_url.startswith("https://"):
183
+ return {**media, "url": f"https://{url.removeprefix('http://')}"}
184
+ return media