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,6 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .venv/
@@ -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,5 @@
1
+ """Official Python client for the Limely API."""
2
+
3
+ from .client import LimelyClient, LimelyApiError
4
+
5
+ __all__ = ["LimelyClient", "LimelyApiError"]
@@ -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