mytutor-sdk 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.
- mytutor/__init__.py +26 -0
- mytutor/client.py +283 -0
- mytutor/errors.py +34 -0
- mytutor/types.py +79 -0
- mytutor_sdk-0.1.0.dist-info/METADATA +84 -0
- mytutor_sdk-0.1.0.dist-info/RECORD +8 -0
- mytutor_sdk-0.1.0.dist-info/WHEEL +5 -0
- mytutor_sdk-0.1.0.dist-info/top_level.txt +1 -0
mytutor/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .client import MyTutor
|
|
2
|
+
from .errors import MyTutorError, TutorAskError
|
|
3
|
+
from .types import (
|
|
4
|
+
Session,
|
|
5
|
+
Subject,
|
|
6
|
+
TeachingMode,
|
|
7
|
+
Topic,
|
|
8
|
+
TutorAskResult,
|
|
9
|
+
TutorMessage,
|
|
10
|
+
TutorResponse,
|
|
11
|
+
Whiteboard,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MyTutor",
|
|
16
|
+
"MyTutorError",
|
|
17
|
+
"Session",
|
|
18
|
+
"Subject",
|
|
19
|
+
"TeachingMode",
|
|
20
|
+
"Topic",
|
|
21
|
+
"TutorAskError",
|
|
22
|
+
"TutorAskResult",
|
|
23
|
+
"TutorMessage",
|
|
24
|
+
"TutorResponse",
|
|
25
|
+
"Whiteboard",
|
|
26
|
+
]
|
mytutor/client.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.error import HTTPError, URLError
|
|
7
|
+
from urllib.parse import quote, urlsplit
|
|
8
|
+
from urllib.request import Request, urlopen
|
|
9
|
+
|
|
10
|
+
from .errors import MyTutorError, TutorAskError
|
|
11
|
+
from .types import (
|
|
12
|
+
Session,
|
|
13
|
+
Subject,
|
|
14
|
+
TeachingMode,
|
|
15
|
+
TutorAskResult,
|
|
16
|
+
TutorResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.mytutor.site"
|
|
20
|
+
DEFAULT_TIMEOUT = 120.0
|
|
21
|
+
TEACHING_MODES = {"guided", "explain", "practice", "check_my_work"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_text(value: str, name: str, maximum: int | None = None) -> None:
|
|
25
|
+
if not isinstance(value, str) or not value.strip():
|
|
26
|
+
raise ValueError(f"{name} must be a non-empty string.")
|
|
27
|
+
if maximum is not None and len(value) > maximum:
|
|
28
|
+
raise ValueError(f"{name} must be {maximum} characters or fewer.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_base_url(value: str) -> str:
|
|
32
|
+
try:
|
|
33
|
+
parsed = urlsplit(value)
|
|
34
|
+
hostname = parsed.hostname
|
|
35
|
+
port = parsed.port
|
|
36
|
+
except ValueError as exc:
|
|
37
|
+
raise ValueError("base_url must be an absolute HTTP(S) API origin.") from exc
|
|
38
|
+
|
|
39
|
+
is_loopback = hostname in {"localhost", "127.0.0.1", "::1"}
|
|
40
|
+
if (
|
|
41
|
+
parsed.scheme not in {"https", "http"}
|
|
42
|
+
or not hostname
|
|
43
|
+
or (parsed.scheme == "http" and not is_loopback)
|
|
44
|
+
or parsed.username
|
|
45
|
+
or parsed.password
|
|
46
|
+
or parsed.path not in {"", "/"}
|
|
47
|
+
or parsed.query
|
|
48
|
+
or parsed.fragment
|
|
49
|
+
):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"base_url must be an HTTPS API origin without credentials, path, query, or fragment."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
netloc = hostname if port is None else f"{hostname}:{port}"
|
|
55
|
+
if ":" in hostname and not hostname.startswith("["):
|
|
56
|
+
netloc = f"[{hostname}]" if port is None else f"[{hostname}]:{port}"
|
|
57
|
+
return f"{parsed.scheme}://{netloc}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _api_error(status: int, payload: Any) -> MyTutorError:
|
|
61
|
+
envelope = payload if isinstance(payload, dict) else {}
|
|
62
|
+
error = envelope.get("error")
|
|
63
|
+
if not isinstance(error, dict):
|
|
64
|
+
error = {}
|
|
65
|
+
message = error.get("message")
|
|
66
|
+
code = error.get("code")
|
|
67
|
+
correlation_id = envelope.get("correlation_id")
|
|
68
|
+
return MyTutorError(
|
|
69
|
+
message if isinstance(message, str) else f"The My Tutor API request failed ({status}).",
|
|
70
|
+
status=status,
|
|
71
|
+
code=code if isinstance(code, str) else "http_error",
|
|
72
|
+
details=error.get("details"),
|
|
73
|
+
correlation_id=correlation_id if isinstance(correlation_id, str) else None,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _decode_response(status: int, raw: bytes) -> Any:
|
|
78
|
+
payload: Any = None
|
|
79
|
+
if raw:
|
|
80
|
+
try:
|
|
81
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
82
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
83
|
+
if 200 <= status < 300 and status != 204:
|
|
84
|
+
raise MyTutorError(
|
|
85
|
+
"The My Tutor API returned an invalid JSON response.",
|
|
86
|
+
status=status,
|
|
87
|
+
code="invalid_response",
|
|
88
|
+
) from exc
|
|
89
|
+
|
|
90
|
+
if not 200 <= status < 300:
|
|
91
|
+
raise _api_error(status, payload)
|
|
92
|
+
if status == 204:
|
|
93
|
+
return None
|
|
94
|
+
if not raw:
|
|
95
|
+
raise MyTutorError(
|
|
96
|
+
"The My Tutor API returned an empty response.",
|
|
97
|
+
status=status,
|
|
98
|
+
code="invalid_response",
|
|
99
|
+
)
|
|
100
|
+
return payload
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _Subjects:
|
|
104
|
+
def __init__(self, client: MyTutor) -> None:
|
|
105
|
+
self._client = client
|
|
106
|
+
|
|
107
|
+
def list(self) -> list[Subject]:
|
|
108
|
+
return self._client._request("GET", "/subjects")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _Tutor:
|
|
112
|
+
def __init__(self, client: MyTutor) -> None:
|
|
113
|
+
self._client = client
|
|
114
|
+
|
|
115
|
+
def create_session(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
subject_slug: str,
|
|
119
|
+
topic: str = "",
|
|
120
|
+
teaching_mode: TeachingMode | None = None,
|
|
121
|
+
title: str | None = None,
|
|
122
|
+
assignment_question_id: str | None = None,
|
|
123
|
+
assignment_id: str | None = None,
|
|
124
|
+
education_level: str | None = None,
|
|
125
|
+
) -> Session:
|
|
126
|
+
_validate_text(subject_slug, "subject_slug", 60)
|
|
127
|
+
normalized_topic = topic.strip() if isinstance(topic, str) else ""
|
|
128
|
+
if normalized_topic:
|
|
129
|
+
_validate_text(normalized_topic, "topic", 160)
|
|
130
|
+
if title is not None and len(title) > 160:
|
|
131
|
+
raise ValueError("title must be 160 characters or fewer.")
|
|
132
|
+
if education_level is not None and len(education_level) > 60:
|
|
133
|
+
raise ValueError("education_level must be 60 characters or fewer.")
|
|
134
|
+
if teaching_mode is not None and teaching_mode not in TEACHING_MODES:
|
|
135
|
+
raise ValueError("teaching_mode is not a supported tutoring mode.")
|
|
136
|
+
|
|
137
|
+
payload: dict[str, Any] = {"subject_slug": subject_slug, "topic": normalized_topic}
|
|
138
|
+
if teaching_mode is not None:
|
|
139
|
+
payload["teaching_mode"] = teaching_mode
|
|
140
|
+
if title is not None:
|
|
141
|
+
payload["title"] = title
|
|
142
|
+
if assignment_question_id is not None:
|
|
143
|
+
payload["assignment_question_id"] = assignment_question_id
|
|
144
|
+
if assignment_id is not None:
|
|
145
|
+
payload["assignment_id"] = assignment_id
|
|
146
|
+
if education_level is not None:
|
|
147
|
+
payload["education_level"] = education_level
|
|
148
|
+
return self._client._request("POST", "/tutoring/sessions", body=payload)
|
|
149
|
+
|
|
150
|
+
def send_message(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
session_id: str,
|
|
154
|
+
content: str,
|
|
155
|
+
retry: bool | None = None,
|
|
156
|
+
idempotency_key: str | None = None,
|
|
157
|
+
) -> TutorResponse:
|
|
158
|
+
_validate_text(session_id, "session_id")
|
|
159
|
+
_validate_text(content, "content", 4000)
|
|
160
|
+
if idempotency_key is not None:
|
|
161
|
+
_validate_text(idempotency_key, "idempotency_key", 120)
|
|
162
|
+
|
|
163
|
+
payload: dict[str, Any] = {"content": content}
|
|
164
|
+
if retry is not None:
|
|
165
|
+
payload["retry"] = retry
|
|
166
|
+
return self._client._request(
|
|
167
|
+
"POST",
|
|
168
|
+
f"/tutoring/sessions/{quote(session_id, safe='')}/messages",
|
|
169
|
+
body=payload,
|
|
170
|
+
idempotency_key=idempotency_key,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def ask(
|
|
174
|
+
self,
|
|
175
|
+
*,
|
|
176
|
+
question: str,
|
|
177
|
+
session_id: str | None = None,
|
|
178
|
+
subject_slug: str | None = None,
|
|
179
|
+
topic: str = "",
|
|
180
|
+
teaching_mode: TeachingMode | None = None,
|
|
181
|
+
idempotency_key: str | None = None,
|
|
182
|
+
) -> TutorAskResult:
|
|
183
|
+
_validate_text(question, "question", 4000)
|
|
184
|
+
if (session_id is None) == (subject_slug is None):
|
|
185
|
+
raise ValueError("Provide exactly one of session_id or subject_slug.")
|
|
186
|
+
if idempotency_key is not None:
|
|
187
|
+
_validate_text(idempotency_key, "idempotency_key", 120)
|
|
188
|
+
|
|
189
|
+
if session_id is not None:
|
|
190
|
+
response = self.send_message(
|
|
191
|
+
session_id=session_id,
|
|
192
|
+
content=question,
|
|
193
|
+
idempotency_key=idempotency_key,
|
|
194
|
+
)
|
|
195
|
+
return {"session_id": session_id, "response": response}
|
|
196
|
+
|
|
197
|
+
if subject_slug is None:
|
|
198
|
+
raise ValueError("Provide a subject_slug to start a new tutoring session.")
|
|
199
|
+
session = self.create_session(
|
|
200
|
+
subject_slug=subject_slug,
|
|
201
|
+
topic=topic,
|
|
202
|
+
teaching_mode=teaching_mode,
|
|
203
|
+
)
|
|
204
|
+
try:
|
|
205
|
+
response = self.send_message(
|
|
206
|
+
session_id=session["id"],
|
|
207
|
+
content=question,
|
|
208
|
+
idempotency_key=idempotency_key,
|
|
209
|
+
)
|
|
210
|
+
except MyTutorError as exc:
|
|
211
|
+
raise TutorAskError(session["id"], exc) from exc
|
|
212
|
+
return {"session_id": session["id"], "response": response}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class MyTutor:
|
|
216
|
+
"""Synchronous client for the server-to-server My Tutor developer API."""
|
|
217
|
+
|
|
218
|
+
def __init__(
|
|
219
|
+
self,
|
|
220
|
+
*,
|
|
221
|
+
api_key: str,
|
|
222
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
223
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
224
|
+
) -> None:
|
|
225
|
+
_validate_text(api_key, "api_key")
|
|
226
|
+
if "\r" in api_key or "\n" in api_key:
|
|
227
|
+
raise ValueError("api_key must not contain newline characters.")
|
|
228
|
+
if not isinstance(timeout, (int, float)) or isinstance(timeout, bool):
|
|
229
|
+
raise TypeError("timeout must be a positive finite number of seconds.")
|
|
230
|
+
if not math.isfinite(timeout) or timeout <= 0:
|
|
231
|
+
raise ValueError("timeout must be a positive finite number of seconds.")
|
|
232
|
+
|
|
233
|
+
self._api_key = api_key.strip()
|
|
234
|
+
self._base_url = _normalize_base_url(base_url)
|
|
235
|
+
self._timeout = float(timeout)
|
|
236
|
+
self.subjects = _Subjects(self)
|
|
237
|
+
self.tutor = _Tutor(self)
|
|
238
|
+
|
|
239
|
+
def _request(
|
|
240
|
+
self,
|
|
241
|
+
method: str,
|
|
242
|
+
path: str,
|
|
243
|
+
*,
|
|
244
|
+
body: dict[str, Any] | None = None,
|
|
245
|
+
idempotency_key: str | None = None,
|
|
246
|
+
) -> Any:
|
|
247
|
+
headers = {
|
|
248
|
+
"Accept": "application/json",
|
|
249
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
250
|
+
}
|
|
251
|
+
data = None
|
|
252
|
+
if body is not None:
|
|
253
|
+
headers["Content-Type"] = "application/json"
|
|
254
|
+
data = json.dumps(body).encode("utf-8")
|
|
255
|
+
if idempotency_key is not None:
|
|
256
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
257
|
+
|
|
258
|
+
request = Request(
|
|
259
|
+
f"{self._base_url}/api/v1{path}",
|
|
260
|
+
data=data,
|
|
261
|
+
headers=headers,
|
|
262
|
+
method=method,
|
|
263
|
+
)
|
|
264
|
+
try:
|
|
265
|
+
with urlopen(request, timeout=self._timeout) as response:
|
|
266
|
+
return _decode_response(response.status, response.read())
|
|
267
|
+
except HTTPError as exc:
|
|
268
|
+
try:
|
|
269
|
+
return _decode_response(exc.code, exc.read())
|
|
270
|
+
finally:
|
|
271
|
+
exc.close()
|
|
272
|
+
except TimeoutError as exc:
|
|
273
|
+
raise MyTutorError(
|
|
274
|
+
f"The My Tutor API request timed out after {self._timeout:g} seconds.",
|
|
275
|
+
status=None,
|
|
276
|
+
code="timeout",
|
|
277
|
+
) from exc
|
|
278
|
+
except URLError as exc:
|
|
279
|
+
raise MyTutorError(
|
|
280
|
+
"The My Tutor API request could not be completed.",
|
|
281
|
+
status=None,
|
|
282
|
+
code="network_error",
|
|
283
|
+
) from exc
|
mytutor/errors.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MyTutorError(Exception):
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: str,
|
|
10
|
+
*,
|
|
11
|
+
status: int | None,
|
|
12
|
+
code: str,
|
|
13
|
+
details: Any = None,
|
|
14
|
+
correlation_id: str | None = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status = status
|
|
18
|
+
self.code = code
|
|
19
|
+
self.details = details
|
|
20
|
+
self.correlation_id = correlation_id
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TutorAskError(MyTutorError):
|
|
24
|
+
def __init__(self, session_id: str, cause: MyTutorError) -> None:
|
|
25
|
+
super().__init__(
|
|
26
|
+
"The tutoring session was created, but the question could not be completed. "
|
|
27
|
+
"Retry by sending the question to this session.",
|
|
28
|
+
status=cause.status,
|
|
29
|
+
code=cause.code,
|
|
30
|
+
details=cause.details,
|
|
31
|
+
correlation_id=cause.correlation_id,
|
|
32
|
+
)
|
|
33
|
+
self.session_id = session_id
|
|
34
|
+
self.cause = cause
|
mytutor/types.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, TypedDict
|
|
4
|
+
|
|
5
|
+
TeachingMode = Literal["guided", "explain", "practice", "check_my_work"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Topic(TypedDict):
|
|
9
|
+
id: str
|
|
10
|
+
title: str
|
|
11
|
+
slug: str
|
|
12
|
+
curriculum_id: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Subject(TypedDict):
|
|
16
|
+
id: str
|
|
17
|
+
slug: str
|
|
18
|
+
name: str
|
|
19
|
+
description: str
|
|
20
|
+
accent: str
|
|
21
|
+
topics: list[Topic]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Session(TypedDict):
|
|
25
|
+
id: str
|
|
26
|
+
title: str
|
|
27
|
+
subject_slug: str | None
|
|
28
|
+
subject_name: str | None
|
|
29
|
+
teaching_mode: TeachingMode
|
|
30
|
+
education_level: str
|
|
31
|
+
status: str
|
|
32
|
+
topic: str
|
|
33
|
+
assignment_id: str | None
|
|
34
|
+
assignment_filename: str
|
|
35
|
+
created_at: str
|
|
36
|
+
updated_at: str
|
|
37
|
+
last_message_at: str | None
|
|
38
|
+
message_count: int
|
|
39
|
+
preview: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TutorMessage(TypedDict):
|
|
43
|
+
id: str
|
|
44
|
+
role: str
|
|
45
|
+
content: str
|
|
46
|
+
structured: dict[str, Any] | None
|
|
47
|
+
created_at: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Whiteboard(TypedDict):
|
|
51
|
+
schema_version: int
|
|
52
|
+
title: str
|
|
53
|
+
subject: str
|
|
54
|
+
steps: list[dict[str, Any]]
|
|
55
|
+
annotations: list[dict[str, Any]]
|
|
56
|
+
updated_at: str | None
|
|
57
|
+
boards: list[dict[str, Any]]
|
|
58
|
+
active_board_id: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TutorResponse(TypedDict):
|
|
62
|
+
message: TutorMessage
|
|
63
|
+
user_message: TutorMessage | None
|
|
64
|
+
steps: list[dict[str, Any]]
|
|
65
|
+
follow_up: str
|
|
66
|
+
check_question: str | None
|
|
67
|
+
topic: str
|
|
68
|
+
verification_summary: dict[str, int]
|
|
69
|
+
whiteboard: Whiteboard
|
|
70
|
+
prompt_version: str
|
|
71
|
+
model: str
|
|
72
|
+
credit_action: str
|
|
73
|
+
credits_charged: int
|
|
74
|
+
credits_available: int | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TutorAskResult(TypedDict):
|
|
78
|
+
session_id: str
|
|
79
|
+
response: TutorResponse
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mytutor-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the My Tutor developer API
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# My Tutor Python SDK
|
|
9
|
+
|
|
10
|
+
The `mytutor-sdk` distribution provides a typed, synchronous Python client for
|
|
11
|
+
the stable My Tutor developer API v1 endpoints: subjects, tutoring sessions,
|
|
12
|
+
and tutor messages. It has no runtime dependencies.
|
|
13
|
+
|
|
14
|
+
> **Preview:** This package is not published to PyPI yet. The source and local
|
|
15
|
+
> package are available in this repository; `pip install mytutor-sdk` will work
|
|
16
|
+
> after the first PyPI release. Publishing is intentionally not automated in
|
|
17
|
+
> this repository.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Python 3.11 or newer
|
|
22
|
+
- A My Tutor developer API key
|
|
23
|
+
- A trusted server environment; never put the secret key in browser or mobile
|
|
24
|
+
application code
|
|
25
|
+
|
|
26
|
+
## Install from a local checkout
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
python -m pip install ./packages/sdk-python
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
After PyPI publication, install with:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
python -m pip install mytutor-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
Set `MYTUTOR_API_KEY` in your server environment. Supply a subject slug to
|
|
41
|
+
start a session; keep the returned `session_id` to continue the conversation.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import os
|
|
45
|
+
|
|
46
|
+
from mytutor import MyTutor
|
|
47
|
+
|
|
48
|
+
client = MyTutor(api_key=os.environ["MYTUTOR_API_KEY"])
|
|
49
|
+
subjects = client.subjects.list()
|
|
50
|
+
result = client.tutor.ask(
|
|
51
|
+
subject_slug=subjects[0]["slug"],
|
|
52
|
+
topic="Fractions",
|
|
53
|
+
question="Explain how to add fractions with different denominators.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
print(result["response"]["message"]["content"])
|
|
57
|
+
|
|
58
|
+
next_turn = client.tutor.ask(
|
|
59
|
+
session_id=result["session_id"],
|
|
60
|
+
question="Can you show another example?",
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`tutor.ask` requires exactly one of `session_id` or `subject_slug`. A
|
|
65
|
+
`subject_slug` creates a new tutoring session before sending the question. Use
|
|
66
|
+
`client.tutor.create_session()` and `client.tutor.send_message()` separately
|
|
67
|
+
when you need full control.
|
|
68
|
+
|
|
69
|
+
## Errors and retries
|
|
70
|
+
|
|
71
|
+
Catch `MyTutorError` to inspect the HTTP `status`, API `code`, optional
|
|
72
|
+
`details`, and `correlation_id`. Requests are not retried automatically. For a
|
|
73
|
+
retry of the same tutor message, pass the same `idempotency_key`.
|
|
74
|
+
|
|
75
|
+
If a new-session `ask` creates the session but cannot complete the message, it
|
|
76
|
+
raises `TutorAskError`, which includes `session_id` so the message can be
|
|
77
|
+
retried against that session.
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
`base_url` defaults to `https://api.mytutor.site`. Override it with your
|
|
82
|
+
deployment's HTTPS API origin when needed. Plain HTTP is permitted only for
|
|
83
|
+
loopback development URLs. `timeout` defaults to 120 seconds and is expressed
|
|
84
|
+
in seconds.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mytutor/__init__.py,sha256=qc29rL-uANq6m4fT9tm1ZYEHKrX_o2VWr44Fc-8BL_0,468
|
|
2
|
+
mytutor/client.py,sha256=vyTS-77HzHH9zgtHLBoMMXut4UXZ4kIgccQ7roqQ_pc,10197
|
|
3
|
+
mytutor/errors.py,sha256=FRV5uiRjHO4lb9NXqBVNdL8gIGYrmJVAB6TQ8d5ndj8,994
|
|
4
|
+
mytutor/types.py,sha256=chxgMhKoSrXqFtYPuhiIByK6QHqi29fyPa_L9vZycXQ,1666
|
|
5
|
+
mytutor_sdk-0.1.0.dist-info/METADATA,sha256=RHiAMdrKrwxgUYhvmvNHbUtk4WJ327isI3W4vYGDres,2669
|
|
6
|
+
mytutor_sdk-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
mytutor_sdk-0.1.0.dist-info/top_level.txt,sha256=U_Xqj1M6b8xOjWGcVBi85xImsWPk2fycEEOEevPu09M,8
|
|
8
|
+
mytutor_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mytutor
|