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 +21 -0
- kirimel/client.py +122 -0
- kirimel/exceptions.py +38 -0
- kirimel/http_client.py +128 -0
- kirimel/resources/__init__.py +437 -0
- kirimel_python-0.1.0.dist-info/METADATA +504 -0
- kirimel_python-0.1.0.dist-info/RECORD +10 -0
- kirimel_python-0.1.0.dist-info/WHEEL +5 -0
- kirimel_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- kirimel_python-0.1.0.dist-info/top_level.txt +1 -0
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,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
|