limely-sdk 0.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: limely-sdk
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Official Python client for the Limely API (chatbots, knowledge bases, forms, conversations, analytics).
|
|
5
|
+
Project-URL: Homepage, https://limely.co
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: api-client,chatbot,limely,sdk
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# limely-sdk (Python)
|
|
13
|
+
|
|
14
|
+
Official Python client for the [Limely](https://limely.co) API. Manage chatbots,
|
|
15
|
+
knowledge bases, forms, conversations, and analytics.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install limely-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Imports as `limely`.
|
|
24
|
+
|
|
25
|
+
## Auth
|
|
26
|
+
|
|
27
|
+
The Limely API authenticates with a Clerk bearer token and scopes every call to
|
|
28
|
+
the token owner's workspace.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from limely import LimelyClient
|
|
32
|
+
|
|
33
|
+
with LimelyClient(token="...") as limely:
|
|
34
|
+
types = limely.chatbot_types.list()
|
|
35
|
+
|
|
36
|
+
kb = limely.knowledge_bases.create_from_text(
|
|
37
|
+
name="Support docs",
|
|
38
|
+
content="Our return window is 30 days...",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
bot = limely.chatbots.create(
|
|
42
|
+
name="Support bot",
|
|
43
|
+
chatbot_type_id=types[0]["_id"],
|
|
44
|
+
knowledge_base_ids=[kb["_id"]],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
stats = limely.analytics.chatbot(bot["_id"], start="2026-06-01", end="2026-06-30")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
- `workspace.get()`
|
|
53
|
+
- `chatbot_types.list()`
|
|
54
|
+
- `chatbots.list() | get(id) | create(...) | toggle_status(id) | delete(id)`
|
|
55
|
+
- `knowledge_bases.list() | create_from_text(...) | delete(id)`
|
|
56
|
+
- `forms.list() | get(id) | create(...) | submissions(id) | templates()`
|
|
57
|
+
- `analytics.dashboard() | billing() | chatbot(id, start=, end=)`
|
|
58
|
+
- `chats.list(start_date=, end_date=) | get(session_id)`
|
|
59
|
+
|
|
60
|
+
Errors are raised as `LimelyApiError` with `.status_code` and `.body`.
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
Python >= 3.9. Depends on `httpx`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# limely-sdk (Python)
|
|
2
|
+
|
|
3
|
+
Official Python client for the [Limely](https://limely.co) API. Manage chatbots,
|
|
4
|
+
knowledge bases, forms, conversations, and analytics.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install limely-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Imports as `limely`.
|
|
13
|
+
|
|
14
|
+
## Auth
|
|
15
|
+
|
|
16
|
+
The Limely API authenticates with a Clerk bearer token and scopes every call to
|
|
17
|
+
the token owner's workspace.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from limely import LimelyClient
|
|
21
|
+
|
|
22
|
+
with LimelyClient(token="...") as limely:
|
|
23
|
+
types = limely.chatbot_types.list()
|
|
24
|
+
|
|
25
|
+
kb = limely.knowledge_bases.create_from_text(
|
|
26
|
+
name="Support docs",
|
|
27
|
+
content="Our return window is 30 days...",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
bot = limely.chatbots.create(
|
|
31
|
+
name="Support bot",
|
|
32
|
+
chatbot_type_id=types[0]["_id"],
|
|
33
|
+
knowledge_base_ids=[kb["_id"]],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
stats = limely.analytics.chatbot(bot["_id"], start="2026-06-01", end="2026-06-30")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
- `workspace.get()`
|
|
42
|
+
- `chatbot_types.list()`
|
|
43
|
+
- `chatbots.list() | get(id) | create(...) | toggle_status(id) | delete(id)`
|
|
44
|
+
- `knowledge_bases.list() | create_from_text(...) | delete(id)`
|
|
45
|
+
- `forms.list() | get(id) | create(...) | submissions(id) | templates()`
|
|
46
|
+
- `analytics.dashboard() | billing() | chatbot(id, start=, end=)`
|
|
47
|
+
- `chats.list(start_date=, end_date=) | get(session_id)`
|
|
48
|
+
|
|
49
|
+
Errors are raised as `LimelyApiError` with `.status_code` and `.body`.
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
Python >= 3.9. Depends on `httpx`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "limely-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Official Python client for the Limely API (chatbots, knowledge bases, forms, conversations, analytics)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["limely", "sdk", "chatbot", "api-client"]
|
|
13
|
+
dependencies = ["httpx>=0.27"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://limely.co"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.version]
|
|
19
|
+
source = "vcs"
|
|
20
|
+
# The release tag is `vX.Y.Z`; strip the local segment so PyPI accepts it.
|
|
21
|
+
# fallback_version is used when building outside a tagged commit (local dev).
|
|
22
|
+
raw-options = { local_scheme = "no-local-version", fallback_version = "0.0.0" }
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/limely"]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Synchronous client for the Limely API.
|
|
2
|
+
|
|
3
|
+
The Limely API authenticates with a Clerk bearer token and scopes every call to
|
|
4
|
+
the token owner's workspace. Construct the client with that token::
|
|
5
|
+
|
|
6
|
+
from limely import LimelyClient
|
|
7
|
+
|
|
8
|
+
limely = LimelyClient(token="...")
|
|
9
|
+
bots = limely.chatbots.list()
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.limely.co"
|
|
20
|
+
DEFAULT_TIMEOUT = 30.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LimelyApiError(Exception):
|
|
24
|
+
"""Raised when the Limely API returns a non-2xx response."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, status_code: int, body: Any) -> None:
|
|
27
|
+
self.status_code = status_code
|
|
28
|
+
self.body = body
|
|
29
|
+
detail = body if isinstance(body, str) else json.dumps(body)
|
|
30
|
+
super().__init__(f"Limely API responded {status_code}: {detail}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _clean_params(params: Optional[Mapping[str, Any]]) -> dict[str, Any]:
|
|
34
|
+
if not params:
|
|
35
|
+
return {}
|
|
36
|
+
return {k: v for k, v in params.items() if v is not None and v != ""}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LimelyClient:
|
|
40
|
+
"""Client for the Limely API."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
token: str,
|
|
45
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
46
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
47
|
+
http_client: Optional[httpx.Client] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
if not token:
|
|
50
|
+
raise ValueError("LimelyClient requires a `token`.")
|
|
51
|
+
self._client = http_client or httpx.Client(
|
|
52
|
+
base_url=base_url.rstrip("/"),
|
|
53
|
+
timeout=timeout,
|
|
54
|
+
headers={
|
|
55
|
+
"Authorization": f"Bearer {token}",
|
|
56
|
+
"Accept": "application/json",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
self.workspace = _WorkspaceResource(self)
|
|
60
|
+
self.chatbot_types = _ChatbotTypesResource(self)
|
|
61
|
+
self.chatbots = _ChatbotsResource(self)
|
|
62
|
+
self.knowledge_bases = _KnowledgeBasesResource(self)
|
|
63
|
+
self.forms = _FormsResource(self)
|
|
64
|
+
self.analytics = _AnalyticsResource(self)
|
|
65
|
+
self.chats = _ChatsResource(self)
|
|
66
|
+
|
|
67
|
+
# -- low-level ----------------------------------------------------------
|
|
68
|
+
def request(
|
|
69
|
+
self,
|
|
70
|
+
method: str,
|
|
71
|
+
path: str,
|
|
72
|
+
*,
|
|
73
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
74
|
+
json_body: Any = None,
|
|
75
|
+
data: Optional[Mapping[str, Any]] = None,
|
|
76
|
+
files: Optional[Mapping[str, Any]] = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
response = self._client.request(
|
|
79
|
+
method,
|
|
80
|
+
path,
|
|
81
|
+
params=_clean_params(params),
|
|
82
|
+
json=json_body,
|
|
83
|
+
data=data,
|
|
84
|
+
files=files,
|
|
85
|
+
)
|
|
86
|
+
text = response.text
|
|
87
|
+
try:
|
|
88
|
+
parsed: Any = response.json() if text else None
|
|
89
|
+
except ValueError:
|
|
90
|
+
parsed = text
|
|
91
|
+
if response.is_error:
|
|
92
|
+
raise LimelyApiError(response.status_code, parsed)
|
|
93
|
+
return parsed
|
|
94
|
+
|
|
95
|
+
# -- lifecycle ----------------------------------------------------------
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
self._client.close()
|
|
98
|
+
|
|
99
|
+
def __enter__(self) -> "LimelyClient":
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def __exit__(self, *_exc: object) -> None:
|
|
103
|
+
self.close()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _Resource:
|
|
107
|
+
def __init__(self, client: LimelyClient) -> None:
|
|
108
|
+
self._c = client
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _WorkspaceResource(_Resource):
|
|
112
|
+
def get(self) -> Any:
|
|
113
|
+
return self._c.request("GET", "/workspace")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _ChatbotTypesResource(_Resource):
|
|
117
|
+
def list(self) -> Any:
|
|
118
|
+
return self._c.request("GET", "/chatbot-type")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _ChatbotsResource(_Resource):
|
|
122
|
+
def list(self) -> Any:
|
|
123
|
+
return self._c.request("GET", "/chatbot")
|
|
124
|
+
|
|
125
|
+
def get(self, chatbot_id: str) -> Any:
|
|
126
|
+
return self._c.request("GET", f"/chatbot/{chatbot_id}")
|
|
127
|
+
|
|
128
|
+
def create(
|
|
129
|
+
self,
|
|
130
|
+
*,
|
|
131
|
+
name: str,
|
|
132
|
+
chatbot_type_id: str,
|
|
133
|
+
knowledge_base_ids: Sequence[str],
|
|
134
|
+
description: Optional[str] = None,
|
|
135
|
+
voice_chat: Optional[bool] = None,
|
|
136
|
+
rate_limiting: Optional[int] = None,
|
|
137
|
+
behavior: Optional[Mapping[str, Any]] = None,
|
|
138
|
+
appearance: Optional[Mapping[str, Any]] = None,
|
|
139
|
+
) -> Any:
|
|
140
|
+
body: dict[str, Any] = {
|
|
141
|
+
"name": name,
|
|
142
|
+
"chatbotTypeId": chatbot_type_id,
|
|
143
|
+
"knowledgeBaseIds": list(knowledge_base_ids),
|
|
144
|
+
}
|
|
145
|
+
if description is not None:
|
|
146
|
+
body["description"] = description
|
|
147
|
+
if voice_chat is not None:
|
|
148
|
+
body["voice_chat"] = voice_chat
|
|
149
|
+
if rate_limiting is not None:
|
|
150
|
+
body["rateLimiting"] = rate_limiting
|
|
151
|
+
if behavior is not None:
|
|
152
|
+
body["behavior"] = dict(behavior)
|
|
153
|
+
if appearance is not None:
|
|
154
|
+
body["appearance"] = dict(appearance)
|
|
155
|
+
return self._c.request("POST", "/chatbot", json_body=body)
|
|
156
|
+
|
|
157
|
+
def toggle_status(self, chatbot_id: str) -> Any:
|
|
158
|
+
return self._c.request("PATCH", f"/chatbot/{chatbot_id}/toggle-status")
|
|
159
|
+
|
|
160
|
+
def delete(self, chatbot_id: str) -> Any:
|
|
161
|
+
return self._c.request("DELETE", f"/chatbot/{chatbot_id}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class _KnowledgeBasesResource(_Resource):
|
|
165
|
+
def list(self) -> Any:
|
|
166
|
+
return self._c.request("GET", "/knowledgeBase")
|
|
167
|
+
|
|
168
|
+
def create_from_text(
|
|
169
|
+
self,
|
|
170
|
+
*,
|
|
171
|
+
name: str,
|
|
172
|
+
content: str,
|
|
173
|
+
description: Optional[str] = None,
|
|
174
|
+
tags: Optional[Sequence[str]] = None,
|
|
175
|
+
filename: Optional[str] = None,
|
|
176
|
+
) -> Any:
|
|
177
|
+
data: dict[str, Any] = {
|
|
178
|
+
"name": name,
|
|
179
|
+
"fileCharacter": json.dumps([{"characters": len(content)}]),
|
|
180
|
+
}
|
|
181
|
+
if description is not None:
|
|
182
|
+
data["description"] = description
|
|
183
|
+
if tags:
|
|
184
|
+
data["tags"] = json.dumps(list(tags))
|
|
185
|
+
base = (filename or f"{name}.txt").strip()
|
|
186
|
+
if not base.endswith(".txt"):
|
|
187
|
+
base = f"{base}.txt"
|
|
188
|
+
files = {"files": (base, content.encode("utf-8"), "text/plain")}
|
|
189
|
+
return self._c.request("POST", "/knowledgeBase", data=data, files=files)
|
|
190
|
+
|
|
191
|
+
def delete(self, knowledge_base_id: str) -> Any:
|
|
192
|
+
return self._c.request("DELETE", f"/knowledgeBase/{knowledge_base_id}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class _FormsResource(_Resource):
|
|
196
|
+
def list(self) -> Any:
|
|
197
|
+
return self._c.request("GET", "/forms")
|
|
198
|
+
|
|
199
|
+
def get(self, form_id: str) -> Any:
|
|
200
|
+
return self._c.request("GET", f"/forms/{form_id}")
|
|
201
|
+
|
|
202
|
+
def create(
|
|
203
|
+
self,
|
|
204
|
+
*,
|
|
205
|
+
name: str,
|
|
206
|
+
form_type: Optional[str] = None,
|
|
207
|
+
status: Optional[str] = None,
|
|
208
|
+
questions: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
209
|
+
theme_settings: Optional[Mapping[str, Any]] = None,
|
|
210
|
+
) -> Any:
|
|
211
|
+
body: dict[str, Any] = {"name": name}
|
|
212
|
+
if form_type is not None:
|
|
213
|
+
body["form_type"] = form_type
|
|
214
|
+
if status is not None:
|
|
215
|
+
body["status"] = status
|
|
216
|
+
if questions is not None:
|
|
217
|
+
body["questions"] = [dict(q) for q in questions]
|
|
218
|
+
if theme_settings is not None:
|
|
219
|
+
body["theme_settings"] = dict(theme_settings)
|
|
220
|
+
return self._c.request("POST", "/forms", json_body=body)
|
|
221
|
+
|
|
222
|
+
def submissions(self, form_id: str) -> Any:
|
|
223
|
+
return self._c.request("GET", f"/forms/{form_id}/submissions")
|
|
224
|
+
|
|
225
|
+
def templates(self) -> Any:
|
|
226
|
+
return self._c.request("GET", "/form-templates")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class _AnalyticsResource(_Resource):
|
|
230
|
+
def dashboard(self) -> Any:
|
|
231
|
+
return self._c.request("GET", "/analytics/dashboard")
|
|
232
|
+
|
|
233
|
+
def billing(self) -> Any:
|
|
234
|
+
return self._c.request("GET", "/analytics/billing")
|
|
235
|
+
|
|
236
|
+
def chatbot(
|
|
237
|
+
self,
|
|
238
|
+
chatbot_id: str,
|
|
239
|
+
*,
|
|
240
|
+
start: Optional[str] = None,
|
|
241
|
+
end: Optional[str] = None,
|
|
242
|
+
) -> Any:
|
|
243
|
+
return self._c.request(
|
|
244
|
+
"GET",
|
|
245
|
+
f"/analytics/chatbot/{chatbot_id}",
|
|
246
|
+
params={"start": start, "end": end},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class _ChatsResource(_Resource):
|
|
251
|
+
def list(
|
|
252
|
+
self,
|
|
253
|
+
*,
|
|
254
|
+
start_date: Optional[str] = None,
|
|
255
|
+
end_date: Optional[str] = None,
|
|
256
|
+
) -> Any:
|
|
257
|
+
return self._c.request(
|
|
258
|
+
"GET",
|
|
259
|
+
"/chats/user",
|
|
260
|
+
params={"startDate": start_date, "endDate": end_date},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def get(self, session_id: str) -> Any:
|
|
264
|
+
return self._c.request("GET", f"/chats/session/{session_id}")
|
|
File without changes
|