rayrun-sdk 0.8.0__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.
rayrun/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .client import AsyncRayrun, Rayrun, RayrunApiError
2
+
3
+ __all__ = ["AsyncRayrun", "Rayrun", "RayrunApiError"]
rayrun/client.py ADDED
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Mapping, Optional
4
+
5
+ import httpx
6
+
7
+
8
+ JsonObject = Mapping[str, Any]
9
+
10
+
11
+ def _page_params(limit: int, cursor: Optional[str]) -> Dict[str, Any]:
12
+ return {"limit": limit, **({"cursor": cursor} if cursor is not None else {})}
13
+
14
+
15
+ class RayrunApiError(RuntimeError):
16
+ def __init__(
17
+ self,
18
+ message: str,
19
+ *,
20
+ code: str,
21
+ request_id: Optional[str],
22
+ status: int,
23
+ ) -> None:
24
+ super().__init__(message)
25
+ self.code = code
26
+ self.request_id = request_id
27
+ self.status = status
28
+
29
+
30
+ def _raise_for_status(response: httpx.Response) -> None:
31
+ if response.is_success:
32
+ return
33
+ try:
34
+ body = response.json()
35
+ except ValueError:
36
+ body = {}
37
+ error = body.get("error", {}) if isinstance(body, dict) else {}
38
+ raise RayrunApiError(
39
+ error.get("message", f"Rayrun API request failed with HTTP {response.status_code}."),
40
+ code=error.get("code", "request_failed"),
41
+ request_id=error.get("requestId") or response.headers.get("rayrun-request-id"),
42
+ status=response.status_code,
43
+ )
44
+
45
+
46
+ class ExternalPrincipals:
47
+ def __init__(self, client: httpx.Client) -> None:
48
+ self._client = client
49
+
50
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
51
+ response = self._client.request(method, path, **kwargs)
52
+ _raise_for_status(response)
53
+ return None if response.status_code == 204 else response.json()
54
+
55
+ def upsert(
56
+ self,
57
+ external_id: str,
58
+ *,
59
+ display_name: Optional[str] = None,
60
+ metadata: Optional[JsonObject] = None,
61
+ ) -> Dict[str, str]:
62
+ return self._request(
63
+ "POST",
64
+ "/external-principals",
65
+ json={
66
+ "displayName": display_name,
67
+ "externalId": external_id,
68
+ "metadata": dict(metadata or {}),
69
+ },
70
+ )
71
+
72
+ def list(self, *, limit: int = 50, cursor: Optional[str] = None) -> Dict[str, Any]:
73
+ return self._request(
74
+ "GET", "/external-principals", params=_page_params(limit, cursor)
75
+ )
76
+
77
+ def delete(self, principal_uid: str) -> None:
78
+ self._request("DELETE", f"/external-principals/{principal_uid}")
79
+
80
+ def create_session(
81
+ self,
82
+ principal_uid: str,
83
+ *,
84
+ expires_in_seconds: int = 3600,
85
+ runtime_context: Optional[JsonObject] = None,
86
+ tool_access_profile_uid: Optional[str] = None,
87
+ ) -> Dict[str, Any]:
88
+ return self._request(
89
+ "POST",
90
+ f"/external-principals/{principal_uid}/sessions",
91
+ json={
92
+ "expiresInSeconds": expires_in_seconds,
93
+ "runtimeContext": dict(runtime_context or {}),
94
+ "toolAccessProfileUid": tool_access_profile_uid,
95
+ },
96
+ )
97
+
98
+ def list_sessions(
99
+ self,
100
+ principal_uid: str,
101
+ *,
102
+ limit: int = 50,
103
+ cursor: Optional[str] = None,
104
+ ) -> Dict[str, Any]:
105
+ return self._request(
106
+ "GET",
107
+ f"/external-principals/{principal_uid}/sessions",
108
+ params=_page_params(limit, cursor),
109
+ )
110
+
111
+ def revoke_session(self, principal_uid: str, session_uid: str) -> None:
112
+ self._request("DELETE", f"/external-principals/{principal_uid}/sessions/{session_uid}")
113
+
114
+ def create_connect_link(self, principal_uid: str, connection_uid: str) -> Dict[str, str]:
115
+ return self._request(
116
+ "POST",
117
+ f"/external-principals/{principal_uid}/connect-links",
118
+ json={"connectionUid": connection_uid},
119
+ )
120
+
121
+
122
+ class AsyncExternalPrincipals:
123
+ def __init__(self, client: httpx.AsyncClient) -> None:
124
+ self._client = client
125
+
126
+ async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
127
+ response = await self._client.request(method, path, **kwargs)
128
+ _raise_for_status(response)
129
+ return None if response.status_code == 204 else response.json()
130
+
131
+ async def upsert(
132
+ self,
133
+ external_id: str,
134
+ *,
135
+ display_name: Optional[str] = None,
136
+ metadata: Optional[JsonObject] = None,
137
+ ) -> Dict[str, str]:
138
+ return await self._request(
139
+ "POST",
140
+ "/external-principals",
141
+ json={
142
+ "displayName": display_name,
143
+ "externalId": external_id,
144
+ "metadata": dict(metadata or {}),
145
+ },
146
+ )
147
+
148
+ async def list(
149
+ self, *, limit: int = 50, cursor: Optional[str] = None
150
+ ) -> Dict[str, Any]:
151
+ return await self._request(
152
+ "GET", "/external-principals", params=_page_params(limit, cursor)
153
+ )
154
+
155
+ async def delete(self, principal_uid: str) -> None:
156
+ await self._request("DELETE", f"/external-principals/{principal_uid}")
157
+
158
+ async def create_session(
159
+ self,
160
+ principal_uid: str,
161
+ *,
162
+ expires_in_seconds: int = 3600,
163
+ runtime_context: Optional[JsonObject] = None,
164
+ tool_access_profile_uid: Optional[str] = None,
165
+ ) -> Dict[str, Any]:
166
+ return await self._request(
167
+ "POST",
168
+ f"/external-principals/{principal_uid}/sessions",
169
+ json={
170
+ "expiresInSeconds": expires_in_seconds,
171
+ "runtimeContext": dict(runtime_context or {}),
172
+ "toolAccessProfileUid": tool_access_profile_uid,
173
+ },
174
+ )
175
+
176
+ async def list_sessions(
177
+ self,
178
+ principal_uid: str,
179
+ *,
180
+ limit: int = 50,
181
+ cursor: Optional[str] = None,
182
+ ) -> Dict[str, Any]:
183
+ return await self._request(
184
+ "GET",
185
+ f"/external-principals/{principal_uid}/sessions",
186
+ params=_page_params(limit, cursor),
187
+ )
188
+
189
+ async def revoke_session(self, principal_uid: str, session_uid: str) -> None:
190
+ await self._request("DELETE", f"/external-principals/{principal_uid}/sessions/{session_uid}")
191
+
192
+ async def create_connect_link(
193
+ self, principal_uid: str, connection_uid: str
194
+ ) -> Dict[str, str]:
195
+ return await self._request(
196
+ "POST",
197
+ f"/external-principals/{principal_uid}/connect-links",
198
+ json={"connectionUid": connection_uid},
199
+ )
200
+
201
+
202
+ class Rayrun:
203
+ def __init__(
204
+ self,
205
+ api_key: str,
206
+ *,
207
+ base_url: str = "https://ray.run",
208
+ timeout: float = 30.0,
209
+ transport: Optional[httpx.BaseTransport] = None,
210
+ ) -> None:
211
+ if not api_key:
212
+ raise ValueError("Rayrun requires an api_key.")
213
+ self._client = httpx.Client(
214
+ base_url=f"{base_url.rstrip('/')}/v1",
215
+ headers={"Authorization": f"Bearer {api_key}", "User-Agent": "rayrun-python/0.8.0"},
216
+ timeout=timeout,
217
+ transport=transport,
218
+ )
219
+ self.external_principals = ExternalPrincipals(self._client)
220
+
221
+ def close(self) -> None:
222
+ self._client.close()
223
+
224
+ def __enter__(self) -> "Rayrun":
225
+ return self
226
+
227
+ def __exit__(self, *_args: Any) -> None:
228
+ self.close()
229
+
230
+
231
+ class AsyncRayrun:
232
+ def __init__(
233
+ self,
234
+ api_key: str,
235
+ *,
236
+ base_url: str = "https://ray.run",
237
+ timeout: float = 30.0,
238
+ transport: Optional[httpx.AsyncBaseTransport] = None,
239
+ ) -> None:
240
+ if not api_key:
241
+ raise ValueError("Rayrun requires an api_key.")
242
+ self._client = httpx.AsyncClient(
243
+ base_url=f"{base_url.rstrip('/')}/v1",
244
+ headers={"Authorization": f"Bearer {api_key}", "User-Agent": "rayrun-python/0.8.0"},
245
+ timeout=timeout,
246
+ transport=transport,
247
+ )
248
+ self.external_principals = AsyncExternalPrincipals(self._client)
249
+
250
+ async def close(self) -> None:
251
+ await self._client.aclose()
252
+
253
+ async def __aenter__(self) -> "AsyncRayrun":
254
+ return self
255
+
256
+ async def __aexit__(self, *_args: Any) -> None:
257
+ await self.close()
rayrun/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.5
2
+ Name: rayrun-sdk
3
+ Version: 0.8.0
4
+ Summary: Python SDK for Rayrun embedded agents and MCP infrastructure
5
+ Project-URL: Documentation, https://ray.run/docs/public-api
6
+ Project-URL: Homepage, https://ray.run
7
+ Project-URL: Repository, https://github.com/rayrundev/rayrun
8
+ Author-email: Rayrun <support@ray.run>
9
+ License-Expression: MIT
10
+ Keywords: agents,mcp,model-context-protocol,rayrun
11
+ Requires-Python: >=3.9
12
+ Requires-Dist: httpx<1,>=0.28
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest<9,>=8; extra == 'test'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Rayrun Python SDK
18
+
19
+ ```sh
20
+ pip install rayrun-sdk
21
+ ```
22
+
23
+ Create one short-lived MCP session for an application user:
24
+
25
+ ```python
26
+ import os
27
+
28
+ from rayrun import Rayrun
29
+
30
+ with Rayrun(os.environ["RAYRUN_API_KEY"]) as rayrun:
31
+ principal = rayrun.external_principals.upsert(
32
+ "customer_123",
33
+ display_name="Ada",
34
+ metadata={"plan": "pro"},
35
+ )
36
+ session = rayrun.external_principals.create_session(
37
+ principal["uid"],
38
+ runtime_context={"accountId": "customer_123", "plan": "pro"},
39
+ )
40
+
41
+ page = rayrun.external_principals.list_sessions(principal["uid"], limit=50)
42
+ if page["page"]["nextCursor"]:
43
+ next_page = rayrun.external_principals.list_sessions(
44
+ principal["uid"], cursor=page["page"]["nextCursor"], limit=50
45
+ )
46
+
47
+ print(session["endpoint"], session["sessionToken"])
48
+ ```
49
+
50
+ `AsyncRayrun` exposes the same methods for async applications. Use a key with
51
+ `principals:read`, `principals:write`, `sessions:read`, and `sessions:write`. Create a Connect Link
52
+ with `create_connect_link(principal_uid, connection_uid)` when the user must authorize an upstream
53
+ OAuth account. Correlate the returned `uid` with `connectLinkUid` in either terminal webhook:
54
+ `external.authorization.completed` or `external.authorization.failed` (`provider_denied`,
55
+ `incomplete_response`, `authorization_failed`, or `expired`). A session created without
56
+ `tool_access_profile_uid` is read-only; pass an access profile to define an explicit tool ceiling that
57
+ includes approved mutations.
@@ -0,0 +1,6 @@
1
+ rayrun/__init__.py,sha256=_KixIpQE2Wxx33NPQ6ojNmmKMoDp5Q1Km7Sq1-Xbj8I,111
2
+ rayrun/client.py,sha256=eEO6dI5YqFsgEWBtkn5c-LUwkKr6xfOsqFjZcIIEJRg,8010
3
+ rayrun/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
+ rayrun_sdk-0.8.0.dist-info/METADATA,sha256=GcxPdHMEfAJsL67Wi1RniMaqF8fOJs0r2V3pcC5vRBo,2078
5
+ rayrun_sdk-0.8.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
+ rayrun_sdk-0.8.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any