synquic-slide 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.
@@ -0,0 +1,62 @@
1
+ from .client import AsyncSlideClient, SlideClient
2
+ from .errors import (
3
+ SlideAuthError,
4
+ SlideError,
5
+ SlideNotFoundError,
6
+ SlideScopeError,
7
+ SlideValidationError,
8
+ )
9
+ from .types import (
10
+ Contact,
11
+ DiscountValidation,
12
+ EmailContact,
13
+ EmailTemplate,
14
+ InitiateOutboundCallResponse,
15
+ InstagramConversation,
16
+ InstagramProfile,
17
+ PaginatedResponse,
18
+ PaginationMeta,
19
+ SendEmailResponse,
20
+ SendInstagramMessageResponse,
21
+ SendWhatsAppTemplateResponse,
22
+ ShopifyCollection,
23
+ ShopifyOrder,
24
+ ShopifyProduct,
25
+ VoiceAgent,
26
+ VoiceAnalytics,
27
+ VoiceCall,
28
+ WhatsAppLog,
29
+ WhatsAppTemplate,
30
+ )
31
+
32
+ __all__ = [
33
+ "SlideClient",
34
+ "AsyncSlideClient",
35
+ "SlideError",
36
+ "SlideAuthError",
37
+ "SlideScopeError",
38
+ "SlideNotFoundError",
39
+ "SlideValidationError",
40
+ "PaginationMeta",
41
+ "PaginatedResponse",
42
+ "Contact",
43
+ "SendEmailResponse",
44
+ "EmailTemplate",
45
+ "EmailContact",
46
+ "SendWhatsAppTemplateResponse",
47
+ "WhatsAppLog",
48
+ "WhatsAppTemplate",
49
+ "InstagramProfile",
50
+ "InstagramConversation",
51
+ "SendInstagramMessageResponse",
52
+ "ShopifyProduct",
53
+ "ShopifyCollection",
54
+ "ShopifyOrder",
55
+ "DiscountValidation",
56
+ "VoiceAgent",
57
+ "VoiceCall",
58
+ "InitiateOutboundCallResponse",
59
+ "VoiceAnalytics",
60
+ ]
61
+
62
+ __version__ = "0.1.0"
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import httpx
6
+
7
+ from .errors import SlideAuthError, SlideError, SlideNotFoundError, SlideScopeError, SlideValidationError
8
+ from .resources import (
9
+ AsyncContactsResource,
10
+ AsyncEmailResource,
11
+ AsyncInstagramResource,
12
+ AsyncShopifyResource,
13
+ AsyncVoiceResource,
14
+ AsyncWhatsAppResource,
15
+ ContactsResource,
16
+ EmailResource,
17
+ InstagramResource,
18
+ ShopifyResource,
19
+ VoiceResource,
20
+ WhatsAppResource,
21
+ )
22
+
23
+ _DEFAULT_BASE_URL = "https://slide.synquic.com/api"
24
+ _DEFAULT_TIMEOUT = 30.0
25
+
26
+
27
+ def _clean_params(params: Optional[Dict]) -> Optional[Dict]:
28
+ if params is None:
29
+ return None
30
+ return {k: v for k, v in params.items() if v is not None}
31
+
32
+
33
+ def _raise_for_status(response: httpx.Response) -> None:
34
+ if response.is_success:
35
+ return
36
+ try:
37
+ body = response.json()
38
+ except Exception:
39
+ body = {"message": response.text}
40
+ message: str = body.get("message", response.reason_phrase or "Request failed")
41
+
42
+ if response.status_code == 400:
43
+ raise SlideValidationError(message, body)
44
+ if response.status_code == 401:
45
+ raise SlideAuthError(message)
46
+ if response.status_code == 403:
47
+ raise SlideScopeError(message)
48
+ if response.status_code == 404:
49
+ raise SlideNotFoundError(message)
50
+ raise SlideError(response.status_code, message, body)
51
+
52
+
53
+ class SlideClient:
54
+ """Synchronous Slide API client."""
55
+
56
+ def __init__(
57
+ self,
58
+ *,
59
+ api_key: str,
60
+ base_url: str = _DEFAULT_BASE_URL,
61
+ timeout: float = _DEFAULT_TIMEOUT,
62
+ ) -> None:
63
+ if not api_key:
64
+ raise ValueError("api_key is required")
65
+ self._http = httpx.Client(
66
+ base_url=base_url.rstrip("/") + "/",
67
+ headers={"Authorization": f"Bearer {api_key}"},
68
+ timeout=timeout,
69
+ )
70
+ self.contacts = ContactsResource(self)
71
+ self.email = EmailResource(self)
72
+ self.whatsapp = WhatsAppResource(self)
73
+ self.instagram = InstagramResource(self)
74
+ self.shopify = ShopifyResource(self)
75
+ self.voice = VoiceResource(self)
76
+
77
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
78
+ if "params" in kwargs:
79
+ kwargs["params"] = _clean_params(kwargs["params"])
80
+ response = self._http.request(method, path, **kwargs)
81
+ _raise_for_status(response)
82
+ if response.status_code == 204:
83
+ return None
84
+ return response.json()
85
+
86
+ def _request_multipart(self, method: str, path: str, *, files: Dict, data: Dict) -> Any:
87
+ response = self._http.request(method, path, files=files, data=data)
88
+ _raise_for_status(response)
89
+ return response.json()
90
+
91
+ def close(self) -> None:
92
+ self._http.close()
93
+
94
+ def __enter__(self) -> SlideClient:
95
+ return self
96
+
97
+ def __exit__(self, *_: Any) -> None:
98
+ self.close()
99
+
100
+
101
+ class AsyncSlideClient:
102
+ """Async Slide API client — use with `async with` or `await client.close()`."""
103
+
104
+ def __init__(
105
+ self,
106
+ *,
107
+ api_key: str,
108
+ base_url: str = _DEFAULT_BASE_URL,
109
+ timeout: float = _DEFAULT_TIMEOUT,
110
+ ) -> None:
111
+ if not api_key:
112
+ raise ValueError("api_key is required")
113
+ self._http = httpx.AsyncClient(
114
+ base_url=base_url.rstrip("/") + "/",
115
+ headers={"Authorization": f"Bearer {api_key}"},
116
+ timeout=timeout,
117
+ )
118
+ self.contacts = AsyncContactsResource(self)
119
+ self.email = AsyncEmailResource(self)
120
+ self.whatsapp = AsyncWhatsAppResource(self)
121
+ self.instagram = AsyncInstagramResource(self)
122
+ self.shopify = AsyncShopifyResource(self)
123
+ self.voice = AsyncVoiceResource(self)
124
+
125
+ async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
126
+ if "params" in kwargs:
127
+ kwargs["params"] = _clean_params(kwargs["params"])
128
+ response = await self._http.request(method, path, **kwargs)
129
+ _raise_for_status(response)
130
+ if response.status_code == 204:
131
+ return None
132
+ return response.json()
133
+
134
+ async def _request_multipart(self, method: str, path: str, *, files: Dict, data: Dict) -> Any:
135
+ response = await self._http.request(method, path, files=files, data=data)
136
+ _raise_for_status(response)
137
+ return response.json()
138
+
139
+ async def close(self) -> None:
140
+ await self._http.aclose()
141
+
142
+ async def __aenter__(self) -> AsyncSlideClient:
143
+ return self
144
+
145
+ async def __aexit__(self, *_: Any) -> None:
146
+ await self.close()
@@ -0,0 +1,28 @@
1
+ from typing import Any, Optional
2
+
3
+
4
+ class SlideError(Exception):
5
+ def __init__(self, status_code: int, message: str, body: Optional[Any] = None) -> None:
6
+ super().__init__(message)
7
+ self.status_code = status_code
8
+ self.body = body
9
+
10
+
11
+ class SlideAuthError(SlideError):
12
+ def __init__(self, message: str = "Invalid or missing API key") -> None:
13
+ super().__init__(401, message)
14
+
15
+
16
+ class SlideScopeError(SlideError):
17
+ def __init__(self, message: str) -> None:
18
+ super().__init__(403, message)
19
+
20
+
21
+ class SlideNotFoundError(SlideError):
22
+ def __init__(self, message: str) -> None:
23
+ super().__init__(404, message)
24
+
25
+
26
+ class SlideValidationError(SlideError):
27
+ def __init__(self, message: str, body: Optional[Any] = None) -> None:
28
+ super().__init__(400, message, body)
@@ -0,0 +1,15 @@
1
+ from .contacts import ContactsResource, AsyncContactsResource
2
+ from .email import EmailResource, AsyncEmailResource
3
+ from .whatsapp import WhatsAppResource, AsyncWhatsAppResource
4
+ from .instagram import InstagramResource, AsyncInstagramResource
5
+ from .shopify import ShopifyResource, AsyncShopifyResource
6
+ from .voice import VoiceResource, AsyncVoiceResource
7
+
8
+ __all__ = [
9
+ "ContactsResource", "AsyncContactsResource",
10
+ "EmailResource", "AsyncEmailResource",
11
+ "WhatsAppResource", "AsyncWhatsAppResource",
12
+ "InstagramResource", "AsyncInstagramResource",
13
+ "ShopifyResource", "AsyncShopifyResource",
14
+ "VoiceResource", "AsyncVoiceResource",
15
+ ]
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ if TYPE_CHECKING:
5
+ from ..client import SlideClient, AsyncSlideClient
6
+
7
+ from ..types import Contact, PaginatedResponse
8
+
9
+
10
+ class ContactsResource:
11
+ def __init__(self, client: SlideClient) -> None:
12
+ self._c = client
13
+
14
+ def list(
15
+ self,
16
+ *,
17
+ page: int = 1,
18
+ limit: int = 50,
19
+ search: Optional[str] = None,
20
+ ) -> PaginatedResponse[Contact]:
21
+ return self._c._request("GET", "v1/contacts", params={
22
+ "page": page, "limit": limit, "search": search,
23
+ })
24
+
25
+
26
+ class AsyncContactsResource:
27
+ def __init__(self, client: AsyncSlideClient) -> None:
28
+ self._c = client
29
+
30
+ async def list(
31
+ self,
32
+ *,
33
+ page: int = 1,
34
+ limit: int = 50,
35
+ search: Optional[str] = None,
36
+ ) -> PaginatedResponse[Contact]:
37
+ return await self._c._request("GET", "v1/contacts", params={
38
+ "page": page, "limit": limit, "search": search,
39
+ })
@@ -0,0 +1,158 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Dict, List, Optional
3
+
4
+ if TYPE_CHECKING:
5
+ from ..client import SlideClient, AsyncSlideClient
6
+
7
+ from ..types import (
8
+ EmailContact,
9
+ EmailTemplate,
10
+ PaginatedResponse,
11
+ SendEmailResponse,
12
+ )
13
+
14
+
15
+ class EmailResource:
16
+ def __init__(self, client: SlideClient) -> None:
17
+ self._c = client
18
+
19
+ def send(
20
+ self,
21
+ *,
22
+ recipient: Dict,
23
+ template_id: str,
24
+ from_name: str,
25
+ from_email: str,
26
+ subject: Optional[str] = None,
27
+ reply_to: Optional[str] = None,
28
+ variables: Optional[Dict[str, str]] = None,
29
+ ) -> SendEmailResponse:
30
+ body = {
31
+ "recipient": recipient,
32
+ "templateId": template_id,
33
+ "fromName": from_name,
34
+ "fromEmail": from_email,
35
+ }
36
+ if subject is not None:
37
+ body["subject"] = subject
38
+ if reply_to is not None:
39
+ body["replyTo"] = reply_to
40
+ if variables is not None:
41
+ body["variables"] = variables
42
+ return self._c._request("POST", "v1/email/send", json=body)
43
+
44
+ def list_templates(self) -> Dict:
45
+ return self._c._request("GET", "v1/email/templates")
46
+
47
+ def get_template(self, template_id: str) -> EmailTemplate:
48
+ return self._c._request("GET", f"v1/email/templates/{template_id}")
49
+
50
+ def list_contacts(
51
+ self,
52
+ *,
53
+ page: int = 1,
54
+ limit: int = 20,
55
+ search: Optional[str] = None,
56
+ ) -> PaginatedResponse[EmailContact]:
57
+ return self._c._request("GET", "v1/email/contacts", params={
58
+ "page": page, "limit": limit, "search": search,
59
+ })
60
+
61
+ def upsert_contact(
62
+ self,
63
+ *,
64
+ email: str,
65
+ first_name: Optional[str] = None,
66
+ last_name: Optional[str] = None,
67
+ phone: Optional[str] = None,
68
+ company: Optional[str] = None,
69
+ tags: Optional[List[str]] = None,
70
+ custom_fields: Optional[Dict[str, str]] = None,
71
+ ) -> EmailContact:
72
+ body: Dict = {"email": email}
73
+ if first_name is not None:
74
+ body["firstName"] = first_name
75
+ if last_name is not None:
76
+ body["lastName"] = last_name
77
+ if phone is not None:
78
+ body["phone"] = phone
79
+ if company is not None:
80
+ body["company"] = company
81
+ if tags is not None:
82
+ body["tags"] = tags
83
+ if custom_fields is not None:
84
+ body["customFields"] = custom_fields
85
+ return self._c._request("POST", "v1/email/contacts", json=body)
86
+
87
+
88
+ class AsyncEmailResource:
89
+ def __init__(self, client: AsyncSlideClient) -> None:
90
+ self._c = client
91
+
92
+ async def send(
93
+ self,
94
+ *,
95
+ recipient: Dict,
96
+ template_id: str,
97
+ from_name: str,
98
+ from_email: str,
99
+ subject: Optional[str] = None,
100
+ reply_to: Optional[str] = None,
101
+ variables: Optional[Dict[str, str]] = None,
102
+ ) -> SendEmailResponse:
103
+ body = {
104
+ "recipient": recipient,
105
+ "templateId": template_id,
106
+ "fromName": from_name,
107
+ "fromEmail": from_email,
108
+ }
109
+ if subject is not None:
110
+ body["subject"] = subject
111
+ if reply_to is not None:
112
+ body["replyTo"] = reply_to
113
+ if variables is not None:
114
+ body["variables"] = variables
115
+ return await self._c._request("POST", "v1/email/send", json=body)
116
+
117
+ async def list_templates(self) -> Dict:
118
+ return await self._c._request("GET", "v1/email/templates")
119
+
120
+ async def get_template(self, template_id: str) -> EmailTemplate:
121
+ return await self._c._request("GET", f"v1/email/templates/{template_id}")
122
+
123
+ async def list_contacts(
124
+ self,
125
+ *,
126
+ page: int = 1,
127
+ limit: int = 20,
128
+ search: Optional[str] = None,
129
+ ) -> PaginatedResponse[EmailContact]:
130
+ return await self._c._request("GET", "v1/email/contacts", params={
131
+ "page": page, "limit": limit, "search": search,
132
+ })
133
+
134
+ async def upsert_contact(
135
+ self,
136
+ *,
137
+ email: str,
138
+ first_name: Optional[str] = None,
139
+ last_name: Optional[str] = None,
140
+ phone: Optional[str] = None,
141
+ company: Optional[str] = None,
142
+ tags: Optional[List[str]] = None,
143
+ custom_fields: Optional[Dict[str, str]] = None,
144
+ ) -> EmailContact:
145
+ body: Dict = {"email": email}
146
+ if first_name is not None:
147
+ body["firstName"] = first_name
148
+ if last_name is not None:
149
+ body["lastName"] = last_name
150
+ if phone is not None:
151
+ body["phone"] = phone
152
+ if company is not None:
153
+ body["company"] = company
154
+ if tags is not None:
155
+ body["tags"] = tags
156
+ if custom_fields is not None:
157
+ body["customFields"] = custom_fields
158
+ return await self._c._request("POST", "v1/email/contacts", json=body)
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Dict, Optional
3
+
4
+ if TYPE_CHECKING:
5
+ from ..client import SlideClient, AsyncSlideClient
6
+
7
+ from ..types import (
8
+ InstagramConversation,
9
+ InstagramProfile,
10
+ PaginatedResponse,
11
+ SendInstagramMessageResponse,
12
+ )
13
+
14
+
15
+ class InstagramResource:
16
+ def __init__(self, client: SlideClient) -> None:
17
+ self._c = client
18
+
19
+ def get_profile(self) -> InstagramProfile:
20
+ return self._c._request("GET", "v1/instagram/profile")
21
+
22
+ def list_conversations(
23
+ self, *, page: int = 1, limit: int = 20
24
+ ) -> PaginatedResponse[InstagramConversation]:
25
+ return self._c._request("GET", "v1/instagram/conversations", params={"page": page, "limit": limit})
26
+
27
+ def send_message(
28
+ self, *, recipient_igsid: str, message: str
29
+ ) -> SendInstagramMessageResponse:
30
+ return self._c._request("POST", "v1/instagram/messages", json={
31
+ "recipientIgsid": recipient_igsid,
32
+ "message": message,
33
+ })
34
+
35
+ def get_insights(
36
+ self,
37
+ *,
38
+ period: Optional[str] = None,
39
+ since: Optional[str] = None,
40
+ until: Optional[str] = None,
41
+ ) -> Dict:
42
+ return self._c._request("GET", "v1/instagram/insights", params={
43
+ "period": period, "since": since, "until": until,
44
+ })
45
+
46
+
47
+ class AsyncInstagramResource:
48
+ def __init__(self, client: AsyncSlideClient) -> None:
49
+ self._c = client
50
+
51
+ async def get_profile(self) -> InstagramProfile:
52
+ return await self._c._request("GET", "v1/instagram/profile")
53
+
54
+ async def list_conversations(
55
+ self, *, page: int = 1, limit: int = 20
56
+ ) -> PaginatedResponse[InstagramConversation]:
57
+ return await self._c._request("GET", "v1/instagram/conversations", params={"page": page, "limit": limit})
58
+
59
+ async def send_message(
60
+ self, *, recipient_igsid: str, message: str
61
+ ) -> SendInstagramMessageResponse:
62
+ return await self._c._request("POST", "v1/instagram/messages", json={
63
+ "recipientIgsid": recipient_igsid,
64
+ "message": message,
65
+ })
66
+
67
+ async def get_insights(
68
+ self,
69
+ *,
70
+ period: Optional[str] = None,
71
+ since: Optional[str] = None,
72
+ until: Optional[str] = None,
73
+ ) -> Dict:
74
+ return await self._c._request("GET", "v1/instagram/insights", params={
75
+ "period": period, "since": since, "until": until,
76
+ })
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Dict, List, Optional, Union
3
+
4
+ if TYPE_CHECKING:
5
+ from ..client import SlideClient, AsyncSlideClient
6
+
7
+ from ..types import (
8
+ DiscountValidation,
9
+ ShopifyCollection,
10
+ ShopifyOrder,
11
+ ShopifyProduct,
12
+ )
13
+
14
+
15
+ class ShopifyResource:
16
+ def __init__(self, client: SlideClient) -> None:
17
+ self._c = client
18
+
19
+ def search_products(self, *, q: str, limit: int = 10) -> Dict:
20
+ return self._c._request("GET", "v1/shopify/products/search", params={"q": q, "limit": limit})
21
+
22
+ def get_product_types(self) -> Dict:
23
+ return self._c._request("GET", "v1/shopify/products/types")
24
+
25
+ def get_product(self, product_id: str) -> ShopifyProduct:
26
+ return self._c._request("GET", f"v1/shopify/products/{product_id}")
27
+
28
+ def list_collections(self) -> Dict:
29
+ return self._c._request("GET", "v1/shopify/collections")
30
+
31
+ def get_collection_products(self, collection_id: str, *, limit: int = 50) -> Dict:
32
+ return self._c._request("GET", f"v1/shopify/collections/{collection_id}/products", params={"limit": limit})
33
+
34
+ def get_order_status(self, *, identifier: str) -> Union[ShopifyOrder, List[ShopifyOrder]]:
35
+ return self._c._request("GET", "v1/shopify/orders/status", params={"identifier": identifier})
36
+
37
+ def get_customer_orders(self, *, identifier: str) -> Dict:
38
+ return self._c._request("GET", "v1/shopify/customers/orders", params={"identifier": identifier})
39
+
40
+ def validate_discount(self, *, code: str) -> DiscountValidation:
41
+ return self._c._request("GET", "v1/shopify/discounts/validate", params={"code": code})
42
+
43
+
44
+ class AsyncShopifyResource:
45
+ def __init__(self, client: AsyncSlideClient) -> None:
46
+ self._c = client
47
+
48
+ async def search_products(self, *, q: str, limit: int = 10) -> Dict:
49
+ return await self._c._request("GET", "v1/shopify/products/search", params={"q": q, "limit": limit})
50
+
51
+ async def get_product_types(self) -> Dict:
52
+ return await self._c._request("GET", "v1/shopify/products/types")
53
+
54
+ async def get_product(self, product_id: str) -> ShopifyProduct:
55
+ return await self._c._request("GET", f"v1/shopify/products/{product_id}")
56
+
57
+ async def list_collections(self) -> Dict:
58
+ return await self._c._request("GET", "v1/shopify/collections")
59
+
60
+ async def get_collection_products(self, collection_id: str, *, limit: int = 50) -> Dict:
61
+ return await self._c._request("GET", f"v1/shopify/collections/{collection_id}/products", params={"limit": limit})
62
+
63
+ async def get_order_status(self, *, identifier: str) -> Union[ShopifyOrder, List[ShopifyOrder]]:
64
+ return await self._c._request("GET", "v1/shopify/orders/status", params={"identifier": identifier})
65
+
66
+ async def get_customer_orders(self, *, identifier: str) -> Dict:
67
+ return await self._c._request("GET", "v1/shopify/customers/orders", params={"identifier": identifier})
68
+
69
+ async def validate_discount(self, *, code: str) -> DiscountValidation:
70
+ return await self._c._request("GET", "v1/shopify/discounts/validate", params={"code": code})