deutero 0.2.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.
- deutero/__init__.py +258 -0
- deutero/_http.py +217 -0
- deutero/client.py +215 -0
- deutero/exceptions.py +140 -0
- deutero/models.py +1708 -0
- deutero/py.typed +0 -0
- deutero/resources/__init__.py +53 -0
- deutero/resources/analysis.py +178 -0
- deutero/resources/characteristics.py +210 -0
- deutero/resources/credits.py +34 -0
- deutero/resources/embed.py +162 -0
- deutero/resources/graph.py +199 -0
- deutero/resources/interviews.py +187 -0
- deutero/resources/personas.py +120 -0
- deutero/resources/projects.py +89 -0
- deutero/resources/questions.py +310 -0
- deutero/resources/recruitment.py +92 -0
- deutero/resources/screening.py +187 -0
- deutero/resources/simulations.py +135 -0
- deutero/resources/studies.py +522 -0
- deutero/resources/transcripts.py +165 -0
- deutero/resources/webhooks.py +207 -0
- deutero/resources/welcome.py +154 -0
- deutero/webhooks.py +378 -0
- deutero-0.2.0.dist-info/METADATA +588 -0
- deutero-0.2.0.dist-info/RECORD +28 -0
- deutero-0.2.0.dist-info/WHEEL +4 -0
- deutero-0.2.0.dist-info/licenses/LICENSE +21 -0
deutero/__init__.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Deutero Python SDK — bindings for the Deutero Study Management API.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from deutero import Deutero
|
|
6
|
+
|
|
7
|
+
client = Deutero(api_key="your-api-key")
|
|
8
|
+
project = client.projects.create(name="Onboarding research")
|
|
9
|
+
study = client.studies.create(
|
|
10
|
+
project_id=project.id,
|
|
11
|
+
name="Why new users drop off",
|
|
12
|
+
survey_type="user_experience",
|
|
13
|
+
)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from deutero import webhooks
|
|
17
|
+
from deutero.client import AsyncDeutero, Deutero
|
|
18
|
+
from deutero.exceptions import (
|
|
19
|
+
APIError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
BadGatewayError,
|
|
22
|
+
ConflictError,
|
|
23
|
+
ConnectionError,
|
|
24
|
+
DeuteroError,
|
|
25
|
+
InsufficientCreditsError,
|
|
26
|
+
InternalServerError,
|
|
27
|
+
NotFoundError,
|
|
28
|
+
PermissionDeniedError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
TimeoutError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
WebhookVerificationError,
|
|
33
|
+
)
|
|
34
|
+
from deutero.models import (
|
|
35
|
+
AnalysisCategory,
|
|
36
|
+
AnalyzableQuestion,
|
|
37
|
+
AnalyzableQuestionsOut,
|
|
38
|
+
CapturedVariableOut,
|
|
39
|
+
Centroid,
|
|
40
|
+
CharacteristicQuestionOut,
|
|
41
|
+
CharacteristicsOut,
|
|
42
|
+
CharacteristicsSettingsOut,
|
|
43
|
+
CharacteristicValueOut,
|
|
44
|
+
ClusteringOut,
|
|
45
|
+
ClusterPoints,
|
|
46
|
+
CreditBalanceOut,
|
|
47
|
+
CreditCheckOut,
|
|
48
|
+
DecisionOutcomeOut,
|
|
49
|
+
EmbedKeyListOut,
|
|
50
|
+
EmbedMetadataItem,
|
|
51
|
+
EmbedSnippetOut,
|
|
52
|
+
EthicsCheck,
|
|
53
|
+
FlowCheckError,
|
|
54
|
+
FlowDocument,
|
|
55
|
+
FlowEdge,
|
|
56
|
+
FlowNode,
|
|
57
|
+
FlowSignalOut,
|
|
58
|
+
FlowSignalsOut,
|
|
59
|
+
GeneratedQuestionsOut,
|
|
60
|
+
GraphCheckOut,
|
|
61
|
+
GraphModeOut,
|
|
62
|
+
GraphOut,
|
|
63
|
+
InterviewDetailListOut,
|
|
64
|
+
InterviewDetailOut,
|
|
65
|
+
InterviewEffectsOut,
|
|
66
|
+
InterviewListOut,
|
|
67
|
+
InterviewSummary,
|
|
68
|
+
InterviewTranscript,
|
|
69
|
+
IssueLocation,
|
|
70
|
+
IssueOut,
|
|
71
|
+
LanguageIssues,
|
|
72
|
+
LatestValidationOut,
|
|
73
|
+
ModelTier,
|
|
74
|
+
NodeFetchOut,
|
|
75
|
+
NodeType,
|
|
76
|
+
OptimalClustersOut,
|
|
77
|
+
OptionCount,
|
|
78
|
+
OptionsResponsesOut,
|
|
79
|
+
PatchOp,
|
|
80
|
+
PauseOut,
|
|
81
|
+
PersonaGenerateOut,
|
|
82
|
+
PersonaListOut,
|
|
83
|
+
PersonaOut,
|
|
84
|
+
ProjectListOut,
|
|
85
|
+
ProjectOut,
|
|
86
|
+
PublicationStatusOut,
|
|
87
|
+
PublishOut,
|
|
88
|
+
QualificationOut,
|
|
89
|
+
QuestionImageOut,
|
|
90
|
+
QuestionListOut,
|
|
91
|
+
QuestionOut,
|
|
92
|
+
QuestionValidation,
|
|
93
|
+
RecruitmentOut,
|
|
94
|
+
RedundancyCheck,
|
|
95
|
+
ScaleConfig,
|
|
96
|
+
ScaleResponsesOut,
|
|
97
|
+
ScreeningOut,
|
|
98
|
+
ScreeningQuestionOut,
|
|
99
|
+
ScreeningSettingsOut,
|
|
100
|
+
SearchHit,
|
|
101
|
+
SearchMode,
|
|
102
|
+
SearchOut,
|
|
103
|
+
SignalDeliveryOut,
|
|
104
|
+
SimulationListOut,
|
|
105
|
+
SimulationOut,
|
|
106
|
+
SimulationStartOut,
|
|
107
|
+
StudyDraft,
|
|
108
|
+
StudyDraftFromSiteOut,
|
|
109
|
+
StudyListOut,
|
|
110
|
+
StudyOut,
|
|
111
|
+
StudyStatsOut,
|
|
112
|
+
StudySummary,
|
|
113
|
+
StudyType,
|
|
114
|
+
SuccessResponse,
|
|
115
|
+
TranscriptMessage,
|
|
116
|
+
TranscriptOut,
|
|
117
|
+
TranscriptsBulkOut,
|
|
118
|
+
ValidationOut,
|
|
119
|
+
ValidationRunOut,
|
|
120
|
+
WebhookCreatedOut,
|
|
121
|
+
WebhookDeleteOut,
|
|
122
|
+
WebhookDeliveryListOut,
|
|
123
|
+
WebhookDeliveryOut,
|
|
124
|
+
WebhookEventTypeListOut,
|
|
125
|
+
WebhookEventTypeOut,
|
|
126
|
+
WebhookListOut,
|
|
127
|
+
WebhookOut,
|
|
128
|
+
WebhookSecretOut,
|
|
129
|
+
WelcomeDraftOut,
|
|
130
|
+
WelcomeOut,
|
|
131
|
+
WelcomeTranslationListOut,
|
|
132
|
+
WelcomeTranslationOut,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
# Clients
|
|
137
|
+
"Deutero",
|
|
138
|
+
"AsyncDeutero",
|
|
139
|
+
# Exceptions
|
|
140
|
+
"DeuteroError",
|
|
141
|
+
"APIError",
|
|
142
|
+
"AuthenticationError",
|
|
143
|
+
"BadGatewayError",
|
|
144
|
+
"ConflictError",
|
|
145
|
+
"ConnectionError",
|
|
146
|
+
"InsufficientCreditsError",
|
|
147
|
+
"InternalServerError",
|
|
148
|
+
"NotFoundError",
|
|
149
|
+
"PermissionDeniedError",
|
|
150
|
+
"RateLimitError",
|
|
151
|
+
"TimeoutError",
|
|
152
|
+
"ValidationError",
|
|
153
|
+
"WebhookVerificationError",
|
|
154
|
+
# Webhook payload handling
|
|
155
|
+
"webhooks",
|
|
156
|
+
# Enums
|
|
157
|
+
"AnalysisCategory",
|
|
158
|
+
"ModelTier",
|
|
159
|
+
"NodeType",
|
|
160
|
+
"SearchMode",
|
|
161
|
+
"StudyType",
|
|
162
|
+
# Models
|
|
163
|
+
"AnalyzableQuestion",
|
|
164
|
+
"AnalyzableQuestionsOut",
|
|
165
|
+
"CapturedVariableOut",
|
|
166
|
+
"Centroid",
|
|
167
|
+
"CharacteristicQuestionOut",
|
|
168
|
+
"CharacteristicValueOut",
|
|
169
|
+
"CharacteristicsOut",
|
|
170
|
+
"CharacteristicsSettingsOut",
|
|
171
|
+
"ClusterPoints",
|
|
172
|
+
"ClusteringOut",
|
|
173
|
+
"CreditBalanceOut",
|
|
174
|
+
"CreditCheckOut",
|
|
175
|
+
"DecisionOutcomeOut",
|
|
176
|
+
"EmbedKeyListOut",
|
|
177
|
+
"EmbedMetadataItem",
|
|
178
|
+
"EmbedSnippetOut",
|
|
179
|
+
"EthicsCheck",
|
|
180
|
+
"FlowCheckError",
|
|
181
|
+
"FlowDocument",
|
|
182
|
+
"FlowEdge",
|
|
183
|
+
"FlowNode",
|
|
184
|
+
"FlowSignalOut",
|
|
185
|
+
"FlowSignalsOut",
|
|
186
|
+
"GeneratedQuestionsOut",
|
|
187
|
+
"GraphCheckOut",
|
|
188
|
+
"GraphModeOut",
|
|
189
|
+
"GraphOut",
|
|
190
|
+
"InterviewDetailListOut",
|
|
191
|
+
"InterviewDetailOut",
|
|
192
|
+
"InterviewEffectsOut",
|
|
193
|
+
"InterviewListOut",
|
|
194
|
+
"InterviewSummary",
|
|
195
|
+
"InterviewTranscript",
|
|
196
|
+
"IssueLocation",
|
|
197
|
+
"IssueOut",
|
|
198
|
+
"LanguageIssues",
|
|
199
|
+
"LatestValidationOut",
|
|
200
|
+
"NodeFetchOut",
|
|
201
|
+
"OptimalClustersOut",
|
|
202
|
+
"OptionCount",
|
|
203
|
+
"OptionsResponsesOut",
|
|
204
|
+
"PatchOp",
|
|
205
|
+
"PauseOut",
|
|
206
|
+
"PersonaGenerateOut",
|
|
207
|
+
"PersonaListOut",
|
|
208
|
+
"PersonaOut",
|
|
209
|
+
"ProjectListOut",
|
|
210
|
+
"ProjectOut",
|
|
211
|
+
"PublicationStatusOut",
|
|
212
|
+
"PublishOut",
|
|
213
|
+
"QualificationOut",
|
|
214
|
+
"QuestionImageOut",
|
|
215
|
+
"QuestionListOut",
|
|
216
|
+
"QuestionOut",
|
|
217
|
+
"QuestionValidation",
|
|
218
|
+
"RecruitmentOut",
|
|
219
|
+
"RedundancyCheck",
|
|
220
|
+
"ScaleConfig",
|
|
221
|
+
"ScaleResponsesOut",
|
|
222
|
+
"ScreeningOut",
|
|
223
|
+
"ScreeningQuestionOut",
|
|
224
|
+
"ScreeningSettingsOut",
|
|
225
|
+
"SearchHit",
|
|
226
|
+
"SearchOut",
|
|
227
|
+
"SignalDeliveryOut",
|
|
228
|
+
"SimulationListOut",
|
|
229
|
+
"SimulationOut",
|
|
230
|
+
"SimulationStartOut",
|
|
231
|
+
"StudyDraft",
|
|
232
|
+
"StudyDraftFromSiteOut",
|
|
233
|
+
"StudyListOut",
|
|
234
|
+
"StudyOut",
|
|
235
|
+
"StudyStatsOut",
|
|
236
|
+
"StudySummary",
|
|
237
|
+
"SuccessResponse",
|
|
238
|
+
"TranscriptMessage",
|
|
239
|
+
"TranscriptOut",
|
|
240
|
+
"TranscriptsBulkOut",
|
|
241
|
+
"ValidationOut",
|
|
242
|
+
"ValidationRunOut",
|
|
243
|
+
"WebhookCreatedOut",
|
|
244
|
+
"WebhookDeleteOut",
|
|
245
|
+
"WebhookDeliveryListOut",
|
|
246
|
+
"WebhookDeliveryOut",
|
|
247
|
+
"WebhookEventTypeListOut",
|
|
248
|
+
"WebhookEventTypeOut",
|
|
249
|
+
"WebhookListOut",
|
|
250
|
+
"WebhookOut",
|
|
251
|
+
"WebhookSecretOut",
|
|
252
|
+
"WelcomeDraftOut",
|
|
253
|
+
"WelcomeOut",
|
|
254
|
+
"WelcomeTranslationListOut",
|
|
255
|
+
"WelcomeTranslationOut",
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
__version__ = "0.2.0"
|
deutero/_http.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Low-level HTTP transport for the Deutero SDK.
|
|
2
|
+
|
|
3
|
+
Provides both synchronous and asynchronous HTTP clients backed by httpx.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import enum
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from deutero.exceptions import ConnectionError, TimeoutError, raise_for_status
|
|
17
|
+
|
|
18
|
+
_DEFAULT_TIMEOUT = 120.0
|
|
19
|
+
_DEFAULT_BASE_URL = "https://dashboard.deutero.ai/study-api"
|
|
20
|
+
_USER_AGENT = "deutero-python/0.2.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_headers(api_key: str, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
|
24
|
+
headers = {
|
|
25
|
+
"X-API-Key": api_key,
|
|
26
|
+
"User-Agent": _USER_AGENT,
|
|
27
|
+
"Accept": "application/json",
|
|
28
|
+
}
|
|
29
|
+
if extra:
|
|
30
|
+
headers.update(extra)
|
|
31
|
+
return headers
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_response(response: httpx.Response) -> Any:
|
|
35
|
+
request_id = response.headers.get("x-request-id")
|
|
36
|
+
if response.status_code == 204 or not response.content:
|
|
37
|
+
raise_for_status(response.status_code, None, request_id=request_id)
|
|
38
|
+
return None
|
|
39
|
+
try:
|
|
40
|
+
body = response.json()
|
|
41
|
+
except Exception:
|
|
42
|
+
body = response.text
|
|
43
|
+
|
|
44
|
+
raise_for_status(response.status_code, body, request_id=request_id)
|
|
45
|
+
return body
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SyncHTTPClient:
|
|
49
|
+
"""Synchronous HTTP client."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
api_key: str,
|
|
55
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
56
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
57
|
+
http_client: Optional[httpx.Client] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._api_key = api_key
|
|
60
|
+
self._base_url = base_url.rstrip("/")
|
|
61
|
+
self._timeout = timeout
|
|
62
|
+
self._headers = _build_headers(api_key)
|
|
63
|
+
self._owns_client = http_client is None
|
|
64
|
+
self._client = http_client or httpx.Client(
|
|
65
|
+
base_url=self._base_url,
|
|
66
|
+
headers=self._headers,
|
|
67
|
+
timeout=timeout,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def request(
|
|
71
|
+
self,
|
|
72
|
+
method: str,
|
|
73
|
+
path: str,
|
|
74
|
+
*,
|
|
75
|
+
json: Optional[Any] = None,
|
|
76
|
+
params: Optional[Dict[str, Any]] = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
try:
|
|
79
|
+
response = self._client.request(
|
|
80
|
+
method,
|
|
81
|
+
self._url(path),
|
|
82
|
+
json=json,
|
|
83
|
+
params=_clean_params(params),
|
|
84
|
+
headers=self._headers,
|
|
85
|
+
)
|
|
86
|
+
except httpx.ConnectError as exc:
|
|
87
|
+
raise ConnectionError(f"Failed to connect to {self._base_url}: {exc}") from exc
|
|
88
|
+
except httpx.TimeoutException as exc:
|
|
89
|
+
raise TimeoutError(f"Request to {path} timed out after {self._timeout}s") from exc
|
|
90
|
+
|
|
91
|
+
return _parse_response(response)
|
|
92
|
+
|
|
93
|
+
def _url(self, path: str) -> str:
|
|
94
|
+
# A caller-supplied httpx client may not carry our base URL.
|
|
95
|
+
return path if str(self._client.base_url) else self._base_url + path
|
|
96
|
+
|
|
97
|
+
def get(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
98
|
+
return self.request("GET", path, params=params)
|
|
99
|
+
|
|
100
|
+
def post(self, path: str, *, json: Optional[Any] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
101
|
+
return self.request("POST", path, json=json, params=params)
|
|
102
|
+
|
|
103
|
+
def put(self, path: str, *, json: Optional[Any] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
104
|
+
return self.request("PUT", path, json=json, params=params)
|
|
105
|
+
|
|
106
|
+
def patch(self, path: str, *, json: Optional[Any] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
107
|
+
return self.request("PATCH", path, json=json, params=params)
|
|
108
|
+
|
|
109
|
+
def delete(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
110
|
+
return self.request("DELETE", path, params=params)
|
|
111
|
+
|
|
112
|
+
def close(self) -> None:
|
|
113
|
+
if self._owns_client:
|
|
114
|
+
self._client.close()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AsyncHTTPClient:
|
|
118
|
+
"""Asynchronous HTTP client."""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
*,
|
|
123
|
+
api_key: str,
|
|
124
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
125
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
126
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
self._api_key = api_key
|
|
129
|
+
self._base_url = base_url.rstrip("/")
|
|
130
|
+
self._timeout = timeout
|
|
131
|
+
self._headers = _build_headers(api_key)
|
|
132
|
+
self._owns_client = http_client is None
|
|
133
|
+
self._client = http_client or httpx.AsyncClient(
|
|
134
|
+
base_url=self._base_url,
|
|
135
|
+
headers=self._headers,
|
|
136
|
+
timeout=timeout,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def request(
|
|
140
|
+
self,
|
|
141
|
+
method: str,
|
|
142
|
+
path: str,
|
|
143
|
+
*,
|
|
144
|
+
json: Optional[Any] = None,
|
|
145
|
+
params: Optional[Dict[str, Any]] = None,
|
|
146
|
+
) -> Any:
|
|
147
|
+
try:
|
|
148
|
+
response = await self._client.request(
|
|
149
|
+
method,
|
|
150
|
+
self._url(path),
|
|
151
|
+
json=json,
|
|
152
|
+
params=_clean_params(params),
|
|
153
|
+
headers=self._headers,
|
|
154
|
+
)
|
|
155
|
+
except httpx.ConnectError as exc:
|
|
156
|
+
raise ConnectionError(f"Failed to connect to {self._base_url}: {exc}") from exc
|
|
157
|
+
except httpx.TimeoutException as exc:
|
|
158
|
+
raise TimeoutError(f"Request to {path} timed out after {self._timeout}s") from exc
|
|
159
|
+
|
|
160
|
+
return _parse_response(response)
|
|
161
|
+
|
|
162
|
+
def _url(self, path: str) -> str:
|
|
163
|
+
# A caller-supplied httpx client may not carry our base URL.
|
|
164
|
+
return path if str(self._client.base_url) else self._base_url + path
|
|
165
|
+
|
|
166
|
+
async def get(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
167
|
+
return await self.request("GET", path, params=params)
|
|
168
|
+
|
|
169
|
+
async def post(self, path: str, *, json: Optional[Any] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
170
|
+
return await self.request("POST", path, json=json, params=params)
|
|
171
|
+
|
|
172
|
+
async def put(self, path: str, *, json: Optional[Any] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
173
|
+
return await self.request("PUT", path, json=json, params=params)
|
|
174
|
+
|
|
175
|
+
async def patch(self, path: str, *, json: Optional[Any] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
176
|
+
return await self.request("PATCH", path, json=json, params=params)
|
|
177
|
+
|
|
178
|
+
async def delete(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
179
|
+
return await self.request("DELETE", path, params=params)
|
|
180
|
+
|
|
181
|
+
async def close(self) -> None:
|
|
182
|
+
if self._owns_client:
|
|
183
|
+
await self._client.aclose()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _clean_params(params: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
187
|
+
"""Remove None values from query parameters and stringify the rest."""
|
|
188
|
+
if params is None:
|
|
189
|
+
return None
|
|
190
|
+
return {k: _param_value(v) for k, v in params.items() if v is not None}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _param_value(value: Any) -> str:
|
|
194
|
+
if isinstance(value, bool):
|
|
195
|
+
return "true" if value else "false"
|
|
196
|
+
if isinstance(value, datetime):
|
|
197
|
+
return value.isoformat()
|
|
198
|
+
if isinstance(value, enum.Enum):
|
|
199
|
+
return str(value.value)
|
|
200
|
+
return value if isinstance(value, str) else str(value)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def compact(**fields: Any) -> Dict[str, Any]:
|
|
204
|
+
"""Build a JSON body from keyword arguments, dropping those left as ``None``."""
|
|
205
|
+
return {k: _jsonable(v) for k, v in fields.items() if v is not None}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _jsonable(value: Any) -> Any:
|
|
209
|
+
if isinstance(value, BaseModel):
|
|
210
|
+
return value.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
211
|
+
if isinstance(value, UUID):
|
|
212
|
+
return str(value)
|
|
213
|
+
if isinstance(value, enum.Enum):
|
|
214
|
+
return value.value
|
|
215
|
+
if isinstance(value, list):
|
|
216
|
+
return [_jsonable(v) for v in value]
|
|
217
|
+
return value
|
deutero/client.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Deutero API client — synchronous and asynchronous."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from deutero._http import _DEFAULT_BASE_URL, _DEFAULT_TIMEOUT, AsyncHTTPClient, SyncHTTPClient
|
|
11
|
+
from deutero.resources.analysis import Analysis, AsyncAnalysis
|
|
12
|
+
from deutero.resources.characteristics import AsyncCharacteristics, Characteristics
|
|
13
|
+
from deutero.resources.credits import AsyncCredits, Credits
|
|
14
|
+
from deutero.resources.embed import AsyncEmbed, Embed
|
|
15
|
+
from deutero.resources.graph import AsyncGraph, Graph
|
|
16
|
+
from deutero.resources.interviews import AsyncInterviews, Interviews
|
|
17
|
+
from deutero.resources.personas import AsyncPersonas, Personas
|
|
18
|
+
from deutero.resources.projects import AsyncProjects, Projects
|
|
19
|
+
from deutero.resources.questions import AsyncQuestions, Questions
|
|
20
|
+
from deutero.resources.recruitment import AsyncRecruitment, Recruitment
|
|
21
|
+
from deutero.resources.screening import AsyncScreening, Screening
|
|
22
|
+
from deutero.resources.simulations import AsyncSimulations, Simulations
|
|
23
|
+
from deutero.resources.studies import AsyncStudies, Studies
|
|
24
|
+
from deutero.resources.transcripts import AsyncTranscripts, Transcripts
|
|
25
|
+
from deutero.resources.webhooks import AsyncWebhooks, Webhooks
|
|
26
|
+
from deutero.resources.welcome import AsyncWelcome, Welcome
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_api_key(api_key: Optional[str]) -> str:
|
|
30
|
+
resolved_key = api_key or os.environ.get("DEUTERO_API_KEY", "")
|
|
31
|
+
if not resolved_key:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"No API key provided. Pass api_key= or set the DEUTERO_API_KEY environment variable."
|
|
34
|
+
)
|
|
35
|
+
return resolved_key
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Deutero:
|
|
39
|
+
"""Synchronous client for the Deutero Study Management API.
|
|
40
|
+
|
|
41
|
+
Usage::
|
|
42
|
+
|
|
43
|
+
from deutero import Deutero
|
|
44
|
+
|
|
45
|
+
client = Deutero(api_key="your-api-key")
|
|
46
|
+
|
|
47
|
+
project = client.projects.create(name="Onboarding research")
|
|
48
|
+
study = client.studies.create(project_id=project.id, name="Why new users drop off")
|
|
49
|
+
|
|
50
|
+
The API key can also be set via the ``DEUTERO_API_KEY`` environment variable.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
api_key: Your Deutero API key. Falls back to ``DEUTERO_API_KEY`` env var.
|
|
54
|
+
base_url: Override the base URL (default: ``https://dashboard.deutero.ai/study-api``).
|
|
55
|
+
timeout: Request timeout in seconds (default: 120).
|
|
56
|
+
http_client: Optional pre-configured :class:`httpx.Client`.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
projects: Projects
|
|
60
|
+
studies: Studies
|
|
61
|
+
welcome: Welcome
|
|
62
|
+
screening: Screening
|
|
63
|
+
characteristics: Characteristics
|
|
64
|
+
questions: Questions
|
|
65
|
+
graph: Graph
|
|
66
|
+
recruitment: Recruitment
|
|
67
|
+
embed: Embed
|
|
68
|
+
personas: Personas
|
|
69
|
+
simulations: Simulations
|
|
70
|
+
interviews: Interviews
|
|
71
|
+
transcripts: Transcripts
|
|
72
|
+
analysis: Analysis
|
|
73
|
+
webhooks: Webhooks
|
|
74
|
+
credits: Credits
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
api_key: Optional[str] = None,
|
|
80
|
+
base_url: Optional[str] = None,
|
|
81
|
+
timeout: Optional[float] = None,
|
|
82
|
+
http_client: Optional[httpx.Client] = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._http = SyncHTTPClient(
|
|
85
|
+
api_key=_resolve_api_key(api_key),
|
|
86
|
+
base_url=base_url or _DEFAULT_BASE_URL,
|
|
87
|
+
timeout=timeout or _DEFAULT_TIMEOUT,
|
|
88
|
+
http_client=http_client,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.projects = Projects(self._http)
|
|
92
|
+
self.studies = Studies(self._http)
|
|
93
|
+
self.welcome = Welcome(self._http)
|
|
94
|
+
self.screening = Screening(self._http)
|
|
95
|
+
self.characteristics = Characteristics(self._http)
|
|
96
|
+
self.questions = Questions(self._http)
|
|
97
|
+
self.graph = Graph(self._http)
|
|
98
|
+
self.recruitment = Recruitment(self._http)
|
|
99
|
+
self.embed = Embed(self._http)
|
|
100
|
+
self.personas = Personas(self._http)
|
|
101
|
+
self.simulations = Simulations(self._http)
|
|
102
|
+
self.interviews = Interviews(self._http)
|
|
103
|
+
self.transcripts = Transcripts(self._http)
|
|
104
|
+
self.analysis = Analysis(self._http)
|
|
105
|
+
self.webhooks = Webhooks(self._http)
|
|
106
|
+
self.credits = Credits(self._http)
|
|
107
|
+
|
|
108
|
+
def health(self) -> Dict[str, Any]:
|
|
109
|
+
"""Liveness check. Does not require a valid API key."""
|
|
110
|
+
data: Dict[str, Any] = self._http.get("/health")
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
def close(self) -> None:
|
|
114
|
+
"""Close the underlying HTTP connection pool."""
|
|
115
|
+
self._http.close()
|
|
116
|
+
|
|
117
|
+
def __enter__(self) -> Deutero:
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def __exit__(self, *args: object) -> None:
|
|
121
|
+
self.close()
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
return f"Deutero(base_url={self._http._base_url!r})"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AsyncDeutero:
|
|
128
|
+
"""Asynchronous client for the Deutero Study Management API.
|
|
129
|
+
|
|
130
|
+
Usage::
|
|
131
|
+
|
|
132
|
+
import asyncio
|
|
133
|
+
from deutero import AsyncDeutero
|
|
134
|
+
|
|
135
|
+
async def main():
|
|
136
|
+
async with AsyncDeutero(api_key="your-api-key") as client:
|
|
137
|
+
projects = await client.projects.list()
|
|
138
|
+
|
|
139
|
+
asyncio.run(main())
|
|
140
|
+
|
|
141
|
+
The API key can also be set via the ``DEUTERO_API_KEY`` environment variable.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
api_key: Your Deutero API key. Falls back to ``DEUTERO_API_KEY`` env var.
|
|
145
|
+
base_url: Override the base URL (default: ``https://dashboard.deutero.ai/study-api``).
|
|
146
|
+
timeout: Request timeout in seconds (default: 120).
|
|
147
|
+
http_client: Optional pre-configured :class:`httpx.AsyncClient`.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
projects: AsyncProjects
|
|
151
|
+
studies: AsyncStudies
|
|
152
|
+
welcome: AsyncWelcome
|
|
153
|
+
screening: AsyncScreening
|
|
154
|
+
characteristics: AsyncCharacteristics
|
|
155
|
+
questions: AsyncQuestions
|
|
156
|
+
graph: AsyncGraph
|
|
157
|
+
recruitment: AsyncRecruitment
|
|
158
|
+
embed: AsyncEmbed
|
|
159
|
+
personas: AsyncPersonas
|
|
160
|
+
simulations: AsyncSimulations
|
|
161
|
+
interviews: AsyncInterviews
|
|
162
|
+
transcripts: AsyncTranscripts
|
|
163
|
+
analysis: AsyncAnalysis
|
|
164
|
+
webhooks: AsyncWebhooks
|
|
165
|
+
credits: AsyncCredits
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
api_key: Optional[str] = None,
|
|
171
|
+
base_url: Optional[str] = None,
|
|
172
|
+
timeout: Optional[float] = None,
|
|
173
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
self._http = AsyncHTTPClient(
|
|
176
|
+
api_key=_resolve_api_key(api_key),
|
|
177
|
+
base_url=base_url or _DEFAULT_BASE_URL,
|
|
178
|
+
timeout=timeout or _DEFAULT_TIMEOUT,
|
|
179
|
+
http_client=http_client,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
self.projects = AsyncProjects(self._http)
|
|
183
|
+
self.studies = AsyncStudies(self._http)
|
|
184
|
+
self.welcome = AsyncWelcome(self._http)
|
|
185
|
+
self.screening = AsyncScreening(self._http)
|
|
186
|
+
self.characteristics = AsyncCharacteristics(self._http)
|
|
187
|
+
self.questions = AsyncQuestions(self._http)
|
|
188
|
+
self.graph = AsyncGraph(self._http)
|
|
189
|
+
self.recruitment = AsyncRecruitment(self._http)
|
|
190
|
+
self.embed = AsyncEmbed(self._http)
|
|
191
|
+
self.personas = AsyncPersonas(self._http)
|
|
192
|
+
self.simulations = AsyncSimulations(self._http)
|
|
193
|
+
self.interviews = AsyncInterviews(self._http)
|
|
194
|
+
self.transcripts = AsyncTranscripts(self._http)
|
|
195
|
+
self.analysis = AsyncAnalysis(self._http)
|
|
196
|
+
self.webhooks = AsyncWebhooks(self._http)
|
|
197
|
+
self.credits = AsyncCredits(self._http)
|
|
198
|
+
|
|
199
|
+
async def health(self) -> Dict[str, Any]:
|
|
200
|
+
"""Liveness check. See :meth:`Deutero.health`."""
|
|
201
|
+
data: Dict[str, Any] = await self._http.get("/health")
|
|
202
|
+
return data
|
|
203
|
+
|
|
204
|
+
async def close(self) -> None:
|
|
205
|
+
"""Close the underlying HTTP connection pool."""
|
|
206
|
+
await self._http.close()
|
|
207
|
+
|
|
208
|
+
async def __aenter__(self) -> AsyncDeutero:
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
async def __aexit__(self, *args: object) -> None:
|
|
212
|
+
await self.close()
|
|
213
|
+
|
|
214
|
+
def __repr__(self) -> str:
|
|
215
|
+
return f"AsyncDeutero(base_url={self._http._base_url!r})"
|