restless-stream 0.1.1__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.
@@ -0,0 +1,65 @@
1
+ """Python SDK for Restless Stream."""
2
+
3
+ from ._client import AsyncRestlessStreamClient, RestlessStreamClient
4
+ from ._errors import RestlessStreamAPIError, RestlessStreamError, RestlessStreamParseError
5
+ from ._hmac import compute_hmac_signature, verify_hmac_signature
6
+ from ._models import (
7
+ BaseActionResponse,
8
+ ConnectionSnippetsResponse,
9
+ CreditUsageChargeTypeBreakdown,
10
+ DailyCreditUsage,
11
+ DirectSessionResponse,
12
+ DirectSetupResponse,
13
+ HttpMethod,
14
+ PayloadMode,
15
+ PollingStrategy,
16
+ RestlessModel,
17
+ Stream,
18
+ StreamCreditUsageStats,
19
+ StreamErrorDetail,
20
+ StreamEvent,
21
+ StreamEventMeta,
22
+ StreamsResponse,
23
+ StreamStatus,
24
+ )
25
+ from ._urls import (
26
+ DEFAULT_STREAM_BASE_URL,
27
+ build_direct_sse_url,
28
+ build_direct_websocket_url,
29
+ build_sse_url,
30
+ build_websocket_url,
31
+ to_websocket_url,
32
+ )
33
+
34
+ __all__ = [
35
+ "AsyncRestlessStreamClient",
36
+ "BaseActionResponse",
37
+ "ConnectionSnippetsResponse",
38
+ "CreditUsageChargeTypeBreakdown",
39
+ "DEFAULT_STREAM_BASE_URL",
40
+ "DailyCreditUsage",
41
+ "DirectSessionResponse",
42
+ "DirectSetupResponse",
43
+ "HttpMethod",
44
+ "PayloadMode",
45
+ "PollingStrategy",
46
+ "RestlessModel",
47
+ "RestlessStreamAPIError",
48
+ "RestlessStreamClient",
49
+ "RestlessStreamError",
50
+ "RestlessStreamParseError",
51
+ "Stream",
52
+ "StreamCreditUsageStats",
53
+ "StreamErrorDetail",
54
+ "StreamEvent",
55
+ "StreamEventMeta",
56
+ "StreamStatus",
57
+ "StreamsResponse",
58
+ "compute_hmac_signature",
59
+ "verify_hmac_signature",
60
+ "build_direct_sse_url",
61
+ "build_direct_websocket_url",
62
+ "build_sse_url",
63
+ "build_websocket_url",
64
+ "to_websocket_url",
65
+ ]
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Mapping
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from ._constants import DEFAULT_API_BASE_URL
9
+ from ._models import (
10
+ BaseActionResponse,
11
+ ConnectionSnippetsResponse,
12
+ DirectSessionResponse,
13
+ DirectSetupResponse,
14
+ Stream,
15
+ StreamCreditUsageStats,
16
+ StreamEvent,
17
+ StreamsResponse,
18
+ )
19
+ from ._resources import AsyncStreams
20
+ from ._streaming import async_sse_events, websocket_events
21
+ from ._urls import DEFAULT_STREAM_BASE_URL, build_direct_sse_url, build_direct_websocket_url
22
+ from ._utils import dump_body, pop_stream_runtime_options, raise_for_status, with_api_key
23
+
24
+
25
+ class AsyncRestlessStreamClient:
26
+ """Asynchronous REST, SSE, and WebSocket client."""
27
+
28
+ def __init__(
29
+ self,
30
+ api_key: str | None = None,
31
+ *,
32
+ base_url: str = DEFAULT_API_BASE_URL,
33
+ stream_base_url: str = DEFAULT_STREAM_BASE_URL,
34
+ timeout: float = 30.0,
35
+ http_client: httpx.AsyncClient | None = None,
36
+ ) -> None:
37
+ self.api_key = api_key
38
+ self.stream_base_url = stream_base_url.rstrip("/")
39
+ self._owns_client = http_client is None
40
+ self._client = http_client or httpx.AsyncClient(
41
+ base_url=base_url.rstrip("/"),
42
+ timeout=timeout,
43
+ )
44
+ if api_key:
45
+ self._client.headers.setdefault("x-api-key", api_key)
46
+ self.streams = AsyncStreams(self)
47
+
48
+ async def aclose(self) -> None:
49
+ if self._owns_client:
50
+ await self._client.aclose()
51
+
52
+ async def __aenter__(self) -> AsyncRestlessStreamClient:
53
+ return self
54
+
55
+ async def __aexit__(self, *_: object) -> None:
56
+ await self.aclose()
57
+
58
+ async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
59
+ response = await self._client.request(method, path, **kwargs)
60
+ raise_for_status(response)
61
+ if response.status_code == 204 or not response.content:
62
+ return None
63
+ return response.json()
64
+
65
+ async def list_streams(self, *, limit: int = 20, offset: int = 0) -> StreamsResponse:
66
+ return StreamsResponse.model_validate(
67
+ await self._request("GET", "/streams", params={"limit": limit, "offset": offset})
68
+ )
69
+
70
+ async def get_stream(self, stream_id: str) -> Stream:
71
+ return Stream.model_validate(await self._request("GET", f"/streams/{stream_id}"))
72
+
73
+ async def create_stream(self, data: Mapping[str, Any] | None = None, **kwargs: Any) -> Stream:
74
+ body = with_api_key(dump_body(data, kwargs), self.api_key)
75
+ return Stream.model_validate(await self._request("POST", "/streams", json=body))
76
+
77
+ async def update_stream(
78
+ self, stream_id: str, data: Mapping[str, Any] | None = None, **kwargs: Any
79
+ ) -> Stream:
80
+ body = with_api_key(dump_body(data, kwargs), self.api_key)
81
+ return Stream.model_validate(await self._request("PATCH", f"/streams/{stream_id}", json=body))
82
+
83
+ async def start_stream(self, stream_id: str) -> Stream:
84
+ return Stream.model_validate(await self._request("POST", f"/streams/{stream_id}/start"))
85
+
86
+ async def stop_stream(self, stream_id: str) -> Stream:
87
+ return Stream.model_validate(await self._request("POST", f"/streams/{stream_id}/stop"))
88
+
89
+ async def delete_stream(self, stream_id: str) -> BaseActionResponse:
90
+ return BaseActionResponse.model_validate(await self._request("DELETE", f"/streams/{stream_id}"))
91
+
92
+ async def validate_stream_api_key(self, api_key: str | None = None) -> BaseActionResponse:
93
+ return BaseActionResponse.model_validate(
94
+ await self._request(
95
+ "POST", "/streams/validate-api-key", json={"apiKey": api_key or self.api_key}
96
+ )
97
+ )
98
+
99
+ async def credit_usage_stats(self, stream_id: str) -> StreamCreditUsageStats:
100
+ return StreamCreditUsageStats.model_validate(
101
+ await self._request("GET", f"/streams/{stream_id}/credit-usage-stats")
102
+ )
103
+
104
+ async def connection_snippets(
105
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
106
+ ) -> ConnectionSnippetsResponse:
107
+ return ConnectionSnippetsResponse.model_validate(
108
+ await self._request("POST", "/streams/connection-snippets", json=dump_body(data, kwargs))
109
+ )
110
+
111
+ async def direct_setup(
112
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
113
+ ) -> DirectSetupResponse:
114
+ return DirectSetupResponse.model_validate(
115
+ await self._request("POST", "/streams/direct/setup", json=dump_body(data, kwargs))
116
+ )
117
+
118
+ async def direct_session(
119
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
120
+ ) -> DirectSessionResponse:
121
+ return DirectSessionResponse.model_validate(
122
+ await self._request("POST", "/streams/direct/sessions", json=dump_body(data, kwargs))
123
+ )
124
+
125
+ def subscribe_sse(self, url: str, **kwargs: Any) -> AsyncIterator[StreamEvent]:
126
+ return async_sse_events(self._client, url, api_key=self.api_key, **kwargs)
127
+
128
+ def subscribe_websocket(self, url: str, **kwargs: Any) -> AsyncIterator[StreamEvent]:
129
+ return websocket_events(url, api_key=self.api_key, **kwargs)
130
+
131
+ def subscribe_direct_sse(self, **kwargs: Any) -> AsyncIterator[StreamEvent]:
132
+ stream_base_url = kwargs.pop("stream_base_url", self.stream_base_url)
133
+ stream_options = pop_stream_runtime_options(kwargs)
134
+ return self.subscribe_sse(
135
+ build_direct_sse_url(stream_base_url=stream_base_url, **kwargs),
136
+ **stream_options,
137
+ )
138
+
139
+ def subscribe_direct_websocket(self, **kwargs: Any) -> AsyncIterator[StreamEvent]:
140
+ stream_base_url = kwargs.pop("stream_base_url", self.stream_base_url)
141
+ stream_options = pop_stream_runtime_options(kwargs, websocket=True)
142
+ return self.subscribe_websocket(
143
+ build_direct_websocket_url(stream_base_url=stream_base_url, **kwargs),
144
+ **stream_options,
145
+ )
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from ._async_client import AsyncRestlessStreamClient
4
+ from ._constants import DEFAULT_API_BASE_URL
5
+ from ._sync_client import RestlessStreamClient
6
+
7
+ __all__ = ["AsyncRestlessStreamClient", "DEFAULT_API_BASE_URL", "RestlessStreamClient"]
@@ -0,0 +1 @@
1
+ DEFAULT_API_BASE_URL = "https://api.restlessapi.stream"
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class RestlessStreamError(Exception):
5
+ """Base SDK exception."""
6
+
7
+
8
+ class RestlessStreamAPIError(RestlessStreamError):
9
+ """Raised when the REST API returns a non-successful response."""
10
+
11
+ def __init__(self, status_code: int, message: str) -> None:
12
+ super().__init__(f"Restless Stream API error {status_code}: {message}")
13
+ self.status_code = status_code
14
+ self.message = message
15
+
16
+
17
+ class RestlessStreamParseError(RestlessStreamError):
18
+ """Raised when a runtime event cannot be parsed."""
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import hmac
4
+ import json
5
+ from hashlib import sha256
6
+ from typing import Any
7
+
8
+
9
+ def _canonical_json_bytes(value: Any) -> bytes:
10
+ return json.dumps(
11
+ value,
12
+ ensure_ascii=False,
13
+ separators=(",", ":"),
14
+ sort_keys=True,
15
+ ).encode("utf-8")
16
+
17
+
18
+ def compute_hmac_signature(secret: str, payload: Any) -> str:
19
+ """Return the Restless Stream HMAC-SHA256 signature for a JSON payload."""
20
+
21
+ return hmac.new(secret.encode("utf-8"), _canonical_json_bytes(payload), sha256).hexdigest()
22
+
23
+
24
+ def verify_hmac_signature(secret: str, payload: Any, signature: str) -> bool:
25
+ """Constant-time verification for a runtime event `signature` value."""
26
+
27
+ expected = compute_hmac_signature(secret, payload)
28
+ return hmac.compare_digest(expected, signature)
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ def to_camel(value: str) -> str:
10
+ head, *tail = value.split("_")
11
+ return head + "".join(part[:1].upper() + part[1:] for part in tail)
12
+
13
+
14
+ class RestlessModel(BaseModel):
15
+ model_config = ConfigDict(
16
+ alias_generator=to_camel,
17
+ extra="allow",
18
+ populate_by_name=True,
19
+ use_enum_values=True,
20
+ )
21
+
22
+
23
+ class HttpMethod(str, Enum):
24
+ GET = "GET"
25
+ POST = "POST"
26
+ PUT = "PUT"
27
+ PATCH = "PATCH"
28
+ DELETE = "DELETE"
29
+ OPTIONS = "OPTIONS"
30
+
31
+
32
+ class StreamStatus(str, Enum):
33
+ ACTIVE = "ACTIVE"
34
+ INACTIVE = "INACTIVE"
35
+
36
+
37
+ class PayloadMode(str, Enum):
38
+ FULL_DATA = "FULL_DATA"
39
+ JSON_PATCH = "JSON_PATCH"
40
+
41
+
42
+ class PollingStrategy(RestlessModel):
43
+ start_time: str
44
+ end_time: str
45
+ interval_seconds: int
46
+
47
+
48
+ class Stream(RestlessModel):
49
+ id: str
50
+ business_id: str | None = None
51
+ name: str
52
+ description: str | None = None
53
+ status: StreamStatus
54
+ method: HttpMethod
55
+ url: str
56
+ headers: dict[str, Any] | None = None
57
+ body: dict[str, Any] | None = None
58
+ payload_mode: PayloadMode
59
+ jq_filter: str | None = None
60
+ hmac_signature_enabled: bool = False
61
+ polling_interval: int
62
+ polling_strategies: list[PollingStrategy] | None = None
63
+ created_at: str | None = None
64
+ updated_at: str | None = None
65
+ ws_url: str | None = None
66
+ sse_url: str | None = None
67
+
68
+
69
+ class PaginationInfo(RestlessModel):
70
+ has_next_page: bool = False
71
+
72
+
73
+ class StreamsResponse(RestlessModel):
74
+ streams: list[Stream]
75
+ pagination_info: PaginationInfo = Field(default_factory=PaginationInfo)
76
+
77
+
78
+ class BaseActionResponse(RestlessModel):
79
+ id: str
80
+ affected: bool
81
+
82
+
83
+ class CreditUsageChargeTypeBreakdown(RestlessModel):
84
+ charge_type: str
85
+ credits_used: int
86
+
87
+
88
+ class DailyCreditUsage(RestlessModel):
89
+ date: str
90
+ credits_used: int
91
+ charge_type_breakdown: list[CreditUsageChargeTypeBreakdown]
92
+
93
+
94
+ class StreamCreditUsageStats(RestlessModel):
95
+ total_burned_last_week: int
96
+ total_burned_last_month: int
97
+ daily_usage: list[DailyCreditUsage]
98
+
99
+
100
+ class SnippetSet(RestlessModel):
101
+ curl: str
102
+ wget: str
103
+ sse: str
104
+ websocket: str
105
+
106
+
107
+ class SnippetLanguageOption(RestlessModel):
108
+ label: str
109
+ value: str
110
+
111
+
112
+ class SnippetPayload(RestlessModel):
113
+ available_languages: list[SnippetLanguageOption]
114
+ selected_language: str
115
+ selected_snippets: SnippetSet
116
+ snippets_by_language: dict[str, SnippetSet] | None = None
117
+
118
+
119
+ class RuntimeUrls(RestlessModel):
120
+ sse_url: str
121
+ websocket_url: str
122
+
123
+
124
+ class ConnectionSnippetsResponse(RestlessModel):
125
+ runtime: RuntimeUrls
126
+ snippets: SnippetPayload
127
+
128
+
129
+ class DirectCommandSet(RestlessModel):
130
+ curl: str
131
+ wget: str
132
+
133
+
134
+ class DirectCommands(RestlessModel):
135
+ header: DirectCommandSet
136
+ url_param: DirectCommandSet
137
+
138
+
139
+ class DirectSetupRuntimeAuth(RestlessModel):
140
+ api_key_location: str
141
+ header_example: str
142
+
143
+
144
+ class DirectSetupRuntime(RestlessModel):
145
+ auth: DirectSetupRuntimeAuth
146
+ direct_sse_url: str
147
+ direct_websocket_url: str
148
+
149
+
150
+ class DirectSetupStreamConfig(RestlessModel):
151
+ target_url: str
152
+ method: HttpMethod
153
+ headers: dict[str, Any] | None = None
154
+ body: dict[str, Any] | None = None
155
+ jq_filter: str | None = None
156
+ payload_mode: PayloadMode | None = None
157
+ polling_interval: int | None = None
158
+ polling_strategies: list[PollingStrategy] | None = None
159
+
160
+
161
+ class DirectSetupResponse(RestlessModel):
162
+ commands: DirectCommands
163
+ runtime: DirectSetupRuntime
164
+ snippets: SnippetPayload
165
+ stream_config: DirectSetupStreamConfig
166
+
167
+
168
+ class DirectSessionResponse(RestlessModel):
169
+ session_id: str
170
+ ws_url: str
171
+ sse_url: str
172
+ expires_at: str
173
+ created: bool
174
+ dedupe_key: str
175
+
176
+
177
+ class StreamEventMeta(RestlessModel):
178
+ timestamp: str
179
+ attempt_id: str | None = None
180
+ status: int | None = None
181
+ latency_ms: int | None = None
182
+ payload_mode_fallback: str | None = None
183
+
184
+
185
+ class StreamErrorDetail(RestlessModel):
186
+ code: str
187
+ message: str
188
+
189
+
190
+ class StreamEvent(RestlessModel):
191
+ type: str
192
+ meta: StreamEventMeta
193
+ data: Any | None = None
194
+ error: StreamErrorDetail | None = None
195
+ signature: str | None = None
196
+ event_id: str | None = None
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Iterator, Mapping
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from ._models import (
7
+ BaseActionResponse,
8
+ ConnectionSnippetsResponse,
9
+ DirectSessionResponse,
10
+ DirectSetupResponse,
11
+ Stream,
12
+ StreamCreditUsageStats,
13
+ StreamEvent,
14
+ StreamsResponse,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from ._async_client import AsyncRestlessStreamClient
19
+ from ._sync_client import RestlessStreamClient
20
+
21
+
22
+ class SyncStreams:
23
+ def __init__(self, client: RestlessStreamClient) -> None:
24
+ self._client = client
25
+
26
+ def list(self, **kwargs: Any) -> StreamsResponse:
27
+ return self._client.list_streams(**kwargs)
28
+
29
+ def get(self, stream_id: str) -> Stream:
30
+ return self._client.get_stream(stream_id)
31
+
32
+ def create(self, data: Mapping[str, Any] | None = None, **kwargs: Any) -> Stream:
33
+ return self._client.create_stream(data, **kwargs)
34
+
35
+ def update(self, stream_id: str, data: Mapping[str, Any] | None = None, **kwargs: Any) -> Stream:
36
+ return self._client.update_stream(stream_id, data, **kwargs)
37
+
38
+ def start(self, stream_id: str) -> Stream:
39
+ return self._client.start_stream(stream_id)
40
+
41
+ def stop(self, stream_id: str) -> Stream:
42
+ return self._client.stop_stream(stream_id)
43
+
44
+ def delete(self, stream_id: str) -> BaseActionResponse:
45
+ return self._client.delete_stream(stream_id)
46
+
47
+ def validate_api_key(self, api_key: str | None = None) -> BaseActionResponse:
48
+ return self._client.validate_stream_api_key(api_key)
49
+
50
+ def credit_usage_stats(self, stream_id: str) -> StreamCreditUsageStats:
51
+ return self._client.credit_usage_stats(stream_id)
52
+
53
+ def connection_snippets(
54
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
55
+ ) -> ConnectionSnippetsResponse:
56
+ return self._client.connection_snippets(data, **kwargs)
57
+
58
+ def direct_setup(
59
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
60
+ ) -> DirectSetupResponse:
61
+ return self._client.direct_setup(data, **kwargs)
62
+
63
+ def direct_session(
64
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
65
+ ) -> DirectSessionResponse:
66
+ return self._client.direct_session(data, **kwargs)
67
+
68
+ def subscribe_sse(self, url: str, **kwargs: Any) -> Iterator[StreamEvent]:
69
+ return self._client.subscribe_sse(url, **kwargs)
70
+
71
+ def subscribe_direct_sse(self, **kwargs: Any) -> Iterator[StreamEvent]:
72
+ return self._client.subscribe_direct_sse(**kwargs)
73
+
74
+
75
+ class AsyncStreams:
76
+ def __init__(self, client: AsyncRestlessStreamClient) -> None:
77
+ self._client = client
78
+
79
+ async def list(self, **kwargs: Any) -> StreamsResponse:
80
+ return await self._client.list_streams(**kwargs)
81
+
82
+ async def get(self, stream_id: str) -> Stream:
83
+ return await self._client.get_stream(stream_id)
84
+
85
+ async def create(self, data: Mapping[str, Any] | None = None, **kwargs: Any) -> Stream:
86
+ return await self._client.create_stream(data, **kwargs)
87
+
88
+ async def update(
89
+ self, stream_id: str, data: Mapping[str, Any] | None = None, **kwargs: Any
90
+ ) -> Stream:
91
+ return await self._client.update_stream(stream_id, data, **kwargs)
92
+
93
+ async def start(self, stream_id: str) -> Stream:
94
+ return await self._client.start_stream(stream_id)
95
+
96
+ async def stop(self, stream_id: str) -> Stream:
97
+ return await self._client.stop_stream(stream_id)
98
+
99
+ async def delete(self, stream_id: str) -> BaseActionResponse:
100
+ return await self._client.delete_stream(stream_id)
101
+
102
+ async def validate_api_key(self, api_key: str | None = None) -> BaseActionResponse:
103
+ return await self._client.validate_stream_api_key(api_key)
104
+
105
+ async def credit_usage_stats(self, stream_id: str) -> StreamCreditUsageStats:
106
+ return await self._client.credit_usage_stats(stream_id)
107
+
108
+ async def connection_snippets(
109
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
110
+ ) -> ConnectionSnippetsResponse:
111
+ return await self._client.connection_snippets(data, **kwargs)
112
+
113
+ async def direct_setup(
114
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
115
+ ) -> DirectSetupResponse:
116
+ return await self._client.direct_setup(data, **kwargs)
117
+
118
+ async def direct_session(
119
+ self, data: Mapping[str, Any] | None = None, **kwargs: Any
120
+ ) -> DirectSessionResponse:
121
+ return await self._client.direct_session(data, **kwargs)
122
+
123
+ def subscribe_sse(self, url: str, **kwargs: Any) -> AsyncIterator[StreamEvent]:
124
+ return self._client.subscribe_sse(url, **kwargs)
125
+
126
+ def subscribe_websocket(self, url: str, **kwargs: Any) -> AsyncIterator[StreamEvent]:
127
+ return self._client.subscribe_websocket(url, **kwargs)
128
+
129
+ def subscribe_direct_sse(self, **kwargs: Any) -> AsyncIterator[StreamEvent]:
130
+ return self._client.subscribe_direct_sse(**kwargs)
131
+
132
+ def subscribe_direct_websocket(self, **kwargs: Any) -> AsyncIterator[StreamEvent]:
133
+ return self._client.subscribe_direct_websocket(**kwargs)