promtexpress 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.
- promtexpress/__init__.py +29 -0
- promtexpress/_client.py +141 -0
- promtexpress/_errors.py +97 -0
- promtexpress/_types.py +81 -0
- promtexpress/py.typed +0 -0
- promtexpress-0.1.0.dist-info/METADATA +76 -0
- promtexpress-0.1.0.dist-info/RECORD +9 -0
- promtexpress-0.1.0.dist-info/WHEEL +5 -0
- promtexpress-0.1.0.dist-info/top_level.txt +1 -0
promtexpress/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Official Python client for the PromtExpress API."""
|
|
2
|
+
|
|
3
|
+
from ._client import DEFAULT_BASE_URL, MODALITIES, PromtExpress
|
|
4
|
+
from ._errors import (
|
|
5
|
+
APIConnectionError,
|
|
6
|
+
APIError,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
InsufficientCreditsError,
|
|
9
|
+
InvalidRequestError,
|
|
10
|
+
PromtExpressError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
ServerError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"DEFAULT_BASE_URL",
|
|
19
|
+
"MODALITIES",
|
|
20
|
+
"PromtExpress",
|
|
21
|
+
"PromtExpressError",
|
|
22
|
+
"APIError",
|
|
23
|
+
"APIConnectionError",
|
|
24
|
+
"InvalidRequestError",
|
|
25
|
+
"AuthenticationError",
|
|
26
|
+
"InsufficientCreditsError",
|
|
27
|
+
"RateLimitError",
|
|
28
|
+
"ServerError",
|
|
29
|
+
]
|
promtexpress/_client.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
from ._errors import APIConnectionError, PromtExpressError, RateLimitError, error_from_response
|
|
13
|
+
from ._types import Answer, GenerateResult, HistoryPage, HistoryRow, Iteration, Template
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://promtexpress.com/api/v1"
|
|
16
|
+
MODALITIES = ("text", "code", "image", "video", "audio", "music")
|
|
17
|
+
|
|
18
|
+
_USER_AGENT = "promtexpress-python/0.1.0"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PromtExpress:
|
|
22
|
+
"""Client for the PromtExpress API.
|
|
23
|
+
|
|
24
|
+
>>> client = PromtExpress() # reads PROMTEXPRESS_API_KEY
|
|
25
|
+
>>> result = client.generate("launch email for my CRM", "text")
|
|
26
|
+
>>> print(result["output"])
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
api_key: Optional[str] = None,
|
|
32
|
+
*,
|
|
33
|
+
base_url: Optional[str] = None,
|
|
34
|
+
timeout: float = 120.0,
|
|
35
|
+
max_retries: int = 2,
|
|
36
|
+
) -> None:
|
|
37
|
+
api_key = api_key or os.environ.get("PROMTEXPRESS_API_KEY")
|
|
38
|
+
if not api_key:
|
|
39
|
+
raise PromtExpressError(
|
|
40
|
+
"Missing API key: pass api_key or set PROMTEXPRESS_API_KEY. "
|
|
41
|
+
"Keys are created in the PromtExpress dashboard under API Keys."
|
|
42
|
+
)
|
|
43
|
+
self.base_url = (base_url or os.environ.get("PROMTEXPRESS_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
44
|
+
self._api_key = api_key
|
|
45
|
+
self._timeout = timeout
|
|
46
|
+
self._max_retries = max_retries
|
|
47
|
+
|
|
48
|
+
def generate(
|
|
49
|
+
self,
|
|
50
|
+
intent: str,
|
|
51
|
+
modality: str,
|
|
52
|
+
*,
|
|
53
|
+
target_engine_id: Optional[str] = None,
|
|
54
|
+
answers: Optional[Sequence[Answer]] = None,
|
|
55
|
+
iteration: Optional[Iteration] = None,
|
|
56
|
+
) -> GenerateResult:
|
|
57
|
+
"""Compile a plain-language intent into a production-ready prompt. Consumes credits."""
|
|
58
|
+
if not isinstance(intent, str) or not 3 <= len(intent) <= 4000:
|
|
59
|
+
raise PromtExpressError("intent must be a string of 3 to 4000 characters")
|
|
60
|
+
if modality not in MODALITIES:
|
|
61
|
+
raise PromtExpressError(f"modality must be one of: {', '.join(MODALITIES)}")
|
|
62
|
+
if answers is not None and len(answers) > 10:
|
|
63
|
+
raise PromtExpressError("answers accepts at most 10 items")
|
|
64
|
+
|
|
65
|
+
body: Dict[str, Any] = {"intent": intent, "modality": modality}
|
|
66
|
+
if target_engine_id is not None:
|
|
67
|
+
body["targetEngineId"] = target_engine_id
|
|
68
|
+
if answers:
|
|
69
|
+
body["answers"] = list(answers)
|
|
70
|
+
if iteration:
|
|
71
|
+
body["iteration"] = dict(iteration)
|
|
72
|
+
return self._request("POST", "/generate", body=body)
|
|
73
|
+
|
|
74
|
+
def list_templates(self, modality: Optional[str] = None) -> List[Template]:
|
|
75
|
+
"""List published prompt templates."""
|
|
76
|
+
return self._request("GET", "/templates", query={"modality": modality})["templates"]
|
|
77
|
+
|
|
78
|
+
def list_history(self, *, page: int = 0, limit: int = 20, modality: Optional[str] = None) -> HistoryPage:
|
|
79
|
+
"""Fetch one page of your generation history, newest first."""
|
|
80
|
+
return self._request("GET", "/history", query={"page": page, "limit": limit, "modality": modality})
|
|
81
|
+
|
|
82
|
+
def iter_history(self, *, limit: int = 100, modality: Optional[str] = None) -> Iterator[HistoryRow]:
|
|
83
|
+
"""Walk your whole generation history, requesting pages as needed."""
|
|
84
|
+
page = 0
|
|
85
|
+
while True:
|
|
86
|
+
result = self.list_history(page=page, limit=limit, modality=modality)
|
|
87
|
+
yield from result["rows"]
|
|
88
|
+
if not result["rows"] or (page + 1) * result["pageSize"] >= result["total"]:
|
|
89
|
+
return
|
|
90
|
+
page += 1
|
|
91
|
+
|
|
92
|
+
def _request(
|
|
93
|
+
self,
|
|
94
|
+
method: str,
|
|
95
|
+
path: str,
|
|
96
|
+
*,
|
|
97
|
+
query: Optional[Mapping[str, Any]] = None,
|
|
98
|
+
body: Optional[Mapping[str, Any]] = None,
|
|
99
|
+
) -> Any:
|
|
100
|
+
url = self.base_url + path
|
|
101
|
+
params = {key: value for key, value in (query or {}).items() if value is not None}
|
|
102
|
+
if params:
|
|
103
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
104
|
+
|
|
105
|
+
headers = {
|
|
106
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
107
|
+
"Accept": "application/json",
|
|
108
|
+
"User-Agent": _USER_AGENT,
|
|
109
|
+
}
|
|
110
|
+
data = None
|
|
111
|
+
if body is not None:
|
|
112
|
+
headers["Content-Type"] = "application/json"
|
|
113
|
+
data = json.dumps(body).encode("utf-8")
|
|
114
|
+
|
|
115
|
+
attempt = 0
|
|
116
|
+
while True:
|
|
117
|
+
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
118
|
+
try:
|
|
119
|
+
with urllib.request.urlopen(request, timeout=self._timeout) as response:
|
|
120
|
+
return _parse(response.read())
|
|
121
|
+
except urllib.error.HTTPError as exc:
|
|
122
|
+
error = error_from_response(exc.code, _parse(exc.read()), exc.headers)
|
|
123
|
+
# The API rejects rate-limited calls before charging credits, so retrying is safe even for generate.
|
|
124
|
+
if isinstance(error, RateLimitError) and error.retry_after is not None and attempt < self._max_retries:
|
|
125
|
+
attempt += 1
|
|
126
|
+
time.sleep(error.retry_after)
|
|
127
|
+
continue
|
|
128
|
+
raise error from None
|
|
129
|
+
except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError) as exc:
|
|
130
|
+
reason = getattr(exc, "reason", exc)
|
|
131
|
+
raise APIConnectionError(f"Could not reach {self.base_url}: {reason}") from exc
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse(raw: bytes) -> Any:
|
|
135
|
+
if not raw:
|
|
136
|
+
return None
|
|
137
|
+
text = raw.decode("utf-8", errors="replace")
|
|
138
|
+
try:
|
|
139
|
+
return json.loads(text)
|
|
140
|
+
except ValueError:
|
|
141
|
+
return text
|
promtexpress/_errors.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PromtExpressError(Exception):
|
|
7
|
+
"""Base class for every error raised by this SDK."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIError(PromtExpressError):
|
|
11
|
+
"""The API responded with a non-2xx status."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, status: int, message: str, body: Any) -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.status = status
|
|
16
|
+
self.message = message
|
|
17
|
+
self.body = body
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InvalidRequestError(APIError):
|
|
21
|
+
"""400: the request body failed server-side validation."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthenticationError(APIError):
|
|
25
|
+
"""401: the API key is missing, invalid, expired or revoked."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InsufficientCreditsError(APIError):
|
|
29
|
+
"""402: not enough credits left for this generation."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, status: int, message: str, body: Any) -> None:
|
|
32
|
+
super().__init__(status, message, body)
|
|
33
|
+
self.remaining = _number_field(body, "remaining")
|
|
34
|
+
self.required = _number_field(body, "required")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RateLimitError(APIError):
|
|
38
|
+
"""429: rate limited, or the iteration limit for a prompt was reached."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, status: int, message: str, body: Any, retry_after: Optional[float]) -> None:
|
|
41
|
+
super().__init__(status, message, body)
|
|
42
|
+
#: Seconds to wait before retrying; None when the limit is not time-based.
|
|
43
|
+
self.retry_after = retry_after
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ServerError(APIError):
|
|
47
|
+
"""5xx: the generation pipeline or an upstream engine failed."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class APIConnectionError(PromtExpressError):
|
|
51
|
+
"""The request never got a response: network failure or timeout."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def error_from_response(status: int, body: Any, headers: Mapping[str, str]) -> APIError:
|
|
55
|
+
message = _describe(status, body)
|
|
56
|
+
if status == 400:
|
|
57
|
+
return InvalidRequestError(status, message, body)
|
|
58
|
+
if status == 401:
|
|
59
|
+
return AuthenticationError(status, message, body)
|
|
60
|
+
if status == 402:
|
|
61
|
+
return InsufficientCreditsError(status, message, body)
|
|
62
|
+
if status == 429:
|
|
63
|
+
retry_after = _number_field(body, "retryAfterSec")
|
|
64
|
+
if retry_after is None:
|
|
65
|
+
retry_after = _parse_retry_after(headers.get("Retry-After"))
|
|
66
|
+
return RateLimitError(status, message, body, retry_after)
|
|
67
|
+
if status >= 500:
|
|
68
|
+
return ServerError(status, message, body)
|
|
69
|
+
return APIError(status, message, body)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _describe(status: int, body: Any) -> str:
|
|
73
|
+
error = _string_field(body, "error")
|
|
74
|
+
detail = _string_field(body, "message")
|
|
75
|
+
if error and detail:
|
|
76
|
+
return f"{error}: {detail}"
|
|
77
|
+
return error or detail or f"HTTP {status}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _parse_retry_after(value: Optional[str]) -> Optional[float]:
|
|
81
|
+
if value is None:
|
|
82
|
+
return None
|
|
83
|
+
try:
|
|
84
|
+
seconds = float(value)
|
|
85
|
+
except ValueError:
|
|
86
|
+
return None
|
|
87
|
+
return seconds if seconds >= 0 else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _number_field(body: Any, key: str) -> Optional[float]:
|
|
91
|
+
value = body.get(key) if isinstance(body, dict) else None
|
|
92
|
+
return value if isinstance(value, (int, float)) and not isinstance(value, bool) else None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _string_field(body: Any, key: str) -> Optional[str]:
|
|
96
|
+
value = body.get(key) if isinstance(body, dict) else None
|
|
97
|
+
return value if isinstance(value, str) and value else None
|
promtexpress/_types.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Literal, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Answer(TypedDict):
|
|
7
|
+
question: str
|
|
8
|
+
answer: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Iteration(TypedDict):
|
|
12
|
+
ofPromptId: str
|
|
13
|
+
feedback: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChipQuestion(TypedDict):
|
|
17
|
+
label: str
|
|
18
|
+
options: List[str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Assumption(TypedDict):
|
|
22
|
+
key: str
|
|
23
|
+
value: str
|
|
24
|
+
label_tr: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RecentEntry(TypedDict):
|
|
28
|
+
id: str
|
|
29
|
+
mod: str
|
|
30
|
+
title: str
|
|
31
|
+
userInput: str
|
|
32
|
+
date: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _GenerateResultOptional(TypedDict, total=False):
|
|
36
|
+
chipQuestions: List[ChipQuestion]
|
|
37
|
+
ambiguityClarifications: List[str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GenerateResult(_GenerateResultOptional):
|
|
41
|
+
promptId: str
|
|
42
|
+
output: str
|
|
43
|
+
creditsUsed: int
|
|
44
|
+
creditsRemaining: int
|
|
45
|
+
latencyMs: int
|
|
46
|
+
validationScore: Optional[float]
|
|
47
|
+
validationIssues: List[str]
|
|
48
|
+
assumptions: List[Assumption]
|
|
49
|
+
traceId: str
|
|
50
|
+
scenario: Literal["A", "B", "C"]
|
|
51
|
+
recentEntry: RecentEntry
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Template(TypedDict):
|
|
55
|
+
id: str
|
|
56
|
+
title: str
|
|
57
|
+
description: Optional[str]
|
|
58
|
+
category: str
|
|
59
|
+
modality: str
|
|
60
|
+
engine: Optional[str]
|
|
61
|
+
variables: object
|
|
62
|
+
version: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class HistoryRow(TypedDict):
|
|
66
|
+
id: str
|
|
67
|
+
title: str
|
|
68
|
+
modality: str
|
|
69
|
+
engine: str
|
|
70
|
+
credits: int
|
|
71
|
+
date: str
|
|
72
|
+
status: Literal["Done", "Failed"]
|
|
73
|
+
userInput: str
|
|
74
|
+
result: Optional[str]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HistoryPage(TypedDict):
|
|
78
|
+
rows: List[HistoryRow]
|
|
79
|
+
total: int
|
|
80
|
+
page: int
|
|
81
|
+
pageSize: int
|
promtexpress/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: promtexpress
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the PromtExpress API
|
|
5
|
+
Author: Webitro
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://promtexpress.com
|
|
8
|
+
Project-URL: Source, https://github.com/WebitroHQ/promtexpress-oss
|
|
9
|
+
Project-URL: Issues, https://github.com/WebitroHQ/promtexpress-oss/issues
|
|
10
|
+
Keywords: promtexpress,prompt,prompt-engineering,llm,ai,sdk
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# promtexpress
|
|
18
|
+
|
|
19
|
+
Official Python client for the [PromtExpress](https://promtexpress.com) API. Standard library only, Python 3.9+.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install promtexpress
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from promtexpress import PromtExpress
|
|
29
|
+
|
|
30
|
+
client = PromtExpress() # reads PROMTEXPRESS_API_KEY
|
|
31
|
+
|
|
32
|
+
result = client.generate(
|
|
33
|
+
"product launch email for a note-taking app, friendly tone",
|
|
34
|
+
"text", # text | code | image | video | audio | music
|
|
35
|
+
)
|
|
36
|
+
print(result["output"])
|
|
37
|
+
print(result["creditsRemaining"], "credits left")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Clarifying questions and iteration
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
if result.get("chipQuestions"):
|
|
44
|
+
better = client.generate(
|
|
45
|
+
"product launch email for a note-taking app",
|
|
46
|
+
"text",
|
|
47
|
+
answers=[{"question": result["chipQuestions"][0]["label"], "answer": "developers"}],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
refined = client.generate(
|
|
51
|
+
"product launch email for a note-taking app",
|
|
52
|
+
"text",
|
|
53
|
+
iteration={"ofPromptId": result["promptId"], "feedback": "shorter, mention the free tier"},
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Templates and history
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
templates = client.list_templates(modality="image")
|
|
61
|
+
|
|
62
|
+
page = client.list_history(page=0, limit=50)
|
|
63
|
+
|
|
64
|
+
for entry in client.iter_history(modality="video"):
|
|
65
|
+
print(entry["date"], entry["title"])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Errors
|
|
69
|
+
|
|
70
|
+
All errors inherit from `PromtExpressError`: `InvalidRequestError` (400), `AuthenticationError` (401), `InsufficientCreditsError` (402, with `remaining` and `required`), `RateLimitError` (429, with `retry_after`), `ServerError` (5xx) and `APIConnectionError` (network or timeout).
|
|
71
|
+
|
|
72
|
+
Time-based rate limits are retried automatically (`max_retries`, default 2).
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
promtexpress/__init__.py,sha256=FOrrVaNArtbfOWmJUiFWqj8O-2UQ_koVuzbLT4utsgY,617
|
|
2
|
+
promtexpress/_client.py,sha256=kp7ZsgdqGUPMnbI3tDEMM6xq10U3viR6p6nDoeiqAYU,5626
|
|
3
|
+
promtexpress/_errors.py,sha256=s96Q3FdX5ZAy2kEphoR5gG0_-3aZG5Eheak363oI6WY,3219
|
|
4
|
+
promtexpress/_types.py,sha256=ieny8LI8Z0foH_guoExTq5U4psHJ9H2KXuWvn4B34JQ,1441
|
|
5
|
+
promtexpress/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
promtexpress-0.1.0.dist-info/METADATA,sha256=jR6qdJHTqQnluSa7RjlaCQ1glXkn80PQU38zM8Lr4WY,2197
|
|
7
|
+
promtexpress-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
promtexpress-0.1.0.dist-info/top_level.txt,sha256=Pop3ojYVe7qQNy0LwZ9fOatlYZ_-jV3g7geIqHGjV68,13
|
|
9
|
+
promtexpress-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
promtexpress
|