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 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.