ccai-python 1.0.0__tar.gz

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,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: ccai-python
3
+ Version: 1.0.0
4
+ Summary: Python client for CloudContactAI API
5
+ Author: CloudContactAI LLC
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/cloudcontactai/ccai-python
8
+ Project-URL: Bug Tracker, https://github.com/cloudcontactai/ccai-python/issues
9
+ Keywords: sms,api,client,cloud,contact,ai,cloudcontactai
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests>=2.31.0
19
+ Requires-Dist: pydantic>=2.5.0
20
+
21
+ # CCAI Python Client
22
+
23
+ A Python client for interacting with the CloudContactAI API.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install ccai-python
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from ccai_python import CCAI
35
+
36
+ # Initialize the client
37
+ ccai = CCAI(
38
+ client_id="YOUR-CLIENT-ID",
39
+ api_key="YOUR-API-KEY"
40
+ )
41
+
42
+ # Send a single SMS
43
+ response = ccai.sms.send_single(
44
+ first_name="John",
45
+ last_name="Doe",
46
+ phone="+15551234567",
47
+ message="Hello ${first_name}, this is a test message!",
48
+ title="Test Campaign"
49
+ )
50
+
51
+ print(f"Message sent with ID: {response.id}")
52
+
53
+ # Send to multiple recipients
54
+ accounts = [
55
+ {"first_name": "John", "last_name": "Doe", "phone": "+15551234567"},
56
+ {"first_name": "Jane", "last_name": "Smith", "phone": "+15559876543"}
57
+ ]
58
+
59
+ campaign_response = ccai.sms.send(
60
+ accounts=accounts,
61
+ message="Hello ${first_name} ${last_name}, this is a test message!",
62
+ title="Bulk Test Campaign"
63
+ )
64
+
65
+ print(f"Campaign sent with ID: {campaign_response.campaign_id}")
66
+ ```
67
+
68
+ ## Features
69
+
70
+ - Send SMS messages to single or multiple recipients
71
+ - Variable substitution in messages
72
+ - Async support
73
+ - Type hints for better IDE integration
74
+ - Comprehensive error handling
75
+
76
+ ## Requirements
77
+
78
+ - Python 3.10 or higher
79
+ - `requests` library
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,63 @@
1
+ # CCAI Python Client
2
+
3
+ A Python client for interacting with the CloudContactAI API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install ccai-python
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from ccai_python import CCAI
15
+
16
+ # Initialize the client
17
+ ccai = CCAI(
18
+ client_id="YOUR-CLIENT-ID",
19
+ api_key="YOUR-API-KEY"
20
+ )
21
+
22
+ # Send a single SMS
23
+ response = ccai.sms.send_single(
24
+ first_name="John",
25
+ last_name="Doe",
26
+ phone="+15551234567",
27
+ message="Hello ${first_name}, this is a test message!",
28
+ title="Test Campaign"
29
+ )
30
+
31
+ print(f"Message sent with ID: {response.id}")
32
+
33
+ # Send to multiple recipients
34
+ accounts = [
35
+ {"first_name": "John", "last_name": "Doe", "phone": "+15551234567"},
36
+ {"first_name": "Jane", "last_name": "Smith", "phone": "+15559876543"}
37
+ ]
38
+
39
+ campaign_response = ccai.sms.send(
40
+ accounts=accounts,
41
+ message="Hello ${first_name} ${last_name}, this is a test message!",
42
+ title="Bulk Test Campaign"
43
+ )
44
+
45
+ print(f"Campaign sent with ID: {campaign_response.campaign_id}")
46
+ ```
47
+
48
+ ## Features
49
+
50
+ - Send SMS messages to single or multiple recipients
51
+ - Variable substitution in messages
52
+ - Async support
53
+ - Type hints for better IDE integration
54
+ - Comprehensive error handling
55
+
56
+ ## Requirements
57
+
58
+ - Python 3.10 or higher
59
+ - `requests` library
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ccai-python"
7
+ version = "1.0.0"
8
+ description = "Python client for CloudContactAI API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "CloudContactAI LLC"}
14
+ ]
15
+ keywords = ["sms", "api", "client", "cloud", "contact", "ai", "cloudcontactai"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ ]
24
+ dependencies = [
25
+ "requests>=2.31.0",
26
+ "pydantic>=2.5.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ "Homepage" = "https://github.com/cloudcontactai/ccai-python"
31
+ "Bug Tracker" = "https://github.com/cloudcontactai/ccai-python/issues"
32
+
33
+ [tool.setuptools]
34
+ package-dir = {"" = "src"}
35
+
36
+ [tool.pytest]
37
+ testpaths = ["tests"]
38
+
39
+ [tool.mypy]
40
+ python_version = "3.10"
41
+ warn_return_any = true
42
+ warn_unused_configs = true
43
+ disallow_untyped_defs = true
44
+ disallow_incomplete_defs = true
45
+
46
+ [tool.black]
47
+ line-length = 88
48
+ target-version = ["py310"]
49
+
50
+ [tool.isort]
51
+ profile = "black"
52
+ line_length = 88
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """
2
+ Main export file for the CCAI Python module
3
+
4
+ :license: MIT
5
+ :copyright: 2025 CloudContactAI LLC
6
+ """
7
+
8
+ from .ccai import CCAI, Account, CCAIConfig
9
+ from .sms.sms import SMS, SMSCampaign, SMSResponse, SMSOptions
10
+
11
+ __all__ = [
12
+ 'CCAI',
13
+ 'SMS',
14
+ 'Account',
15
+ 'CCAIConfig',
16
+ 'SMSCampaign',
17
+ 'SMSResponse',
18
+ 'SMSOptions'
19
+ ]
20
+
21
+ __version__ = '1.0.0'
@@ -0,0 +1,149 @@
1
+ """
2
+ ccai.py - A Python module for interacting with the Cloud Contact AI API
3
+ This module provides functionality to send SMS messages through the CCAI platform.
4
+
5
+ :license: MIT
6
+ :copyright: 2025 CloudContactAI LLC
7
+ """
8
+
9
+ from typing import Any, Dict, Optional, TypedDict, cast
10
+ import requests
11
+ from pydantic import BaseModel, Field
12
+
13
+ from .sms.sms import SMS
14
+
15
+
16
+ class Account(BaseModel):
17
+ """Account model representing a recipient"""
18
+ first_name: str = Field(..., description="Recipient's first name")
19
+ last_name: str = Field(..., description="Recipient's last name")
20
+ phone: str = Field(..., description="Recipient's phone number in E.164 format")
21
+
22
+
23
+ class CCAIConfig(BaseModel):
24
+ """Configuration for the CCAI client"""
25
+ client_id: str = Field(..., description="Client ID for authentication")
26
+ api_key: str = Field(..., description="API key for authentication")
27
+ base_url: str = Field(
28
+ default="https://core.cloudcontactai.com/api",
29
+ description="Base URL for the API"
30
+ )
31
+
32
+
33
+ class APIError(Exception):
34
+ """Exception raised for API errors"""
35
+ def __init__(self, status_code: int, message: str):
36
+ self.status_code = status_code
37
+ self.message = message
38
+ super().__init__(f"API Error: {status_code} - {message}")
39
+
40
+
41
+ class CCAI:
42
+ """
43
+ Main client for interacting with the CloudContactAI API
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ client_id: str,
49
+ api_key: str,
50
+ base_url: Optional[str] = None
51
+ ) -> None:
52
+ """
53
+ Create a new CCAI client instance
54
+
55
+ Args:
56
+ client_id: Client ID for authentication
57
+ api_key: API key for authentication
58
+ base_url: Optional base URL for the API
59
+
60
+ Raises:
61
+ ValueError: If client_id or api_key is not provided
62
+ """
63
+ if not client_id:
64
+ raise ValueError("Client ID is required")
65
+ if not api_key:
66
+ raise ValueError("API Key is required")
67
+
68
+ self._config = CCAIConfig(
69
+ client_id=client_id,
70
+ api_key=api_key,
71
+ base_url=base_url or "https://core.cloudcontactai.com/api"
72
+ )
73
+
74
+ # Initialize the SMS service
75
+ self.sms = SMS(self)
76
+
77
+ @property
78
+ def client_id(self) -> str:
79
+ """Get the client ID"""
80
+ return self._config.client_id
81
+
82
+ @property
83
+ def api_key(self) -> str:
84
+ """Get the API key"""
85
+ return self._config.api_key
86
+
87
+ @property
88
+ def base_url(self) -> str:
89
+ """Get the base URL"""
90
+ return self._config.base_url
91
+
92
+ def request(
93
+ self,
94
+ method: str,
95
+ endpoint: str,
96
+ data: Optional[Dict[str, Any]] = None,
97
+ timeout: int = 30
98
+ ) -> Dict[str, Any]:
99
+ """
100
+ Make an authenticated API request to the CCAI API
101
+
102
+ Args:
103
+ method: HTTP method (GET, POST, etc.)
104
+ endpoint: API endpoint
105
+ data: Request data
106
+ timeout: Request timeout in seconds
107
+
108
+ Returns:
109
+ API response as a dictionary
110
+
111
+ Raises:
112
+ APIError: If the API returns an error
113
+ requests.RequestException: For network-related errors
114
+ """
115
+ url = f"{self.base_url}{endpoint}"
116
+
117
+ headers = {
118
+ "Authorization": f"Bearer {self.api_key}",
119
+ "Content-Type": "application/json",
120
+ "Accept": "*/*"
121
+ }
122
+
123
+ try:
124
+ response = requests.request(
125
+ method=method.upper(),
126
+ url=url,
127
+ headers=headers,
128
+ json=data,
129
+ timeout=timeout
130
+ )
131
+
132
+ # Raise an exception for HTTP errors
133
+ response.raise_for_status()
134
+
135
+ return cast(Dict[str, Any], response.json())
136
+ except requests.HTTPError as e:
137
+ # Handle API errors with response
138
+ if e.response is not None:
139
+ try:
140
+ error_data = e.response.json()
141
+ error_message = str(error_data)
142
+ except (ValueError, TypeError):
143
+ error_message = e.response.text or str(e)
144
+
145
+ raise APIError(e.response.status_code, error_message)
146
+ raise
147
+ except requests.RequestException as e:
148
+ # Handle network errors
149
+ raise APIError(0, f"Network error: {str(e)}")
@@ -0,0 +1,248 @@
1
+ """
2
+ Async example using the CCAI Python client with asyncio
3
+
4
+ :license: MIT
5
+ :copyright: 2025 CloudContactAI LLC
6
+ """
7
+
8
+ import asyncio
9
+ from typing import Dict, Any, List
10
+
11
+ import aiohttp
12
+ from pydantic import BaseModel, Field
13
+
14
+ # Import the synchronous client for type definitions
15
+ from ccai_python import Account, SMSResponse, SMSOptions
16
+
17
+
18
+ class AsyncCCAI:
19
+ """
20
+ Async version of the CCAI client for CloudContactAI API
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ client_id: str,
26
+ api_key: str,
27
+ base_url: str = "https://core.cloudcontactai.com/api"
28
+ ) -> None:
29
+ """
30
+ Create a new async CCAI client instance
31
+
32
+ Args:
33
+ client_id: Client ID for authentication
34
+ api_key: API key for authentication
35
+ base_url: Base URL for the API
36
+ """
37
+ if not client_id:
38
+ raise ValueError("Client ID is required")
39
+ if not api_key:
40
+ raise ValueError("API Key is required")
41
+
42
+ self._client_id = client_id
43
+ self._api_key = api_key
44
+ self._base_url = base_url
45
+ self.sms = AsyncSMS(self)
46
+
47
+ @property
48
+ def client_id(self) -> str:
49
+ """Get the client ID"""
50
+ return self._client_id
51
+
52
+ async def request(
53
+ self,
54
+ method: str,
55
+ endpoint: str,
56
+ data: Dict[str, Any] = None,
57
+ timeout: int = 30
58
+ ) -> Dict[str, Any]:
59
+ """
60
+ Make an authenticated API request to the CCAI API
61
+
62
+ Args:
63
+ method: HTTP method
64
+ endpoint: API endpoint
65
+ data: Request data
66
+ timeout: Request timeout in seconds
67
+
68
+ Returns:
69
+ API response as a dictionary
70
+ """
71
+ url = f"{self._base_url}{endpoint}"
72
+
73
+ headers = {
74
+ "Authorization": f"Bearer {self._api_key}",
75
+ "Content-Type": "application/json",
76
+ "Accept": "*/*"
77
+ }
78
+
79
+ async with aiohttp.ClientSession() as session:
80
+ async with session.request(
81
+ method=method.upper(),
82
+ url=url,
83
+ headers=headers,
84
+ json=data,
85
+ timeout=aiohttp.ClientTimeout(total=timeout)
86
+ ) as response:
87
+ # Raise an exception for HTTP errors
88
+ response.raise_for_status()
89
+
90
+ # Parse the response as JSON
91
+ return await response.json()
92
+
93
+
94
+ class AsyncSMS:
95
+ """
96
+ Async SMS service for sending messages through the CCAI API
97
+ """
98
+
99
+ def __init__(self, ccai: AsyncCCAI) -> None:
100
+ """
101
+ Create a new async SMS service instance
102
+
103
+ Args:
104
+ ccai: The parent AsyncCCAI instance
105
+ """
106
+ self._ccai = ccai
107
+
108
+ async def send(
109
+ self,
110
+ accounts: List[Account],
111
+ message: str,
112
+ title: str,
113
+ options: SMSOptions = None
114
+ ) -> SMSResponse:
115
+ """
116
+ Send an SMS message to one or more recipients asynchronously
117
+
118
+ Args:
119
+ accounts: List of recipient accounts
120
+ message: Message content
121
+ title: Campaign title
122
+ options: Optional settings for the SMS send operation
123
+
124
+ Returns:
125
+ API response
126
+ """
127
+ # Validate inputs
128
+ if not accounts:
129
+ raise ValueError("At least one account is required")
130
+ if not message:
131
+ raise ValueError("Message is required")
132
+ if not title:
133
+ raise ValueError("Campaign title is required")
134
+
135
+ # Prepare the endpoint and data
136
+ endpoint = f"/clients/{self._ccai.client_id}/campaigns/direct"
137
+
138
+ # Convert Account objects to dictionaries with camelCase keys for API compatibility
139
+ accounts_data = [
140
+ {
141
+ "firstName": account.first_name,
142
+ "lastName": account.last_name,
143
+ "phone": account.phone
144
+ }
145
+ for account in accounts
146
+ ]
147
+
148
+ campaign_data = {
149
+ "accounts": accounts_data,
150
+ "message": message,
151
+ "title": title
152
+ }
153
+
154
+ # Make the API request
155
+ timeout = options.timeout if options else 30
156
+ response_data = await self._ccai.request(
157
+ method="post",
158
+ endpoint=endpoint,
159
+ data=campaign_data,
160
+ timeout=timeout
161
+ )
162
+
163
+ # Convert response to SMSResponse object
164
+ return SMSResponse(**response_data)
165
+
166
+ async def send_single(
167
+ self,
168
+ first_name: str,
169
+ last_name: str,
170
+ phone: str,
171
+ message: str,
172
+ title: str,
173
+ options: SMSOptions = None
174
+ ) -> SMSResponse:
175
+ """
176
+ Send a single SMS message to one recipient asynchronously
177
+
178
+ Args:
179
+ first_name: Recipient's first name
180
+ last_name: Recipient's last name
181
+ phone: Recipient's phone number (E.164 format)
182
+ message: Message content
183
+ title: Campaign title
184
+ options: Optional settings for the SMS send operation
185
+
186
+ Returns:
187
+ API response
188
+ """
189
+ account = Account(
190
+ first_name=first_name,
191
+ last_name=last_name,
192
+ phone=phone
193
+ )
194
+
195
+ return await self.send([account], message, title, options)
196
+
197
+
198
+ async def main() -> None:
199
+ """Example of using the async CCAI client"""
200
+ # Create a new async CCAI client
201
+ ccai = AsyncCCAI(
202
+ client_id="YOUR-CLIENT-ID",
203
+ api_key="API-KEY-TOKEN"
204
+ )
205
+
206
+ # Example recipients
207
+ accounts = [
208
+ Account(
209
+ first_name="John",
210
+ last_name="Doe",
211
+ phone="+15551234567" # Use E.164 format
212
+ )
213
+ ]
214
+
215
+ # Message with variable placeholders
216
+ message = "Hello ${first_name} ${last_name}, this is a test message!"
217
+ title = "Async Test Campaign"
218
+
219
+ try:
220
+ # Send SMS to multiple recipients
221
+ print('Sending campaign to multiple recipients asynchronously...')
222
+ campaign_response = await ccai.sms.send(
223
+ accounts=accounts,
224
+ message=message,
225
+ title=title
226
+ )
227
+ print('SMS campaign sent successfully!')
228
+ print(campaign_response.model_dump())
229
+
230
+ # Send SMS to a single recipient
231
+ print('\nSending message to a single recipient asynchronously...')
232
+ single_response = await ccai.sms.send_single(
233
+ first_name="Jane",
234
+ last_name="Smith",
235
+ phone="+15559876543",
236
+ message="Hi ${first_name}, thanks for your interest!",
237
+ title="Single Async Message Test"
238
+ )
239
+ print('Single SMS sent successfully!')
240
+ print(single_response.model_dump())
241
+
242
+ print('\nAll messages sent successfully!')
243
+ except Exception as error:
244
+ print(f'Error sending SMS: {str(error)}')
245
+
246
+
247
+ if __name__ == "__main__":
248
+ asyncio.run(main())
@@ -0,0 +1,79 @@
1
+ """
2
+ Basic example using the CCAI Python client
3
+
4
+ :license: MIT
5
+ :copyright: 2025 CloudContactAI LLC
6
+ """
7
+
8
+ from ccai_python import CCAI, Account, SMSResponse
9
+
10
+ # Create a new CCAI client
11
+ ccai = CCAI(
12
+ client_id="YOUR-CLIENT-ID",
13
+ api_key="API-KEY-TOKEN"
14
+ )
15
+
16
+ # Example recipients
17
+ accounts = [
18
+ Account(
19
+ first_name="John",
20
+ last_name="Doe",
21
+ phone="+15551234567" # Use E.164 format
22
+ )
23
+ ]
24
+
25
+ # Alternative dictionary format
26
+ dict_accounts = [
27
+ {
28
+ "first_name": "John",
29
+ "last_name": "Doe",
30
+ "phone": "+15551234567" # Use E.164 format
31
+ }
32
+ ]
33
+
34
+ # Message with variable placeholders
35
+ message = "Hello ${first_name} ${last_name}, this is a test message!"
36
+ title = "Test Campaign"
37
+
38
+
39
+ def send_messages() -> dict:
40
+ """Example of sending SMS messages"""
41
+ try:
42
+ # Method 1: Send SMS to multiple recipients
43
+ print('Sending campaign to multiple recipients...')
44
+ campaign_response: SMSResponse = ccai.sms.send(
45
+ accounts=accounts,
46
+ message=message,
47
+ title=title
48
+ )
49
+ print('SMS campaign sent successfully!')
50
+ print(campaign_response.model_dump())
51
+
52
+ # Method 2: Send SMS to a single recipient
53
+ print('\nSending message to a single recipient...')
54
+ single_response: SMSResponse = ccai.sms.send_single(
55
+ first_name="Jane",
56
+ last_name="Smith",
57
+ phone="+15559876543",
58
+ message="Hi ${first_name}, thanks for your interest!",
59
+ title="Single Message Test"
60
+ )
61
+ print('Single SMS sent successfully!')
62
+ print(single_response.model_dump())
63
+
64
+ return {
65
+ "campaign_response": campaign_response.model_dump(),
66
+ "single_response": single_response.model_dump()
67
+ }
68
+ except Exception as error:
69
+ print(f'Error sending SMS: {str(error)}')
70
+ raise
71
+
72
+
73
+ if __name__ == "__main__":
74
+ try:
75
+ results = send_messages()
76
+ print('\nAll messages sent successfully!')
77
+ print(f'\nResults: {results}')
78
+ except Exception:
79
+ print('\nFailed to send one or more messages.')
@@ -0,0 +1,10 @@
1
+ """
2
+ SMS package for the CCAI Python client
3
+
4
+ :license: MIT
5
+ :copyright: 2025 CloudContactAI LLC
6
+ """
7
+
8
+ from .sms import SMS, Account, SMSCampaign, SMSResponse, SMSOptions
9
+
10
+ __all__ = ['SMS', 'Account', 'SMSCampaign', 'SMSResponse', 'SMSOptions']
@@ -0,0 +1,215 @@
1
+ """
2
+ sms.py - SMS service for the CCAI API
3
+ Handles sending SMS messages through the Cloud Contact AI platform.
4
+
5
+ :license: MIT
6
+ :copyright: 2025 CloudContactAI LLC
7
+ """
8
+
9
+ from typing import Any, Callable, Dict, List, Optional, Protocol, TypedDict, Union, cast
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class Account(BaseModel):
14
+ """Account model representing a recipient"""
15
+ first_name: str = Field(..., description="Recipient's first name")
16
+ last_name: str = Field(..., description="Recipient's last name")
17
+ phone: str = Field(..., description="Recipient's phone number in E.164 format")
18
+
19
+
20
+ class SMSCampaign(BaseModel):
21
+ """SMS campaign data model"""
22
+ accounts: List[Account] = Field(..., description="List of recipient accounts")
23
+ message: str = Field(..., description="Message content with optional variables")
24
+ title: str = Field(..., description="Campaign title")
25
+
26
+
27
+ class SMSResponse(BaseModel):
28
+ """Response from the SMS API"""
29
+ id: Optional[str] = Field(None, description="Message ID")
30
+ status: Optional[str] = Field(None, description="Message status")
31
+ campaign_id: Optional[str] = Field(None, description="Campaign ID")
32
+ messages_sent: Optional[int] = Field(None, description="Number of messages sent")
33
+ timestamp: Optional[str] = Field(None, description="Timestamp of the operation")
34
+
35
+ # Allow additional fields
36
+ model_config = {
37
+ "extra": "allow",
38
+ }
39
+
40
+
41
+ class SMSOptions(BaseModel):
42
+ """Options for SMS operations"""
43
+ timeout: Optional[int] = Field(None, description="Request timeout in seconds")
44
+ retries: Optional[int] = Field(None, description="Number of retry attempts")
45
+ on_progress: Optional[Callable[[str], None]] = Field(
46
+ None,
47
+ description="Callback for tracking progress"
48
+ )
49
+
50
+
51
+ class CCAIProtocol(Protocol):
52
+ """Protocol defining the required methods for the CCAI client"""
53
+ @property
54
+ def client_id(self) -> str:
55
+ ...
56
+
57
+ def request(
58
+ self,
59
+ method: str,
60
+ endpoint: str,
61
+ data: Optional[Dict[str, Any]] = None,
62
+ timeout: Optional[int] = None
63
+ ) -> Dict[str, Any]:
64
+ ...
65
+
66
+
67
+ class SMS:
68
+ """
69
+ SMS service for sending messages through the CCAI API
70
+ """
71
+
72
+ def __init__(self, ccai: CCAIProtocol) -> None:
73
+ """
74
+ Create a new SMS service instance
75
+
76
+ Args:
77
+ ccai: The parent CCAI instance
78
+ """
79
+ self._ccai = ccai
80
+
81
+ def send(
82
+ self,
83
+ accounts: List[Union[Account, Dict[str, str]]],
84
+ message: str,
85
+ title: str,
86
+ options: Optional[SMSOptions] = None
87
+ ) -> SMSResponse:
88
+ """
89
+ Send an SMS message to one or more recipients
90
+
91
+ Args:
92
+ accounts: List of recipient accounts
93
+ message: Message content (can include ${first_name} and ${last_name} variables)
94
+ title: Campaign title
95
+ options: Optional settings for the SMS send operation
96
+
97
+ Returns:
98
+ API response
99
+
100
+ Raises:
101
+ ValueError: If required parameters are missing or invalid
102
+ """
103
+ # Validate inputs
104
+ if not accounts:
105
+ raise ValueError("At least one account is required")
106
+ if not message:
107
+ raise ValueError("Message is required")
108
+ if not title:
109
+ raise ValueError("Campaign title is required")
110
+
111
+ # Convert dict accounts to Account objects if needed
112
+ normalized_accounts: List[Account] = []
113
+ for idx, account in enumerate(accounts):
114
+ if isinstance(account, dict):
115
+ try:
116
+ # Convert dictionary keys from snake_case to camelCase if needed
117
+ account_data = {}
118
+ for key, value in account.items():
119
+ if key == "first_name":
120
+ account_data["first_name"] = value
121
+ elif key == "lastName":
122
+ account_data["last_name"] = value
123
+ elif key == "firstName":
124
+ account_data["first_name"] = value
125
+ elif key == "last_name":
126
+ account_data["last_name"] = value
127
+ else:
128
+ account_data[key] = value
129
+
130
+ normalized_accounts.append(Account(**account_data))
131
+ except Exception as e:
132
+ raise ValueError(f"Invalid account at index {idx}: {str(e)}")
133
+ else:
134
+ normalized_accounts.append(account)
135
+
136
+ # Notify progress if callback provided
137
+ if options and options.on_progress:
138
+ options.on_progress("Preparing to send SMS")
139
+
140
+ # Prepare the endpoint and data
141
+ endpoint = f"/clients/{self._ccai.client_id}/campaigns/direct"
142
+
143
+ # Convert Account objects to dictionaries with camelCase keys for API compatibility
144
+ accounts_data = [
145
+ {
146
+ "firstName": account.first_name,
147
+ "lastName": account.last_name,
148
+ "phone": account.phone
149
+ }
150
+ for account in normalized_accounts
151
+ ]
152
+
153
+ campaign_data = {
154
+ "accounts": accounts_data,
155
+ "message": message,
156
+ "title": title
157
+ }
158
+
159
+ try:
160
+ # Notify progress if callback provided
161
+ if options and options.on_progress:
162
+ options.on_progress("Sending SMS")
163
+
164
+ # Make the API request
165
+ timeout = options.timeout if options else None
166
+ response_data = self._ccai.request(
167
+ method="post",
168
+ endpoint=endpoint,
169
+ data=campaign_data,
170
+ timeout=timeout
171
+ )
172
+
173
+ # Notify progress if callback provided
174
+ if options and options.on_progress:
175
+ options.on_progress("SMS sent successfully")
176
+
177
+ # Convert response to SMSResponse object
178
+ return SMSResponse(**response_data)
179
+ except Exception as e:
180
+ # Notify progress if callback provided
181
+ if options and options.on_progress:
182
+ options.on_progress("SMS sending failed")
183
+
184
+ raise e
185
+
186
+ def send_single(
187
+ self,
188
+ first_name: str,
189
+ last_name: str,
190
+ phone: str,
191
+ message: str,
192
+ title: str,
193
+ options: Optional[SMSOptions] = None
194
+ ) -> SMSResponse:
195
+ """
196
+ Send a single SMS message to one recipient
197
+
198
+ Args:
199
+ first_name: Recipient's first name
200
+ last_name: Recipient's last name
201
+ phone: Recipient's phone number (E.164 format)
202
+ message: Message content (can include ${first_name} and ${last_name} variables)
203
+ title: Campaign title
204
+ options: Optional settings for the SMS send operation
205
+
206
+ Returns:
207
+ API response
208
+ """
209
+ account = Account(
210
+ first_name=first_name,
211
+ last_name=last_name,
212
+ phone=phone
213
+ )
214
+
215
+ return self.send([account], message, title, options)
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: ccai-python
3
+ Version: 1.0.0
4
+ Summary: Python client for CloudContactAI API
5
+ Author: CloudContactAI LLC
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/cloudcontactai/ccai-python
8
+ Project-URL: Bug Tracker, https://github.com/cloudcontactai/ccai-python/issues
9
+ Keywords: sms,api,client,cloud,contact,ai,cloudcontactai
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests>=2.31.0
19
+ Requires-Dist: pydantic>=2.5.0
20
+
21
+ # CCAI Python Client
22
+
23
+ A Python client for interacting with the CloudContactAI API.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install ccai-python
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from ccai_python import CCAI
35
+
36
+ # Initialize the client
37
+ ccai = CCAI(
38
+ client_id="YOUR-CLIENT-ID",
39
+ api_key="YOUR-API-KEY"
40
+ )
41
+
42
+ # Send a single SMS
43
+ response = ccai.sms.send_single(
44
+ first_name="John",
45
+ last_name="Doe",
46
+ phone="+15551234567",
47
+ message="Hello ${first_name}, this is a test message!",
48
+ title="Test Campaign"
49
+ )
50
+
51
+ print(f"Message sent with ID: {response.id}")
52
+
53
+ # Send to multiple recipients
54
+ accounts = [
55
+ {"first_name": "John", "last_name": "Doe", "phone": "+15551234567"},
56
+ {"first_name": "Jane", "last_name": "Smith", "phone": "+15559876543"}
57
+ ]
58
+
59
+ campaign_response = ccai.sms.send(
60
+ accounts=accounts,
61
+ message="Hello ${first_name} ${last_name}, this is a test message!",
62
+ title="Bulk Test Campaign"
63
+ )
64
+
65
+ print(f"Campaign sent with ID: {campaign_response.campaign_id}")
66
+ ```
67
+
68
+ ## Features
69
+
70
+ - Send SMS messages to single or multiple recipients
71
+ - Variable substitution in messages
72
+ - Async support
73
+ - Type hints for better IDE integration
74
+ - Comprehensive error handling
75
+
76
+ ## Requirements
77
+
78
+ - Python 3.10 or higher
79
+ - `requests` library
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/ccai_python/__init__.py
4
+ src/ccai_python/ccai.py
5
+ src/ccai_python.egg-info/PKG-INFO
6
+ src/ccai_python.egg-info/SOURCES.txt
7
+ src/ccai_python.egg-info/dependency_links.txt
8
+ src/ccai_python.egg-info/requires.txt
9
+ src/ccai_python.egg-info/top_level.txt
10
+ src/ccai_python/examples/async_example.py
11
+ src/ccai_python/examples/basic_example.py
12
+ src/ccai_python/sms/__init__.py
13
+ src/ccai_python/sms/sms.py
14
+ tests/test_ccai.py
15
+ tests/test_sms.py
@@ -0,0 +1,2 @@
1
+ requests>=2.31.0
2
+ pydantic>=2.5.0
@@ -0,0 +1,3 @@
1
+ ccai_python
2
+ examples
3
+ sms
@@ -0,0 +1,88 @@
1
+ """
2
+ Tests for the CCAI client
3
+
4
+ :license: MIT
5
+ :copyright: 2025 CloudContactAI LLC
6
+ """
7
+
8
+ import unittest
9
+ from unittest.mock import patch, MagicMock
10
+
11
+ from ccai_python import CCAI, Account
12
+
13
+
14
+ class TestCCAI(unittest.TestCase):
15
+ """Test cases for the CCAI client"""
16
+
17
+ def setUp(self):
18
+ """Set up test fixtures"""
19
+ self.client_id = "test-client-id"
20
+ self.api_key = "test-api-key"
21
+ self.ccai = CCAI(client_id=self.client_id, api_key=self.api_key)
22
+
23
+ def test_initialization(self):
24
+ """Test client initialization"""
25
+ self.assertEqual(self.ccai.client_id, self.client_id)
26
+ self.assertEqual(self.ccai.api_key, self.api_key)
27
+ self.assertEqual(self.ccai.base_url, "https://core.cloudcontactai.com/api")
28
+
29
+ # Test custom base URL
30
+ custom_url = "https://custom.api.example.com"
31
+ ccai = CCAI(client_id=self.client_id, api_key=self.api_key, base_url=custom_url)
32
+ self.assertEqual(ccai.base_url, custom_url)
33
+
34
+ def test_initialization_validation(self):
35
+ """Test validation during initialization"""
36
+ with self.assertRaises(ValueError):
37
+ CCAI(client_id="", api_key=self.api_key)
38
+
39
+ with self.assertRaises(ValueError):
40
+ CCAI(client_id=self.client_id, api_key="")
41
+
42
+ @patch('requests.request')
43
+ def test_request(self, mock_request):
44
+ """Test the request method"""
45
+ # Mock response
46
+ mock_response = MagicMock()
47
+ mock_response.json.return_value = {"status": "success"}
48
+ mock_response.raise_for_status.return_value = None
49
+ mock_request.return_value = mock_response
50
+
51
+ # Test GET request
52
+ result = self.ccai.request("get", "/test-endpoint")
53
+ self.assertEqual(result, {"status": "success"})
54
+
55
+ # Verify request was made correctly
56
+ mock_request.assert_called_with(
57
+ method="GET",
58
+ url="https://core.cloudcontactai.com/api/test-endpoint",
59
+ headers={
60
+ "Authorization": f"Bearer {self.api_key}",
61
+ "Content-Type": "application/json",
62
+ "Accept": "*/*"
63
+ },
64
+ json=None,
65
+ timeout=30
66
+ )
67
+
68
+ # Test POST request with data
69
+ data = {"key": "value"}
70
+ result = self.ccai.request("post", "/test-endpoint", data=data)
71
+ self.assertEqual(result, {"status": "success"})
72
+
73
+ # Verify request was made correctly
74
+ mock_request.assert_called_with(
75
+ method="POST",
76
+ url="https://core.cloudcontactai.com/api/test-endpoint",
77
+ headers={
78
+ "Authorization": f"Bearer {self.api_key}",
79
+ "Content-Type": "application/json",
80
+ "Accept": "*/*"
81
+ },
82
+ json=data,
83
+ timeout=30
84
+ )
85
+
86
+
87
+ if __name__ == '__main__':
88
+ unittest.main()
@@ -0,0 +1,237 @@
1
+ """
2
+ Tests for the SMS service
3
+
4
+ :license: MIT
5
+ :copyright: 2025 CloudContactAI LLC
6
+ """
7
+
8
+ import unittest
9
+ from unittest.mock import patch, MagicMock
10
+
11
+ from ccai_python import CCAI, Account, SMSOptions
12
+
13
+
14
+ class TestSMS(unittest.TestCase):
15
+ """Test cases for the SMS service"""
16
+
17
+ def setUp(self):
18
+ """Set up test fixtures"""
19
+ self.client_id = "test-client-id"
20
+ self.api_key = "test-api-key"
21
+ self.ccai = CCAI(client_id=self.client_id, api_key=self.api_key)
22
+
23
+ # Sample account
24
+ self.account = Account(
25
+ first_name="John",
26
+ last_name="Doe",
27
+ phone="+15551234567"
28
+ )
29
+
30
+ # Sample message and title
31
+ self.message = "Hello ${first_name}, this is a test message!"
32
+ self.title = "Test Campaign"
33
+
34
+ @patch.object(CCAI, 'request')
35
+ def test_send(self, mock_request):
36
+ """Test sending SMS to multiple recipients"""
37
+ # Mock response
38
+ mock_request.return_value = {
39
+ "id": "msg-123",
40
+ "status": "sent",
41
+ "campaign_id": "camp-456",
42
+ "messages_sent": 1,
43
+ "timestamp": "2025-06-06T12:00:00Z"
44
+ }
45
+
46
+ # Send SMS
47
+ response = self.ccai.sms.send(
48
+ accounts=[self.account],
49
+ message=self.message,
50
+ title=self.title
51
+ )
52
+
53
+ # Verify response
54
+ self.assertEqual(response.id, "msg-123")
55
+ self.assertEqual(response.status, "sent")
56
+ self.assertEqual(response.campaign_id, "camp-456")
57
+ self.assertEqual(response.messages_sent, 1)
58
+ self.assertEqual(response.timestamp, "2025-06-06T12:00:00Z")
59
+
60
+ # Verify request was made correctly
61
+ mock_request.assert_called_with(
62
+ method="post",
63
+ endpoint=f"/clients/{self.client_id}/campaigns/direct",
64
+ data={
65
+ "accounts": [
66
+ {
67
+ "firstName": "John",
68
+ "lastName": "Doe",
69
+ "phone": "+15551234567"
70
+ }
71
+ ],
72
+ "message": self.message,
73
+ "title": self.title
74
+ },
75
+ timeout=None
76
+ )
77
+
78
+ @patch.object(CCAI, 'request')
79
+ def test_send_with_dict_accounts(self, mock_request):
80
+ """Test sending SMS with dictionary accounts"""
81
+ # Mock response
82
+ mock_request.return_value = {"id": "msg-123", "status": "sent"}
83
+
84
+ # Send SMS with dictionary accounts
85
+ response = self.ccai.sms.send(
86
+ accounts=[{
87
+ "first_name": "John",
88
+ "last_name": "Doe",
89
+ "phone": "+15551234567"
90
+ }],
91
+ message=self.message,
92
+ title=self.title
93
+ )
94
+
95
+ # Verify response
96
+ self.assertEqual(response.id, "msg-123")
97
+ self.assertEqual(response.status, "sent")
98
+
99
+ # Verify request was made correctly
100
+ mock_request.assert_called_with(
101
+ method="post",
102
+ endpoint=f"/clients/{self.client_id}/campaigns/direct",
103
+ data={
104
+ "accounts": [
105
+ {
106
+ "firstName": "John",
107
+ "lastName": "Doe",
108
+ "phone": "+15551234567"
109
+ }
110
+ ],
111
+ "message": self.message,
112
+ "title": self.title
113
+ },
114
+ timeout=None
115
+ )
116
+
117
+ @patch.object(CCAI, 'request')
118
+ def test_send_single(self, mock_request):
119
+ """Test sending SMS to a single recipient"""
120
+ # Mock response
121
+ mock_request.return_value = {"id": "msg-123", "status": "sent"}
122
+
123
+ # Send SMS to a single recipient
124
+ response = self.ccai.sms.send_single(
125
+ first_name="Jane",
126
+ last_name="Smith",
127
+ phone="+15559876543",
128
+ message="Hi ${first_name}, thanks for your interest!",
129
+ title="Single Message Test"
130
+ )
131
+
132
+ # Verify response
133
+ self.assertEqual(response.id, "msg-123")
134
+ self.assertEqual(response.status, "sent")
135
+
136
+ # Verify request was made correctly
137
+ mock_request.assert_called_with(
138
+ method="post",
139
+ endpoint=f"/clients/{self.client_id}/campaigns/direct",
140
+ data={
141
+ "accounts": [
142
+ {
143
+ "firstName": "Jane",
144
+ "lastName": "Smith",
145
+ "phone": "+15559876543"
146
+ }
147
+ ],
148
+ "message": "Hi ${first_name}, thanks for your interest!",
149
+ "title": "Single Message Test"
150
+ },
151
+ timeout=None
152
+ )
153
+
154
+ @patch.object(CCAI, 'request')
155
+ def test_send_with_options(self, mock_request):
156
+ """Test sending SMS with options"""
157
+ # Mock response
158
+ mock_request.return_value = {"id": "msg-123", "status": "sent"}
159
+
160
+ # Create progress tracking callback
161
+ progress_updates = []
162
+ def track_progress(status: str):
163
+ progress_updates.append(status)
164
+
165
+ # Create options
166
+ options = SMSOptions(
167
+ timeout=60,
168
+ retries=3,
169
+ on_progress=track_progress
170
+ )
171
+
172
+ # Send SMS with options
173
+ response = self.ccai.sms.send(
174
+ accounts=[self.account],
175
+ message=self.message,
176
+ title=self.title,
177
+ options=options
178
+ )
179
+
180
+ # Verify response
181
+ self.assertEqual(response.id, "msg-123")
182
+ self.assertEqual(response.status, "sent")
183
+
184
+ # Verify progress updates
185
+ self.assertEqual(progress_updates, [
186
+ "Preparing to send SMS",
187
+ "Sending SMS",
188
+ "SMS sent successfully"
189
+ ])
190
+
191
+ # Verify request was made correctly
192
+ mock_request.assert_called_with(
193
+ method="post",
194
+ endpoint=f"/clients/{self.client_id}/campaigns/direct",
195
+ data={
196
+ "accounts": [
197
+ {
198
+ "firstName": "John",
199
+ "lastName": "Doe",
200
+ "phone": "+15551234567"
201
+ }
202
+ ],
203
+ "message": self.message,
204
+ "title": self.title
205
+ },
206
+ timeout=60
207
+ )
208
+
209
+ def test_validation(self):
210
+ """Test input validation"""
211
+ # Test empty accounts
212
+ with self.assertRaises(ValueError):
213
+ self.ccai.sms.send(
214
+ accounts=[],
215
+ message=self.message,
216
+ title=self.title
217
+ )
218
+
219
+ # Test empty message
220
+ with self.assertRaises(ValueError):
221
+ self.ccai.sms.send(
222
+ accounts=[self.account],
223
+ message="",
224
+ title=self.title
225
+ )
226
+
227
+ # Test empty title
228
+ with self.assertRaises(ValueError):
229
+ self.ccai.sms.send(
230
+ accounts=[self.account],
231
+ message=self.message,
232
+ title=""
233
+ )
234
+
235
+
236
+ if __name__ == '__main__':
237
+ unittest.main()