kirimel-python 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.
kirimel/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """
2
+ KiriMel Python SDK
3
+
4
+ Official Python SDK for KiriMel Email Marketing API.
5
+ """
6
+ from .client import KiriMel
7
+ from .exceptions import (
8
+ ApiException,
9
+ AuthenticationException,
10
+ RateLimitException,
11
+ ValidationException,
12
+ )
13
+
14
+ __version__ = "0.1.0"
15
+ __all__ = [
16
+ "KiriMel",
17
+ "ApiException",
18
+ "AuthenticationException",
19
+ "RateLimitException",
20
+ "ValidationException",
21
+ ]
kirimel/client.py ADDED
@@ -0,0 +1,122 @@
1
+ """
2
+ KiriMel Python SDK Client
3
+ """
4
+ from typing import Optional
5
+ from .http_client import HttpClient
6
+ from .resources import (
7
+ Campaigns,
8
+ Subscribers,
9
+ Lists,
10
+ Segments,
11
+ Templates,
12
+ Forms,
13
+ Conversions,
14
+ LandingPages,
15
+ Workflows,
16
+ )
17
+
18
+
19
+ class KiriMel:
20
+ """
21
+ KiriMel API Client
22
+
23
+ Example:
24
+ >>> import kirimel
25
+ >>> client = kirimel.KiriMel(api_key="sk_test_xxx")
26
+ >>> campaigns = client.campaigns.list()
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ api_key: Optional[str] = None,
32
+ base_url: str = "https://api.kirimel.com/v2",
33
+ timeout: int = 30,
34
+ retries: int = 3,
35
+ ):
36
+ """
37
+ Create a new API client
38
+
39
+ Args:
40
+ api_key: API key (or use KIRIMEL_API_KEY env variable)
41
+ base_url: Base URL (default: https://api.kirimel.com/v2)
42
+ timeout: Request timeout in seconds (default: 30)
43
+ retries: Number of retries (default: 3)
44
+ """
45
+ self._http_client = HttpClient(
46
+ api_key=api_key,
47
+ base_url=base_url,
48
+ timeout=timeout,
49
+ retries=retries,
50
+ )
51
+ self._campaigns: Optional[Campaigns] = None
52
+ self._subscribers: Optional[Subscribers] = None
53
+ self._lists: Optional[Lists] = None
54
+ self._segments: Optional[Segments] = None
55
+ self._templates: Optional[Templates] = None
56
+ self._forms: Optional[Forms] = None
57
+ self._conversions: Optional[Conversions] = None
58
+ self._landing_pages: Optional[LandingPages] = None
59
+ self._workflows: Optional[Workflows] = None
60
+
61
+ @property
62
+ def campaigns(self) -> Campaigns:
63
+ """Get campaigns resource client"""
64
+ if self._campaigns is None:
65
+ self._campaigns = Campaigns(self._http_client)
66
+ return self._campaigns
67
+
68
+ @property
69
+ def subscribers(self) -> Subscribers:
70
+ """Get subscribers resource client"""
71
+ if self._subscribers is None:
72
+ self._subscribers = Subscribers(self._http_client)
73
+ return self._subscribers
74
+
75
+ @property
76
+ def lists(self) -> Lists:
77
+ """Get lists resource client"""
78
+ if self._lists is None:
79
+ self._lists = Lists(self._http_client)
80
+ return self._lists
81
+
82
+ @property
83
+ def segments(self) -> Segments:
84
+ """Get segments resource client"""
85
+ if self._segments is None:
86
+ self._segments = Segments(self._http_client)
87
+ return self._segments
88
+
89
+ @property
90
+ def templates(self) -> Templates:
91
+ """Get templates resource client"""
92
+ if self._templates is None:
93
+ self._templates = Templates(self._http_client)
94
+ return self._templates
95
+
96
+ @property
97
+ def forms(self) -> Forms:
98
+ """Get forms resource client"""
99
+ if self._forms is None:
100
+ self._forms = Forms(self._http_client)
101
+ return self._forms
102
+
103
+ @property
104
+ def conversions(self) -> Conversions:
105
+ """Get conversions resource client"""
106
+ if self._conversions is None:
107
+ self._conversions = Conversions(self._http_client)
108
+ return self._conversions
109
+
110
+ @property
111
+ def landing_pages(self) -> LandingPages:
112
+ """Get landing pages resource client"""
113
+ if self._landing_pages is None:
114
+ self._landing_pages = LandingPages(self._http_client)
115
+ return self._landing_pages
116
+
117
+ @property
118
+ def workflows(self) -> Workflows:
119
+ """Get workflows resource client"""
120
+ if self._workflows is None:
121
+ self._workflows = Workflows(self._http_client)
122
+ return self._workflows
kirimel/exceptions.py ADDED
@@ -0,0 +1,38 @@
1
+ """
2
+ KiriMel SDK Exception Classes
3
+ """
4
+
5
+
6
+ class ApiException(Exception):
7
+ """Base API exception"""
8
+
9
+ def __init__(self, message: str, status_code: int = None, errors: dict = None):
10
+ self.message = message
11
+ self.status_code = status_code
12
+ self.errors = errors
13
+ super().__init__(self.message)
14
+
15
+ def __str__(self):
16
+ return self.message
17
+
18
+
19
+ class AuthenticationException(ApiException):
20
+ """Authentication exception (401)"""
21
+
22
+ error_type = "authentication_error"
23
+
24
+
25
+ class RateLimitException(ApiException):
26
+ """Rate limit exception (429)"""
27
+
28
+ error_type = "rate_limit_error"
29
+
30
+ def __init__(self, message: str, status_code: int = None, errors: dict = None, retry_after: int = None):
31
+ super().__init__(message, status_code, errors)
32
+ self.retry_after = retry_after
33
+
34
+
35
+ class ValidationException(ApiException):
36
+ """Validation exception (422)"""
37
+
38
+ error_type = "validation_error"
kirimel/http_client.py ADDED
@@ -0,0 +1,128 @@
1
+ """
2
+ HTTP Client for KiriMel API
3
+ """
4
+ import os
5
+ import time
6
+ import logging
7
+ from typing import Optional, Dict, Any, List
8
+ import requests
9
+
10
+ from .exceptions import ApiException, AuthenticationException, RateLimitException, ValidationException
11
+
12
+
13
+ class HttpClient:
14
+ """HTTP client for making API requests"""
15
+
16
+ def __init__(
17
+ self,
18
+ api_key: Optional[str] = None,
19
+ base_url: str = "https://api.kirimel.com/v2",
20
+ timeout: int = 30,
21
+ retries: int = 3,
22
+ ):
23
+ self.base_url = base_url.rstrip("/")
24
+ self.api_key = api_key or os.getenv("KIRIMEL_API_KEY")
25
+ self.timeout = timeout
26
+ self.retries = retries
27
+ self.session = requests.Session()
28
+ self.logger = logging.getLogger(__name__)
29
+
30
+ def set_logger(self, logger: logging.Logger) -> None:
31
+ """Set a custom logger"""
32
+ self.logger = logger
33
+
34
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
35
+ """Make a GET request"""
36
+ url = self._build_url(path, params or {})
37
+ return self._request("GET", url)
38
+
39
+ def post(self, path: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
40
+ """Make a POST request"""
41
+ return self._request("POST", self._build_url(path), data)
42
+
43
+ def put(self, path: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
44
+ """Make a PUT request"""
45
+ return self._request("PUT", self._build_url(path), data)
46
+
47
+ def delete(self, path: str) -> Dict[str, Any]:
48
+ """Make a DELETE request"""
49
+ return self._request("DELETE", self._build_url(path))
50
+
51
+ def _build_url(self, path: str, params: Optional[Dict[str, Any]] = None) -> str:
52
+ """Build URL with query parameters"""
53
+ url = f"{self.base_url}/{path.lstrip('/')}"
54
+ if params:
55
+ import urllib.parse
56
+ query_string = urllib.parse.urlencode(params)
57
+ url = f"{url}?{query_string}"
58
+ return url
59
+
60
+ def _request(
61
+ self,
62
+ method: str,
63
+ url: str,
64
+ data: Optional[Dict[str, Any]] = None,
65
+ attempt: int = 0,
66
+ ) -> Dict[str, Any]:
67
+ """Make HTTP request with retry logic"""
68
+ self.logger.debug(f"Making {method} request to {url}", extra={"attempt": attempt + 1})
69
+
70
+ headers = self._build_headers()
71
+
72
+ try:
73
+ response = self.session.request(
74
+ method=method,
75
+ url=url,
76
+ json=data,
77
+ headers=headers,
78
+ timeout=self.timeout,
79
+ )
80
+ except requests.RequestException as e:
81
+ # Network error - retry if attempts remain
82
+ if attempt < self.retries - 1:
83
+ self.logger.warning(f"Network error, retrying...", extra={"error": str(e)})
84
+ time.sleep(0.1 * (attempt + 1)) # Exponential backoff
85
+ return self._request(method, url, data, attempt + 1)
86
+ raise ApiException(f"Network error: {str(e)}")
87
+
88
+ if response.status_code >= 400:
89
+ self._handle_error(response, url, attempt)
90
+
91
+ return response.json()
92
+
93
+ def _build_headers(self) -> Dict[str, str]:
94
+ """Build request headers"""
95
+ headers = {
96
+ "Content-Type": "application/json",
97
+ "Accept": "application/json",
98
+ "User-Agent": "KiriMel-Python-SDK/0.1.0",
99
+ }
100
+
101
+ if self.api_key:
102
+ headers["Authorization"] = f"Bearer {self.api_key}"
103
+
104
+ return headers
105
+
106
+ def _handle_error(self, response: requests.Response, url: str, attempt: int) -> None:
107
+ """Handle API errors"""
108
+ try:
109
+ data = response.json()
110
+ message = data.get("message", "API request failed")
111
+ errors = data.get("errors")
112
+ except ValueError:
113
+ message = response.text or "API request failed"
114
+ errors = None
115
+
116
+ if response.status_code == 401:
117
+ raise AuthenticationException(message, response.status_code, errors)
118
+ elif response.status_code == 429:
119
+ retry_after = data.get("retry_after") if errors else None
120
+ if retry_after and attempt < self.retries - 1:
121
+ self.logger.info(f"Rate limited, waiting {retry_after}s...")
122
+ time.sleep(retry_after)
123
+ return # Will retry
124
+ raise RateLimitException(message, response.status_code, errors, retry_after)
125
+ elif response.status_code == 422:
126
+ raise ValidationException(message, response.status_code, errors)
127
+ else:
128
+ raise ApiException(message, response.status_code, errors)
@@ -0,0 +1,437 @@
1
+ """
2
+ Resource clients for KiriMel API
3
+ """
4
+ from typing import Optional, Dict, Any, List
5
+ from .http_client import HttpClient
6
+
7
+
8
+ class ResourceClient:
9
+ """Base class for resource clients"""
10
+
11
+ def __init__(self, http_client: HttpClient):
12
+ self._http_client = http_client
13
+
14
+
15
+ class Campaigns(ResourceClient):
16
+ """Campaigns resource client"""
17
+
18
+ def list(self, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
19
+ """List campaigns"""
20
+ return self._http_client.get("campaigns", params or {})
21
+
22
+ def recent(self) -> Dict[str, Any]:
23
+ """Get recent campaigns"""
24
+ return self._http_client.get("campaigns/recent")
25
+
26
+ def get(self, campaign_id: int) -> Dict[str, Any]:
27
+ """Get single campaign"""
28
+ return self._http_client.get(f"campaigns/{campaign_id}")
29
+
30
+ def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
31
+ """Create campaign"""
32
+ return self._http_client.post("campaigns", data)
33
+
34
+ def update(self, campaign_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
35
+ """Update campaign"""
36
+ return self._http_client.post(f"campaigns/{campaign_id}", data)
37
+
38
+ def delete(self, campaign_id: int) -> Dict[str, Any]:
39
+ """Delete campaign"""
40
+ return self._http_client.post(f"campaigns/{campaign_id}/delete")
41
+
42
+ def duplicate(self, campaign_id: int) -> Dict[str, Any]:
43
+ """Duplicate campaign"""
44
+ return self._http_client.post(f"campaigns/{campaign_id}/duplicate")
45
+
46
+ def schedule(self, campaign_id: int, scheduled_at: str) -> Dict[str, Any]:
47
+ """Schedule campaign"""
48
+ return self._http_client.post(f"campaigns/{campaign_id}/schedule", {"scheduled_at": scheduled_at})
49
+
50
+ def pause(self, campaign_id: int) -> Dict[str, Any]:
51
+ """Pause campaign"""
52
+ return self._http_client.post(f"campaigns/{campaign_id}/pause")
53
+
54
+ def resume(self, campaign_id: int) -> Dict[str, Any]:
55
+ """Resume campaign"""
56
+ return self._http_client.post(f"campaigns/{campaign_id}/resume")
57
+
58
+ def stats(self, campaign_id: int) -> Dict[str, Any]:
59
+ """Get campaign statistics"""
60
+ return self._http_client.get(f"campaigns/{campaign_id}/stats")
61
+
62
+
63
+ class Subscribers(ResourceClient):
64
+ """Subscribers resource client"""
65
+
66
+ def list(self, list_id: int, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
67
+ """List subscribers for a list"""
68
+ return self._http_client.get(f"lists/{list_id}/subscribers", params or {})
69
+
70
+ def get(self, subscriber_id: int) -> Dict[str, Any]:
71
+ """Get single subscriber"""
72
+ return self._http_client.get(f"subscribers/{subscriber_id}")
73
+
74
+ def create(self, list_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
75
+ """Add subscriber to a list"""
76
+ return self._http_client.post(f"lists/{list_id}/subscribers", data)
77
+
78
+ def update(self, subscriber_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
79
+ """Update subscriber"""
80
+ return self._http_client.post(f"subscribers/{subscriber_id}", data)
81
+
82
+ def delete(self, subscriber_id: int) -> Dict[str, Any]:
83
+ """Delete subscriber"""
84
+ return self._http_client.post(f"subscribers/{subscriber_id}/delete")
85
+
86
+ def unsubscribe(self, subscriber_id: int) -> Dict[str, Any]:
87
+ """Unsubscribe subscriber"""
88
+ return self._http_client.post(f"subscribers/{subscriber_id}/unsubscribe")
89
+
90
+ def bulk_unsubscribe(self, subscriber_ids: List[int]) -> Dict[str, Any]:
91
+ """Bulk unsubscribe"""
92
+ return self._http_client.post("subscribers/bulk-unsubscribe", {"subscriber_ids": subscriber_ids})
93
+
94
+ def bulk_delete(self, subscriber_ids: List[int]) -> Dict[str, Any]:
95
+ """Bulk delete"""
96
+ return self._http_client.post("subscribers/bulk-delete", {"subscriber_ids": subscriber_ids})
97
+
98
+ def activity(self, subscriber_id: int) -> Dict[str, Any]:
99
+ """Get subscriber activity"""
100
+ return self._http_client.get(f"subscribers/{subscriber_id}/activity")
101
+
102
+ def stats(self, subscriber_id: int) -> Dict[str, Any]:
103
+ """Get subscriber statistics"""
104
+ return self._http_client.get(f"subscribers/{subscriber_id}/stats")
105
+
106
+ def toggle_vip(self, subscriber_id: int) -> Dict[str, Any]:
107
+ """Toggle VIP status"""
108
+ return self._http_client.post(f"subscribers/{subscriber_id}/toggle-vip")
109
+
110
+ def search(self, query: str) -> Dict[str, Any]:
111
+ """Search subscribers"""
112
+ return self._http_client.get("subscribers/search", {"q": query})
113
+
114
+ def add_tag(self, subscriber_id: int, tag: str) -> Dict[str, Any]:
115
+ """Add tag to subscriber"""
116
+ return self._http_client.post(f"subscribers/{subscriber_id}/tags", {"tag": tag})
117
+
118
+ def remove_tag(self, subscriber_id: int, tag: str) -> Dict[str, Any]:
119
+ """Remove tag from subscriber"""
120
+ return self._http_client.post(f"subscribers/{subscriber_id}/tags/{tag}/remove")
121
+
122
+ def import_subscribers(self, list_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
123
+ """Import subscribers"""
124
+ return self._http_client.post(f"lists/{list_id}/subscribers/import", data)
125
+
126
+
127
+ class Lists(ResourceClient):
128
+ """Lists resource client"""
129
+
130
+ def list(self, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
131
+ """List all lists"""
132
+ return self._http_client.get("lists", params or {})
133
+
134
+ def get(self, list_id: int) -> Dict[str, Any]:
135
+ """Get single list"""
136
+ return self._http_client.get(f"lists/{list_id}")
137
+
138
+ def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
139
+ """Create list"""
140
+ return self._http_client.post("lists", data)
141
+
142
+ def update(self, list_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
143
+ """Update list"""
144
+ return self._http_client.post(f"lists/{list_id}", data)
145
+
146
+ def delete(self, list_id: int) -> Dict[str, Any]:
147
+ """Delete list"""
148
+ return self._http_client.post(f"lists/{list_id}/delete")
149
+
150
+ def stats(self, list_id: int) -> Dict[str, Any]:
151
+ """Get list statistics"""
152
+ return self._http_client.get(f"lists/{list_id}/stats")
153
+
154
+
155
+ class Segments(ResourceClient):
156
+ """Segments resource client"""
157
+
158
+ def list(self, list_id: int) -> Dict[str, Any]:
159
+ """List segments for a list"""
160
+ return self._http_client.get(f"lists/{list_id}/segments")
161
+
162
+ def get(self, segment_id: int) -> Dict[str, Any]:
163
+ """Get single segment"""
164
+ return self._http_client.get(f"segments/{segment_id}")
165
+
166
+ def create(self, list_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
167
+ """Create segment"""
168
+ return self._http_client.post(f"lists/{list_id}/segments", data)
169
+
170
+ def update(self, segment_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
171
+ """Update segment"""
172
+ return self._http_client.post(f"segments/{segment_id}", data)
173
+
174
+ def delete(self, segment_id: int) -> Dict[str, Any]:
175
+ """Delete segment"""
176
+ return self._http_client.post(f"segments/{segment_id}/delete")
177
+
178
+ def preview(self, list_id: int, conditions: List[Dict[str, Any]]) -> Dict[str, Any]:
179
+ """Preview segment (without saving)"""
180
+ return self._http_client.post(f"lists/{list_id}/segments/preview", {"conditions": conditions})
181
+
182
+ def subscribers(self, segment_id: int, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
183
+ """Get segment subscribers"""
184
+ return self._http_client.get(f"segments/{segment_id}/subscribers", params or {})
185
+
186
+ def refresh(self, segment_id: int) -> Dict[str, Any]:
187
+ """Refresh segment count"""
188
+ return self._http_client.post(f"segments/{segment_id}/refresh")
189
+
190
+ def logs(self, segment_id: int) -> Dict[str, Any]:
191
+ """Get segment build logs"""
192
+ return self._http_client.get(f"segments/{segment_id}/logs")
193
+
194
+ def templates(self) -> Dict[str, Any]:
195
+ """Get segment templates"""
196
+ return self._http_client.get("segments/templates")
197
+
198
+ def fields(self) -> Dict[str, Any]:
199
+ """Get available fields"""
200
+ return self._http_client.get("segments/fields")
201
+
202
+
203
+ class Templates(ResourceClient):
204
+ """Templates resource client"""
205
+
206
+ def list(self, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
207
+ """List all templates"""
208
+ return self._http_client.get("templates", params or {})
209
+
210
+ def get(self, template_id: int) -> Dict[str, Any]:
211
+ """Get single template"""
212
+ return self._http_client.get(f"templates/{template_id}")
213
+
214
+ def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
215
+ """Create template"""
216
+ return self._http_client.post("templates", data)
217
+
218
+ def update(self, template_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
219
+ """Update template"""
220
+ return self._http_client.post(f"templates/{template_id}", data)
221
+
222
+ def delete(self, template_id: int) -> Dict[str, Any]:
223
+ """Delete template"""
224
+ return self._http_client.post(f"templates/{template_id}/delete")
225
+
226
+ def duplicate(self, template_id: int) -> Dict[str, Any]:
227
+ """Duplicate template"""
228
+ return self._http_client.post(f"templates/{template_id}/duplicate")
229
+
230
+ def by_category(self, category: str) -> Dict[str, Any]:
231
+ """Get templates by category"""
232
+ return self._http_client.get(f"templates/category/{category}")
233
+
234
+ def search(self, query: str) -> Dict[str, Any]:
235
+ """Search templates"""
236
+ return self._http_client.get("templates/search", {"q": query})
237
+
238
+ def categories(self) -> Dict[str, Any]:
239
+ """Get categories"""
240
+ return self._http_client.get("templates/categories")
241
+
242
+
243
+ class Forms(ResourceClient):
244
+ """Forms resource client"""
245
+
246
+ def list(self) -> Dict[str, Any]:
247
+ """List all forms"""
248
+ return self._http_client.get("forms")
249
+
250
+ def get(self, form_id: int) -> Dict[str, Any]:
251
+ """Get single form"""
252
+ return self._http_client.get(f"forms/{form_id}")
253
+
254
+ def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
255
+ """Create form"""
256
+ return self._http_client.post("forms", data)
257
+
258
+ def update(self, form_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
259
+ """Update form"""
260
+ return self._http_client.post(f"forms/{form_id}", data)
261
+
262
+ def delete(self, form_id: int) -> Dict[str, Any]:
263
+ """Delete form"""
264
+ return self._http_client.post(f"forms/{form_id}/delete")
265
+
266
+ def duplicate(self, form_id: int) -> Dict[str, Any]:
267
+ """Duplicate form"""
268
+ return self._http_client.post(f"forms/{form_id}/duplicate")
269
+
270
+ def analytics(self, form_id: int) -> Dict[str, Any]:
271
+ """Get form analytics"""
272
+ return self._http_client.get(f"forms/{form_id}/analytics")
273
+
274
+ def submissions(self, form_id: int, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
275
+ """Get form submissions"""
276
+ return self._http_client.get(f"forms/{form_id}/submissions", params or {})
277
+
278
+ def embed(self, form_id: int) -> Dict[str, Any]:
279
+ """Get embed code"""
280
+ return self._http_client.get(f"forms/{form_id}/embed")
281
+
282
+
283
+ class Conversions(ResourceClient):
284
+ """Conversions resource client"""
285
+
286
+ def list(self) -> Dict[str, Any]:
287
+ """List all conversion goals"""
288
+ return self._http_client.get("conversions")
289
+
290
+ def get(self, goal_id: int) -> Dict[str, Any]:
291
+ """Get single conversion goal"""
292
+ return self._http_client.get(f"conversions/{goal_id}")
293
+
294
+ def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
295
+ """Create conversion goal"""
296
+ return self._http_client.post("conversions", data)
297
+
298
+ def update(self, goal_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
299
+ """Update conversion goal"""
300
+ return self._http_client.post(f"conversions/{goal_id}", data)
301
+
302
+ def delete(self, goal_id: int) -> Dict[str, Any]:
303
+ """Delete conversion goal"""
304
+ return self._http_client.post(f"conversions/{goal_id}/delete")
305
+
306
+ def track(self, data: Dict[str, Any]) -> Dict[str, Any]:
307
+ """Track a conversion"""
308
+ return self._http_client.post("conversions/track", data)
309
+
310
+ def conversions(self, goal_id: int) -> Dict[str, Any]:
311
+ """Get conversions for a goal"""
312
+ return self._http_client.get(f"conversions/{goal_id}/conversions")
313
+
314
+ def roi(self) -> Dict[str, Any]:
315
+ """Get ROI report"""
316
+ return self._http_client.get("conversions/roi")
317
+
318
+ def funnel(self, goal_id: int) -> Dict[str, Any]:
319
+ """Get funnel analysis"""
320
+ return self._http_client.get(f"conversions/{goal_id}/funnel")
321
+
322
+
323
+ class LandingPages(ResourceClient):
324
+ """Landing Pages resource client"""
325
+
326
+ def list(self) -> Dict[str, Any]:
327
+ """List all landing pages"""
328
+ return self._http_client.get("landing-pages")
329
+
330
+ def get(self, page_id: int) -> Dict[str, Any]:
331
+ """Get single landing page"""
332
+ return self._http_client.get(f"landing-pages/{page_id}")
333
+
334
+ def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
335
+ """Create landing page"""
336
+ return self._http_client.post("landing-pages", data)
337
+
338
+ def update(self, page_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
339
+ """Update landing page"""
340
+ return self._http_client.post(f"landing-pages/{page_id}", data)
341
+
342
+ def delete(self, page_id: int) -> Dict[str, Any]:
343
+ """Delete landing page"""
344
+ return self._http_client.post(f"landing-pages/{page_id}/delete")
345
+
346
+ def duplicate(self, page_id: int) -> Dict[str, Any]:
347
+ """Duplicate landing page"""
348
+ return self._http_client.post(f"landing-pages/{page_id}/duplicate")
349
+
350
+ def publish(self, page_id: int) -> Dict[str, Any]:
351
+ """Publish landing page"""
352
+ return self._http_client.post(f"landing-pages/{page_id}/publish")
353
+
354
+ def unpublish(self, page_id: int) -> Dict[str, Any]:
355
+ """Unpublish landing page"""
356
+ return self._http_client.post(f"landing-pages/{page_id}/unpublish")
357
+
358
+ def analytics(self, page_id: int) -> Dict[str, Any]:
359
+ """Get landing page analytics"""
360
+ return self._http_client.get(f"landing-pages/{page_id}/analytics")
361
+
362
+ def templates(self) -> Dict[str, Any]:
363
+ """Get templates"""
364
+ return self._http_client.get("landing-pages/templates")
365
+
366
+
367
+ class Workflows(ResourceClient):
368
+ """Workflows resource client"""
369
+
370
+ def list(self) -> Dict[str, Any]:
371
+ """List all workflows"""
372
+ return self._http_client.get("workflows")
373
+
374
+ def get(self, workflow_id: int) -> Dict[str, Any]:
375
+ """Get single workflow"""
376
+ return self._http_client.get(f"workflows/{workflow_id}")
377
+
378
+ def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
379
+ """Create workflow"""
380
+ return self._http_client.post("workflows", data)
381
+
382
+ def update(self, workflow_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
383
+ """Update workflow"""
384
+ return self._http_client.post(f"workflows/{workflow_id}", data)
385
+
386
+ def delete(self, workflow_id: int) -> Dict[str, Any]:
387
+ """Delete workflow"""
388
+ return self._http_client.post(f"workflows/{workflow_id}/delete")
389
+
390
+ def duplicate(self, workflow_id: int) -> Dict[str, Any]:
391
+ """Duplicate workflow"""
392
+ return self._http_client.post(f"workflows/{workflow_id}/duplicate")
393
+
394
+ def activate(self, workflow_id: int) -> Dict[str, Any]:
395
+ """Activate workflow"""
396
+ return self._http_client.post(f"workflows/{workflow_id}/activate")
397
+
398
+ def pause(self, workflow_id: int) -> Dict[str, Any]:
399
+ """Pause workflow"""
400
+ return self._http_client.post(f"workflows/{workflow_id}/pause")
401
+
402
+ def validate(self, workflow_id: int) -> Dict[str, Any]:
403
+ """Validate workflow"""
404
+ return self._http_client.post(f"workflows/{workflow_id}/validate")
405
+
406
+ def executions(self, workflow_id: int) -> Dict[str, Any]:
407
+ """Get workflow executions"""
408
+ return self._http_client.get(f"workflows/{workflow_id}/executions")
409
+
410
+ def templates(self) -> Dict[str, Any]:
411
+ """Get workflow templates"""
412
+ return self._http_client.get("workflows/templates")
413
+
414
+ def from_template(self, template_id: int) -> Dict[str, Any]:
415
+ """Create workflow from template"""
416
+ return self._http_client.post("workflows/from-template", {"template_id": template_id})
417
+
418
+ def node_types(self) -> Dict[str, Any]:
419
+ """Get available node types"""
420
+ return self._http_client.get("workflows/node-types")
421
+
422
+ def get_data(self, workflow_id: int) -> Dict[str, Any]:
423
+ """Get workflow data"""
424
+ return self._http_client.get(f"workflows/{workflow_id}/data")
425
+
426
+
427
+ __all__ = [
428
+ "Campaigns",
429
+ "Subscribers",
430
+ "Lists",
431
+ "Segments",
432
+ "Templates",
433
+ "Forms",
434
+ "Conversions",
435
+ "LandingPages",
436
+ "Workflows",
437
+ ]
@@ -0,0 +1,504 @@
1
+ Metadata-Version: 2.4
2
+ Name: kirimel-python
3
+ Version: 0.1.0
4
+ Summary: Official KiriMel Python SDK
5
+ Author-email: KiriMel <support@kirimel.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kirimel/kirimel-python-sdk
8
+ Project-URL: Documentation, https://docs.kirimel.com
9
+ Project-URL: Repository, https://github.com/kirimel/kirimel-python-sdk
10
+ Project-URL: Issues, https://github.com/kirimel/kirimel-python-sdk/issues
11
+ Keywords: kirimel,email,marketing,sdk,api
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests>=2.28.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
27
+ Requires-Dist: black>=23.0; extra == "dev"
28
+ Requires-Dist: mypy>=1.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # KiriMel Python SDK
32
+
33
+ Official Python SDK for KiriMel Email Marketing API.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install kirimel-python
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ import kirimel
45
+
46
+ # Initialize the client
47
+ client = kirimel.KiriMel(
48
+ api_key='sk_test_xxx', # Or set KIRIMEL_API_KEY env variable
49
+ base_url='https://api.kirimel.com/v2',
50
+ timeout=30,
51
+ retries=3
52
+ )
53
+
54
+ # List campaigns
55
+ campaigns = client.campaigns.list(status='sent', limit=20)
56
+
57
+ # Create a campaign
58
+ campaign = client.campaigns.create({
59
+ 'name': 'Welcome Email',
60
+ 'subject': 'Welcome to KiriMel!',
61
+ 'list_id': 123,
62
+ 'template_id': 456
63
+ })
64
+
65
+ # Get campaign statistics
66
+ stats = client.campaigns.stats(campaign['id'])
67
+ ```
68
+
69
+ ## Authentication
70
+
71
+ The SDK supports two authentication methods:
72
+
73
+ ```python
74
+ # Method 1: API Key (recommended)
75
+ client = kirimel.KiriMel(api_key='sk_test_xxx')
76
+
77
+ # Method 2: Environment variable
78
+ # Set KIRIMEL_API_KEY=sk_test_xxx in your environment
79
+ client = kirimel.KiriMel()
80
+ ```
81
+
82
+ ## Resources
83
+
84
+ ### Campaigns
85
+
86
+ ```python
87
+ # List campaigns
88
+ campaigns = client.campaigns.list(limit=20, status='sent')
89
+
90
+ # Get recent campaigns
91
+ recent = client.campaigns.recent()
92
+
93
+ # Get single campaign
94
+ campaign = client.campaigns.get(id)
95
+
96
+ # Create campaign
97
+ campaign = client.campaigns.create({
98
+ 'name': 'Summer Sale',
99
+ 'subject': '50% Off Everything!',
100
+ 'list_id': 123,
101
+ 'template_id': 456
102
+ })
103
+
104
+ # Update campaign
105
+ client.campaigns.update(id, {'subject': 'New Subject'})
106
+
107
+ # Delete campaign
108
+ client.campaigns.delete(id)
109
+
110
+ # Duplicate campaign
111
+ duplicate = client.campaigns.duplicate(id)
112
+
113
+ # Schedule campaign
114
+ client.campaigns.schedule(id, '2024-06-01 10:00:00')
115
+
116
+ # Pause campaign
117
+ client.campaigns.pause(id)
118
+
119
+ # Resume campaign
120
+ client.campaigns.resume(id)
121
+
122
+ # Get campaign statistics
123
+ stats = client.campaigns.stats(id)
124
+ ```
125
+
126
+ ### Subscribers
127
+
128
+ ```python
129
+ # List subscribers for a list
130
+ subscribers = client.subscribers.list(list_id, limit=50)
131
+
132
+ # Get single subscriber
133
+ subscriber = client.subscribers.get(id)
134
+
135
+ # Add subscriber to a list
136
+ subscriber = client.subscribers.create(list_id, {
137
+ 'email': 'user@example.com',
138
+ 'first_name': 'John',
139
+ 'last_name': 'Doe'
140
+ })
141
+
142
+ # Update subscriber
143
+ client.subscribers.update(id, {'first_name': 'Jane'})
144
+
145
+ # Delete subscriber
146
+ client.subscribers.delete(id)
147
+
148
+ # Unsubscribe subscriber
149
+ client.subscribers.unsubscribe(id)
150
+
151
+ # Bulk unsubscribe
152
+ client.subscribers.bulk_unsubscribe([id1, id2, id3])
153
+
154
+ # Bulk delete
155
+ client.subscribers.bulk_delete([id1, id2, id3])
156
+
157
+ # Get subscriber activity
158
+ activity = client.subscribers.activity(id)
159
+
160
+ # Get subscriber statistics
161
+ stats = client.subscribers.stats(id)
162
+
163
+ # Toggle VIP status
164
+ client.subscribers.toggle_vip(id)
165
+
166
+ # Search subscribers
167
+ results = client.subscribers.search('john@example.com')
168
+
169
+ # Add tag
170
+ client.subscribers.add_tag(id, 'premium-customer')
171
+
172
+ # Remove tag
173
+ client.subscribers.remove_tag(id, 'premium-customer')
174
+
175
+ # Import subscribers
176
+ result = client.subscribers.import_subscribers(list_id, {
177
+ 'subscribers': [
178
+ {'email': 'user1@example.com', 'first_name': 'User 1'},
179
+ {'email': 'user2@example.com', 'first_name': 'User 2'}
180
+ ]
181
+ })
182
+ ```
183
+
184
+ ### Lists
185
+
186
+ ```python
187
+ # List all lists
188
+ lists = client.lists.list()
189
+
190
+ # Get single list
191
+ lst = client.lists.get(id)
192
+
193
+ # Create list
194
+ lst = client.lists.create({
195
+ 'name': 'Newsletter Subscribers',
196
+ 'description': 'Monthly newsletter'
197
+ })
198
+
199
+ # Update list
200
+ client.lists.update(id, {'name': 'Updated Name'})
201
+
202
+ # Delete list
203
+ client.lists.delete(id)
204
+
205
+ # Get list statistics
206
+ stats = client.lists.stats(id)
207
+ ```
208
+
209
+ ### Segments
210
+
211
+ ```python
212
+ # List segments for a list
213
+ segments = client.segments.list(list_id)
214
+
215
+ # Get single segment
216
+ segment = client.segments.get(id)
217
+
218
+ # Create segment
219
+ segment = client.segments.create(list_id, {
220
+ 'name': 'Active Subscribers',
221
+ 'conditions': [
222
+ {'field': 'status', 'operator': 'equals', 'value': 'active'}
223
+ ]
224
+ })
225
+
226
+ # Update segment
227
+ client.segments.update(id, {'name': 'Updated Name'})
228
+
229
+ # Delete segment
230
+ client.segments.delete(id)
231
+
232
+ # Preview segment (without saving)
233
+ preview = client.segments.preview(list_id, [
234
+ {'field': 'status', 'operator': 'equals', 'value': 'active'}
235
+ ])
236
+
237
+ # Get segment subscribers
238
+ subscribers = client.segments.subscribers(id)
239
+
240
+ # Refresh segment count
241
+ client.segments.refresh(id)
242
+
243
+ # Get segment build logs
244
+ logs = client.segments.logs(id)
245
+
246
+ # Get segment templates
247
+ templates = client.segments.templates()
248
+
249
+ # Get available fields
250
+ fields = client.segments.fields()
251
+ ```
252
+
253
+ ### Templates
254
+
255
+ ```python
256
+ # List all templates
257
+ templates = client.templates.list(limit=20)
258
+
259
+ # Get single template
260
+ template = client.templates.get(id)
261
+
262
+ # Create template
263
+ template = client.templates.create({
264
+ 'name': 'Welcome Email',
265
+ 'subject': 'Welcome!',
266
+ 'html_content': '<h1>Hello {{name}}</h1>',
267
+ 'category': 'transactional'
268
+ })
269
+
270
+ # Update template
271
+ client.templates.update(id, {'name': 'Updated Name'})
272
+
273
+ # Delete template
274
+ client.templates.delete(id)
275
+
276
+ # Duplicate template
277
+ duplicate = client.templates.duplicate(id)
278
+
279
+ # Get templates by category
280
+ templates = client.templates.by_category('newsletter')
281
+
282
+ # Search templates
283
+ results = client.templates.search('welcome')
284
+
285
+ # Get categories
286
+ categories = client.templates.categories()
287
+ ```
288
+
289
+ ### Forms
290
+
291
+ ```python
292
+ # List all forms
293
+ forms = client.forms.list()
294
+
295
+ # Get single form
296
+ form = client.forms.get(id)
297
+
298
+ # Create form
299
+ form = client.forms.create({
300
+ 'name': 'Newsletter Signup',
301
+ 'list_id': 123,
302
+ 'fields': [
303
+ {'name': 'email', 'type': 'email', 'required': True},
304
+ {'name': 'first_name', 'type': 'text', 'required': False}
305
+ ]
306
+ })
307
+
308
+ # Update form
309
+ client.forms.update(id, {'name': 'Updated Name'})
310
+
311
+ # Delete form
312
+ client.forms.delete(id)
313
+
314
+ # Duplicate form
315
+ duplicate = client.forms.duplicate(id)
316
+
317
+ # Get form analytics
318
+ analytics = client.forms.analytics(id)
319
+
320
+ # Get form submissions
321
+ submissions = client.forms.submissions(id)
322
+
323
+ # Get embed code
324
+ embed = client.forms.embed(id)
325
+ ```
326
+
327
+ ### Conversions
328
+
329
+ ```python
330
+ # List all conversion goals
331
+ conversions = client.conversions.list()
332
+
333
+ # Get single conversion goal
334
+ goal = client.conversions.get(id)
335
+
336
+ # Create conversion goal
337
+ goal = client.conversions.create({
338
+ 'name': 'Purchase',
339
+ 'event_type': 'purchase',
340
+ 'value': 100
341
+ })
342
+
343
+ # Update conversion goal
344
+ client.conversions.update(id, {'name': 'Updated Name'})
345
+
346
+ # Delete conversion goal
347
+ client.conversions.delete(id)
348
+
349
+ # Track a conversion
350
+ client.conversions.track({
351
+ 'goal_id': id,
352
+ 'subscriber_id': 123,
353
+ 'value': 50
354
+ })
355
+
356
+ # Get conversions for a goal
357
+ conversions = client.conversions.conversions(id)
358
+
359
+ # Get ROI report
360
+ roi = client.conversions.roi()
361
+
362
+ # Get funnel analysis
363
+ funnel = client.conversions.funnel(id)
364
+ ```
365
+
366
+ ### Landing Pages
367
+
368
+ ```python
369
+ # List all landing pages
370
+ pages = client.landing_pages.list()
371
+
372
+ # Get single landing page
373
+ page = client.landing_pages.get(id)
374
+
375
+ # Create landing page
376
+ page = client.landing_pages.create({
377
+ 'name': 'Thank You Page',
378
+ 'slug': 'thank-you',
379
+ 'html_content': '<h1>Thank you!</h1>'
380
+ })
381
+
382
+ # Update landing page
383
+ client.landing_pages.update(id, {'name': 'Updated Name'})
384
+
385
+ # Delete landing page
386
+ client.landing_pages.delete(id)
387
+
388
+ # Duplicate landing page
389
+ duplicate = client.landing_pages.duplicate(id)
390
+
391
+ # Publish landing page
392
+ client.landing_pages.publish(id)
393
+
394
+ # Unpublish landing page
395
+ client.landing_pages.unpublish(id)
396
+
397
+ # Get landing page analytics
398
+ analytics = client.landing_pages.analytics(id)
399
+
400
+ # Get templates
401
+ templates = client.landing_pages.templates()
402
+ ```
403
+
404
+ ### Workflows
405
+
406
+ ```python
407
+ # List all workflows
408
+ workflows = client.workflows.list()
409
+
410
+ # Get single workflow
411
+ workflow = client.workflows.get(id)
412
+
413
+ # Create workflow
414
+ workflow = client.workflows.create({
415
+ 'name': 'Welcome Series',
416
+ 'nodes': [...],
417
+ 'edges': [...]
418
+ })
419
+
420
+ # Update workflow
421
+ client.workflows.update(id, {'name': 'Updated Name'})
422
+
423
+ # Delete workflow
424
+ client.workflows.delete(id)
425
+
426
+ # Duplicate workflow
427
+ duplicate = client.workflows.duplicate(id)
428
+
429
+ # Activate workflow
430
+ client.workflows.activate(id)
431
+
432
+ # Pause workflow
433
+ client.workflows.pause(id)
434
+
435
+ # Validate workflow
436
+ validation = client.workflows.validate(id)
437
+
438
+ # Get workflow executions
439
+ executions = client.workflows.executions(id)
440
+
441
+ # Get workflow templates
442
+ templates = client.workflows.templates()
443
+
444
+ # Create workflow from template
445
+ workflow = client.workflows.from_template(template_id)
446
+
447
+ # Get available node types
448
+ node_types = client.workflows.node_types()
449
+
450
+ # Get workflow data
451
+ data = client.workflows.get_data(id)
452
+ ```
453
+
454
+ ## Error Handling
455
+
456
+ ```python
457
+ from kirimel import (
458
+ ApiException,
459
+ AuthenticationException,
460
+ RateLimitException,
461
+ ValidationException
462
+ )
463
+
464
+ try:
465
+ campaign = client.campaigns.create(data)
466
+ except AuthenticationException as e:
467
+ # Invalid API key
468
+ print(f"Authentication failed: {e.message}")
469
+ except RateLimitException as e:
470
+ # Too many requests
471
+ print(f"Rate limited. Retry after: {e.retry_after} seconds")
472
+ except ValidationException as e:
473
+ # Invalid data
474
+ print(f"Validation errors: {e.errors}")
475
+ except ApiException as e:
476
+ # General API error
477
+ print(f"API error ({e.status_code}): {e.message}")
478
+ ```
479
+
480
+ ## Logging
481
+
482
+ ```python
483
+ import logging
484
+
485
+ # Set up logging
486
+ logging.basicConfig(level=logging.DEBUG)
487
+
488
+ # The SDK will use the root logger by default
489
+ ```
490
+
491
+ ## Requirements
492
+
493
+ - Python 3.8 or higher
494
+ - requests >= 2.28.0
495
+
496
+ ## License
497
+
498
+ MIT License
499
+
500
+ ## Support
501
+
502
+ - Documentation: https://docs.kirimel.com
503
+ - GitHub: https://github.com/hualiglobal/kirimel-python-sdk
504
+ - Issues: https://github.com/hualiglobal/kirimel-python-sdk/issues
@@ -0,0 +1,10 @@
1
+ kirimel/__init__.py,sha256=wMYmn0-EMr1F_aOoSPslMueRfjVj8KhUzx8sv7LZp8Y,389
2
+ kirimel/client.py,sha256=plRDNMnM5FTq7y_M8IkALNN-IqtuG95JemuCaTD-daQ,3601
3
+ kirimel/exceptions.py,sha256=M-xnViNC3h7fdHA6EzsmZEWVFar165rKceR6YarIOdw,941
4
+ kirimel/http_client.py,sha256=cMolOCvSAKbJv5Or4qiDOn4V36TEqZfdgeSofKZL8ok,4613
5
+ kirimel/resources/__init__.py,sha256=ZMcaVwoDL6BSUKz3DXqHbQQOWuuAOgmqad71VqJuBVU,16687
6
+ kirimel_python-0.1.0.dist-info/licenses/LICENSE,sha256=7Om6VWyG3CquT1oA7zTeIhMJMzzO1mAyHI2T_1RRxFE,1064
7
+ kirimel_python-0.1.0.dist-info/METADATA,sha256=FlJ7m-fV0GMehMKoY55ZXacoE8mW87WFaAqmZEihVOE,10595
8
+ kirimel_python-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
9
+ kirimel_python-0.1.0.dist-info/top_level.txt,sha256=8bso5h6_Im2tu-GdN4MRfXGDPR7qJ51o0-XusPi_ZI8,8
10
+ kirimel_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KiriMel
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.
@@ -0,0 +1 @@
1
+ kirimel