forjio-suppuo 0.1.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.
- forjio_suppuo-0.1.0/.gitignore +4 -0
- forjio_suppuo-0.1.0/PKG-INFO +57 -0
- forjio_suppuo-0.1.0/README.md +43 -0
- forjio_suppuo-0.1.0/forjio_suppuo/__init__.py +6 -0
- forjio_suppuo-0.1.0/forjio_suppuo/client.py +279 -0
- forjio_suppuo-0.1.0/forjio_suppuo/errors.py +47 -0
- forjio_suppuo-0.1.0/pyproject.toml +25 -0
- forjio_suppuo-0.1.0/tests/test_client.py +21 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forjio-suppuo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Suppuo SDK — typed Python client for the suppuo.com REST API. Sister to the JS + Go SDKs.
|
|
5
|
+
Project-URL: Homepage, https://suppuo.com/docs/sdk/python
|
|
6
|
+
Project-URL: Repository, https://github.com/hachimi-cat/saas-suppuo
|
|
7
|
+
Author-email: Forjio <support@forjio.com>
|
|
8
|
+
License: Proprietary
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# forjio-suppuo
|
|
16
|
+
|
|
17
|
+
Typed Python client for the [suppuo.com](https://suppuo.com) helpdesk REST API.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install forjio-suppuo
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from forjio_suppuo import SuppuoClient
|
|
25
|
+
|
|
26
|
+
# Bearer token from token= or the SUPPUO_TOKEN env var.
|
|
27
|
+
client = SuppuoClient(token="...")
|
|
28
|
+
|
|
29
|
+
# Agent workspace surface
|
|
30
|
+
page = client.tickets.list(status="open")
|
|
31
|
+
ticket = client.tickets.get(page["tickets"][0]["id"])
|
|
32
|
+
client.tickets.reply(ticket["id"], body="On it!", is_internal=False)
|
|
33
|
+
client.tickets.update(ticket["id"], status="resolved")
|
|
34
|
+
|
|
35
|
+
# Canned replies
|
|
36
|
+
client.canned_replies.create(title="Refund policy", body="...")
|
|
37
|
+
|
|
38
|
+
# Public (requester) surface — no token required
|
|
39
|
+
out = client.public.submit_ticket(
|
|
40
|
+
account_id="acc_...",
|
|
41
|
+
subject="Order question",
|
|
42
|
+
body="Where is my order?",
|
|
43
|
+
email="customer@example.com",
|
|
44
|
+
)
|
|
45
|
+
view = client.public.get_ticket(out["accessToken"])
|
|
46
|
+
client.public.reply_ticket(out["accessToken"], body="Any update?")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Errors raise `SuppuoError` carrying the API envelope's `error.code`
|
|
50
|
+
(`NOT_FOUND`, `VALIDATION_ERROR`, `AUTH_REQUIRED`, ...), the HTTP
|
|
51
|
+
status, and the `meta.requestId`.
|
|
52
|
+
|
|
53
|
+
## Family
|
|
54
|
+
|
|
55
|
+
Sister to:
|
|
56
|
+
- [`@forjio/suppuo`](https://www.npmjs.com/package/@forjio/suppuo) (JS/TS)
|
|
57
|
+
- [`hachimi-cat/suppuo-go`](https://github.com/hachimi-cat/suppuo-go) (Go)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# forjio-suppuo
|
|
2
|
+
|
|
3
|
+
Typed Python client for the [suppuo.com](https://suppuo.com) helpdesk REST API.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install forjio-suppuo
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from forjio_suppuo import SuppuoClient
|
|
11
|
+
|
|
12
|
+
# Bearer token from token= or the SUPPUO_TOKEN env var.
|
|
13
|
+
client = SuppuoClient(token="...")
|
|
14
|
+
|
|
15
|
+
# Agent workspace surface
|
|
16
|
+
page = client.tickets.list(status="open")
|
|
17
|
+
ticket = client.tickets.get(page["tickets"][0]["id"])
|
|
18
|
+
client.tickets.reply(ticket["id"], body="On it!", is_internal=False)
|
|
19
|
+
client.tickets.update(ticket["id"], status="resolved")
|
|
20
|
+
|
|
21
|
+
# Canned replies
|
|
22
|
+
client.canned_replies.create(title="Refund policy", body="...")
|
|
23
|
+
|
|
24
|
+
# Public (requester) surface — no token required
|
|
25
|
+
out = client.public.submit_ticket(
|
|
26
|
+
account_id="acc_...",
|
|
27
|
+
subject="Order question",
|
|
28
|
+
body="Where is my order?",
|
|
29
|
+
email="customer@example.com",
|
|
30
|
+
)
|
|
31
|
+
view = client.public.get_ticket(out["accessToken"])
|
|
32
|
+
client.public.reply_ticket(out["accessToken"], body="Any update?")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Errors raise `SuppuoError` carrying the API envelope's `error.code`
|
|
36
|
+
(`NOT_FOUND`, `VALIDATION_ERROR`, `AUTH_REQUIRED`, ...), the HTTP
|
|
37
|
+
status, and the `meta.requestId`.
|
|
38
|
+
|
|
39
|
+
## Family
|
|
40
|
+
|
|
41
|
+
Sister to:
|
|
42
|
+
- [`@forjio/suppuo`](https://www.npmjs.com/package/@forjio/suppuo) (JS/TS)
|
|
43
|
+
- [`hachimi-cat/suppuo-go`](https://github.com/hachimi-cat/suppuo-go) (Go)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Suppuo client — mirrors ``@forjio/suppuo`` (JS) 1:1.
|
|
2
|
+
|
|
3
|
+
Auth = Bearer JWT (a Huudis-minted access token). Pass ``token=`` or set
|
|
4
|
+
``SUPPUO_TOKEN``. The ``public`` namespace (requester-facing hosted-form
|
|
5
|
+
endpoints) needs no token at all.
|
|
6
|
+
|
|
7
|
+
Every response rides the Forjio envelope ``{data, error, meta}``; the
|
|
8
|
+
client unwraps it and raises :class:`SuppuoError` (with the envelope's
|
|
9
|
+
``error.code``) on failure.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
from urllib.parse import quote, urlencode
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from .errors import SuppuoError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _qs(params: Optional[Dict[str, Any]]) -> str:
|
|
24
|
+
if not params:
|
|
25
|
+
return ""
|
|
26
|
+
entries = [(k, v) for k, v in params.items() if v is not None]
|
|
27
|
+
if not entries:
|
|
28
|
+
return ""
|
|
29
|
+
return "?" + urlencode([(k, str(v)) for k, v in entries])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _enc(segment: str) -> str:
|
|
33
|
+
return quote(segment, safe="")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Tickets:
|
|
37
|
+
"""Agent workspace surface (Bearer auth)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, c: "SuppuoClient") -> None:
|
|
40
|
+
self._c = c
|
|
41
|
+
|
|
42
|
+
def list(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
status: Optional[str] = None,
|
|
46
|
+
limit: Optional[int] = None,
|
|
47
|
+
) -> Dict[str, Any]:
|
|
48
|
+
"""GET /api/v1/tickets — ``{"tickets": [...], "counts": {...}}``."""
|
|
49
|
+
return self._c.request(
|
|
50
|
+
"GET", "/api/v1/tickets" + _qs({"status": status, "limit": limit})
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def get(self, id: str) -> Dict[str, Any]:
|
|
54
|
+
"""GET /api/v1/tickets/:id — full ticket incl. message thread."""
|
|
55
|
+
return self._c.request("GET", f"/api/v1/tickets/{_enc(id)}")
|
|
56
|
+
|
|
57
|
+
def create(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
subject: str,
|
|
61
|
+
body: str,
|
|
62
|
+
requester_email: str,
|
|
63
|
+
requester_name: Optional[str] = None,
|
|
64
|
+
priority: Optional[str] = None,
|
|
65
|
+
channel: Optional[str] = None,
|
|
66
|
+
) -> Dict[str, Any]:
|
|
67
|
+
"""POST /api/v1/tickets — agent-logged ticket (e.g. arrived via WhatsApp)."""
|
|
68
|
+
payload: Dict[str, Any] = {
|
|
69
|
+
"subject": subject,
|
|
70
|
+
"body": body,
|
|
71
|
+
"requesterEmail": requester_email,
|
|
72
|
+
}
|
|
73
|
+
if requester_name is not None:
|
|
74
|
+
payload["requesterName"] = requester_name
|
|
75
|
+
if priority is not None:
|
|
76
|
+
payload["priority"] = priority
|
|
77
|
+
if channel is not None:
|
|
78
|
+
payload["channel"] = channel
|
|
79
|
+
return self._c.request("POST", "/api/v1/tickets", body=payload)
|
|
80
|
+
|
|
81
|
+
def reply(
|
|
82
|
+
self,
|
|
83
|
+
id: str,
|
|
84
|
+
*,
|
|
85
|
+
body: str,
|
|
86
|
+
is_internal: Optional[bool] = None,
|
|
87
|
+
author_name: Optional[str] = None,
|
|
88
|
+
) -> Dict[str, Any]:
|
|
89
|
+
"""POST /api/v1/tickets/:id/messages — agent reply (or internal note)."""
|
|
90
|
+
payload: Dict[str, Any] = {"body": body}
|
|
91
|
+
if is_internal is not None:
|
|
92
|
+
payload["isInternal"] = is_internal
|
|
93
|
+
if author_name is not None:
|
|
94
|
+
payload["authorName"] = author_name
|
|
95
|
+
return self._c.request("POST", f"/api/v1/tickets/{_enc(id)}/messages", body=payload)
|
|
96
|
+
|
|
97
|
+
def update(
|
|
98
|
+
self,
|
|
99
|
+
id: str,
|
|
100
|
+
*,
|
|
101
|
+
status: Optional[str] = None,
|
|
102
|
+
priority: Optional[str] = None,
|
|
103
|
+
assignee_sub: Optional[str] = ..., # type: ignore[assignment]
|
|
104
|
+
) -> Dict[str, Any]:
|
|
105
|
+
"""PATCH /api/v1/tickets/:id — status / priority / assignee.
|
|
106
|
+
|
|
107
|
+
Pass ``assignee_sub=None`` explicitly to unassign.
|
|
108
|
+
"""
|
|
109
|
+
payload: Dict[str, Any] = {}
|
|
110
|
+
if status is not None:
|
|
111
|
+
payload["status"] = status
|
|
112
|
+
if priority is not None:
|
|
113
|
+
payload["priority"] = priority
|
|
114
|
+
if assignee_sub is not ...:
|
|
115
|
+
payload["assigneeSub"] = assignee_sub
|
|
116
|
+
return self._c.request("PATCH", f"/api/v1/tickets/{_enc(id)}", body=payload)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _CannedReplies:
|
|
120
|
+
"""Per-workspace saved reply snippets (Bearer auth)."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, c: "SuppuoClient") -> None:
|
|
123
|
+
self._c = c
|
|
124
|
+
|
|
125
|
+
def list(self) -> Dict[str, Any]:
|
|
126
|
+
"""GET /api/v1/canned-replies — ``{"cannedReplies": [...]}``."""
|
|
127
|
+
return self._c.request("GET", "/api/v1/canned-replies")
|
|
128
|
+
|
|
129
|
+
def create(self, *, title: str, body: str) -> Dict[str, Any]:
|
|
130
|
+
"""POST /api/v1/canned-replies."""
|
|
131
|
+
return self._c.request(
|
|
132
|
+
"POST", "/api/v1/canned-replies", body={"title": title, "body": body}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def update(
|
|
136
|
+
self,
|
|
137
|
+
id: str,
|
|
138
|
+
*,
|
|
139
|
+
title: Optional[str] = None,
|
|
140
|
+
body: Optional[str] = None,
|
|
141
|
+
) -> Dict[str, Any]:
|
|
142
|
+
"""PATCH /api/v1/canned-replies/:id."""
|
|
143
|
+
payload: Dict[str, Any] = {}
|
|
144
|
+
if title is not None:
|
|
145
|
+
payload["title"] = title
|
|
146
|
+
if body is not None:
|
|
147
|
+
payload["body"] = body
|
|
148
|
+
return self._c.request("PATCH", f"/api/v1/canned-replies/{_enc(id)}", body=payload)
|
|
149
|
+
|
|
150
|
+
def delete(self, id: str) -> Dict[str, Any]:
|
|
151
|
+
"""DELETE /api/v1/canned-replies/:id — ``{"deleted": true}``."""
|
|
152
|
+
return self._c.request("DELETE", f"/api/v1/canned-replies/{_enc(id)}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class _Public:
|
|
156
|
+
"""Requester-facing, unauthenticated surface."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, c: "SuppuoClient") -> None:
|
|
159
|
+
self._c = c
|
|
160
|
+
|
|
161
|
+
def submit_ticket(
|
|
162
|
+
self,
|
|
163
|
+
*,
|
|
164
|
+
account_id: str,
|
|
165
|
+
subject: str,
|
|
166
|
+
body: str,
|
|
167
|
+
email: str,
|
|
168
|
+
name: Optional[str] = None,
|
|
169
|
+
) -> Dict[str, Any]:
|
|
170
|
+
"""POST /api/v1/public/tickets — ``{"number", "accessToken"}``.
|
|
171
|
+
|
|
172
|
+
The returned ``accessToken`` is the requester's only credential.
|
|
173
|
+
"""
|
|
174
|
+
payload: Dict[str, Any] = {
|
|
175
|
+
"accountId": account_id,
|
|
176
|
+
"subject": subject,
|
|
177
|
+
"body": body,
|
|
178
|
+
"email": email,
|
|
179
|
+
}
|
|
180
|
+
if name is not None:
|
|
181
|
+
payload["name"] = name
|
|
182
|
+
return self._c.request("POST", "/api/v1/public/tickets", body=payload, no_auth=True)
|
|
183
|
+
|
|
184
|
+
def get_ticket(self, access_token: str) -> Dict[str, Any]:
|
|
185
|
+
"""GET /api/v1/public/tickets/:accessToken — tokenized status view."""
|
|
186
|
+
return self._c.request(
|
|
187
|
+
"GET", f"/api/v1/public/tickets/{_enc(access_token)}", no_auth=True
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def reply_ticket(self, access_token: str, *, body: str) -> Dict[str, Any]:
|
|
191
|
+
"""POST /api/v1/public/tickets/:accessToken/messages — requester reply."""
|
|
192
|
+
return self._c.request(
|
|
193
|
+
"POST",
|
|
194
|
+
f"/api/v1/public/tickets/{_enc(access_token)}/messages",
|
|
195
|
+
body={"body": body},
|
|
196
|
+
no_auth=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class SuppuoClient:
|
|
201
|
+
"""Suppuo typed client.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
client = SuppuoClient(token=os.environ["SUPPUO_TOKEN"])
|
|
205
|
+
page = client.tickets.list(status="open")
|
|
206
|
+
client.tickets.reply(page["tickets"][0]["id"], body="On it!")
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(
|
|
210
|
+
self,
|
|
211
|
+
*,
|
|
212
|
+
token: Optional[str] = None,
|
|
213
|
+
base_url: str = "https://suppuo.com",
|
|
214
|
+
timeout: float = 30.0,
|
|
215
|
+
) -> None:
|
|
216
|
+
self._token = token if token is not None else os.environ.get("SUPPUO_TOKEN")
|
|
217
|
+
self._base_url = base_url.rstrip("/")
|
|
218
|
+
self._timeout = timeout
|
|
219
|
+
|
|
220
|
+
self.tickets = _Tickets(self)
|
|
221
|
+
self.canned_replies = _CannedReplies(self)
|
|
222
|
+
self.public = _Public(self)
|
|
223
|
+
|
|
224
|
+
def request(
|
|
225
|
+
self,
|
|
226
|
+
method: str,
|
|
227
|
+
path: str,
|
|
228
|
+
*,
|
|
229
|
+
body: Optional[Dict[str, Any]] = None,
|
|
230
|
+
no_auth: bool = False,
|
|
231
|
+
) -> Any:
|
|
232
|
+
headers = {"Accept": "application/json"}
|
|
233
|
+
if not no_auth:
|
|
234
|
+
if not self._token:
|
|
235
|
+
raise SuppuoError(
|
|
236
|
+
0,
|
|
237
|
+
"AUTH_REQUIRED",
|
|
238
|
+
"No token configured. Pass token= or set SUPPUO_TOKEN.",
|
|
239
|
+
)
|
|
240
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
resp = httpx.request(
|
|
244
|
+
method,
|
|
245
|
+
self._base_url + path,
|
|
246
|
+
json=body,
|
|
247
|
+
headers=headers,
|
|
248
|
+
timeout=self._timeout,
|
|
249
|
+
)
|
|
250
|
+
except httpx.TimeoutException as e:
|
|
251
|
+
raise SuppuoError(0, "TIMEOUT", f"request timed out: {e}") from e
|
|
252
|
+
except httpx.HTTPError as e:
|
|
253
|
+
raise SuppuoError(0, "NETWORK_ERROR", str(e)) from e
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
envelope = resp.json()
|
|
257
|
+
except ValueError as e:
|
|
258
|
+
raise SuppuoError(
|
|
259
|
+
resp.status_code,
|
|
260
|
+
"INVALID_RESPONSE",
|
|
261
|
+
f"non-JSON response (HTTP {resp.status_code})",
|
|
262
|
+
) from e
|
|
263
|
+
|
|
264
|
+
error = envelope.get("error") if isinstance(envelope, dict) else None
|
|
265
|
+
meta = envelope.get("meta") if isinstance(envelope, dict) else None
|
|
266
|
+
request_id = meta.get("requestId") if isinstance(meta, dict) else None
|
|
267
|
+
|
|
268
|
+
if resp.status_code >= 400 or error:
|
|
269
|
+
raise SuppuoError(
|
|
270
|
+
resp.status_code,
|
|
271
|
+
(error or {}).get("code", "UNKNOWN"),
|
|
272
|
+
(error or {}).get("message", f"HTTP {resp.status_code}"),
|
|
273
|
+
request_id,
|
|
274
|
+
(error or {}).get("param"),
|
|
275
|
+
)
|
|
276
|
+
return envelope.get("data") if isinstance(envelope, dict) else envelope
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
__all__ = ["SuppuoClient", "SuppuoError"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Typed error class for the Suppuo SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SuppuoError(Exception):
|
|
9
|
+
"""Raised when a Suppuo API call fails.
|
|
10
|
+
|
|
11
|
+
Attributes
|
|
12
|
+
----------
|
|
13
|
+
status:
|
|
14
|
+
HTTP status code (0 for transport-level errors like timeouts).
|
|
15
|
+
code:
|
|
16
|
+
Machine-readable error code from the API envelope
|
|
17
|
+
(``NOT_FOUND``, ``VALIDATION_ERROR``, ``AUTH_REQUIRED``, ...)
|
|
18
|
+
or one of ``TIMEOUT`` / ``NETWORK_ERROR`` / ``INVALID_RESPONSE``
|
|
19
|
+
for SDK-side failures.
|
|
20
|
+
message:
|
|
21
|
+
Human-readable description.
|
|
22
|
+
request_id:
|
|
23
|
+
The ``meta.requestId`` echoed by the API, when available.
|
|
24
|
+
param:
|
|
25
|
+
The offending parameter on validation errors, when available.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
status: int,
|
|
31
|
+
code: str,
|
|
32
|
+
message: str,
|
|
33
|
+
request_id: Optional[str] = None,
|
|
34
|
+
param: Optional[str] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.status = status
|
|
38
|
+
self.code = code
|
|
39
|
+
self.message = message
|
|
40
|
+
self.request_id = request_id
|
|
41
|
+
self.param = param
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return (
|
|
45
|
+
f"SuppuoError(status={self.status}, code={self.code!r}, "
|
|
46
|
+
f"message={self.message!r}, request_id={self.request_id!r})"
|
|
47
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "forjio-suppuo"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Suppuo SDK — typed Python client for the suppuo.com REST API. Sister to the JS + Go SDKs."
|
|
5
|
+
authors = [{ name = "Forjio", email = "support@forjio.com" }]
|
|
6
|
+
license = { text = "Proprietary" }
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"httpx>=0.27.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = ["pytest>=8.0.0"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://suppuo.com/docs/sdk/python"
|
|
18
|
+
Repository = "https://github.com/hachimi-cat/saas-suppuo"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["hatchling"]
|
|
22
|
+
build-backend = "hatchling.build"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["forjio_suppuo"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Smoke tests for SuppuoClient."""
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from forjio_suppuo import SuppuoClient, SuppuoError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_construct():
|
|
8
|
+
c = SuppuoClient(token="test")
|
|
9
|
+
assert c is not None
|
|
10
|
+
assert c.tickets is not None
|
|
11
|
+
assert c.canned_replies is not None
|
|
12
|
+
assert c.public is not None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_authed_call_without_token_raises(monkeypatch):
|
|
16
|
+
monkeypatch.delenv("SUPPUO_TOKEN", raising=False)
|
|
17
|
+
c = SuppuoClient()
|
|
18
|
+
with pytest.raises(SuppuoError) as exc:
|
|
19
|
+
c.canned_replies.list()
|
|
20
|
+
assert exc.value.code == "AUTH_REQUIRED"
|
|
21
|
+
assert exc.value.status == 0
|