aircall-api 1.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.
aircall/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ """Aircall API Python SDK."""
2
+
3
+ from aircall.client import AircallClient
4
+ from aircall.exceptions import (
5
+ AircallAPIError,
6
+ AircallConnectionError,
7
+ AircallError,
8
+ AircallPermissionError,
9
+ AircallTimeoutError,
10
+ AuthenticationError,
11
+ NotFoundError,
12
+ RateLimitError,
13
+ ServerError,
14
+ UnprocessableEntityError,
15
+ ValidationError,
16
+ )
17
+ from aircall.logging_config import configure_logging
18
+
19
+ __all__ = [
20
+ # Client
21
+ "AircallClient",
22
+ # Base exceptions
23
+ "AircallError",
24
+ "AircallAPIError",
25
+ # API exceptions
26
+ "AuthenticationError",
27
+ "AircallPermissionError",
28
+ "NotFoundError",
29
+ "ValidationError",
30
+ "UnprocessableEntityError",
31
+ "RateLimitError",
32
+ "ServerError",
33
+ # Connection exceptions
34
+ "AircallConnectionError",
35
+ "AircallTimeoutError",
36
+ # Logging
37
+ "configure_logging",
38
+ ]
aircall/client.py ADDED
@@ -0,0 +1,273 @@
1
+ """Aircall API client."""
2
+
3
+ import base64
4
+ import logging
5
+ import time
6
+ import requests
7
+ from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError
8
+
9
+ from aircall.exceptions import (
10
+ AircallAPIError,
11
+ AircallConnectionError,
12
+ AircallTimeoutError,
13
+ AuthenticationError,
14
+ #AircallPermissionError,
15
+ NotFoundError,
16
+ RateLimitError,
17
+ ServerError,
18
+ UnprocessableEntityError,
19
+ ValidationError,
20
+ )
21
+ from aircall.resources import (
22
+ CallResource,
23
+ CompanyResource,
24
+ ContactResource,
25
+ DialerCampaignResource,
26
+ IntegrationResource,
27
+ MessageResource,
28
+ NumberResource,
29
+ TagResource,
30
+ TeamResource,
31
+ UserResource,
32
+ WebhookResource,
33
+ )
34
+
35
+
36
+ class AircallClient:
37
+ """
38
+ Main client for interacting with the Aircall API.
39
+
40
+ Handles authentication and provides access to all API resources.
41
+
42
+ Example:
43
+ >>> client = AircallClient(api_id="your_id", api_token="your_token")
44
+ >>> numbers = client.number.list()
45
+ >>> number = client.number.get(12345)
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ api_id: str,
51
+ api_token: str,
52
+ timeout: int = 30,
53
+ verbose: bool = False
54
+ ) -> None:
55
+ """
56
+ Initialize the Aircall API client.
57
+
58
+ Args:
59
+ api_id: Your Aircall API ID
60
+ api_token: Your Aircall API token
61
+ timeout: Default request timeout in seconds (default: 30)
62
+ verbose: Enable verbose logging for debugging (default: False)
63
+ When True, sets the logger level to DEBUG
64
+ """
65
+ self.base_url = "https://api.aircall.io/v1"
66
+ credentials = base64.b64encode(f"{api_id}:{api_token}".encode()).decode('utf-8')
67
+ self.timeout = timeout
68
+
69
+ # Initialize logger
70
+ self.logger = logging.getLogger('aircall.client')
71
+
72
+ # If verbose is enabled, set logger to DEBUG level
73
+ if verbose:
74
+ self.logger.setLevel(logging.DEBUG)
75
+ # Ensure handlers exist for the aircall logger
76
+ aircall_logger = logging.getLogger('aircall')
77
+ if not aircall_logger.handlers:
78
+ handler = logging.StreamHandler()
79
+ formatter = logging.Formatter(
80
+ fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
81
+ datefmt='%Y-%m-%d %H:%M:%S'
82
+ )
83
+ handler.setFormatter(formatter)
84
+ aircall_logger.addHandler(handler)
85
+ aircall_logger.setLevel(logging.DEBUG)
86
+
87
+ self.session = requests.Session()
88
+ self.session.headers.update({"Authorization": f"Basic {credentials}"})
89
+
90
+ self.logger.info("Aircall client initialized")
91
+
92
+ # Initialize resources
93
+ self.call = CallResource(self)
94
+ self.company = CompanyResource(self)
95
+ self.contact = ContactResource(self)
96
+ self.dialer_campaign = DialerCampaignResource(self)
97
+ self.integration = IntegrationResource(self)
98
+ self.message = MessageResource(self)
99
+ self.number = NumberResource(self)
100
+ self.tag = TagResource(self)
101
+ self.team = TeamResource(self)
102
+ self.user = UserResource(self)
103
+ self.webhook = WebhookResource(self)
104
+
105
+ def _request(
106
+ self,
107
+ method: str,
108
+ endpoint: str,
109
+ params: dict = None,
110
+ json: dict = None,
111
+ timeout: int = None
112
+ ) -> dict:
113
+ """
114
+ Make an HTTP request to the Aircall API.
115
+
116
+ Args:
117
+ method: HTTP method (GET, POST, PUT, DELETE)
118
+ endpoint: API endpoint (e.g., "/numbers", "/contacts/123")
119
+ params: Query parameters as dict (e.g., {"page": 1, "per_page": 50})
120
+ json: Request body as dict for POST/PUT requests
121
+ timeout: Request timeout in seconds (uses self.timeout if not specified)
122
+
123
+ Returns:
124
+ dict: Parsed JSON response
125
+
126
+ Raises:
127
+ ValidationError: When request validation fails (400)
128
+ AuthenticationError: When authentication fails (401 or 403)
129
+ NotFoundError: When resource is not found (404)
130
+ UnprocessableEntityError: When server cannot process request (422)
131
+ RateLimitError: When rate limit is exceeded (429)
132
+ ServerError: When server returns 5xx error
133
+ AircallConnectionError: When connection to API fails
134
+ AircallTimeoutError: When request times out
135
+ AircallAPIError: For other API errors
136
+ """
137
+ url = self.base_url + endpoint
138
+
139
+ # Log the request details
140
+ self.logger.debug("Request: %s %s", method, url)
141
+ if params:
142
+ self.logger.debug(" Query params: %s", params)
143
+ if json:
144
+ self.logger.debug(" Request body: %s", json)
145
+
146
+ start_time = time.time()
147
+
148
+ try:
149
+ # Use requests library to handle params and json automatically
150
+ response = self.session.request(
151
+ method=method,
152
+ url=url,
153
+ params=params, # requests converts dict to query string
154
+ json=json, # requests converts dict to JSON body and sets Content-Type
155
+ timeout=timeout or self.timeout
156
+ )
157
+ except Timeout as e:
158
+ elapsed = time.time() - start_time
159
+ self.logger.error(
160
+ "Request timeout: %s %s - Failed after %ss (elapsed: %.2fs)",
161
+ method, url, timeout or self.timeout, elapsed
162
+ )
163
+ raise AircallTimeoutError(
164
+ f"Request to {url} timed out after {timeout or self.timeout} seconds"
165
+ ) from e
166
+ except RequestsConnectionError as e:
167
+ elapsed = time.time() - start_time
168
+ self.logger.error(
169
+ "Connection error: %s %s - %s (elapsed: %.2fs)",
170
+ method, url, str(e), elapsed
171
+ )
172
+ raise AircallConnectionError(
173
+ f"Failed to connect to {url}: {str(e)}"
174
+ ) from e
175
+
176
+ elapsed = time.time() - start_time
177
+
178
+ # Handle successful responses
179
+ if 200 <= response.status_code < 300:
180
+ self.logger.debug(
181
+ "Response: %s %s %s (took %.2fs)",
182
+ response.status_code, method, url, elapsed
183
+ )
184
+ if response.status_code == 204: # No Content
185
+ return {}
186
+ return response.json()
187
+
188
+ # Parse error response
189
+ error_data = None
190
+ error_message = response.reason
191
+ try:
192
+ error_data = response.json()
193
+ if isinstance(error_data, dict):
194
+ # Try to get message from various possible fields
195
+ error_message = error_data.get('message') or error_data.get('error') or error_message
196
+ except Exception:
197
+ # If response is not JSON, use text content
198
+ if response.text:
199
+ error_message = response.text
200
+
201
+ # Map status codes to appropriate exceptions based on Aircall API docs
202
+ status_code = response.status_code
203
+
204
+ # Log the error response
205
+ self.logger.warning(
206
+ "API error: %s %s %s - %s (took %.2fs)",
207
+ status_code, method, url, error_message, elapsed
208
+ )
209
+
210
+ if status_code == 400:
211
+ # Invalid payload/Bad Request
212
+ raise ValidationError(
213
+ error_message,
214
+ status_code=status_code,
215
+ response_data=error_data
216
+ )
217
+ if status_code == 401:
218
+ raise AuthenticationError(
219
+ error_message,
220
+ status_code=status_code,
221
+ response_data=error_data
222
+ )
223
+ if status_code == 403:
224
+ # Forbidden - Invalid API key or Bearer access token
225
+ raise AuthenticationError(
226
+ error_message,
227
+ status_code=status_code,
228
+ response_data=error_data
229
+ )
230
+ if status_code == 404:
231
+ # Not found - Id does not exist
232
+ raise NotFoundError(
233
+ error_message,
234
+ status_code=status_code,
235
+ response_data=error_data
236
+ )
237
+ if status_code == 422:
238
+ # Server unable to process the request
239
+ raise UnprocessableEntityError(
240
+ error_message,
241
+ status_code=status_code,
242
+ response_data=error_data
243
+ )
244
+ if status_code == 429:
245
+ # Rate limit exceeded
246
+ retry_after = response.headers.get('Retry-After')
247
+ if retry_after:
248
+ self.logger.warning(
249
+ "Rate limit exceeded: %s %s - Retry after %ss",
250
+ method, url, retry_after
251
+ )
252
+ else:
253
+ self.logger.warning("Rate limit exceeded (no retry-after header)")
254
+ raise RateLimitError(
255
+ error_message,
256
+ status_code=status_code,
257
+ response_data=error_data,
258
+ retry_after=int(retry_after) if retry_after else None
259
+ )
260
+ if 500 <= status_code < 600:
261
+ # Server errors
262
+ raise ServerError(
263
+ error_message,
264
+ status_code=status_code,
265
+ response_data=error_data
266
+ )
267
+ else:
268
+ # Generic API error for other status codes
269
+ raise AircallAPIError(
270
+ error_message,
271
+ status_code=status_code,
272
+ response_data=error_data
273
+ )
aircall/exceptions.py ADDED
@@ -0,0 +1,55 @@
1
+ """Custom exceptions for the Aircall API client."""
2
+
3
+
4
+ class AircallError(Exception):
5
+ """Base exception for all Aircall errors"""
6
+
7
+
8
+ class AircallAPIError(AircallError):
9
+ """Raised when the API returns an error response"""
10
+
11
+ def __init__(self, message, status_code=None, response_data=None):
12
+ self.message = message
13
+ self.status_code = status_code
14
+ self.response_data = response_data
15
+ super().__init__(self.message)
16
+
17
+
18
+ class AuthenticationError(AircallAPIError):
19
+ """Raised when authentication fails (401)"""
20
+
21
+
22
+ class AircallPermissionError(AircallAPIError):
23
+ """Raised when user lacks permission (403)"""
24
+
25
+
26
+ class NotFoundError(AircallAPIError):
27
+ """Raised when resource not found (404)"""
28
+
29
+
30
+ class ValidationError(AircallAPIError):
31
+ """Raised when request validation fails (400)"""
32
+
33
+
34
+ class UnprocessableEntityError(AircallAPIError):
35
+ """Raised when server unable to process the request (422)"""
36
+
37
+
38
+ class RateLimitError(AircallAPIError):
39
+ """Raised when rate limit exceeded (429)"""
40
+
41
+ def __init__(self, message, retry_after=None, **kwargs):
42
+ super().__init__(message, **kwargs)
43
+ self.retry_after = retry_after # Seconds until can retry
44
+
45
+
46
+ class ServerError(AircallAPIError):
47
+ """Raised when server returns 5xx error"""
48
+
49
+
50
+ class AircallConnectionError(AircallError):
51
+ """Raised when connection to API fails"""
52
+
53
+
54
+ class AircallTimeoutError(AircallError):
55
+ """Raised when request times out"""
@@ -0,0 +1,81 @@
1
+ """Logging configuration for the Aircall API SDK."""
2
+
3
+ import logging
4
+ import sys
5
+ from typing import Optional
6
+
7
+
8
+ def setup_logger(
9
+ name: str,
10
+ level: Optional[int] = None,
11
+ handler: Optional[logging.Handler] = None
12
+ ) -> logging.Logger:
13
+ """
14
+ Set up a logger with consistent formatting.
15
+
16
+ Args:
17
+ name: Logger name (typically __name__)
18
+ level: Logging level (defaults to WARNING if not set)
19
+ handler: Custom handler (defaults to StreamHandler if not set)
20
+
21
+ Returns:
22
+ logging.Logger: Configured logger instance
23
+ """
24
+ logger = logging.getLogger(name)
25
+
26
+ # Only configure if no handlers exist (avoid duplicate handlers)
27
+ if not logger.handlers:
28
+ if level is None:
29
+ level = logging.WARNING
30
+
31
+ logger.setLevel(level)
32
+
33
+ if handler is None:
34
+ handler = logging.StreamHandler(sys.stdout)
35
+
36
+ # Create formatter with timestamp, level, and message
37
+ formatter = logging.Formatter(
38
+ fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
39
+ datefmt='%Y-%m-%d %H:%M:%S'
40
+ )
41
+ handler.setFormatter(formatter)
42
+ logger.addHandler(handler)
43
+
44
+ return logger
45
+
46
+
47
+ def configure_logging(level: Optional[int] = None) -> None:
48
+ """
49
+ Configure logging for the entire Aircall SDK.
50
+
51
+ This sets the logging level for all Aircall loggers.
52
+
53
+ Args:
54
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
55
+ If None, defaults to WARNING
56
+
57
+ Example:
58
+ >>> import logging
59
+ >>> from aircall.logging_config import configure_logging
60
+ >>> configure_logging(logging.DEBUG)
61
+ """
62
+ if level is None:
63
+ level = logging.WARNING
64
+
65
+ # Configure the root aircall logger
66
+ root_logger = logging.getLogger('aircall')
67
+ root_logger.setLevel(level)
68
+
69
+ # If no handlers exist on root, add a default one
70
+ if not root_logger.handlers:
71
+ handler = logging.StreamHandler(sys.stdout)
72
+ formatter = logging.Formatter(
73
+ fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
74
+ datefmt='%Y-%m-%d %H:%M:%S'
75
+ )
76
+ handler.setFormatter(formatter)
77
+ root_logger.addHandler(handler)
78
+
79
+
80
+ # Create default logger for the SDK
81
+ logger = setup_logger('aircall')
@@ -0,0 +1,83 @@
1
+ """Aircall API models."""
2
+
3
+ # Core resources
4
+ from aircall.models.call import Call, CallComment
5
+ from aircall.models.company import Company
6
+ from aircall.models.contact import Contact, Email, PhoneNumber
7
+ from aircall.models.number import Number, NumberMessages
8
+ from aircall.models.tag import Tag
9
+ from aircall.models.team import Team
10
+ from aircall.models.user import User, UserAvailability
11
+
12
+ # AI and Intelligence
13
+ from aircall.models.ai_voice_agent import AIVoiceAgent
14
+ from aircall.models.content import (
15
+ ActionItemsContent,
16
+ Content,
17
+ SummaryContent,
18
+ TopicsContent,
19
+ Utterance,
20
+ )
21
+ from aircall.models.conversation_intelligence import (
22
+ ConversationIntelligence,
23
+ RealtimeTranscription,
24
+ RealtimeTranscriptionCall,
25
+ RealtimeTranscriptionUtterance,
26
+ )
27
+
28
+ # Communication
29
+ from aircall.models.message import MediaDetail, Message
30
+ from aircall.models.webhook import Webhook
31
+
32
+ # Campaign and Compliance
33
+ from aircall.models.dialer_campaign import DialerCampaign, DialerCampaignPhoneNumber
34
+
35
+ # Call-related
36
+ from aircall.models.ivr_option import IVROption
37
+ from aircall.models.participant import (
38
+ ConversationIntelligenceParticipant,
39
+ Participant,
40
+ )
41
+
42
+ # Integration
43
+ from aircall.models.integration import Integration
44
+
45
+ __all__ = [
46
+ # Core resources
47
+ "User",
48
+ "UserAvailability",
49
+ "Call",
50
+ "CallComment",
51
+ "Contact",
52
+ "PhoneNumber",
53
+ "Email",
54
+ "Number",
55
+ "NumberMessages",
56
+ "Team",
57
+ "Tag",
58
+ "Company",
59
+ # AI and Intelligence
60
+ "AIVoiceAgent",
61
+ "ConversationIntelligence",
62
+ "RealtimeTranscription",
63
+ "RealtimeTranscriptionCall",
64
+ "RealtimeTranscriptionUtterance",
65
+ "Content",
66
+ "Utterance",
67
+ "SummaryContent",
68
+ "TopicsContent",
69
+ "ActionItemsContent",
70
+ # Communication
71
+ "Message",
72
+ "MediaDetail",
73
+ "Webhook",
74
+ # Campaign and Compliance
75
+ "DialerCampaign",
76
+ "DialerCampaignPhoneNumber",
77
+ # Call-related
78
+ "Participant",
79
+ "ConversationIntelligenceParticipant",
80
+ "IVROption",
81
+ # Integration
82
+ "Integration",
83
+ ]
@@ -0,0 +1,49 @@
1
+ """AI Voice Agent models for Aircall API."""
2
+ from datetime import datetime
3
+ from typing import Any, Dict, Literal, Optional
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class AIVoiceAgent(BaseModel):
9
+ """
10
+ AI Voice Agent object representing calls handled by AI agents.
11
+
12
+ Accessible through webhook events:
13
+ - ai_voice_agent.started
14
+ - ai_voice_agent.ended
15
+ - ai_voice_agent.escalated
16
+ - ai_voice_agent.summary
17
+
18
+ Read-only. Not updatable or destroyable via API.
19
+ """
20
+ id: int # Same value as call_id
21
+ call_id: int # Same value as id
22
+ call_uuid: str
23
+ ai_voice_agent_id: str
24
+ ai_voice_agent_name: str
25
+ ai_voice_agent_session_id: str
26
+ number_id: int
27
+
28
+ # Only for started/ended events
29
+ external_caller_number: Optional[str] = None
30
+ aircall_number: Optional[str] = None
31
+
32
+ # Timestamps (Unix timestamps)
33
+ created_at: datetime
34
+ started_at: Optional[datetime] = None # Only for started/ended events
35
+ ended_at: Optional[datetime] = None # Only for ended event
36
+
37
+ # Only for ended event
38
+ call_end_reason: Optional[Literal[
39
+ "answered",
40
+ "escalated",
41
+ "disconnected",
42
+ "caller_hung_up"
43
+ ]] = None
44
+
45
+ # Only for escalated event
46
+ escalation_reason: Optional[str] = None
47
+
48
+ # Only for summary event - answers to intake questions
49
+ extracted_data: Optional[Dict[str, Any]] = None
aircall/models/call.py ADDED
@@ -0,0 +1,94 @@
1
+ """Call models for Aircall API."""
2
+ from datetime import datetime
3
+ from typing import Literal, Optional
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from aircall.models.contact import Contact
8
+ from aircall.models.ivr_option import IVROption
9
+ from aircall.models.number import Number
10
+ from aircall.models.participant import Participant
11
+ from aircall.models.tag import Tag
12
+ from aircall.models.team import Team
13
+ from aircall.models.user import User
14
+
15
+
16
+ class CallComment(BaseModel):
17
+ """
18
+ Comment (Note) on a call.
19
+
20
+ Can be created by Agents or via API.
21
+ """
22
+ id: int
23
+ content: str
24
+ posted_at: datetime
25
+ posted_by: Optional["User"] = None # Null if posted via API
26
+
27
+
28
+ class Call(BaseModel):
29
+ """
30
+ Call resource representing phone interactions.
31
+
32
+ Three types:
33
+ - Inbound: External person → Agent
34
+ - Outbound: Agent → External person
35
+ - Internal: Agent → Agent (not in Public API)
36
+
37
+ Note: Call id is Int64 data type.
38
+ """
39
+ id: int # Int64
40
+ sid: Optional[str] = None # Only in Call APIs (same as call_uuid)
41
+ call_uuid: Optional[str] = None # Only in Webhook events (same as sid)
42
+ direct_link: str
43
+
44
+ # Timestamps (Unix timestamps)
45
+ started_at: datetime
46
+ answered_at: Optional[datetime] = None # Null if not answered
47
+ ended_at: Optional[datetime] = None
48
+
49
+ duration: int # Seconds (ended_at - started_at, includes ringing)
50
+ status: Literal["initial", "answered", "done"]
51
+ direction: Literal["inbound", "outbound"]
52
+ raw_digits: str # International format or "anonymous"
53
+
54
+ # Media URLs (valid for limited time)
55
+ asset: Optional[str] = None # Secured webpage for recording/voicemail
56
+ recording: Optional[str] = None # Direct MP3 URL (1 hour validity)
57
+ recording_short_url: Optional[str] = None # Short URL (3 hours validity)
58
+ voicemail: Optional[str] = None # Direct MP3 URL (1 hour validity)
59
+ voicemail_short_url: Optional[str] = None # Short URL (3 hours validity)
60
+
61
+ archived: Optional[bool] = None
62
+ missed_call_reason: Optional[Literal[
63
+ "out_of_opening_hours",
64
+ "short_abandoned",
65
+ "abandoned_in_ivr",
66
+ "abandoned_in_classic",
67
+ "no_available_agent",
68
+ "agents_did_not_answer"
69
+ ]] = None
70
+
71
+ cost: Optional[str] = None # Deprecated - U.S. cents
72
+
73
+ # Related objects
74
+ number: Optional["Number"] = None
75
+ user: Optional["User"] = None # Who took or made the call
76
+ contact: Optional["Contact"] = None
77
+ assigned_to: Optional["User"] = None
78
+ teams: list["Team"] = [] # Only for inbound calls
79
+
80
+ # Transfer information
81
+ transferred_by: Optional["User"] = None
82
+ transferred_to: Optional["User"] = None # First user of team if transferred to team
83
+ external_transferred_to: Optional[str] = None # Only via call.external_transferred event
84
+ external_caller_number: Optional[str] = None # Only via call.external_transferred event
85
+
86
+ # Collections
87
+ comments: list[CallComment] = []
88
+ tags: list["Tag"] = []
89
+
90
+ # Conference participants (referred as conference_participants in APIs)
91
+ participants: list[Participant] = []
92
+
93
+ # IVR options (requires fetch_call_timeline query param)
94
+ ivr_options_selected: list["IVROption"] = []
@@ -0,0 +1,14 @@
1
+ """Company model for Aircall API."""
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class Company(BaseModel):
6
+ """
7
+ Company object representing an Aircall company.
8
+
9
+ Read-only. Not updatable or destroyable via API.
10
+ Can only be modified via Aircall Dashboard.
11
+ """
12
+ name: str
13
+ users_count: int
14
+ numbers_count: int