listmonk-api 0.6.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.
- listmonk_api/__init__.py +83 -0
- listmonk_api/__main__.py +6 -0
- listmonk_api/agent_server.py +85 -0
- listmonk_api/api_client.py +300 -0
- listmonk_api/auth.py +36 -0
- listmonk_api/listmonk_api.py +734 -0
- listmonk_api/main_agent.json +10 -0
- listmonk_api/mcp_config.json +3 -0
- listmonk_api/mcp_server.py +444 -0
- listmonk_api/models.py +75 -0
- listmonk_api-0.6.0.dist-info/METADATA +303 -0
- listmonk_api-0.6.0.dist-info/RECORD +17 -0
- listmonk_api-0.6.0.dist-info/WHEEL +5 -0
- listmonk_api-0.6.0.dist-info/entry_points.txt +3 -0
- listmonk_api-0.6.0.dist-info/licenses/LICENSE +21 -0
- listmonk_api-0.6.0.dist-info/top_level.txt +2 -0
- tests/test_mcp_server.py +11 -0
listmonk_api/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
# Suppress RequestsDependencyWarning due to chardet 6.x / requests 2.32.x mismatch
|
|
9
|
+
# Centralized here to ensure it runs before any sub-package imports
|
|
10
|
+
warnings.filterwarnings("ignore", message=".*urllib3.*or chardet.*")
|
|
11
|
+
|
|
12
|
+
__all__: list[str] = []
|
|
13
|
+
|
|
14
|
+
CORE_MODULES: list[str] = [
|
|
15
|
+
"listmonk_api.api_client",
|
|
16
|
+
"listmonk_api.models",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
OPTIONAL_MODULES = {
|
|
20
|
+
"listmonk_api.agent_server": "agent_server",
|
|
21
|
+
"listmonk_api.mcp_server": "mcp_server",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _expose_members(module):
|
|
26
|
+
"""Expose public classes and functions from a module into globals and __all__."""
|
|
27
|
+
for name, obj in inspect.getmembers(module):
|
|
28
|
+
if (inspect.isclass(obj) or inspect.isfunction(obj)) and not name.startswith(
|
|
29
|
+
"_"
|
|
30
|
+
):
|
|
31
|
+
globals()[name] = obj
|
|
32
|
+
if name not in __all__:
|
|
33
|
+
__all__.append(name)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Eagerly import core modules (keeps API wrappers fast & light)
|
|
37
|
+
for module_name in CORE_MODULES:
|
|
38
|
+
if module_name:
|
|
39
|
+
module = importlib.import_module(module_name)
|
|
40
|
+
_expose_members(module)
|
|
41
|
+
|
|
42
|
+
# Dynamic/lazy loading of optional modules (agent_server, mcp_server)
|
|
43
|
+
_loaded_optional_modules = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _import_module_safely(module_name: str):
|
|
47
|
+
"""Try to import a module and return it, or None if not available."""
|
|
48
|
+
try:
|
|
49
|
+
return importlib.import_module(module_name)
|
|
50
|
+
except ImportError:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def __getattr__(name: str) -> Any:
|
|
55
|
+
# Handle availability flags dynamically without eager imports
|
|
56
|
+
if name == "_MCP_AVAILABLE":
|
|
57
|
+
mcp_key = next((k for k in OPTIONAL_MODULES if "mcp_server" in k), None)
|
|
58
|
+
if mcp_key:
|
|
59
|
+
return _import_module_safely(mcp_key) is not None
|
|
60
|
+
return False
|
|
61
|
+
if name == "_AGENT_AVAILABLE":
|
|
62
|
+
agent_key = next((k for k in OPTIONAL_MODULES if "agent_server" in k), None)
|
|
63
|
+
if agent_key:
|
|
64
|
+
return _import_module_safely(agent_key) is not None
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
# Check optional modules
|
|
68
|
+
for module_name in OPTIONAL_MODULES:
|
|
69
|
+
if module_name not in _loaded_optional_modules:
|
|
70
|
+
module = _import_module_safely(module_name)
|
|
71
|
+
if module is not None:
|
|
72
|
+
_loaded_optional_modules[module_name] = module
|
|
73
|
+
_expose_members(module)
|
|
74
|
+
|
|
75
|
+
module = _loaded_optional_modules.get(module_name)
|
|
76
|
+
if module is not None and hasattr(module, name):
|
|
77
|
+
return getattr(module, name)
|
|
78
|
+
|
|
79
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def __dir__() -> list[str]:
|
|
83
|
+
return sorted(list(globals().keys()) + __all__)
|
listmonk_api/__main__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
__version__ = "0.6.0"
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(
|
|
10
|
+
level=logging.INFO,
|
|
11
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
12
|
+
handlers=[logging.StreamHandler()],
|
|
13
|
+
)
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DEFAULT_AGENT_NAME = None
|
|
18
|
+
DEFAULT_AGENT_DESCRIPTION = None
|
|
19
|
+
DEFAULT_AGENT_SYSTEM_PROMPT = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def agent_server():
|
|
23
|
+
from agent_utilities import (
|
|
24
|
+
build_system_prompt_from_workspace,
|
|
25
|
+
create_agent_parser,
|
|
26
|
+
create_agent_server,
|
|
27
|
+
initialize_workspace,
|
|
28
|
+
load_identity,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
global DEFAULT_AGENT_NAME, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_SYSTEM_PROMPT
|
|
32
|
+
initialize_workspace()
|
|
33
|
+
meta = load_identity()
|
|
34
|
+
DEFAULT_AGENT_NAME = os.getenv(
|
|
35
|
+
"DEFAULT_AGENT_NAME", meta.get("name", "Listmonk Api")
|
|
36
|
+
)
|
|
37
|
+
DEFAULT_AGENT_DESCRIPTION = os.getenv(
|
|
38
|
+
"AGENT_DESCRIPTION",
|
|
39
|
+
meta.get(
|
|
40
|
+
"description",
|
|
41
|
+
"AI agent for Listmonk Api management.",
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
DEFAULT_AGENT_SYSTEM_PROMPT = os.getenv(
|
|
45
|
+
"AGENT_SYSTEM_PROMPT",
|
|
46
|
+
meta.get("content") or build_system_prompt_from_workspace(),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
warnings.filterwarnings("ignore", message=".*urllib3.*or chardet.*")
|
|
50
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module="fastmcp")
|
|
51
|
+
|
|
52
|
+
print(f"{DEFAULT_AGENT_NAME} v{__version__}", file=sys.stderr)
|
|
53
|
+
parser = create_agent_parser()
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
|
|
56
|
+
if args.debug:
|
|
57
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
58
|
+
logger.debug("Debug mode enabled")
|
|
59
|
+
|
|
60
|
+
# Start server using the auto-discovery pattern (from mcp_config.json)
|
|
61
|
+
create_agent_server(
|
|
62
|
+
mcp_url=args.mcp_url,
|
|
63
|
+
mcp_config=args.mcp_config or "mcp_config.json",
|
|
64
|
+
host=args.host,
|
|
65
|
+
port=args.port,
|
|
66
|
+
provider=args.provider,
|
|
67
|
+
model_id=args.model_id,
|
|
68
|
+
router_model=args.model_id,
|
|
69
|
+
agent_model=args.model_id,
|
|
70
|
+
base_url=args.base_url,
|
|
71
|
+
api_key=args.api_key,
|
|
72
|
+
custom_skills_directory=args.custom_skills_directory,
|
|
73
|
+
enable_web_ui=args.web,
|
|
74
|
+
enable_otel=args.otel,
|
|
75
|
+
otel_endpoint=args.otel_endpoint,
|
|
76
|
+
otel_headers=args.otel_headers,
|
|
77
|
+
otel_public_key=args.otel_public_key,
|
|
78
|
+
otel_secret_key=args.otel_secret_key,
|
|
79
|
+
otel_protocol=args.otel_protocol,
|
|
80
|
+
debug=args.debug,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
agent_server()
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Union
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
from listmonk_api.models import (
|
|
5
|
+
SubscriberCreateRequest,
|
|
6
|
+
ListCreateRequest,
|
|
7
|
+
ListEditRequest,
|
|
8
|
+
ImportSubscribersRequest,
|
|
9
|
+
CampaignCreateRequest,
|
|
10
|
+
CampaignStatusRequest,
|
|
11
|
+
MediaUploadRequest,
|
|
12
|
+
TransactionalMessageRequest,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ListmonkAPI:
|
|
17
|
+
"""API client for Listmonk."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, url: str, token: str):
|
|
20
|
+
self.base_url = url
|
|
21
|
+
self._session = requests.Session()
|
|
22
|
+
self._session.headers.update(
|
|
23
|
+
{
|
|
24
|
+
"Authorization": f"Bearer {token}",
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def get(self, endpoint: str, **kwargs):
|
|
30
|
+
url = self.base_url.rstrip("/") + "/" + endpoint.lstrip("/")
|
|
31
|
+
response = self._session.get(url, **kwargs)
|
|
32
|
+
response.raise_for_status()
|
|
33
|
+
return response
|
|
34
|
+
|
|
35
|
+
def post(self, endpoint: str, **kwargs):
|
|
36
|
+
url = self.base_url.rstrip("/") + "/" + endpoint.lstrip("/")
|
|
37
|
+
response = self._session.post(url, **kwargs)
|
|
38
|
+
response.raise_for_status()
|
|
39
|
+
return response
|
|
40
|
+
|
|
41
|
+
def put(self, endpoint: str, **kwargs):
|
|
42
|
+
url = self.base_url.rstrip("/") + "/" + endpoint.lstrip("/")
|
|
43
|
+
response = self._session.put(url, **kwargs)
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
def delete(self, endpoint: str, **kwargs):
|
|
48
|
+
url = self.base_url.rstrip("/") + "/" + endpoint.lstrip("/")
|
|
49
|
+
response = self._session.delete(url, **kwargs)
|
|
50
|
+
response.raise_for_status()
|
|
51
|
+
return response
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
54
|
+
# Subscribers API
|
|
55
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
56
|
+
def get_subscribers(
|
|
57
|
+
self,
|
|
58
|
+
query: Optional[Dict] = None,
|
|
59
|
+
list_id: Optional[Union[int, List[int]]] = None,
|
|
60
|
+
max_pages: int = 0,
|
|
61
|
+
per_page: int = 100,
|
|
62
|
+
):
|
|
63
|
+
# Fetch the first page to get X-Total-Pages
|
|
64
|
+
response = self.get(f"/subscribers?per_page={per_page}")
|
|
65
|
+
# BUG FIX 1: Safely get X-Total-Pages, defaulting to 1 if not present
|
|
66
|
+
total_pages = int(response.headers.get("X-Total-Pages", 1))
|
|
67
|
+
|
|
68
|
+
results = []
|
|
69
|
+
subscriber_filter = f"?per_page={per_page}"
|
|
70
|
+
|
|
71
|
+
if list_id:
|
|
72
|
+
if isinstance(list_id, list):
|
|
73
|
+
for single_list_id in list_id:
|
|
74
|
+
subscriber_filter += f"&list_id={single_list_id}"
|
|
75
|
+
else:
|
|
76
|
+
subscriber_filter += f"&list_id={list_id}"
|
|
77
|
+
|
|
78
|
+
if max_pages == 0 or max_pages > total_pages:
|
|
79
|
+
max_pages = total_pages
|
|
80
|
+
|
|
81
|
+
data = None
|
|
82
|
+
if query:
|
|
83
|
+
data = query
|
|
84
|
+
|
|
85
|
+
for page in range(1, max_pages + 1):
|
|
86
|
+
# Listmonk usually uses 1-indexed pages for its endpoints like `?page=1`
|
|
87
|
+
response_page = self.get(
|
|
88
|
+
f"/subscribers{subscriber_filter}&page={page}", json=data
|
|
89
|
+
)
|
|
90
|
+
response_json = response_page.json()
|
|
91
|
+
if isinstance(response_json, dict) and "data" in response_json:
|
|
92
|
+
results.extend(response_json["data"].get("results", []))
|
|
93
|
+
elif isinstance(response_json, list):
|
|
94
|
+
results.extend(response_json)
|
|
95
|
+
else:
|
|
96
|
+
results.append(response_json)
|
|
97
|
+
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
def get_subscriber(self, subscriber_id: int):
|
|
101
|
+
return self.get(f"/subscribers/{subscriber_id}").json()
|
|
102
|
+
|
|
103
|
+
def get_subscribers_from_list(self, list_id: int):
|
|
104
|
+
return self.get(f"/subscribers/lists/{list_id}").json()
|
|
105
|
+
|
|
106
|
+
def create_subscriber(self, data: SubscriberCreateRequest):
|
|
107
|
+
return self.post("/subscribers", json=data.model_dump(exclude_none=True)).json()
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
110
|
+
# Lists API
|
|
111
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
112
|
+
def get_lists(
|
|
113
|
+
self,
|
|
114
|
+
query: Optional[Dict] = None,
|
|
115
|
+
order_by: Optional[str] = None,
|
|
116
|
+
order: Optional[str] = None,
|
|
117
|
+
max_pages: int = 0,
|
|
118
|
+
per_page: int = 100,
|
|
119
|
+
):
|
|
120
|
+
response = self.get(f"/lists?per_page={per_page}")
|
|
121
|
+
total_pages = int(response.headers.get("X-Total-Pages", 1))
|
|
122
|
+
|
|
123
|
+
results = []
|
|
124
|
+
list_filter = f"?per_page={per_page}"
|
|
125
|
+
|
|
126
|
+
if order_by and order_by in ["name", "status", "created_at", "updated_at"]:
|
|
127
|
+
list_filter += f"&order_by={order_by}"
|
|
128
|
+
if order and order.upper() in ["ASC", "DESC"]:
|
|
129
|
+
list_filter += f"&order={order}"
|
|
130
|
+
|
|
131
|
+
if max_pages == 0 or max_pages > total_pages:
|
|
132
|
+
max_pages = total_pages
|
|
133
|
+
|
|
134
|
+
data = None
|
|
135
|
+
if query:
|
|
136
|
+
data = query
|
|
137
|
+
|
|
138
|
+
for page in range(1, max_pages + 1):
|
|
139
|
+
response_page = self.get(f"/lists{list_filter}&page={page}", json=data)
|
|
140
|
+
response_json = response_page.json()
|
|
141
|
+
if isinstance(response_json, dict) and "data" in response_json:
|
|
142
|
+
results.extend(response_json["data"].get("results", []))
|
|
143
|
+
elif isinstance(response_json, list):
|
|
144
|
+
results.extend(response_json)
|
|
145
|
+
else:
|
|
146
|
+
results.append(response_json)
|
|
147
|
+
|
|
148
|
+
return results
|
|
149
|
+
|
|
150
|
+
def get_list(self, list_id: int):
|
|
151
|
+
return self.get(f"/lists/{list_id}").json()
|
|
152
|
+
|
|
153
|
+
def create_list(self, data: ListCreateRequest):
|
|
154
|
+
return self.post("/lists", json=data.model_dump(exclude_none=True)).json()
|
|
155
|
+
|
|
156
|
+
def edit_list(self, list_id: int, data: ListEditRequest):
|
|
157
|
+
return self.put(
|
|
158
|
+
f"/lists/{list_id}", json=data.model_dump(exclude_none=True)
|
|
159
|
+
).json()
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
162
|
+
# Import API
|
|
163
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
164
|
+
def get_subscriber_import_status(self):
|
|
165
|
+
return self.get("/import/subscribers").json()
|
|
166
|
+
|
|
167
|
+
def get_subscriber_import_logs(self):
|
|
168
|
+
return self.get("/import/subscribers/logs").json()
|
|
169
|
+
|
|
170
|
+
def import_subscribers(self, data: ImportSubscribersRequest):
|
|
171
|
+
# NOTE: Using self.post instead of self.get for importing as it was incorrect in the old wrapper.
|
|
172
|
+
# Actually listmonk expects a POST for import
|
|
173
|
+
return self.post(
|
|
174
|
+
"/import/subscribers", json=data.model_dump(exclude_none=True)
|
|
175
|
+
).json()
|
|
176
|
+
|
|
177
|
+
def delete_subscriber_import(self):
|
|
178
|
+
return self.delete("/import/subscribers/logs").json()
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
181
|
+
# Campaigns API
|
|
182
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
183
|
+
def get_campaigns(
|
|
184
|
+
self,
|
|
185
|
+
query: Optional[Dict] = None,
|
|
186
|
+
order_by: Optional[str] = None,
|
|
187
|
+
order: Optional[str] = None,
|
|
188
|
+
max_pages: int = 0,
|
|
189
|
+
per_page: int = 100,
|
|
190
|
+
):
|
|
191
|
+
response = self.get(f"/campaigns?per_page={per_page}")
|
|
192
|
+
total_pages = int(response.headers.get("X-Total-Pages", 1))
|
|
193
|
+
|
|
194
|
+
results = []
|
|
195
|
+
campaign_filter = f"?per_page={per_page}"
|
|
196
|
+
|
|
197
|
+
if order_by and order_by in ["name", "status", "created_at", "updated_at"]:
|
|
198
|
+
campaign_filter += f"&order_by={order_by}"
|
|
199
|
+
if order and order.upper() in ["ASC", "DESC"]:
|
|
200
|
+
campaign_filter += f"&order={order}"
|
|
201
|
+
|
|
202
|
+
if max_pages == 0 or max_pages > total_pages:
|
|
203
|
+
max_pages = total_pages
|
|
204
|
+
|
|
205
|
+
data = None
|
|
206
|
+
if query:
|
|
207
|
+
data = query
|
|
208
|
+
|
|
209
|
+
for page in range(1, max_pages + 1):
|
|
210
|
+
response_page = self.get(
|
|
211
|
+
f"/campaigns{campaign_filter}&page={page}", json=data
|
|
212
|
+
)
|
|
213
|
+
response_json = response_page.json()
|
|
214
|
+
if isinstance(response_json, dict) and "data" in response_json:
|
|
215
|
+
results.extend(response_json["data"].get("results", []))
|
|
216
|
+
elif isinstance(response_json, list):
|
|
217
|
+
results.extend(response_json)
|
|
218
|
+
else:
|
|
219
|
+
results.append(response_json)
|
|
220
|
+
|
|
221
|
+
return results
|
|
222
|
+
|
|
223
|
+
def get_campaign(self, campaign_id: int):
|
|
224
|
+
return self.get(f"/campaigns/{campaign_id}").json()
|
|
225
|
+
|
|
226
|
+
def get_campaign_preview(self, campaign_id: int):
|
|
227
|
+
return self.get(f"/campaigns/{campaign_id}/preview").json()
|
|
228
|
+
|
|
229
|
+
def get_campaign_stats(self, campaign_id: int):
|
|
230
|
+
return self.get(f"/campaigns/{campaign_id}/running/stats").json()
|
|
231
|
+
|
|
232
|
+
def create_campaign(self, data: CampaignCreateRequest):
|
|
233
|
+
# BUG FIX 2 & 3: Included attachments in data model and mapped send_type to type via pydantic alias (by_alias=True)
|
|
234
|
+
return self.post(
|
|
235
|
+
"/campaigns", json=data.model_dump(by_alias=True, exclude_none=True)
|
|
236
|
+
).json()
|
|
237
|
+
|
|
238
|
+
def set_campaign_status(self, campaign_id: int, data: CampaignStatusRequest):
|
|
239
|
+
return self.put(
|
|
240
|
+
f"/campaigns/{campaign_id}/status", json=data.model_dump(exclude_none=True)
|
|
241
|
+
).json()
|
|
242
|
+
|
|
243
|
+
def delete_campaign(self, campaign_id: int):
|
|
244
|
+
return self.delete(f"/campaigns/{campaign_id}").json()
|
|
245
|
+
|
|
246
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
247
|
+
# Media API
|
|
248
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
249
|
+
def get_media(self, media_id: int):
|
|
250
|
+
return self.get("/media").json()
|
|
251
|
+
|
|
252
|
+
def upload_media(self, data: MediaUploadRequest):
|
|
253
|
+
return self.post("/media", json=data.model_dump(exclude_none=True)).json()
|
|
254
|
+
|
|
255
|
+
def delete_media(self, media_id: int):
|
|
256
|
+
return self.delete(f"/media/{media_id}").json()
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
259
|
+
# Templates API
|
|
260
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
261
|
+
def get_templates(self, max_pages: int = 0, per_page: int = 100):
|
|
262
|
+
response = self.get(f"/templates?per_page={per_page}")
|
|
263
|
+
total_pages = int(response.headers.get("X-Total-Pages", 1))
|
|
264
|
+
|
|
265
|
+
results = []
|
|
266
|
+
template_filter = f"?per_page={per_page}"
|
|
267
|
+
|
|
268
|
+
if max_pages == 0 or max_pages > total_pages:
|
|
269
|
+
max_pages = total_pages
|
|
270
|
+
|
|
271
|
+
for page in range(1, max_pages + 1):
|
|
272
|
+
response_page = self.get(f"/templates{template_filter}&page={page}")
|
|
273
|
+
response_json = response_page.json()
|
|
274
|
+
if isinstance(response_json, dict) and "data" in response_json:
|
|
275
|
+
results.extend(response_json["data"].get("results", []))
|
|
276
|
+
elif isinstance(response_json, list):
|
|
277
|
+
results.extend(response_json)
|
|
278
|
+
else:
|
|
279
|
+
results.append(response_json)
|
|
280
|
+
|
|
281
|
+
return results
|
|
282
|
+
|
|
283
|
+
def get_template(self, template_id: int):
|
|
284
|
+
return self.get(f"/templates/{template_id}").json()
|
|
285
|
+
|
|
286
|
+
def get_template_preview(self, template_id: int):
|
|
287
|
+
return self.get(f"/templates/{template_id}/preview").json()
|
|
288
|
+
|
|
289
|
+
def set_default_template(self, template_id: int):
|
|
290
|
+
return self.put(f"/templates/{template_id}/default").json()
|
|
291
|
+
|
|
292
|
+
def delete_template(self, template_id: int):
|
|
293
|
+
return self.delete(f"/templates/{template_id}").json()
|
|
294
|
+
|
|
295
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
296
|
+
# Transactional API
|
|
297
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
298
|
+
def transactional_message(self, data: TransactionalMessageRequest):
|
|
299
|
+
# BUG FIX 4: Included attachments in data model and excluded None
|
|
300
|
+
return self.post("/tx", json=data.model_dump(exclude_none=True)).json()
|
listmonk_api/auth.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import urllib3
|
|
6
|
+
|
|
7
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
8
|
+
|
|
9
|
+
from agent_utilities.core.exceptions import AuthError, UnauthorizedError
|
|
10
|
+
|
|
11
|
+
from listmonk_api.api_client import ListmonkAPI
|
|
12
|
+
|
|
13
|
+
_client = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_client():
|
|
17
|
+
"""Get or create a singleton API client instance."""
|
|
18
|
+
global _client
|
|
19
|
+
if _client is None:
|
|
20
|
+
base_url = os.getenv("LISTMONK_URL", "http://localhost:8080")
|
|
21
|
+
token = os.getenv("LISTMONK_TOKEN", "")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
_client = ListmonkAPI(
|
|
25
|
+
url=base_url,
|
|
26
|
+
token=token,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
except (AuthError, UnauthorizedError) as e:
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
f"AUTHENTICATION ERROR: The credentials provided are not valid for '{base_url}'. "
|
|
32
|
+
f"Please check your LISTMONK_TOKEN and LISTMONK_URL environment variables. "
|
|
33
|
+
f"Error details: {str(e)}"
|
|
34
|
+
) from e
|
|
35
|
+
|
|
36
|
+
return _client
|