mitra-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.
- mitra/__init__.py +129 -0
- mitra/_core.py +205 -0
- mitra/_version.py +1 -0
- mitra/client.py +1267 -0
- mitra/errors.py +220 -0
- mitra/models.py +515 -0
- mitra/py.typed +0 -0
- mitra/webhooks.py +72 -0
- mitra_sdk-0.1.0.dist-info/METADATA +274 -0
- mitra_sdk-0.1.0.dist-info/RECORD +13 -0
- mitra_sdk-0.1.0.dist-info/WHEEL +4 -0
- mitra_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
- mitra_sdk-0.1.0.dist-info/licenses/NOTICE +5 -0
mitra/__init__.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Python client for the Matih public API."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .client import AsyncMitra, Mitra
|
|
5
|
+
from .errors import (
|
|
6
|
+
EMBED_REFUSAL_CODES,
|
|
7
|
+
AuthError,
|
|
8
|
+
BusyError,
|
|
9
|
+
CreditsError,
|
|
10
|
+
EmbedNotFoundError,
|
|
11
|
+
EmbedRefusedError,
|
|
12
|
+
JobFailedError,
|
|
13
|
+
JobTimeoutError,
|
|
14
|
+
MitraError,
|
|
15
|
+
PermissionDeniedError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
SourceTestFailedError,
|
|
18
|
+
WebhookVerificationError,
|
|
19
|
+
)
|
|
20
|
+
from .models import (
|
|
21
|
+
Answer,
|
|
22
|
+
Catalog,
|
|
23
|
+
Clarification,
|
|
24
|
+
Connector,
|
|
25
|
+
ConnectorField,
|
|
26
|
+
ConnectorList,
|
|
27
|
+
Conversation,
|
|
28
|
+
ConversationList,
|
|
29
|
+
ConversationMessage,
|
|
30
|
+
Dashboard,
|
|
31
|
+
DashboardList,
|
|
32
|
+
DashboardWidget,
|
|
33
|
+
Embed,
|
|
34
|
+
EmbedKey,
|
|
35
|
+
EmbedList,
|
|
36
|
+
EmbedMember,
|
|
37
|
+
EmbedResource,
|
|
38
|
+
EmbedSummary,
|
|
39
|
+
File,
|
|
40
|
+
FileList,
|
|
41
|
+
Gem,
|
|
42
|
+
GemList,
|
|
43
|
+
Heard,
|
|
44
|
+
Invitation,
|
|
45
|
+
InvitationList,
|
|
46
|
+
Job,
|
|
47
|
+
JobError,
|
|
48
|
+
Member,
|
|
49
|
+
MemberList,
|
|
50
|
+
ResultTable,
|
|
51
|
+
Source,
|
|
52
|
+
SourceCreated,
|
|
53
|
+
SourceList,
|
|
54
|
+
SourceReadiness,
|
|
55
|
+
SourceTest,
|
|
56
|
+
StreamEvent,
|
|
57
|
+
Webhook,
|
|
58
|
+
WebhookDelivery,
|
|
59
|
+
WebhookTestResult,
|
|
60
|
+
Workspace,
|
|
61
|
+
WorkspaceInvitation,
|
|
62
|
+
WorkspaceList,
|
|
63
|
+
embed_src,
|
|
64
|
+
)
|
|
65
|
+
from .webhooks import assert_webhook, verify_webhook
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"EMBED_REFUSAL_CODES",
|
|
69
|
+
"Answer",
|
|
70
|
+
"AsyncMitra",
|
|
71
|
+
"AuthError",
|
|
72
|
+
"BusyError",
|
|
73
|
+
"Catalog",
|
|
74
|
+
"Clarification",
|
|
75
|
+
"Connector",
|
|
76
|
+
"ConnectorField",
|
|
77
|
+
"ConnectorList",
|
|
78
|
+
"Conversation",
|
|
79
|
+
"ConversationList",
|
|
80
|
+
"ConversationMessage",
|
|
81
|
+
"CreditsError",
|
|
82
|
+
"Dashboard",
|
|
83
|
+
"DashboardList",
|
|
84
|
+
"DashboardWidget",
|
|
85
|
+
"Embed",
|
|
86
|
+
"EmbedKey",
|
|
87
|
+
"EmbedList",
|
|
88
|
+
"EmbedMember",
|
|
89
|
+
"EmbedNotFoundError",
|
|
90
|
+
"EmbedRefusedError",
|
|
91
|
+
"EmbedResource",
|
|
92
|
+
"EmbedSummary",
|
|
93
|
+
"File",
|
|
94
|
+
"FileList",
|
|
95
|
+
"Gem",
|
|
96
|
+
"GemList",
|
|
97
|
+
"Heard",
|
|
98
|
+
"Invitation",
|
|
99
|
+
"InvitationList",
|
|
100
|
+
"Job",
|
|
101
|
+
"JobError",
|
|
102
|
+
"JobFailedError",
|
|
103
|
+
"JobTimeoutError",
|
|
104
|
+
"Member",
|
|
105
|
+
"MemberList",
|
|
106
|
+
"Mitra",
|
|
107
|
+
"MitraError",
|
|
108
|
+
"PermissionDeniedError",
|
|
109
|
+
"RateLimitError",
|
|
110
|
+
"ResultTable",
|
|
111
|
+
"Source",
|
|
112
|
+
"SourceCreated",
|
|
113
|
+
"SourceList",
|
|
114
|
+
"SourceReadiness",
|
|
115
|
+
"SourceTest",
|
|
116
|
+
"SourceTestFailedError",
|
|
117
|
+
"StreamEvent",
|
|
118
|
+
"Webhook",
|
|
119
|
+
"WebhookDelivery",
|
|
120
|
+
"WebhookTestResult",
|
|
121
|
+
"WebhookVerificationError",
|
|
122
|
+
"Workspace",
|
|
123
|
+
"WorkspaceInvitation",
|
|
124
|
+
"WorkspaceList",
|
|
125
|
+
"assert_webhook",
|
|
126
|
+
"embed_src",
|
|
127
|
+
"verify_webhook",
|
|
128
|
+
"__version__",
|
|
129
|
+
]
|
mitra/_core.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Shared request plumbing for the sync and async clients (internal)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import uuid
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from ._version import __version__
|
|
14
|
+
from .errors import MitraError, error_from_response
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://workspace.matih.ai"
|
|
17
|
+
API_PREFIX = "/api/v1"
|
|
18
|
+
RETRY_STATUSES = frozenset({429, 502, 503, 504})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def api_root(base_url: str) -> str:
|
|
22
|
+
"""The API root for a host: ``https://workspace.matih.ai`` -> ``.../api/v1``.
|
|
23
|
+
|
|
24
|
+
``base_url`` is the host only. A value that already ends in ``/api/v1`` is accepted as is."""
|
|
25
|
+
root = base_url.strip().rstrip("/")
|
|
26
|
+
return root if root.endswith(API_PREFIX) else root + API_PREFIX
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Settings:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
api_key: str | None,
|
|
34
|
+
access_token: str | None,
|
|
35
|
+
base_url: str | None,
|
|
36
|
+
workspace_id: str | None,
|
|
37
|
+
language: str | None,
|
|
38
|
+
timeout: float,
|
|
39
|
+
max_retries: int,
|
|
40
|
+
) -> None:
|
|
41
|
+
if api_key and access_token:
|
|
42
|
+
raise ValueError("pass either api_key or access_token, not both")
|
|
43
|
+
credential = (
|
|
44
|
+
api_key
|
|
45
|
+
or access_token
|
|
46
|
+
or os.environ.get("MATIH_API_KEY")
|
|
47
|
+
or os.environ.get("MATIH_ACCESS_TOKEN")
|
|
48
|
+
)
|
|
49
|
+
if not credential:
|
|
50
|
+
raise ValueError("pass api_key=... or access_token=... (or set MATIH_API_KEY)")
|
|
51
|
+
self.credential = credential
|
|
52
|
+
self.base_url = api_root(base_url or os.environ.get("MATIH_BASE_URL") or DEFAULT_BASE_URL)
|
|
53
|
+
self.workspace_id = workspace_id
|
|
54
|
+
self.language = language
|
|
55
|
+
self.timeout = timeout
|
|
56
|
+
self.max_retries = max_retries
|
|
57
|
+
|
|
58
|
+
def headers(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
workspace_id: str | None = None,
|
|
62
|
+
language: str | None = None,
|
|
63
|
+
idempotency_key: str | None = None,
|
|
64
|
+
accept: str = "application/json",
|
|
65
|
+
prefer: str | None = None,
|
|
66
|
+
) -> dict[str, str]:
|
|
67
|
+
headers = {
|
|
68
|
+
"Authorization": f"Bearer {self.credential}",
|
|
69
|
+
"Accept": accept,
|
|
70
|
+
"User-Agent": f"mitra-sdk-python/{__version__}",
|
|
71
|
+
}
|
|
72
|
+
workspace = workspace_id or self.workspace_id
|
|
73
|
+
if workspace:
|
|
74
|
+
headers["X-Matih-Workspace"] = workspace
|
|
75
|
+
lang = language or self.language
|
|
76
|
+
if lang:
|
|
77
|
+
headers["Accept-Language"] = lang
|
|
78
|
+
if idempotency_key:
|
|
79
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
80
|
+
if prefer:
|
|
81
|
+
headers["Prefer"] = prefer
|
|
82
|
+
return headers
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def prepare(
|
|
86
|
+
settings: Settings,
|
|
87
|
+
method: str,
|
|
88
|
+
path: str,
|
|
89
|
+
*,
|
|
90
|
+
json_body: Any = None,
|
|
91
|
+
params: dict[str, Any] | None = None,
|
|
92
|
+
files: Any = None,
|
|
93
|
+
data: dict[str, Any] | None = None,
|
|
94
|
+
workspace_id: str | None = None,
|
|
95
|
+
language: str | None = None,
|
|
96
|
+
idempotent: bool = False,
|
|
97
|
+
idempotency_key: str | None = None,
|
|
98
|
+
accept: str = "application/json",
|
|
99
|
+
prefer: str | None = None,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Build kwargs for ``httpx.Client.request``. The idempotency key is fixed here, once."""
|
|
102
|
+
key = idempotency_key or (str(uuid.uuid4()) if idempotent else None)
|
|
103
|
+
kwargs: dict[str, Any] = {
|
|
104
|
+
"method": method,
|
|
105
|
+
"url": settings.base_url + path,
|
|
106
|
+
"headers": settings.headers(
|
|
107
|
+
workspace_id=workspace_id,
|
|
108
|
+
language=language,
|
|
109
|
+
idempotency_key=key,
|
|
110
|
+
accept=accept,
|
|
111
|
+
prefer=prefer,
|
|
112
|
+
),
|
|
113
|
+
}
|
|
114
|
+
if params:
|
|
115
|
+
kwargs["params"] = {k: v for k, v in params.items() if v is not None}
|
|
116
|
+
if json_body is not None:
|
|
117
|
+
kwargs["json"] = json_body
|
|
118
|
+
if files is not None:
|
|
119
|
+
kwargs["files"] = files
|
|
120
|
+
if data:
|
|
121
|
+
kwargs["data"] = data
|
|
122
|
+
return kwargs
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def should_retry(kwargs: dict[str, Any], status: int | None, attempt: int, max_retries: int) -> bool:
|
|
126
|
+
"""Retry 429 always (not processed), and 5xx or network errors when the call is safe to repeat."""
|
|
127
|
+
if attempt >= max_retries:
|
|
128
|
+
return False
|
|
129
|
+
safe = kwargs["method"] in ("GET", "HEAD", "DELETE") or "Idempotency-Key" in kwargs["headers"]
|
|
130
|
+
if status == 429:
|
|
131
|
+
return True
|
|
132
|
+
return safe
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def retry_delay(response: httpx.Response | None, attempt: int) -> float:
|
|
136
|
+
if response is not None:
|
|
137
|
+
value = response.headers.get("retry-after")
|
|
138
|
+
if value:
|
|
139
|
+
try:
|
|
140
|
+
return min(max(float(value), 0.0), 60.0)
|
|
141
|
+
except ValueError:
|
|
142
|
+
pass
|
|
143
|
+
return min(0.5 * (2**attempt), 8.0)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def raise_for_response(response: httpx.Response) -> None:
|
|
147
|
+
if response.status_code < 400:
|
|
148
|
+
return
|
|
149
|
+
try:
|
|
150
|
+
body: Any = response.json()
|
|
151
|
+
except ValueError:
|
|
152
|
+
body = {"detail": {"message": response.text[:500] or f"HTTP {response.status_code}"}}
|
|
153
|
+
retry_after = None
|
|
154
|
+
raw = response.headers.get("retry-after")
|
|
155
|
+
if raw:
|
|
156
|
+
try:
|
|
157
|
+
retry_after = float(raw)
|
|
158
|
+
except ValueError:
|
|
159
|
+
retry_after = None
|
|
160
|
+
raise error_from_response(
|
|
161
|
+
response.status_code,
|
|
162
|
+
body,
|
|
163
|
+
request_id=response.headers.get("x-request-id"),
|
|
164
|
+
retry_after=retry_after,
|
|
165
|
+
method=response.request.method,
|
|
166
|
+
path=response.request.url.path,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def decode(response: httpx.Response, parse: Callable[[Any], Any] | None, raw: bool) -> Any:
|
|
171
|
+
if raw:
|
|
172
|
+
return response.content
|
|
173
|
+
if not response.content:
|
|
174
|
+
return None
|
|
175
|
+
try:
|
|
176
|
+
payload = response.json()
|
|
177
|
+
except ValueError as exc:
|
|
178
|
+
raise MitraError(
|
|
179
|
+
"response was not JSON",
|
|
180
|
+
status=response.status_code,
|
|
181
|
+
request_id=response.headers.get("x-request-id"),
|
|
182
|
+
) from exc
|
|
183
|
+
return parse(payload) if parse else payload
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def parse_sse(lines: list[str]) -> tuple[str, Any] | None:
|
|
187
|
+
"""Turn the lines of one SSE frame into ``(event, data)``; ``None`` for comments/empty."""
|
|
188
|
+
event = "message"
|
|
189
|
+
data_lines: list[str] = []
|
|
190
|
+
for line in lines:
|
|
191
|
+
if not line or line.startswith(":"):
|
|
192
|
+
continue
|
|
193
|
+
field, _, value = line.partition(":")
|
|
194
|
+
value = value[1:] if value.startswith(" ") else value
|
|
195
|
+
if field == "event":
|
|
196
|
+
event = value
|
|
197
|
+
elif field == "data":
|
|
198
|
+
data_lines.append(value)
|
|
199
|
+
if not data_lines:
|
|
200
|
+
return None
|
|
201
|
+
text = "\n".join(data_lines)
|
|
202
|
+
try:
|
|
203
|
+
return event, json.loads(text)
|
|
204
|
+
except ValueError:
|
|
205
|
+
return event, text
|
mitra/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|