promptmenu-api 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.
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Incognita Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: promptmenu-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the promptmenu API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Jesse Dhillon
|
|
8
|
+
Author-email: jesse@dhillon.com
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: httpx (>=0.25.0)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Promptmenu Python SDK."""
|
|
2
|
+
|
|
3
|
+
from promptmenu_api.client import (
|
|
4
|
+
AsyncPromptmenuClient,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
AuthorizationError,
|
|
7
|
+
Flight,
|
|
8
|
+
FlightSummary,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
PromptmenuClient,
|
|
11
|
+
PromptmenuError,
|
|
12
|
+
Survey,
|
|
13
|
+
SurveyDimension,
|
|
14
|
+
SurveySchema,
|
|
15
|
+
Template,
|
|
16
|
+
TokenResponse,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AsyncPromptmenuClient",
|
|
22
|
+
"AuthenticationError",
|
|
23
|
+
"AuthorizationError",
|
|
24
|
+
"Flight",
|
|
25
|
+
"FlightSummary",
|
|
26
|
+
"NotFoundError",
|
|
27
|
+
"PromptmenuClient",
|
|
28
|
+
"PromptmenuError",
|
|
29
|
+
"Survey",
|
|
30
|
+
"SurveyDimension",
|
|
31
|
+
"SurveySchema",
|
|
32
|
+
"Template",
|
|
33
|
+
"TokenResponse",
|
|
34
|
+
"ValidationError",
|
|
35
|
+
]
|
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
"""Promptmenu Python SDK — sync and async clients for the promptmenu API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import typing as t
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PromptmenuError(Exception):
|
|
13
|
+
"""Base exception for promptmenu SDK errors."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, status_code: int | None = None, detail: str | None = None) -> None:
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.detail = detail
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthenticationError(PromptmenuError):
|
|
22
|
+
"""Raised on 401 Unauthorized responses."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthorizationError(PromptmenuError):
|
|
26
|
+
"""Raised on 403 Forbidden responses."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NotFoundError(PromptmenuError):
|
|
30
|
+
"""Raised on 404 Not Found responses."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ValidationError(PromptmenuError):
|
|
34
|
+
"""Raised on 400/422 Bad Request responses."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Response dataclasses
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Template(object):
|
|
44
|
+
template_id: str
|
|
45
|
+
name: str
|
|
46
|
+
version: int
|
|
47
|
+
content: str
|
|
48
|
+
description: str | None
|
|
49
|
+
is_active: bool
|
|
50
|
+
create_time: str
|
|
51
|
+
update_time: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Flight(object):
|
|
56
|
+
flight_id: str
|
|
57
|
+
template_id: str
|
|
58
|
+
template_name: str | None
|
|
59
|
+
template_version: int | None
|
|
60
|
+
created_by: str
|
|
61
|
+
feature_flags: dict[str, t.Any]
|
|
62
|
+
context: dict[str, t.Any]
|
|
63
|
+
rendered_content: str
|
|
64
|
+
model_provider: str
|
|
65
|
+
model_name: str
|
|
66
|
+
model_config_data: dict[str, t.Any]
|
|
67
|
+
status: str
|
|
68
|
+
started_at: str
|
|
69
|
+
completed_at: str | None
|
|
70
|
+
labels: dict[str, t.Any]
|
|
71
|
+
outcome_metadata: dict[str, t.Any] | None
|
|
72
|
+
create_time: str
|
|
73
|
+
update_time: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class FlightSummary(object):
|
|
78
|
+
flight_id: str
|
|
79
|
+
template_id: str
|
|
80
|
+
template_name: str | None
|
|
81
|
+
template_version: int | None
|
|
82
|
+
created_by: str
|
|
83
|
+
feature_flags: dict[str, t.Any]
|
|
84
|
+
context: dict[str, t.Any]
|
|
85
|
+
model_provider: str
|
|
86
|
+
model_name: str
|
|
87
|
+
model_config_data: dict[str, t.Any]
|
|
88
|
+
status: str
|
|
89
|
+
started_at: str
|
|
90
|
+
completed_at: str | None
|
|
91
|
+
labels: dict[str, t.Any]
|
|
92
|
+
outcome_metadata: dict[str, t.Any] | None
|
|
93
|
+
create_time: str
|
|
94
|
+
update_time: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class SurveyDimension(object):
|
|
99
|
+
name: str
|
|
100
|
+
label: str
|
|
101
|
+
spec: dict[str, t.Any]
|
|
102
|
+
required: bool = True
|
|
103
|
+
help: str | None = None
|
|
104
|
+
tags: list[str] = field(default_factory=list)
|
|
105
|
+
reverse_scored: bool = False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class SurveySchema(object):
|
|
110
|
+
schema_id: str
|
|
111
|
+
name: str
|
|
112
|
+
version: int
|
|
113
|
+
dimensions: list[dict[str, t.Any]]
|
|
114
|
+
dimensions_hash: str
|
|
115
|
+
create_time: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class Survey(object):
|
|
120
|
+
survey_id: str
|
|
121
|
+
flight_id: str
|
|
122
|
+
schema_id: str | None
|
|
123
|
+
submitted_by: str
|
|
124
|
+
ratings: dict[str, t.Any]
|
|
125
|
+
notes: str | None
|
|
126
|
+
tags: list[str]
|
|
127
|
+
create_time: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class TokenResponse(object):
|
|
132
|
+
access_token: str
|
|
133
|
+
token_type: str
|
|
134
|
+
expires_in: int
|
|
135
|
+
scopes: list[str]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# Internal helpers
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
_ERROR_MAP: dict[int, type[PromptmenuError]] = {
|
|
143
|
+
400: ValidationError,
|
|
144
|
+
401: AuthenticationError,
|
|
145
|
+
403: AuthorizationError,
|
|
146
|
+
404: NotFoundError,
|
|
147
|
+
422: ValidationError,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
152
|
+
if response.is_success:
|
|
153
|
+
return
|
|
154
|
+
detail = None
|
|
155
|
+
try:
|
|
156
|
+
body = response.json()
|
|
157
|
+
detail = body.get("detail")
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
exc_cls = _ERROR_MAP.get(response.status_code, PromptmenuError)
|
|
161
|
+
raise exc_cls(
|
|
162
|
+
f"HTTP {response.status_code}: {detail or response.reason_phrase}",
|
|
163
|
+
status_code=response.status_code,
|
|
164
|
+
detail=detail,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _parse_template(data: dict[str, t.Any]) -> Template:
|
|
169
|
+
return Template(
|
|
170
|
+
template_id=data["template_id"],
|
|
171
|
+
name=data["name"],
|
|
172
|
+
version=data["version"],
|
|
173
|
+
content=data["content"],
|
|
174
|
+
description=data.get("description"),
|
|
175
|
+
is_active=data["is_active"],
|
|
176
|
+
create_time=data["create_time"],
|
|
177
|
+
update_time=data["update_time"],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _parse_flight(data: dict[str, t.Any]) -> Flight:
|
|
182
|
+
return Flight(
|
|
183
|
+
flight_id=data["flight_id"],
|
|
184
|
+
template_id=data["template_id"],
|
|
185
|
+
template_name=data.get("template_name"),
|
|
186
|
+
template_version=data.get("template_version"),
|
|
187
|
+
created_by=data["created_by"],
|
|
188
|
+
feature_flags=data["feature_flags"],
|
|
189
|
+
context=data["context"],
|
|
190
|
+
rendered_content=data["rendered_content"],
|
|
191
|
+
model_provider=data["model_provider"],
|
|
192
|
+
model_name=data["model_name"],
|
|
193
|
+
model_config_data=data["model_config_data"],
|
|
194
|
+
status=data["status"],
|
|
195
|
+
started_at=data["started_at"],
|
|
196
|
+
completed_at=data.get("completed_at"),
|
|
197
|
+
labels=data["labels"],
|
|
198
|
+
outcome_metadata=data.get("outcome_metadata"),
|
|
199
|
+
create_time=data["create_time"],
|
|
200
|
+
update_time=data["update_time"],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _parse_flight_summary(data: dict[str, t.Any]) -> FlightSummary:
|
|
205
|
+
return FlightSummary(
|
|
206
|
+
flight_id=data["flight_id"],
|
|
207
|
+
template_id=data["template_id"],
|
|
208
|
+
template_name=data.get("template_name"),
|
|
209
|
+
template_version=data.get("template_version"),
|
|
210
|
+
created_by=data["created_by"],
|
|
211
|
+
feature_flags=data["feature_flags"],
|
|
212
|
+
context=data["context"],
|
|
213
|
+
model_provider=data["model_provider"],
|
|
214
|
+
model_name=data["model_name"],
|
|
215
|
+
model_config_data=data["model_config_data"],
|
|
216
|
+
status=data["status"],
|
|
217
|
+
started_at=data["started_at"],
|
|
218
|
+
completed_at=data.get("completed_at"),
|
|
219
|
+
labels=data["labels"],
|
|
220
|
+
outcome_metadata=data.get("outcome_metadata"),
|
|
221
|
+
create_time=data["create_time"],
|
|
222
|
+
update_time=data["update_time"],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _parse_survey_schema(data: dict[str, t.Any]) -> SurveySchema:
|
|
227
|
+
return SurveySchema(
|
|
228
|
+
schema_id=data["schema_id"],
|
|
229
|
+
name=data["name"],
|
|
230
|
+
version=data["version"],
|
|
231
|
+
dimensions=data["dimensions"],
|
|
232
|
+
dimensions_hash=data["dimensions_hash"],
|
|
233
|
+
create_time=data["create_time"],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _parse_survey(data: dict[str, t.Any]) -> Survey:
|
|
238
|
+
return Survey(
|
|
239
|
+
survey_id=data["survey_id"],
|
|
240
|
+
flight_id=data["flight_id"],
|
|
241
|
+
schema_id=data.get("schema_id"),
|
|
242
|
+
submitted_by=data["submitted_by"],
|
|
243
|
+
ratings=data["ratings"],
|
|
244
|
+
notes=data.get("notes"),
|
|
245
|
+
tags=data.get("tags", []),
|
|
246
|
+
create_time=data["create_time"],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Token cache
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class _TokenCache(object):
|
|
256
|
+
"""Caches a JWT token and auto-refreshes before expiry."""
|
|
257
|
+
|
|
258
|
+
def __init__(self) -> None:
|
|
259
|
+
self._token: str | None = None
|
|
260
|
+
self._expires_at: float = 0.0
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def valid(self) -> bool:
|
|
264
|
+
return self._token is not None and time.monotonic() < self._expires_at
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def token(self) -> str | None:
|
|
268
|
+
if self.valid:
|
|
269
|
+
return self._token
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
def store(self, token_response: TokenResponse) -> None:
|
|
273
|
+
self._token = token_response.access_token
|
|
274
|
+
# Refresh 60s before actual expiry to avoid edge cases
|
|
275
|
+
self._expires_at = time.monotonic() + max(token_response.expires_in - 60, 0)
|
|
276
|
+
|
|
277
|
+
def clear(self) -> None:
|
|
278
|
+
self._token = None
|
|
279
|
+
self._expires_at = 0.0
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Synchronous client
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class PromptmenuClient(object):
|
|
288
|
+
"""Synchronous client for the promptmenu API.
|
|
289
|
+
|
|
290
|
+
Usage::
|
|
291
|
+
|
|
292
|
+
client = PromptmenuClient("https://promptmenu.ai", api_key="your-api-key")
|
|
293
|
+
|
|
294
|
+
# Server-side operations (API key auth)
|
|
295
|
+
template = client.create_template(name="greeting", content="Hello {{ name }}!")
|
|
296
|
+
flight = client.create_flight(
|
|
297
|
+
template="greeting",
|
|
298
|
+
created_by="my-app",
|
|
299
|
+
context={"name": "World"},
|
|
300
|
+
model_provider="anthropic",
|
|
301
|
+
model_name="claude-sonnet-4-5-20250929",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Browser-facing operations (auto-obtains JWT)
|
|
305
|
+
schemas = client.list_survey_schemas()
|
|
306
|
+
client.create_survey(
|
|
307
|
+
flight_id=flight.flight_id,
|
|
308
|
+
submitted_by="user@example.com",
|
|
309
|
+
ratings={"quality": 5},
|
|
310
|
+
)
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
def __init__(
|
|
314
|
+
self,
|
|
315
|
+
base_url: str,
|
|
316
|
+
*,
|
|
317
|
+
api_key: str,
|
|
318
|
+
timeout: float = 30.0,
|
|
319
|
+
) -> None:
|
|
320
|
+
self._base_url = str(base_url).rstrip("/")
|
|
321
|
+
self._api_key = api_key
|
|
322
|
+
self._timeout = timeout
|
|
323
|
+
self._token_cache = _TokenCache()
|
|
324
|
+
|
|
325
|
+
def _api_key_headers(self) -> dict[str, str]:
|
|
326
|
+
return {"Authorization": f"Bearer {self._api_key}"}
|
|
327
|
+
|
|
328
|
+
def _jwt_headers(self) -> dict[str, str]:
|
|
329
|
+
token = self._token_cache.token
|
|
330
|
+
if token is None:
|
|
331
|
+
resp = self.create_token()
|
|
332
|
+
self._token_cache.store(resp)
|
|
333
|
+
token = resp.access_token
|
|
334
|
+
return {"Authorization": f"Bearer {token}"}
|
|
335
|
+
|
|
336
|
+
# -- Auth ---------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
def create_token(self, *, scopes: list[str] | None = None) -> TokenResponse:
|
|
339
|
+
"""Issue a scoped JWT for browser-facing requests."""
|
|
340
|
+
payload: dict[str, t.Any] = {}
|
|
341
|
+
if scopes is not None:
|
|
342
|
+
payload["scopes"] = scopes
|
|
343
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
344
|
+
resp = http.post("/auth/token", json=payload, headers=self._api_key_headers())
|
|
345
|
+
_raise_for_status(resp)
|
|
346
|
+
data = resp.json()
|
|
347
|
+
return TokenResponse(
|
|
348
|
+
access_token=data["access_token"],
|
|
349
|
+
token_type=data["token_type"],
|
|
350
|
+
expires_in=data["expires_in"],
|
|
351
|
+
scopes=data["scopes"],
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# -- Templates ----------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
def create_template(
|
|
357
|
+
self,
|
|
358
|
+
*,
|
|
359
|
+
name: str,
|
|
360
|
+
content: str,
|
|
361
|
+
description: str | None = None,
|
|
362
|
+
) -> Template:
|
|
363
|
+
"""Create a prompt template."""
|
|
364
|
+
payload: dict[str, t.Any] = {"name": name, "content": content}
|
|
365
|
+
if description is not None:
|
|
366
|
+
payload["description"] = description
|
|
367
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
368
|
+
resp = http.post("/templates", json=payload, headers=self._api_key_headers())
|
|
369
|
+
_raise_for_status(resp)
|
|
370
|
+
return _parse_template(resp.json())
|
|
371
|
+
|
|
372
|
+
def get_template(self, template_id: str) -> Template:
|
|
373
|
+
"""Get a template by ID."""
|
|
374
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
375
|
+
resp = http.get(f"/templates/{template_id}", headers=self._api_key_headers())
|
|
376
|
+
_raise_for_status(resp)
|
|
377
|
+
return _parse_template(resp.json())
|
|
378
|
+
|
|
379
|
+
def list_templates(
|
|
380
|
+
self,
|
|
381
|
+
*,
|
|
382
|
+
name: str | None = None,
|
|
383
|
+
is_active: bool | None = None,
|
|
384
|
+
) -> list[Template]:
|
|
385
|
+
"""List templates, optionally filtered."""
|
|
386
|
+
params: dict[str, t.Any] = {}
|
|
387
|
+
if name is not None:
|
|
388
|
+
params["name"] = name
|
|
389
|
+
if is_active is not None:
|
|
390
|
+
params["is_active"] = is_active
|
|
391
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
392
|
+
resp = http.get("/templates", params=params, headers=self._api_key_headers())
|
|
393
|
+
_raise_for_status(resp)
|
|
394
|
+
return [_parse_template(t_) for t_ in resp.json()["templates"]]
|
|
395
|
+
|
|
396
|
+
def update_template(
|
|
397
|
+
self,
|
|
398
|
+
template_id: str,
|
|
399
|
+
*,
|
|
400
|
+
description: str | None = None,
|
|
401
|
+
is_active: bool | None = None,
|
|
402
|
+
) -> Template:
|
|
403
|
+
"""Update a template's metadata."""
|
|
404
|
+
params: dict[str, t.Any] = {}
|
|
405
|
+
if description is not None:
|
|
406
|
+
params["description"] = description
|
|
407
|
+
if is_active is not None:
|
|
408
|
+
params["is_active"] = is_active
|
|
409
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
410
|
+
resp = http.patch(f"/templates/{template_id}", params=params, headers=self._api_key_headers())
|
|
411
|
+
_raise_for_status(resp)
|
|
412
|
+
return _parse_template(resp.json())
|
|
413
|
+
|
|
414
|
+
# -- Flights ------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
def create_flight(
|
|
417
|
+
self,
|
|
418
|
+
*,
|
|
419
|
+
template: str,
|
|
420
|
+
created_by: str,
|
|
421
|
+
model_provider: str,
|
|
422
|
+
model_name: str,
|
|
423
|
+
template_bundle: dict[str, str] | None = None,
|
|
424
|
+
feature_flags: dict[str, t.Any] | None = None,
|
|
425
|
+
context: dict[str, t.Any] | None = None,
|
|
426
|
+
model_config_data: dict[str, t.Any] | None = None,
|
|
427
|
+
labels: dict[str, t.Any] | None = None,
|
|
428
|
+
) -> Flight:
|
|
429
|
+
"""Create a flight by rendering a template."""
|
|
430
|
+
payload: dict[str, t.Any] = {
|
|
431
|
+
"template": template,
|
|
432
|
+
"created_by": created_by,
|
|
433
|
+
"model_provider": model_provider,
|
|
434
|
+
"model_name": model_name,
|
|
435
|
+
}
|
|
436
|
+
if template_bundle is not None:
|
|
437
|
+
payload["template_bundle"] = template_bundle
|
|
438
|
+
if feature_flags is not None:
|
|
439
|
+
payload["feature_flags"] = feature_flags
|
|
440
|
+
if context is not None:
|
|
441
|
+
payload["context"] = context
|
|
442
|
+
if model_config_data is not None:
|
|
443
|
+
payload["model_config_data"] = model_config_data
|
|
444
|
+
if labels is not None:
|
|
445
|
+
payload["labels"] = labels
|
|
446
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
447
|
+
resp = http.post("/flights", json=payload, headers=self._api_key_headers())
|
|
448
|
+
_raise_for_status(resp)
|
|
449
|
+
return _parse_flight(resp.json())
|
|
450
|
+
|
|
451
|
+
def get_flight(self, flight_id: str) -> Flight:
|
|
452
|
+
"""Get a flight by ID."""
|
|
453
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
454
|
+
resp = http.get(f"/flights/{flight_id}", headers=self._api_key_headers())
|
|
455
|
+
_raise_for_status(resp)
|
|
456
|
+
return _parse_flight(resp.json())
|
|
457
|
+
|
|
458
|
+
def list_flights(
|
|
459
|
+
self,
|
|
460
|
+
*,
|
|
461
|
+
template_id: str | None = None,
|
|
462
|
+
status: str | None = None,
|
|
463
|
+
created_by: str | None = None,
|
|
464
|
+
limit: int | None = None,
|
|
465
|
+
) -> list[FlightSummary]:
|
|
466
|
+
"""List flights, optionally filtered."""
|
|
467
|
+
params: dict[str, t.Any] = {}
|
|
468
|
+
if template_id is not None:
|
|
469
|
+
params["template_id"] = template_id
|
|
470
|
+
if status is not None:
|
|
471
|
+
params["status_filter"] = status
|
|
472
|
+
if created_by is not None:
|
|
473
|
+
params["created_by"] = created_by
|
|
474
|
+
if limit is not None:
|
|
475
|
+
params["limit"] = limit
|
|
476
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
477
|
+
resp = http.get("/flights", params=params, headers=self._api_key_headers())
|
|
478
|
+
_raise_for_status(resp)
|
|
479
|
+
return [_parse_flight_summary(f) for f in resp.json()["flights"]]
|
|
480
|
+
|
|
481
|
+
def update_flight(
|
|
482
|
+
self,
|
|
483
|
+
flight_id: str,
|
|
484
|
+
*,
|
|
485
|
+
status: str | None = None,
|
|
486
|
+
outcome_metadata: dict[str, t.Any] | None = None,
|
|
487
|
+
) -> Flight:
|
|
488
|
+
"""Update a flight's status and/or outcome metadata."""
|
|
489
|
+
payload: dict[str, t.Any] = {}
|
|
490
|
+
if status is not None:
|
|
491
|
+
payload["status"] = status
|
|
492
|
+
if outcome_metadata is not None:
|
|
493
|
+
payload["outcome_metadata"] = outcome_metadata
|
|
494
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
495
|
+
resp = http.patch(f"/flights/{flight_id}", json=payload, headers=self._api_key_headers())
|
|
496
|
+
_raise_for_status(resp)
|
|
497
|
+
return _parse_flight(resp.json())
|
|
498
|
+
|
|
499
|
+
def complete_flight(
|
|
500
|
+
self,
|
|
501
|
+
flight_id: str,
|
|
502
|
+
*,
|
|
503
|
+
outcome_metadata: dict[str, t.Any] | None = None,
|
|
504
|
+
) -> Flight:
|
|
505
|
+
"""Mark a flight as completed."""
|
|
506
|
+
params: dict[str, t.Any] = {}
|
|
507
|
+
if outcome_metadata is not None:
|
|
508
|
+
params["outcome_metadata"] = outcome_metadata
|
|
509
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
510
|
+
resp = http.post(f"/flights/{flight_id}/complete", params=params, headers=self._api_key_headers())
|
|
511
|
+
_raise_for_status(resp)
|
|
512
|
+
return _parse_flight(resp.json())
|
|
513
|
+
|
|
514
|
+
# -- Survey schemas -----------------------------------------------------
|
|
515
|
+
|
|
516
|
+
def create_survey_schema(
|
|
517
|
+
self,
|
|
518
|
+
*,
|
|
519
|
+
name: str,
|
|
520
|
+
dimensions: list[SurveyDimension | dict[str, t.Any]],
|
|
521
|
+
) -> SurveySchema:
|
|
522
|
+
"""Create a survey schema."""
|
|
523
|
+
dims = []
|
|
524
|
+
for d in dimensions:
|
|
525
|
+
if isinstance(d, SurveyDimension):
|
|
526
|
+
dim: dict[str, t.Any] = {
|
|
527
|
+
"name": d.name,
|
|
528
|
+
"label": d.label,
|
|
529
|
+
"spec": d.spec,
|
|
530
|
+
"required": d.required,
|
|
531
|
+
"tags": d.tags,
|
|
532
|
+
"reverse_scored": d.reverse_scored,
|
|
533
|
+
}
|
|
534
|
+
if d.help is not None:
|
|
535
|
+
dim["help"] = d.help
|
|
536
|
+
dims.append(dim)
|
|
537
|
+
else:
|
|
538
|
+
dims.append(d)
|
|
539
|
+
payload: dict[str, t.Any] = {"name": name, "dimensions": dims}
|
|
540
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
541
|
+
resp = http.post("/survey-schemas", json=payload, headers=self._api_key_headers())
|
|
542
|
+
_raise_for_status(resp)
|
|
543
|
+
return _parse_survey_schema(resp.json())
|
|
544
|
+
|
|
545
|
+
def get_survey_schema(self, schema_id: str) -> SurveySchema:
|
|
546
|
+
"""Get a survey schema by ID (requires JWT)."""
|
|
547
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
548
|
+
resp = http.get(f"/survey-schemas/{schema_id}", headers=self._jwt_headers())
|
|
549
|
+
_raise_for_status(resp)
|
|
550
|
+
return _parse_survey_schema(resp.json())
|
|
551
|
+
|
|
552
|
+
def get_survey_schema_by_name(self, name: str, *, version: int | None = None) -> SurveySchema:
|
|
553
|
+
"""Get a survey schema by name (requires JWT)."""
|
|
554
|
+
params: dict[str, t.Any] = {}
|
|
555
|
+
if version is not None:
|
|
556
|
+
params["version"] = version
|
|
557
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
558
|
+
resp = http.get(f"/survey-schemas/by-name/{name}", params=params, headers=self._jwt_headers())
|
|
559
|
+
_raise_for_status(resp)
|
|
560
|
+
return _parse_survey_schema(resp.json())
|
|
561
|
+
|
|
562
|
+
def list_survey_schemas(self) -> list[SurveySchema]:
|
|
563
|
+
"""List all survey schemas (requires JWT)."""
|
|
564
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
565
|
+
resp = http.get("/survey-schemas", headers=self._jwt_headers())
|
|
566
|
+
_raise_for_status(resp)
|
|
567
|
+
return [_parse_survey_schema(s) for s in resp.json()["schemas"]]
|
|
568
|
+
|
|
569
|
+
# -- Surveys ------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
def create_survey(
|
|
572
|
+
self,
|
|
573
|
+
flight_id: str,
|
|
574
|
+
*,
|
|
575
|
+
submitted_by: str,
|
|
576
|
+
ratings: dict[str, t.Any],
|
|
577
|
+
schema_id: str | None = None,
|
|
578
|
+
notes: str | None = None,
|
|
579
|
+
tags: list[str] | None = None,
|
|
580
|
+
) -> Survey:
|
|
581
|
+
"""Submit a survey for a flight (requires JWT)."""
|
|
582
|
+
payload: dict[str, t.Any] = {
|
|
583
|
+
"submitted_by": submitted_by,
|
|
584
|
+
"ratings": ratings,
|
|
585
|
+
}
|
|
586
|
+
if schema_id is not None:
|
|
587
|
+
payload["schema_id"] = schema_id
|
|
588
|
+
if notes is not None:
|
|
589
|
+
payload["notes"] = notes
|
|
590
|
+
if tags is not None:
|
|
591
|
+
payload["tags"] = tags
|
|
592
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
593
|
+
resp = http.post(f"/flights/{flight_id}/surveys", json=payload, headers=self._jwt_headers())
|
|
594
|
+
_raise_for_status(resp)
|
|
595
|
+
return _parse_survey(resp.json())
|
|
596
|
+
|
|
597
|
+
def list_surveys(self, flight_id: str) -> list[Survey]:
|
|
598
|
+
"""List surveys for a flight."""
|
|
599
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
600
|
+
resp = http.get(f"/flights/{flight_id}/surveys", headers=self._api_key_headers())
|
|
601
|
+
_raise_for_status(resp)
|
|
602
|
+
return [_parse_survey(s) for s in resp.json()["surveys"]]
|
|
603
|
+
|
|
604
|
+
def get_survey(self, survey_id: str) -> Survey:
|
|
605
|
+
"""Get a survey by ID."""
|
|
606
|
+
with httpx.Client(base_url=self._base_url, timeout=self._timeout) as http:
|
|
607
|
+
resp = http.get(f"/surveys/{survey_id}", headers=self._api_key_headers())
|
|
608
|
+
_raise_for_status(resp)
|
|
609
|
+
return _parse_survey(resp.json())
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# ---------------------------------------------------------------------------
|
|
613
|
+
# Async client
|
|
614
|
+
# ---------------------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class AsyncPromptmenuClient(object):
|
|
618
|
+
"""Async client for the promptmenu API.
|
|
619
|
+
|
|
620
|
+
Usage::
|
|
621
|
+
|
|
622
|
+
async with AsyncPromptmenuClient("https://promptmenu.ai", api_key="key") as client:
|
|
623
|
+
flight = await client.create_flight(...)
|
|
624
|
+
|
|
625
|
+
Can also be used without the context manager::
|
|
626
|
+
|
|
627
|
+
client = AsyncPromptmenuClient("https://promptmenu.ai", api_key="key")
|
|
628
|
+
flight = await client.create_flight(...)
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
def __init__(
|
|
632
|
+
self,
|
|
633
|
+
base_url: str,
|
|
634
|
+
*,
|
|
635
|
+
api_key: str,
|
|
636
|
+
timeout: float = 30.0,
|
|
637
|
+
) -> None:
|
|
638
|
+
self._base_url = str(base_url).rstrip("/")
|
|
639
|
+
self._api_key = api_key
|
|
640
|
+
self._timeout = timeout
|
|
641
|
+
self._token_cache = _TokenCache()
|
|
642
|
+
|
|
643
|
+
async def __aenter__(self) -> AsyncPromptmenuClient:
|
|
644
|
+
return self
|
|
645
|
+
|
|
646
|
+
async def __aexit__(self, *args: t.Any) -> None:
|
|
647
|
+
pass
|
|
648
|
+
|
|
649
|
+
def _api_key_headers(self) -> dict[str, str]:
|
|
650
|
+
return {"Authorization": f"Bearer {self._api_key}"}
|
|
651
|
+
|
|
652
|
+
async def _jwt_headers(self) -> dict[str, str]:
|
|
653
|
+
token = self._token_cache.token
|
|
654
|
+
if token is None:
|
|
655
|
+
resp = await self.create_token()
|
|
656
|
+
self._token_cache.store(resp)
|
|
657
|
+
token = resp.access_token
|
|
658
|
+
return {"Authorization": f"Bearer {token}"}
|
|
659
|
+
|
|
660
|
+
# -- Auth ---------------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
async def create_token(self, *, scopes: list[str] | None = None) -> TokenResponse:
|
|
663
|
+
"""Issue a scoped JWT for browser-facing requests."""
|
|
664
|
+
payload: dict[str, t.Any] = {}
|
|
665
|
+
if scopes is not None:
|
|
666
|
+
payload["scopes"] = scopes
|
|
667
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
668
|
+
resp = await http.post("/auth/token", json=payload, headers=self._api_key_headers())
|
|
669
|
+
_raise_for_status(resp)
|
|
670
|
+
data = resp.json()
|
|
671
|
+
return TokenResponse(
|
|
672
|
+
access_token=data["access_token"],
|
|
673
|
+
token_type=data["token_type"],
|
|
674
|
+
expires_in=data["expires_in"],
|
|
675
|
+
scopes=data["scopes"],
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# -- Templates ----------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
async def create_template(
|
|
681
|
+
self,
|
|
682
|
+
*,
|
|
683
|
+
name: str,
|
|
684
|
+
content: str,
|
|
685
|
+
description: str | None = None,
|
|
686
|
+
) -> Template:
|
|
687
|
+
"""Create a prompt template."""
|
|
688
|
+
payload: dict[str, t.Any] = {"name": name, "content": content}
|
|
689
|
+
if description is not None:
|
|
690
|
+
payload["description"] = description
|
|
691
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
692
|
+
resp = await http.post("/templates", json=payload, headers=self._api_key_headers())
|
|
693
|
+
_raise_for_status(resp)
|
|
694
|
+
return _parse_template(resp.json())
|
|
695
|
+
|
|
696
|
+
async def get_template(self, template_id: str) -> Template:
|
|
697
|
+
"""Get a template by ID."""
|
|
698
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
699
|
+
resp = await http.get(f"/templates/{template_id}", headers=self._api_key_headers())
|
|
700
|
+
_raise_for_status(resp)
|
|
701
|
+
return _parse_template(resp.json())
|
|
702
|
+
|
|
703
|
+
async def list_templates(
|
|
704
|
+
self,
|
|
705
|
+
*,
|
|
706
|
+
name: str | None = None,
|
|
707
|
+
is_active: bool | None = None,
|
|
708
|
+
) -> list[Template]:
|
|
709
|
+
"""List templates, optionally filtered."""
|
|
710
|
+
params: dict[str, t.Any] = {}
|
|
711
|
+
if name is not None:
|
|
712
|
+
params["name"] = name
|
|
713
|
+
if is_active is not None:
|
|
714
|
+
params["is_active"] = is_active
|
|
715
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
716
|
+
resp = await http.get("/templates", params=params, headers=self._api_key_headers())
|
|
717
|
+
_raise_for_status(resp)
|
|
718
|
+
return [_parse_template(t_) for t_ in resp.json()["templates"]]
|
|
719
|
+
|
|
720
|
+
async def update_template(
|
|
721
|
+
self,
|
|
722
|
+
template_id: str,
|
|
723
|
+
*,
|
|
724
|
+
description: str | None = None,
|
|
725
|
+
is_active: bool | None = None,
|
|
726
|
+
) -> Template:
|
|
727
|
+
"""Update a template's metadata."""
|
|
728
|
+
params: dict[str, t.Any] = {}
|
|
729
|
+
if description is not None:
|
|
730
|
+
params["description"] = description
|
|
731
|
+
if is_active is not None:
|
|
732
|
+
params["is_active"] = is_active
|
|
733
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
734
|
+
resp = await http.patch(f"/templates/{template_id}", params=params, headers=self._api_key_headers())
|
|
735
|
+
_raise_for_status(resp)
|
|
736
|
+
return _parse_template(resp.json())
|
|
737
|
+
|
|
738
|
+
# -- Flights ------------------------------------------------------------
|
|
739
|
+
|
|
740
|
+
async def create_flight(
|
|
741
|
+
self,
|
|
742
|
+
*,
|
|
743
|
+
template: str,
|
|
744
|
+
created_by: str,
|
|
745
|
+
model_provider: str,
|
|
746
|
+
model_name: str,
|
|
747
|
+
template_bundle: dict[str, str] | None = None,
|
|
748
|
+
feature_flags: dict[str, t.Any] | None = None,
|
|
749
|
+
context: dict[str, t.Any] | None = None,
|
|
750
|
+
model_config_data: dict[str, t.Any] | None = None,
|
|
751
|
+
labels: dict[str, t.Any] | None = None,
|
|
752
|
+
) -> Flight:
|
|
753
|
+
"""Create a flight by rendering a template."""
|
|
754
|
+
payload: dict[str, t.Any] = {
|
|
755
|
+
"template": template,
|
|
756
|
+
"created_by": created_by,
|
|
757
|
+
"model_provider": model_provider,
|
|
758
|
+
"model_name": model_name,
|
|
759
|
+
}
|
|
760
|
+
if template_bundle is not None:
|
|
761
|
+
payload["template_bundle"] = template_bundle
|
|
762
|
+
if feature_flags is not None:
|
|
763
|
+
payload["feature_flags"] = feature_flags
|
|
764
|
+
if context is not None:
|
|
765
|
+
payload["context"] = context
|
|
766
|
+
if model_config_data is not None:
|
|
767
|
+
payload["model_config_data"] = model_config_data
|
|
768
|
+
if labels is not None:
|
|
769
|
+
payload["labels"] = labels
|
|
770
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
771
|
+
resp = await http.post("/flights", json=payload, headers=self._api_key_headers())
|
|
772
|
+
_raise_for_status(resp)
|
|
773
|
+
return _parse_flight(resp.json())
|
|
774
|
+
|
|
775
|
+
async def get_flight(self, flight_id: str) -> Flight:
|
|
776
|
+
"""Get a flight by ID."""
|
|
777
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
778
|
+
resp = await http.get(f"/flights/{flight_id}", headers=self._api_key_headers())
|
|
779
|
+
_raise_for_status(resp)
|
|
780
|
+
return _parse_flight(resp.json())
|
|
781
|
+
|
|
782
|
+
async def list_flights(
|
|
783
|
+
self,
|
|
784
|
+
*,
|
|
785
|
+
template_id: str | None = None,
|
|
786
|
+
status: str | None = None,
|
|
787
|
+
created_by: str | None = None,
|
|
788
|
+
limit: int | None = None,
|
|
789
|
+
) -> list[FlightSummary]:
|
|
790
|
+
"""List flights, optionally filtered."""
|
|
791
|
+
params: dict[str, t.Any] = {}
|
|
792
|
+
if template_id is not None:
|
|
793
|
+
params["template_id"] = template_id
|
|
794
|
+
if status is not None:
|
|
795
|
+
params["status_filter"] = status
|
|
796
|
+
if created_by is not None:
|
|
797
|
+
params["created_by"] = created_by
|
|
798
|
+
if limit is not None:
|
|
799
|
+
params["limit"] = limit
|
|
800
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
801
|
+
resp = await http.get("/flights", params=params, headers=self._api_key_headers())
|
|
802
|
+
_raise_for_status(resp)
|
|
803
|
+
return [_parse_flight_summary(f) for f in resp.json()["flights"]]
|
|
804
|
+
|
|
805
|
+
async def update_flight(
|
|
806
|
+
self,
|
|
807
|
+
flight_id: str,
|
|
808
|
+
*,
|
|
809
|
+
status: str | None = None,
|
|
810
|
+
outcome_metadata: dict[str, t.Any] | None = None,
|
|
811
|
+
) -> Flight:
|
|
812
|
+
"""Update a flight's status and/or outcome metadata."""
|
|
813
|
+
payload: dict[str, t.Any] = {}
|
|
814
|
+
if status is not None:
|
|
815
|
+
payload["status"] = status
|
|
816
|
+
if outcome_metadata is not None:
|
|
817
|
+
payload["outcome_metadata"] = outcome_metadata
|
|
818
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
819
|
+
resp = await http.patch(f"/flights/{flight_id}", json=payload, headers=self._api_key_headers())
|
|
820
|
+
_raise_for_status(resp)
|
|
821
|
+
return _parse_flight(resp.json())
|
|
822
|
+
|
|
823
|
+
async def complete_flight(
|
|
824
|
+
self,
|
|
825
|
+
flight_id: str,
|
|
826
|
+
*,
|
|
827
|
+
outcome_metadata: dict[str, t.Any] | None = None,
|
|
828
|
+
) -> Flight:
|
|
829
|
+
"""Mark a flight as completed."""
|
|
830
|
+
params: dict[str, t.Any] = {}
|
|
831
|
+
if outcome_metadata is not None:
|
|
832
|
+
params["outcome_metadata"] = outcome_metadata
|
|
833
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
834
|
+
resp = await http.post(f"/flights/{flight_id}/complete", params=params, headers=self._api_key_headers())
|
|
835
|
+
_raise_for_status(resp)
|
|
836
|
+
return _parse_flight(resp.json())
|
|
837
|
+
|
|
838
|
+
# -- Survey schemas -----------------------------------------------------
|
|
839
|
+
|
|
840
|
+
async def create_survey_schema(
|
|
841
|
+
self,
|
|
842
|
+
*,
|
|
843
|
+
name: str,
|
|
844
|
+
dimensions: list[SurveyDimension | dict[str, t.Any]],
|
|
845
|
+
) -> SurveySchema:
|
|
846
|
+
"""Create a survey schema."""
|
|
847
|
+
dims = []
|
|
848
|
+
for d in dimensions:
|
|
849
|
+
if isinstance(d, SurveyDimension):
|
|
850
|
+
dim: dict[str, t.Any] = {
|
|
851
|
+
"name": d.name,
|
|
852
|
+
"label": d.label,
|
|
853
|
+
"spec": d.spec,
|
|
854
|
+
"required": d.required,
|
|
855
|
+
"tags": d.tags,
|
|
856
|
+
"reverse_scored": d.reverse_scored,
|
|
857
|
+
}
|
|
858
|
+
if d.help is not None:
|
|
859
|
+
dim["help"] = d.help
|
|
860
|
+
dims.append(dim)
|
|
861
|
+
else:
|
|
862
|
+
dims.append(d)
|
|
863
|
+
payload: dict[str, t.Any] = {"name": name, "dimensions": dims}
|
|
864
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
865
|
+
resp = await http.post("/survey-schemas", json=payload, headers=self._api_key_headers())
|
|
866
|
+
_raise_for_status(resp)
|
|
867
|
+
return _parse_survey_schema(resp.json())
|
|
868
|
+
|
|
869
|
+
async def get_survey_schema(self, schema_id: str) -> SurveySchema:
|
|
870
|
+
"""Get a survey schema by ID (requires JWT)."""
|
|
871
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
872
|
+
resp = await http.get(f"/survey-schemas/{schema_id}", headers=await self._jwt_headers())
|
|
873
|
+
_raise_for_status(resp)
|
|
874
|
+
return _parse_survey_schema(resp.json())
|
|
875
|
+
|
|
876
|
+
async def get_survey_schema_by_name(self, name: str, *, version: int | None = None) -> SurveySchema:
|
|
877
|
+
"""Get a survey schema by name (requires JWT)."""
|
|
878
|
+
params: dict[str, t.Any] = {}
|
|
879
|
+
if version is not None:
|
|
880
|
+
params["version"] = version
|
|
881
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
882
|
+
resp = await http.get(f"/survey-schemas/by-name/{name}", params=params, headers=await self._jwt_headers())
|
|
883
|
+
_raise_for_status(resp)
|
|
884
|
+
return _parse_survey_schema(resp.json())
|
|
885
|
+
|
|
886
|
+
async def list_survey_schemas(self) -> list[SurveySchema]:
|
|
887
|
+
"""List all survey schemas (requires JWT)."""
|
|
888
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
889
|
+
resp = await http.get("/survey-schemas", headers=await self._jwt_headers())
|
|
890
|
+
_raise_for_status(resp)
|
|
891
|
+
return [_parse_survey_schema(s) for s in resp.json()["schemas"]]
|
|
892
|
+
|
|
893
|
+
# -- Surveys ------------------------------------------------------------
|
|
894
|
+
|
|
895
|
+
async def create_survey(
|
|
896
|
+
self,
|
|
897
|
+
flight_id: str,
|
|
898
|
+
*,
|
|
899
|
+
submitted_by: str,
|
|
900
|
+
ratings: dict[str, t.Any],
|
|
901
|
+
schema_id: str | None = None,
|
|
902
|
+
notes: str | None = None,
|
|
903
|
+
tags: list[str] | None = None,
|
|
904
|
+
) -> Survey:
|
|
905
|
+
"""Submit a survey for a flight (requires JWT)."""
|
|
906
|
+
payload: dict[str, t.Any] = {
|
|
907
|
+
"submitted_by": submitted_by,
|
|
908
|
+
"ratings": ratings,
|
|
909
|
+
}
|
|
910
|
+
if schema_id is not None:
|
|
911
|
+
payload["schema_id"] = schema_id
|
|
912
|
+
if notes is not None:
|
|
913
|
+
payload["notes"] = notes
|
|
914
|
+
if tags is not None:
|
|
915
|
+
payload["tags"] = tags
|
|
916
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
917
|
+
resp = await http.post(f"/flights/{flight_id}/surveys", json=payload, headers=await self._jwt_headers())
|
|
918
|
+
_raise_for_status(resp)
|
|
919
|
+
return _parse_survey(resp.json())
|
|
920
|
+
|
|
921
|
+
async def list_surveys(self, flight_id: str) -> list[Survey]:
|
|
922
|
+
"""List surveys for a flight."""
|
|
923
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
924
|
+
resp = await http.get(f"/flights/{flight_id}/surveys", headers=self._api_key_headers())
|
|
925
|
+
_raise_for_status(resp)
|
|
926
|
+
return [_parse_survey(s) for s in resp.json()["surveys"]]
|
|
927
|
+
|
|
928
|
+
async def get_survey(self, survey_id: str) -> Survey:
|
|
929
|
+
"""Get a survey by ID."""
|
|
930
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as http:
|
|
931
|
+
resp = await http.get(f"/surveys/{survey_id}", headers=self._api_key_headers())
|
|
932
|
+
_raise_for_status(resp)
|
|
933
|
+
return _parse_survey(resp.json())
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
3
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "promptmenu-api"
|
|
7
|
+
description = "Python SDK for the promptmenu API"
|
|
8
|
+
dynamic = [ "version" ]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Jesse Dhillon", email = "jesse@dhillon.com" },
|
|
12
|
+
]
|
|
13
|
+
license = "MIT"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"httpx>=0.25.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
test = [
|
|
20
|
+
"anyio[trio]~=4.0",
|
|
21
|
+
"pytest~=8.3.0",
|
|
22
|
+
"pytest-anyio~=0.0.0",
|
|
23
|
+
]
|
|
24
|
+
dev = [
|
|
25
|
+
"isort~=6.0.0",
|
|
26
|
+
"pyright~=1.1.0",
|
|
27
|
+
"ruff~=0.11.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
32
|
+
python_files = ["test_*.py"]
|
|
33
|
+
addopts = "--strict-markers -svx"
|
|
34
|
+
|
|
35
|
+
[tool.pyright]
|
|
36
|
+
exclude = [
|
|
37
|
+
"**/.venv/**",
|
|
38
|
+
]
|
|
39
|
+
venvPath = "."
|
|
40
|
+
venv = ".venv"
|
|
41
|
+
typeCheckingMode = "strict"
|
|
42
|
+
|
|
43
|
+
reportUnusedImport = false # let ruff catch this
|
|
44
|
+
reportUnusedVariable = false # let ruff catch this
|
|
45
|
+
|
|
46
|
+
reportUnknownParameterType = true
|
|
47
|
+
reportUnknownArgumentType = true
|
|
48
|
+
reportUnknownLambdaType = true
|
|
49
|
+
reportUnknownVariableType = true
|
|
50
|
+
reportUnknownMemberType = false
|
|
51
|
+
reportMissingTypeStubs = false
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
exclude = [
|
|
55
|
+
".venv",
|
|
56
|
+
]
|
|
57
|
+
line-length = 120
|
|
58
|
+
lint.select = [ "E", "F", "B", "T" ]
|
|
59
|
+
lint.extend-ignore = [
|
|
60
|
+
"I",
|
|
61
|
+
]
|
|
62
|
+
preview = true
|
|
63
|
+
target-version = "py310"
|
|
64
|
+
|
|
65
|
+
[tool.black]
|
|
66
|
+
line-length = 120
|
|
67
|
+
target-version = ["py310"]
|
|
68
|
+
skip-string-normalization = true
|
|
69
|
+
|
|
70
|
+
[tool.isort]
|
|
71
|
+
line_length = 120
|
|
72
|
+
multi_line_output = 2 # HANGING_INDENT
|
|
73
|
+
|
|
74
|
+
[tool.poetry]
|
|
75
|
+
name = "promptmenu"
|
|
76
|
+
version = "0.1.0" # placeholder, read from VERSION.txt by poetry-dynamic-versioning
|
|
77
|
+
packages = [{ include = "promptmenu_api" }]
|
|
78
|
+
|
|
79
|
+
[tool.poetry-dynamic-versioning]
|
|
80
|
+
enable = false
|
|
81
|
+
|
|
82
|
+
[tool.poetry-dynamic-versioning.from-file]
|
|
83
|
+
source = "VERSION.txt"
|