vardast 0.1.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.
- vardast/__init__.py +62 -0
- vardast/client.py +690 -0
- vardast/exceptions.py +52 -0
- vardast/models.py +210 -0
- vardast/py.typed +1 -0
- vardast-0.1.0.dist-info/METADATA +183 -0
- vardast-0.1.0.dist-info/RECORD +9 -0
- vardast-0.1.0.dist-info/WHEEL +4 -0
- vardast-0.1.0.dist-info/licenses/LICENSE +21 -0
vardast/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Official Python SDK for the Vardast Public API."""
|
|
2
|
+
|
|
3
|
+
from vardast.client import AsyncVardast, Vardast
|
|
4
|
+
from vardast.exceptions import (
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
ServerError,
|
|
9
|
+
VardastAPIError,
|
|
10
|
+
VardastError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from vardast.models import (
|
|
14
|
+
Assistant,
|
|
15
|
+
AssistantCreate,
|
|
16
|
+
Channel,
|
|
17
|
+
ChannelContact,
|
|
18
|
+
ChannelOrder,
|
|
19
|
+
Contact,
|
|
20
|
+
Message,
|
|
21
|
+
OrderItem,
|
|
22
|
+
Paginated,
|
|
23
|
+
ProcessChatResponse,
|
|
24
|
+
Prompt,
|
|
25
|
+
PromptCreate,
|
|
26
|
+
PromptCreateResponse,
|
|
27
|
+
UsageActivity,
|
|
28
|
+
UsageActivityPeriodItem,
|
|
29
|
+
UsageInfo,
|
|
30
|
+
UsageInfoData,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Assistant",
|
|
35
|
+
"AssistantCreate",
|
|
36
|
+
"AsyncVardast",
|
|
37
|
+
"AuthenticationError",
|
|
38
|
+
"Channel",
|
|
39
|
+
"ChannelContact",
|
|
40
|
+
"ChannelOrder",
|
|
41
|
+
"Contact",
|
|
42
|
+
"Message",
|
|
43
|
+
"NotFoundError",
|
|
44
|
+
"OrderItem",
|
|
45
|
+
"Paginated",
|
|
46
|
+
"ProcessChatResponse",
|
|
47
|
+
"Prompt",
|
|
48
|
+
"PromptCreate",
|
|
49
|
+
"PromptCreateResponse",
|
|
50
|
+
"RateLimitError",
|
|
51
|
+
"ServerError",
|
|
52
|
+
"UsageActivity",
|
|
53
|
+
"UsageActivityPeriodItem",
|
|
54
|
+
"UsageInfo",
|
|
55
|
+
"UsageInfoData",
|
|
56
|
+
"ValidationError",
|
|
57
|
+
"Vardast",
|
|
58
|
+
"VardastAPIError",
|
|
59
|
+
"VardastError",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
__version__ = "0.1.0"
|
vardast/client.py
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"""Synchronous and asynchronous clients for the Vardast Public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any, Dict, List, Mapping, Optional, Sequence, Type, Union
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from vardast.exceptions import (
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
ServerError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
VardastAPIError,
|
|
18
|
+
VardastError,
|
|
19
|
+
)
|
|
20
|
+
from vardast.models import (
|
|
21
|
+
Assistant,
|
|
22
|
+
AssistantCreate,
|
|
23
|
+
AssistantsResponse,
|
|
24
|
+
Channel,
|
|
25
|
+
ChannelContact,
|
|
26
|
+
ChannelOrder,
|
|
27
|
+
ChannelsResponse,
|
|
28
|
+
Contact,
|
|
29
|
+
Message,
|
|
30
|
+
Paginated,
|
|
31
|
+
ProcessChatRequest,
|
|
32
|
+
ProcessChatResponse,
|
|
33
|
+
Prompt,
|
|
34
|
+
PromptCreate,
|
|
35
|
+
PromptCreateResponse,
|
|
36
|
+
UsageActivity,
|
|
37
|
+
UsageActivityEnvelope,
|
|
38
|
+
UsageActivityPeriodItem,
|
|
39
|
+
UsageInfo,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
DEFAULT_BASE_URL = "https://apigw.vardast.chat/uaa/public"
|
|
43
|
+
DEFAULT_TIMEOUT = 30.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _extract_detail(payload: Any) -> Any:
|
|
47
|
+
if isinstance(payload, dict):
|
|
48
|
+
if "detail" in payload:
|
|
49
|
+
return payload["detail"]
|
|
50
|
+
if "error" in payload:
|
|
51
|
+
return payload["error"]
|
|
52
|
+
if "message" in payload:
|
|
53
|
+
return payload["message"]
|
|
54
|
+
return payload
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _format_detail(detail: Any) -> str:
|
|
58
|
+
if detail is None:
|
|
59
|
+
return "Request failed"
|
|
60
|
+
if isinstance(detail, str):
|
|
61
|
+
return detail
|
|
62
|
+
return str(detail)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
66
|
+
if response.is_success:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
payload: Any = response.json()
|
|
71
|
+
except ValueError:
|
|
72
|
+
payload = response.text
|
|
73
|
+
|
|
74
|
+
detail = _extract_detail(payload)
|
|
75
|
+
message = _format_detail(detail)
|
|
76
|
+
status = response.status_code
|
|
77
|
+
kwargs = {"status_code": status, "detail": detail, "response": response}
|
|
78
|
+
|
|
79
|
+
if status in (400, 422):
|
|
80
|
+
raise ValidationError(message, **kwargs)
|
|
81
|
+
if status == 401:
|
|
82
|
+
raise AuthenticationError(message, **kwargs)
|
|
83
|
+
if status == 404:
|
|
84
|
+
raise NotFoundError(message, **kwargs)
|
|
85
|
+
if status == 429:
|
|
86
|
+
raise RateLimitError(message, **kwargs)
|
|
87
|
+
if status >= 500:
|
|
88
|
+
raise ServerError(message, **kwargs)
|
|
89
|
+
raise VardastAPIError(message, **kwargs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _serialize_datetime(value: Union[str, datetime, date]) -> str:
|
|
93
|
+
if isinstance(value, datetime):
|
|
94
|
+
return value.isoformat()
|
|
95
|
+
if isinstance(value, date):
|
|
96
|
+
return value.isoformat()
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _BaseClient:
|
|
101
|
+
"""Shared configuration and request helpers."""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
api_key: str,
|
|
106
|
+
*,
|
|
107
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
108
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
109
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
if not api_key or not str(api_key).strip():
|
|
112
|
+
raise VardastError("api_key is required")
|
|
113
|
+
|
|
114
|
+
self.api_key = str(api_key).strip()
|
|
115
|
+
self.base_url = base_url.rstrip("/")
|
|
116
|
+
self.timeout = timeout
|
|
117
|
+
self._extra_headers = dict(headers or {})
|
|
118
|
+
|
|
119
|
+
def _default_headers(self) -> Dict[str, str]:
|
|
120
|
+
headers = {
|
|
121
|
+
"X-API-Key": self.api_key,
|
|
122
|
+
"Accept": "application/json",
|
|
123
|
+
"User-Agent": "vardast-python/0.1.0",
|
|
124
|
+
}
|
|
125
|
+
headers.update(self._extra_headers)
|
|
126
|
+
return headers
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Vardast(_BaseClient):
|
|
130
|
+
"""Synchronous client for the Vardast Public API.
|
|
131
|
+
|
|
132
|
+
Example::
|
|
133
|
+
|
|
134
|
+
from vardast import Vardast
|
|
135
|
+
|
|
136
|
+
client = Vardast(api_key="your-api-key")
|
|
137
|
+
channels = client.list_channels()
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
api_key: str,
|
|
143
|
+
*,
|
|
144
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
145
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
146
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
147
|
+
http_client: Optional[httpx.Client] = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
super().__init__(api_key, base_url=base_url, timeout=timeout, headers=headers)
|
|
150
|
+
self._owns_client = http_client is None
|
|
151
|
+
self._client = http_client or httpx.Client(
|
|
152
|
+
base_url=f"{self.base_url}/",
|
|
153
|
+
headers=self._default_headers(),
|
|
154
|
+
timeout=self.timeout,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def close(self) -> None:
|
|
158
|
+
if self._owns_client:
|
|
159
|
+
self._client.close()
|
|
160
|
+
|
|
161
|
+
def __enter__(self) -> "Vardast":
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def __exit__(
|
|
165
|
+
self,
|
|
166
|
+
exc_type: Optional[Type[BaseException]],
|
|
167
|
+
exc: Optional[BaseException],
|
|
168
|
+
tb: Optional[TracebackType],
|
|
169
|
+
) -> None:
|
|
170
|
+
self.close()
|
|
171
|
+
|
|
172
|
+
def request(
|
|
173
|
+
self,
|
|
174
|
+
method: str,
|
|
175
|
+
path: str,
|
|
176
|
+
*,
|
|
177
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
178
|
+
json: Optional[Any] = None,
|
|
179
|
+
) -> Any:
|
|
180
|
+
url = path if path.startswith("http") else path.lstrip("/")
|
|
181
|
+
response = self._client.request(
|
|
182
|
+
method,
|
|
183
|
+
url,
|
|
184
|
+
params=_clean_params(params),
|
|
185
|
+
json=json,
|
|
186
|
+
)
|
|
187
|
+
_raise_for_status(response)
|
|
188
|
+
if response.status_code == 204 or not response.content:
|
|
189
|
+
return None
|
|
190
|
+
return response.json()
|
|
191
|
+
|
|
192
|
+
# --- Chat ---
|
|
193
|
+
|
|
194
|
+
def process_message(
|
|
195
|
+
self,
|
|
196
|
+
*,
|
|
197
|
+
message: str,
|
|
198
|
+
channel_id: str,
|
|
199
|
+
contact_id: str,
|
|
200
|
+
assistant_id: Optional[str] = None,
|
|
201
|
+
) -> ProcessChatResponse:
|
|
202
|
+
"""Process a chat message and return the AI response.
|
|
203
|
+
|
|
204
|
+
Maps to ``POST /messenger/api/chat/public/process``.
|
|
205
|
+
"""
|
|
206
|
+
body = ProcessChatRequest(
|
|
207
|
+
message=message,
|
|
208
|
+
channel_id=channel_id,
|
|
209
|
+
contact_id=contact_id,
|
|
210
|
+
assistant_id=assistant_id,
|
|
211
|
+
)
|
|
212
|
+
data = self.request(
|
|
213
|
+
"POST",
|
|
214
|
+
"/messenger/api/chat/public/process",
|
|
215
|
+
json=body.model_dump(exclude_none=True),
|
|
216
|
+
)
|
|
217
|
+
return ProcessChatResponse.model_validate(data)
|
|
218
|
+
|
|
219
|
+
def list_messages(
|
|
220
|
+
self,
|
|
221
|
+
channel_id: str,
|
|
222
|
+
contact_id: str,
|
|
223
|
+
*,
|
|
224
|
+
page: int = 1,
|
|
225
|
+
size: int = 20,
|
|
226
|
+
) -> Paginated[Message]:
|
|
227
|
+
"""List messages for a contact on a channel.
|
|
228
|
+
|
|
229
|
+
Maps to ``GET /messenger/api/chat/{channel_id}/{contact_id}/``.
|
|
230
|
+
"""
|
|
231
|
+
data = self.request(
|
|
232
|
+
"GET",
|
|
233
|
+
f"/messenger/api/chat/{channel_id}/{contact_id}/",
|
|
234
|
+
params={"page": page, "size": size},
|
|
235
|
+
)
|
|
236
|
+
return Paginated[Message].model_validate(data)
|
|
237
|
+
|
|
238
|
+
def list_contacts(
|
|
239
|
+
self,
|
|
240
|
+
*,
|
|
241
|
+
page: int = 1,
|
|
242
|
+
size: int = 20,
|
|
243
|
+
channel_ids: Optional[Union[str, Sequence[str]]] = None,
|
|
244
|
+
platform: Optional[str] = None,
|
|
245
|
+
) -> Paginated[Contact]:
|
|
246
|
+
"""List contacts across channels.
|
|
247
|
+
|
|
248
|
+
Maps to ``GET /messenger/api/chat/contacts/``.
|
|
249
|
+
"""
|
|
250
|
+
params: Dict[str, Any] = {"page": page, "size": size}
|
|
251
|
+
if channel_ids is not None:
|
|
252
|
+
params["channel_ids"] = (
|
|
253
|
+
channel_ids if isinstance(channel_ids, str) else ",".join(channel_ids)
|
|
254
|
+
)
|
|
255
|
+
if platform is not None:
|
|
256
|
+
params["platform"] = platform
|
|
257
|
+
data = self.request("GET", "/messenger/api/chat/contacts/", params=params)
|
|
258
|
+
return Paginated[Contact].model_validate(data)
|
|
259
|
+
|
|
260
|
+
# --- Channels ---
|
|
261
|
+
|
|
262
|
+
def list_channels(self) -> List[Channel]:
|
|
263
|
+
"""List all channels for the authenticated account.
|
|
264
|
+
|
|
265
|
+
Maps to ``GET /messenger/api/channel/``.
|
|
266
|
+
"""
|
|
267
|
+
data = self.request("GET", "/messenger/api/channel/")
|
|
268
|
+
if isinstance(data, list):
|
|
269
|
+
return [Channel.model_validate(item) for item in data]
|
|
270
|
+
return ChannelsResponse.model_validate(data).items
|
|
271
|
+
|
|
272
|
+
# --- Assistants ---
|
|
273
|
+
|
|
274
|
+
def list_assistants(self) -> List[Assistant]:
|
|
275
|
+
"""List assistants sorted newest first.
|
|
276
|
+
|
|
277
|
+
Maps to ``GET /messenger/api/assistants/``.
|
|
278
|
+
"""
|
|
279
|
+
data = self.request("GET", "/messenger/api/assistants/")
|
|
280
|
+
if isinstance(data, list):
|
|
281
|
+
return [Assistant.model_validate(item) for item in data]
|
|
282
|
+
return AssistantsResponse.model_validate(data).items
|
|
283
|
+
|
|
284
|
+
def create_assistant(
|
|
285
|
+
self,
|
|
286
|
+
*,
|
|
287
|
+
assistant_name: str,
|
|
288
|
+
model: str,
|
|
289
|
+
api_key: str,
|
|
290
|
+
kb_id: Optional[str] = None,
|
|
291
|
+
functions: Optional[Sequence[str]] = None,
|
|
292
|
+
) -> Assistant:
|
|
293
|
+
"""Create a new assistant.
|
|
294
|
+
|
|
295
|
+
Maps to ``POST /messenger/api/assistants/``.
|
|
296
|
+
"""
|
|
297
|
+
body = AssistantCreate(
|
|
298
|
+
assistant_name=assistant_name,
|
|
299
|
+
model=model,
|
|
300
|
+
api_key=api_key,
|
|
301
|
+
kb_id=kb_id,
|
|
302
|
+
functions=list(functions) if functions is not None else None,
|
|
303
|
+
)
|
|
304
|
+
data = self.request(
|
|
305
|
+
"POST",
|
|
306
|
+
"/messenger/api/assistants/",
|
|
307
|
+
json=body.model_dump(exclude_none=True),
|
|
308
|
+
)
|
|
309
|
+
return Assistant.model_validate(data)
|
|
310
|
+
|
|
311
|
+
# --- Prompts ---
|
|
312
|
+
|
|
313
|
+
def create_prompt(
|
|
314
|
+
self,
|
|
315
|
+
*,
|
|
316
|
+
assistant_id: str,
|
|
317
|
+
text: str,
|
|
318
|
+
prompt_json: Mapping[str, Any],
|
|
319
|
+
language: str = "EN",
|
|
320
|
+
created_by: str,
|
|
321
|
+
) -> PromptCreateResponse:
|
|
322
|
+
"""Create a prompt for an assistant.
|
|
323
|
+
|
|
324
|
+
Maps to ``POST /messenger/api/prompts/``.
|
|
325
|
+
``language`` defaults to ``\"EN\"`` (also accepts ``\"FA\"``).
|
|
326
|
+
"""
|
|
327
|
+
body = PromptCreate(
|
|
328
|
+
assistant_id=assistant_id,
|
|
329
|
+
text=text,
|
|
330
|
+
prompt_json=dict(prompt_json),
|
|
331
|
+
language=language,
|
|
332
|
+
created_by=created_by,
|
|
333
|
+
)
|
|
334
|
+
data = self.request(
|
|
335
|
+
"POST",
|
|
336
|
+
"/messenger/api/prompts/",
|
|
337
|
+
json=body.model_dump(exclude_none=True),
|
|
338
|
+
)
|
|
339
|
+
return PromptCreateResponse.model_validate(data)
|
|
340
|
+
|
|
341
|
+
def get_prompt(self, prompt_id: str) -> Prompt:
|
|
342
|
+
"""Fetch a prompt by ID.
|
|
343
|
+
|
|
344
|
+
Maps to ``GET /messenger/api/prompts/{prompt_id}``.
|
|
345
|
+
"""
|
|
346
|
+
data = self.request("GET", f"/messenger/api/prompts/{prompt_id}")
|
|
347
|
+
return Prompt.model_validate(data)
|
|
348
|
+
|
|
349
|
+
# --- Reports ---
|
|
350
|
+
|
|
351
|
+
def list_channel_contacts(
|
|
352
|
+
self,
|
|
353
|
+
channel_id: str,
|
|
354
|
+
*,
|
|
355
|
+
id: Optional[str] = None,
|
|
356
|
+
page: int = 1,
|
|
357
|
+
page_size: int = 20,
|
|
358
|
+
) -> Paginated[ChannelContact]:
|
|
359
|
+
"""List report contacts for a channel.
|
|
360
|
+
|
|
361
|
+
Maps to ``GET /report/channel/{channel_id}/contacts``.
|
|
362
|
+
"""
|
|
363
|
+
params: Dict[str, Any] = {"page": page, "page_size": page_size}
|
|
364
|
+
if id is not None:
|
|
365
|
+
params["id"] = id
|
|
366
|
+
data = self.request(
|
|
367
|
+
"GET",
|
|
368
|
+
f"/report/channel/{channel_id}/contacts",
|
|
369
|
+
params=params,
|
|
370
|
+
)
|
|
371
|
+
return Paginated[ChannelContact].model_validate(data)
|
|
372
|
+
|
|
373
|
+
def list_channel_orders(
|
|
374
|
+
self,
|
|
375
|
+
channel_id: str,
|
|
376
|
+
*,
|
|
377
|
+
order_id: Optional[str] = None,
|
|
378
|
+
page: int = 1,
|
|
379
|
+
page_size: int = 20,
|
|
380
|
+
) -> Paginated[ChannelOrder]:
|
|
381
|
+
"""List report orders for a channel.
|
|
382
|
+
|
|
383
|
+
Maps to ``GET /report/channel/{channel_id}/orders``.
|
|
384
|
+
"""
|
|
385
|
+
params: Dict[str, Any] = {"page": page, "page_size": page_size}
|
|
386
|
+
if order_id is not None:
|
|
387
|
+
params["order_id"] = order_id
|
|
388
|
+
data = self.request(
|
|
389
|
+
"GET",
|
|
390
|
+
f"/report/channel/{channel_id}/orders",
|
|
391
|
+
params=params,
|
|
392
|
+
)
|
|
393
|
+
return Paginated[ChannelOrder].model_validate(data)
|
|
394
|
+
|
|
395
|
+
# --- Billing / usage ---
|
|
396
|
+
|
|
397
|
+
def get_usage_info(self) -> UsageInfo:
|
|
398
|
+
"""Return account usage and credit information.
|
|
399
|
+
|
|
400
|
+
Maps to ``GET /payment/get-usage-info/``.
|
|
401
|
+
"""
|
|
402
|
+
data = self.request("GET", "/payment/get-usage-info/")
|
|
403
|
+
return UsageInfo.model_validate(data)
|
|
404
|
+
|
|
405
|
+
def get_usage_activity(
|
|
406
|
+
self,
|
|
407
|
+
*,
|
|
408
|
+
feature: str,
|
|
409
|
+
activity_id: Optional[str] = None,
|
|
410
|
+
date: Optional[Union[str, datetime, date]] = None,
|
|
411
|
+
mode: Optional[str] = None,
|
|
412
|
+
start_date: Optional[Union[str, datetime, date]] = None,
|
|
413
|
+
end_date: Optional[Union[str, datetime, date]] = None,
|
|
414
|
+
) -> Union[UsageActivity, List[UsageActivityPeriodItem]]:
|
|
415
|
+
"""Fetch usage activity by id, date, mode, or period.
|
|
416
|
+
|
|
417
|
+
Maps to ``GET /payment/get-usage-activity/``.
|
|
418
|
+
|
|
419
|
+
``feature`` must be one of ``id``, ``date``, ``mode``, or ``period``.
|
|
420
|
+
Required companion query params depend on ``feature`` as documented.
|
|
421
|
+
"""
|
|
422
|
+
params: Dict[str, Any] = {"feature": feature}
|
|
423
|
+
if activity_id is not None:
|
|
424
|
+
params["activity_id"] = activity_id
|
|
425
|
+
if date is not None:
|
|
426
|
+
params["date"] = _serialize_datetime(date)
|
|
427
|
+
if mode is not None:
|
|
428
|
+
params["mode"] = mode
|
|
429
|
+
if start_date is not None:
|
|
430
|
+
params["start_date"] = _serialize_datetime(start_date)
|
|
431
|
+
if end_date is not None:
|
|
432
|
+
params["end_date"] = _serialize_datetime(end_date)
|
|
433
|
+
|
|
434
|
+
data = self.request("GET", "/payment/get-usage-activity/", params=params)
|
|
435
|
+
envelope = UsageActivityEnvelope.model_validate(data)
|
|
436
|
+
if envelope.response is None:
|
|
437
|
+
raise VardastAPIError("Usage activity response was empty", detail=data)
|
|
438
|
+
return envelope.response
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class AsyncVardast(_BaseClient):
|
|
442
|
+
"""Asynchronous client for the Vardast Public API.
|
|
443
|
+
|
|
444
|
+
Example::
|
|
445
|
+
|
|
446
|
+
async with AsyncVardast(api_key="your-api-key") as client:
|
|
447
|
+
channels = await client.list_channels()
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
def __init__(
|
|
451
|
+
self,
|
|
452
|
+
api_key: str,
|
|
453
|
+
*,
|
|
454
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
455
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
456
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
457
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
458
|
+
) -> None:
|
|
459
|
+
super().__init__(api_key, base_url=base_url, timeout=timeout, headers=headers)
|
|
460
|
+
self._owns_client = http_client is None
|
|
461
|
+
self._client = http_client or httpx.AsyncClient(
|
|
462
|
+
base_url=f"{self.base_url}/",
|
|
463
|
+
headers=self._default_headers(),
|
|
464
|
+
timeout=self.timeout,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
async def aclose(self) -> None:
|
|
468
|
+
if self._owns_client:
|
|
469
|
+
await self._client.aclose()
|
|
470
|
+
|
|
471
|
+
async def __aenter__(self) -> "AsyncVardast":
|
|
472
|
+
return self
|
|
473
|
+
|
|
474
|
+
async def __aexit__(
|
|
475
|
+
self,
|
|
476
|
+
exc_type: Optional[Type[BaseException]],
|
|
477
|
+
exc: Optional[BaseException],
|
|
478
|
+
tb: Optional[TracebackType],
|
|
479
|
+
) -> None:
|
|
480
|
+
await self.aclose()
|
|
481
|
+
|
|
482
|
+
async def request(
|
|
483
|
+
self,
|
|
484
|
+
method: str,
|
|
485
|
+
path: str,
|
|
486
|
+
*,
|
|
487
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
488
|
+
json: Optional[Any] = None,
|
|
489
|
+
) -> Any:
|
|
490
|
+
url = path if path.startswith("http") else path.lstrip("/")
|
|
491
|
+
response = await self._client.request(
|
|
492
|
+
method,
|
|
493
|
+
url,
|
|
494
|
+
params=_clean_params(params),
|
|
495
|
+
json=json,
|
|
496
|
+
)
|
|
497
|
+
_raise_for_status(response)
|
|
498
|
+
if response.status_code == 204 or not response.content:
|
|
499
|
+
return None
|
|
500
|
+
return response.json()
|
|
501
|
+
|
|
502
|
+
async def process_message(
|
|
503
|
+
self,
|
|
504
|
+
*,
|
|
505
|
+
message: str,
|
|
506
|
+
channel_id: str,
|
|
507
|
+
contact_id: str,
|
|
508
|
+
assistant_id: Optional[str] = None,
|
|
509
|
+
) -> ProcessChatResponse:
|
|
510
|
+
body = ProcessChatRequest(
|
|
511
|
+
message=message,
|
|
512
|
+
channel_id=channel_id,
|
|
513
|
+
contact_id=contact_id,
|
|
514
|
+
assistant_id=assistant_id,
|
|
515
|
+
)
|
|
516
|
+
data = await self.request(
|
|
517
|
+
"POST",
|
|
518
|
+
"/messenger/api/chat/public/process",
|
|
519
|
+
json=body.model_dump(exclude_none=True),
|
|
520
|
+
)
|
|
521
|
+
return ProcessChatResponse.model_validate(data)
|
|
522
|
+
|
|
523
|
+
async def list_messages(
|
|
524
|
+
self,
|
|
525
|
+
channel_id: str,
|
|
526
|
+
contact_id: str,
|
|
527
|
+
*,
|
|
528
|
+
page: int = 1,
|
|
529
|
+
size: int = 20,
|
|
530
|
+
) -> Paginated[Message]:
|
|
531
|
+
data = await self.request(
|
|
532
|
+
"GET",
|
|
533
|
+
f"/messenger/api/chat/{channel_id}/{contact_id}/",
|
|
534
|
+
params={"page": page, "size": size},
|
|
535
|
+
)
|
|
536
|
+
return Paginated[Message].model_validate(data)
|
|
537
|
+
|
|
538
|
+
async def list_contacts(
|
|
539
|
+
self,
|
|
540
|
+
*,
|
|
541
|
+
page: int = 1,
|
|
542
|
+
size: int = 20,
|
|
543
|
+
channel_ids: Optional[Union[str, Sequence[str]]] = None,
|
|
544
|
+
platform: Optional[str] = None,
|
|
545
|
+
) -> Paginated[Contact]:
|
|
546
|
+
params: Dict[str, Any] = {"page": page, "size": size}
|
|
547
|
+
if channel_ids is not None:
|
|
548
|
+
params["channel_ids"] = (
|
|
549
|
+
channel_ids if isinstance(channel_ids, str) else ",".join(channel_ids)
|
|
550
|
+
)
|
|
551
|
+
if platform is not None:
|
|
552
|
+
params["platform"] = platform
|
|
553
|
+
data = await self.request("GET", "/messenger/api/chat/contacts/", params=params)
|
|
554
|
+
return Paginated[Contact].model_validate(data)
|
|
555
|
+
|
|
556
|
+
async def list_channels(self) -> List[Channel]:
|
|
557
|
+
data = await self.request("GET", "/messenger/api/channel/")
|
|
558
|
+
if isinstance(data, list):
|
|
559
|
+
return [Channel.model_validate(item) for item in data]
|
|
560
|
+
return ChannelsResponse.model_validate(data).items
|
|
561
|
+
|
|
562
|
+
async def list_assistants(self) -> List[Assistant]:
|
|
563
|
+
data = await self.request("GET", "/messenger/api/assistants/")
|
|
564
|
+
if isinstance(data, list):
|
|
565
|
+
return [Assistant.model_validate(item) for item in data]
|
|
566
|
+
return AssistantsResponse.model_validate(data).items
|
|
567
|
+
|
|
568
|
+
async def create_assistant(
|
|
569
|
+
self,
|
|
570
|
+
*,
|
|
571
|
+
assistant_name: str,
|
|
572
|
+
model: str,
|
|
573
|
+
api_key: str,
|
|
574
|
+
kb_id: Optional[str] = None,
|
|
575
|
+
functions: Optional[Sequence[str]] = None,
|
|
576
|
+
) -> Assistant:
|
|
577
|
+
body = AssistantCreate(
|
|
578
|
+
assistant_name=assistant_name,
|
|
579
|
+
model=model,
|
|
580
|
+
api_key=api_key,
|
|
581
|
+
kb_id=kb_id,
|
|
582
|
+
functions=list(functions) if functions is not None else None,
|
|
583
|
+
)
|
|
584
|
+
data = await self.request(
|
|
585
|
+
"POST",
|
|
586
|
+
"/messenger/api/assistants/",
|
|
587
|
+
json=body.model_dump(exclude_none=True),
|
|
588
|
+
)
|
|
589
|
+
return Assistant.model_validate(data)
|
|
590
|
+
|
|
591
|
+
async def create_prompt(
|
|
592
|
+
self,
|
|
593
|
+
*,
|
|
594
|
+
assistant_id: str,
|
|
595
|
+
text: str,
|
|
596
|
+
prompt_json: Mapping[str, Any],
|
|
597
|
+
language: str = "EN",
|
|
598
|
+
created_by: str,
|
|
599
|
+
) -> PromptCreateResponse:
|
|
600
|
+
body = PromptCreate(
|
|
601
|
+
assistant_id=assistant_id,
|
|
602
|
+
text=text,
|
|
603
|
+
prompt_json=dict(prompt_json),
|
|
604
|
+
language=language,
|
|
605
|
+
created_by=created_by,
|
|
606
|
+
)
|
|
607
|
+
data = await self.request(
|
|
608
|
+
"POST",
|
|
609
|
+
"/messenger/api/prompts/",
|
|
610
|
+
json=body.model_dump(exclude_none=True),
|
|
611
|
+
)
|
|
612
|
+
return PromptCreateResponse.model_validate(data)
|
|
613
|
+
|
|
614
|
+
async def get_prompt(self, prompt_id: str) -> Prompt:
|
|
615
|
+
data = await self.request("GET", f"/messenger/api/prompts/{prompt_id}")
|
|
616
|
+
return Prompt.model_validate(data)
|
|
617
|
+
|
|
618
|
+
async def list_channel_contacts(
|
|
619
|
+
self,
|
|
620
|
+
channel_id: str,
|
|
621
|
+
*,
|
|
622
|
+
id: Optional[str] = None,
|
|
623
|
+
page: int = 1,
|
|
624
|
+
page_size: int = 20,
|
|
625
|
+
) -> Paginated[ChannelContact]:
|
|
626
|
+
params: Dict[str, Any] = {"page": page, "page_size": page_size}
|
|
627
|
+
if id is not None:
|
|
628
|
+
params["id"] = id
|
|
629
|
+
data = await self.request(
|
|
630
|
+
"GET",
|
|
631
|
+
f"/report/channel/{channel_id}/contacts",
|
|
632
|
+
params=params,
|
|
633
|
+
)
|
|
634
|
+
return Paginated[ChannelContact].model_validate(data)
|
|
635
|
+
|
|
636
|
+
async def list_channel_orders(
|
|
637
|
+
self,
|
|
638
|
+
channel_id: str,
|
|
639
|
+
*,
|
|
640
|
+
order_id: Optional[str] = None,
|
|
641
|
+
page: int = 1,
|
|
642
|
+
page_size: int = 20,
|
|
643
|
+
) -> Paginated[ChannelOrder]:
|
|
644
|
+
params: Dict[str, Any] = {"page": page, "page_size": page_size}
|
|
645
|
+
if order_id is not None:
|
|
646
|
+
params["order_id"] = order_id
|
|
647
|
+
data = await self.request(
|
|
648
|
+
"GET",
|
|
649
|
+
f"/report/channel/{channel_id}/orders",
|
|
650
|
+
params=params,
|
|
651
|
+
)
|
|
652
|
+
return Paginated[ChannelOrder].model_validate(data)
|
|
653
|
+
|
|
654
|
+
async def get_usage_info(self) -> UsageInfo:
|
|
655
|
+
data = await self.request("GET", "/payment/get-usage-info/")
|
|
656
|
+
return UsageInfo.model_validate(data)
|
|
657
|
+
|
|
658
|
+
async def get_usage_activity(
|
|
659
|
+
self,
|
|
660
|
+
*,
|
|
661
|
+
feature: str,
|
|
662
|
+
activity_id: Optional[str] = None,
|
|
663
|
+
date: Optional[Union[str, datetime, date]] = None,
|
|
664
|
+
mode: Optional[str] = None,
|
|
665
|
+
start_date: Optional[Union[str, datetime, date]] = None,
|
|
666
|
+
end_date: Optional[Union[str, datetime, date]] = None,
|
|
667
|
+
) -> Union[UsageActivity, List[UsageActivityPeriodItem]]:
|
|
668
|
+
params: Dict[str, Any] = {"feature": feature}
|
|
669
|
+
if activity_id is not None:
|
|
670
|
+
params["activity_id"] = activity_id
|
|
671
|
+
if date is not None:
|
|
672
|
+
params["date"] = _serialize_datetime(date)
|
|
673
|
+
if mode is not None:
|
|
674
|
+
params["mode"] = mode
|
|
675
|
+
if start_date is not None:
|
|
676
|
+
params["start_date"] = _serialize_datetime(start_date)
|
|
677
|
+
if end_date is not None:
|
|
678
|
+
params["end_date"] = _serialize_datetime(end_date)
|
|
679
|
+
|
|
680
|
+
data = await self.request("GET", "/payment/get-usage-activity/", params=params)
|
|
681
|
+
envelope = UsageActivityEnvelope.model_validate(data)
|
|
682
|
+
if envelope.response is None:
|
|
683
|
+
raise VardastAPIError("Usage activity response was empty", detail=data)
|
|
684
|
+
return envelope.response
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _clean_params(params: Optional[Mapping[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
688
|
+
if params is None:
|
|
689
|
+
return None
|
|
690
|
+
return {key: value for key, value in params.items() if value is not None}
|
vardast/exceptions.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Exceptions raised by the Vardast SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VardastError(Exception):
|
|
9
|
+
"""Base exception for all Vardast SDK errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VardastAPIError(VardastError):
|
|
13
|
+
"""HTTP or API-level error returned by the Vardast Public API."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
*,
|
|
19
|
+
status_code: Optional[int] = None,
|
|
20
|
+
detail: Any = None,
|
|
21
|
+
response: Any = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.message = message
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.detail = detail
|
|
27
|
+
self.response = response
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
if self.status_code is not None:
|
|
31
|
+
return f"{self.status_code}: {self.message}"
|
|
32
|
+
return self.message
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AuthenticationError(VardastAPIError):
|
|
36
|
+
"""Raised when authentication fails (HTTP 401)."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NotFoundError(VardastAPIError):
|
|
40
|
+
"""Raised when a resource is not found (HTTP 404)."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ValidationError(VardastAPIError):
|
|
44
|
+
"""Raised when the request is invalid (HTTP 400 / 422)."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RateLimitError(VardastAPIError):
|
|
48
|
+
"""Raised when the API rate limit is exceeded (HTTP 429)."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ServerError(VardastAPIError):
|
|
52
|
+
"""Raised when the API returns a server error (HTTP 5xx)."""
|
vardast/models.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Pydantic models for Vardast Public API request and response payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APIModel(BaseModel):
|
|
14
|
+
"""Base model with permissive parsing for evolving API fields."""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Paginated(APIModel, Generic[T]):
|
|
20
|
+
"""Standard paginated list envelope used across messenger endpoints."""
|
|
21
|
+
|
|
22
|
+
items: List[T] = Field(default_factory=list)
|
|
23
|
+
total: int = 0
|
|
24
|
+
page: int = 1
|
|
25
|
+
size: int = 20
|
|
26
|
+
pages: int = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProcessChatRequest(APIModel):
|
|
30
|
+
message: str
|
|
31
|
+
channel_id: str
|
|
32
|
+
contact_id: str
|
|
33
|
+
assistant_id: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProcessChatResponse(APIModel):
|
|
37
|
+
status: str
|
|
38
|
+
message_id: Optional[str] = None
|
|
39
|
+
response: Optional[str] = None
|
|
40
|
+
error: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Message(APIModel):
|
|
44
|
+
id: str
|
|
45
|
+
text: Optional[str] = None
|
|
46
|
+
platform: Optional[str] = None
|
|
47
|
+
sender_id: Optional[str] = None
|
|
48
|
+
receiver_id: Optional[str] = None
|
|
49
|
+
is_output: Optional[bool] = None
|
|
50
|
+
ai_created: Optional[bool] = None
|
|
51
|
+
channel_id: Optional[str] = None
|
|
52
|
+
timestamp: Optional[datetime] = None
|
|
53
|
+
is_in_thread: Optional[bool] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Contact(APIModel):
|
|
57
|
+
id: str
|
|
58
|
+
name: Optional[str] = None
|
|
59
|
+
username: Optional[str] = None
|
|
60
|
+
identifier: Optional[str] = None
|
|
61
|
+
platform: Optional[str] = None
|
|
62
|
+
channel_id: Optional[str] = None
|
|
63
|
+
is_stopped: Optional[bool] = None
|
|
64
|
+
created_at: Optional[datetime] = None
|
|
65
|
+
updated_at: Optional[datetime] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Channel(APIModel):
|
|
69
|
+
id: str
|
|
70
|
+
name: Optional[str] = None
|
|
71
|
+
platform: Optional[str] = None
|
|
72
|
+
user_id: Optional[str] = None
|
|
73
|
+
identifier: Optional[str] = None
|
|
74
|
+
access_token: Optional[str] = None
|
|
75
|
+
assistant: Optional[str] = None
|
|
76
|
+
is_stopped: Optional[bool] = None
|
|
77
|
+
reply_to_story: Optional[bool] = None
|
|
78
|
+
reply_to_comments: Optional[bool] = None
|
|
79
|
+
account_id: Optional[str] = None
|
|
80
|
+
created_at: Optional[datetime] = None
|
|
81
|
+
updated_at: Optional[datetime] = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ChannelsResponse(APIModel):
|
|
85
|
+
items: List[Channel] = Field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Assistant(APIModel):
|
|
89
|
+
id: str
|
|
90
|
+
assistant_name: Optional[str] = None
|
|
91
|
+
model: Optional[str] = None
|
|
92
|
+
api_key: Optional[str] = None
|
|
93
|
+
kb_id: Optional[str] = None
|
|
94
|
+
functions: Optional[List[str]] = None
|
|
95
|
+
user_id: Optional[str] = None
|
|
96
|
+
created_at: Optional[datetime] = None
|
|
97
|
+
updated_at: Optional[datetime] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AssistantsResponse(APIModel):
|
|
101
|
+
items: List[Assistant] = Field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class AssistantCreate(APIModel):
|
|
105
|
+
assistant_name: str
|
|
106
|
+
model: str
|
|
107
|
+
api_key: str
|
|
108
|
+
kb_id: Optional[str] = None
|
|
109
|
+
functions: Optional[List[str]] = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PromptCreate(APIModel):
|
|
113
|
+
assistant_id: str
|
|
114
|
+
text: str
|
|
115
|
+
prompt_json: Dict[str, Any]
|
|
116
|
+
language: str = "EN"
|
|
117
|
+
created_by: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class PromptCreateResponse(APIModel):
|
|
121
|
+
prompt_id: str
|
|
122
|
+
created_at: Optional[datetime] = None
|
|
123
|
+
updated_at: Optional[datetime] = None
|
|
124
|
+
user_id: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Prompt(APIModel):
|
|
128
|
+
prompt_id: str
|
|
129
|
+
assistant_id: Optional[str] = None
|
|
130
|
+
text: Optional[str] = None
|
|
131
|
+
prompt_json: Optional[Dict[str, Any]] = None
|
|
132
|
+
language: Optional[str] = None
|
|
133
|
+
created_by: Optional[str] = None
|
|
134
|
+
created_at: Optional[datetime] = None
|
|
135
|
+
updated_at: Optional[datetime] = None
|
|
136
|
+
user_id: Optional[str] = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class OrderItem(APIModel):
|
|
140
|
+
product_name: str
|
|
141
|
+
quantity: int
|
|
142
|
+
variants: Optional[str] = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ChannelContact(APIModel):
|
|
146
|
+
id: str
|
|
147
|
+
contact_id: Optional[str] = None
|
|
148
|
+
channel_id: Optional[str] = None
|
|
149
|
+
full_name: Optional[str] = None
|
|
150
|
+
phone_number: Optional[str] = None
|
|
151
|
+
detailed_address: Optional[str] = None
|
|
152
|
+
postal_code: Optional[str] = None
|
|
153
|
+
username: Optional[str] = None
|
|
154
|
+
platform: Optional[str] = None
|
|
155
|
+
comment: Optional[str] = None
|
|
156
|
+
order_list: Optional[List[OrderItem]] = None
|
|
157
|
+
created_at: Optional[datetime] = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ChannelOrder(APIModel):
|
|
161
|
+
id: str
|
|
162
|
+
contact_id: Optional[str] = None
|
|
163
|
+
channel_id: Optional[str] = None
|
|
164
|
+
full_name: Optional[str] = None
|
|
165
|
+
phone_number: Optional[str] = None
|
|
166
|
+
detailed_address: Optional[str] = None
|
|
167
|
+
postal_code: Optional[str] = None
|
|
168
|
+
username: Optional[str] = None
|
|
169
|
+
platform: Optional[str] = None
|
|
170
|
+
comment: Optional[str] = None
|
|
171
|
+
order_list: Optional[List[OrderItem]] = None
|
|
172
|
+
created_at: Optional[datetime] = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class UsageInfoData(APIModel):
|
|
176
|
+
user_id: Optional[str] = None
|
|
177
|
+
mode: Optional[str] = None
|
|
178
|
+
active_features: Optional[Dict[str, bool]] = None
|
|
179
|
+
chat_credit: Optional[int] = None
|
|
180
|
+
all_chat_credit: Optional[int] = None
|
|
181
|
+
total_chats: Optional[int] = None
|
|
182
|
+
contact_credit: Optional[int] = None
|
|
183
|
+
total_contacts: Optional[int] = None
|
|
184
|
+
is_allowed: Optional[bool] = None
|
|
185
|
+
next_due_date: Optional[datetime] = None
|
|
186
|
+
time_credit: Optional[datetime] = None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class UsageInfo(APIModel):
|
|
190
|
+
status: Optional[str] = None
|
|
191
|
+
response: Optional[UsageInfoData] = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class UsageActivity(APIModel):
|
|
195
|
+
id: Optional[str] = None
|
|
196
|
+
user_id: Optional[str] = None
|
|
197
|
+
date: Optional[Union[date, datetime, str]] = None
|
|
198
|
+
new_contacts: Optional[int] = None
|
|
199
|
+
message_requests: Optional[int] = None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class UsageActivityPeriodItem(APIModel):
|
|
203
|
+
date: Optional[Union[date, datetime, str]] = None
|
|
204
|
+
total_new_contacts: Optional[int] = None
|
|
205
|
+
total_message_requests: Optional[int] = None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class UsageActivityEnvelope(APIModel):
|
|
209
|
+
status: Optional[str] = None
|
|
210
|
+
response: Optional[Union[UsageActivity, List[UsageActivityPeriodItem]]] = None
|
vardast/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker for PEP 561 typed package distribution.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vardast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Vardast Public API
|
|
5
|
+
Project-URL: Homepage, https://github.com/technicalvardast/vardast-python
|
|
6
|
+
Project-URL: Documentation, https://vardast.chat/docs/public-api
|
|
7
|
+
Project-URL: Repository, https://github.com/technicalvardast/vardast-python
|
|
8
|
+
Project-URL: Issues, https://github.com/technicalvardast/vardast-python/issues
|
|
9
|
+
Author-email: Vardast <hello@vardast.chat>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,api,chat,messenger,sdk,vardast
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx<1,>=0.27
|
|
26
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Vardast Python SDK
|
|
34
|
+
|
|
35
|
+
Official Python client for the [Vardast Public API](https://vardast.chat/docs/public-api).
|
|
36
|
+
|
|
37
|
+
Package name on PyPI: **`vardast`**
|
|
38
|
+
GitHub: [technicalvardast/vardast-python](https://github.com/technicalvardast/vardast-python)
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install vardast
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Python 3.9+.
|
|
47
|
+
|
|
48
|
+
## Setup
|
|
49
|
+
|
|
50
|
+
1. Create or copy a Public API key from the Vardast panel (channel settings / Public API).
|
|
51
|
+
2. Pass it to the client as `api_key`. The SDK sends it on every request as the `X-API-Key` header.
|
|
52
|
+
3. Default base URL (override only for a private gateway):
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
https://apigw.vardast.chat/uaa/public
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from vardast import Vardast
|
|
60
|
+
|
|
61
|
+
client = Vardast(
|
|
62
|
+
api_key="your-api-key",
|
|
63
|
+
# base_url="https://apigw.vardast.chat/uaa/public", # default
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Prefer a context manager so the HTTP client closes cleanly:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
with Vardast(api_key="your-api-key") as client:
|
|
71
|
+
channels = client.list_channels()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Async support is available via `AsyncVardast`.
|
|
75
|
+
|
|
76
|
+
## Quickstart
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from vardast import Vardast
|
|
80
|
+
|
|
81
|
+
with Vardast(api_key="your-api-key") as client:
|
|
82
|
+
channels = client.list_channels()
|
|
83
|
+
channel = channels[0]
|
|
84
|
+
|
|
85
|
+
result = client.process_message(
|
|
86
|
+
message="Hello",
|
|
87
|
+
channel_id=channel.id,
|
|
88
|
+
contact_id="contact-uuid",
|
|
89
|
+
)
|
|
90
|
+
print(result.response)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See `examples/quickstart.py` and `examples/list_assistants.py`.
|
|
94
|
+
|
|
95
|
+
## Method reference
|
|
96
|
+
|
|
97
|
+
Methods map 1:1 to the documented Public API surface.
|
|
98
|
+
|
|
99
|
+
| Method | HTTP | Path |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `process_message(...)` | `POST` | `/messenger/api/chat/public/process` |
|
|
102
|
+
| `list_messages(channel_id, contact_id, page=1, size=20)` | `GET` | `/messenger/api/chat/{channel_id}/{contact_id}/` |
|
|
103
|
+
| `list_contacts(page=1, size=20, channel_ids=None, platform=None)` | `GET` | `/messenger/api/chat/contacts/` |
|
|
104
|
+
| `list_channels()` | `GET` | `/messenger/api/channel/` |
|
|
105
|
+
| `list_assistants()` | `GET` | `/messenger/api/assistants/` |
|
|
106
|
+
| `create_assistant(...)` | `POST` | `/messenger/api/assistants/` |
|
|
107
|
+
| `create_prompt(...)` | `POST` | `/messenger/api/prompts/` |
|
|
108
|
+
| `get_prompt(prompt_id)` | `GET` | `/messenger/api/prompts/{prompt_id}` |
|
|
109
|
+
| `list_channel_contacts(channel_id, id=None, page=1, page_size=20)` | `GET` | `/report/channel/{channel_id}/contacts` |
|
|
110
|
+
| `list_channel_orders(channel_id, order_id=None, page=1, page_size=20)` | `GET` | `/report/channel/{channel_id}/orders` |
|
|
111
|
+
| `get_usage_info()` | `GET` | `/payment/get-usage-info/` |
|
|
112
|
+
| `get_usage_activity(feature=..., ...)` | `GET` | `/payment/get-usage-activity/` |
|
|
113
|
+
|
|
114
|
+
`channel_ids` may be a comma-separated string or a sequence of IDs.
|
|
115
|
+
`get_usage_activity` requires `feature` in `{id, date, mode, period}` plus the matching query fields from the docs.
|
|
116
|
+
|
|
117
|
+
Responses are typed Pydantic models (`Message`, `Channel`, `Assistant`, `Paginated[...]`, and so on). Extra JSON fields from the API are preserved.
|
|
118
|
+
|
|
119
|
+
### Errors
|
|
120
|
+
|
|
121
|
+
| Exception | When |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `AuthenticationError` | HTTP 401 |
|
|
124
|
+
| `NotFoundError` | HTTP 404 |
|
|
125
|
+
| `ValidationError` | HTTP 400 / 422 |
|
|
126
|
+
| `RateLimitError` | HTTP 429 |
|
|
127
|
+
| `ServerError` | HTTP 5xx |
|
|
128
|
+
| `VardastAPIError` | Other API failures |
|
|
129
|
+
| `VardastError` | Client configuration errors |
|
|
130
|
+
|
|
131
|
+
API error bodies of the form `{"detail": "..."}` are surfaced on `exc.message` / `exc.detail`.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cd vardast-python
|
|
137
|
+
python -m venv .venv
|
|
138
|
+
source .venv/bin/activate
|
|
139
|
+
pip install -e ".[dev]"
|
|
140
|
+
pytest
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Publish to PyPI
|
|
144
|
+
|
|
145
|
+
### One-time Trusted Publisher setup (recommended)
|
|
146
|
+
|
|
147
|
+
1. Use the GitHub repository `technicalvardast/vardast-python`.
|
|
148
|
+
2. On [PyPI](https://pypi.org/manage/account/publishing/), add a Trusted Publisher:
|
|
149
|
+
- Owner: `technicalvardast`
|
|
150
|
+
- Repository: `vardast-python`
|
|
151
|
+
- Workflow: `publish.yml`
|
|
152
|
+
- Environment: `pypi` (optional but recommended)
|
|
153
|
+
3. Create a GitHub Environment named `pypi` if you scoped the publisher that way.
|
|
154
|
+
|
|
155
|
+
### Release with a version tag
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# bump version in pyproject.toml and src/vardast/__init__.py
|
|
159
|
+
git commit -am "Release v0.1.0"
|
|
160
|
+
git tag v0.1.0
|
|
161
|
+
git push origin main --tags
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Pushing a tag matching `v*` runs `.github/workflows/publish.yml`, which builds the sdist/wheel and uploads to PyPI via OIDC (Trusted Publisher). No long-lived PyPI token is required.
|
|
165
|
+
|
|
166
|
+
### Manual / token publish (fallback)
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pip install build twine
|
|
170
|
+
python -m build
|
|
171
|
+
TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-... twine upload dist/*
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Store the token as the `PYPI_API_TOKEN` repository secret if you switch the publish workflow to token auth.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT — see [LICENSE](LICENSE).
|
|
179
|
+
|
|
180
|
+
## Docs
|
|
181
|
+
|
|
182
|
+
- Public API: https://vardast.chat/docs/public-api
|
|
183
|
+
- Product (merchant catalog) API is separate and **not** covered by this SDK.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
vardast/__init__.py,sha256=DVj1L0gxLaf-gKP_kbkJiM1HndumdyJhmnVaSSS-prk,1185
|
|
2
|
+
vardast/client.py,sha256=lgG8hWiVo1fQ0e9h6XvoI7XiKOGzCqjLPK9C320PxQ0,21200
|
|
3
|
+
vardast/exceptions.py,sha256=op1GDt5jqd_BHmgbPJLmzJyOMOv4p0kIGvMu2Y91zvs,1327
|
|
4
|
+
vardast/models.py,sha256=HUUqNiepGsY7jWlABWQAO27TOYMhbhhnBHlxt7x64Z0,5772
|
|
5
|
+
vardast/py.typed,sha256=lMktV_2kqa2aDRWTF8a7dClMxxIUMAbf1PcXU0nilsk,49
|
|
6
|
+
vardast-0.1.0.dist-info/METADATA,sha256=ku-Nvef031oHjiES2fAUituxMUgS7CRJ5N7vDeKypLw,6002
|
|
7
|
+
vardast-0.1.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
8
|
+
vardast-0.1.0.dist-info/licenses/LICENSE,sha256=pe6mAA_r1Sg12HXoy5hKY5wYJIaTumzv_XqJd6GJkdE,1064
|
|
9
|
+
vardast-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vardast
|
|
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.
|