writehuman 0.1.0__tar.gz → 0.2.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.
- {writehuman-0.1.0 → writehuman-0.2.0}/PKG-INFO +15 -2
- {writehuman-0.1.0 → writehuman-0.2.0}/README.md +14 -1
- {writehuman-0.1.0 → writehuman-0.2.0}/pyproject.toml +33 -33
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman/__init__.py +51 -47
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman/_policy.py +83 -83
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman/client.py +17 -5
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman/errors.py +32 -4
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman/types.py +173 -155
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman.egg-info/PKG-INFO +15 -2
- {writehuman-0.1.0 → writehuman-0.2.0}/setup.cfg +0 -0
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman/py.typed +0 -0
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman.egg-info/SOURCES.txt +0 -0
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman.egg-info/dependency_links.txt +0 -0
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman.egg-info/requires.txt +0 -0
- {writehuman-0.1.0 → writehuman-0.2.0}/src/writehuman.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: writehuman
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Official WriteHuman API client for Python: humanize AI text, detect AI authorship, read usage.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://writehuman.ai/api/docs
|
|
@@ -44,6 +44,16 @@ print(score["ai_probability"], score["predicted_class"])
|
|
|
44
44
|
print(wh.usage())
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Premium plans and above can ask for the enhanced model. On a Standard plan this raises
|
|
48
|
+
`PlanRequiredError` (nothing is charged) with `upgrade_url` set; while the enhanced model is
|
|
49
|
+
switched off on the API it raises `FeatureUnavailableError`, so send without `enhanced`. Every
|
|
50
|
+
humanize response's `model` (`"standard"` or `"enhanced"`) says which model served it:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
out = wh.humanize(text, enhanced=True)
|
|
54
|
+
print(out["model"]) # "enhanced"
|
|
55
|
+
```
|
|
56
|
+
|
|
47
57
|
Async, same methods:
|
|
48
58
|
|
|
49
59
|
```python
|
|
@@ -57,13 +67,16 @@ async with AsyncWriteHuman("wh_live_...") as wh:
|
|
|
57
67
|
|
|
58
68
|
Every failure raises a subclass of `WriteHumanError` with `code` (the API's stable error
|
|
59
69
|
code), `status`, `retry_after` (seconds, when the API sent one) and, on
|
|
60
|
-
`InsufficientBalanceError`, `words_remaining`.
|
|
70
|
+
`InsufficientBalanceError`, `words_remaining`; on `PlanRequiredError`, `upgrade_url`.
|
|
61
71
|
|
|
62
72
|
| exception | API `code` |
|
|
63
73
|
|---|---|
|
|
64
74
|
| `AuthenticationError` | `missing_api_key`, `invalid_api_key` |
|
|
65
75
|
| `NoSubscriptionError` | `no_subscription` |
|
|
66
76
|
| `InsufficientBalanceError` | `insufficient_balance` |
|
|
77
|
+
| `ReverifyRequiredError` | `reverify_required` (403: the account owner must re-verify by email at writehuman.ai; not retried) |
|
|
78
|
+
| `PlanRequiredError` | `enhanced_requires_premium` (403: `enhanced=True` on a Standard plan; `upgrade_url` says where to upgrade; nothing charged; not retried) |
|
|
79
|
+
| `FeatureUnavailableError` | `enhanced_unavailable` (403: the enhanced model is switched off for everyone; send without `enhanced`; nothing charged; not retried) |
|
|
67
80
|
| `InvalidRequestError` | `invalid_request`, `forbidden_param`, `word_limit` |
|
|
68
81
|
| `ModerationError` | `moderation_blocked` |
|
|
69
82
|
| `RateLimitError` | `rate_limited`, `auth_locked_out`, `queue_full`, `queue_timeout` |
|
|
@@ -24,6 +24,16 @@ print(score["ai_probability"], score["predicted_class"])
|
|
|
24
24
|
print(wh.usage())
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
Premium plans and above can ask for the enhanced model. On a Standard plan this raises
|
|
28
|
+
`PlanRequiredError` (nothing is charged) with `upgrade_url` set; while the enhanced model is
|
|
29
|
+
switched off on the API it raises `FeatureUnavailableError`, so send without `enhanced`. Every
|
|
30
|
+
humanize response's `model` (`"standard"` or `"enhanced"`) says which model served it:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
out = wh.humanize(text, enhanced=True)
|
|
34
|
+
print(out["model"]) # "enhanced"
|
|
35
|
+
```
|
|
36
|
+
|
|
27
37
|
Async, same methods:
|
|
28
38
|
|
|
29
39
|
```python
|
|
@@ -37,13 +47,16 @@ async with AsyncWriteHuman("wh_live_...") as wh:
|
|
|
37
47
|
|
|
38
48
|
Every failure raises a subclass of `WriteHumanError` with `code` (the API's stable error
|
|
39
49
|
code), `status`, `retry_after` (seconds, when the API sent one) and, on
|
|
40
|
-
`InsufficientBalanceError`, `words_remaining`.
|
|
50
|
+
`InsufficientBalanceError`, `words_remaining`; on `PlanRequiredError`, `upgrade_url`.
|
|
41
51
|
|
|
42
52
|
| exception | API `code` |
|
|
43
53
|
|---|---|
|
|
44
54
|
| `AuthenticationError` | `missing_api_key`, `invalid_api_key` |
|
|
45
55
|
| `NoSubscriptionError` | `no_subscription` |
|
|
46
56
|
| `InsufficientBalanceError` | `insufficient_balance` |
|
|
57
|
+
| `ReverifyRequiredError` | `reverify_required` (403: the account owner must re-verify by email at writehuman.ai; not retried) |
|
|
58
|
+
| `PlanRequiredError` | `enhanced_requires_premium` (403: `enhanced=True` on a Standard plan; `upgrade_url` says where to upgrade; nothing charged; not retried) |
|
|
59
|
+
| `FeatureUnavailableError` | `enhanced_unavailable` (403: the enhanced model is switched off for everyone; send without `enhanced`; nothing charged; not retried) |
|
|
47
60
|
| `InvalidRequestError` | `invalid_request`, `forbidden_param`, `word_limit` |
|
|
48
61
|
| `ModerationError` | `moderation_blocked` |
|
|
49
62
|
| `RateLimitError` | `rate_limited`, `auth_locked_out`, `queue_full`, `queue_timeout` |
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=68"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "writehuman"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Official WriteHuman API client for Python: humanize AI text, detect AI authorship, read usage."
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
license = { text = "MIT" }
|
|
11
|
-
requires-python = ">=3.9"
|
|
12
|
-
dependencies = ["httpx>=0.25,<1"]
|
|
13
|
-
keywords = ["writehuman", "humanize", "ai-detection", "api-client"]
|
|
14
|
-
classifiers = [
|
|
15
|
-
"Development Status :: 4 - Beta",
|
|
16
|
-
"Intended Audience :: Developers",
|
|
17
|
-
"License :: OSI Approved :: MIT License",
|
|
18
|
-
"Programming Language :: Python :: 3",
|
|
19
|
-
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
-
"Typing :: Typed",
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
[project.urls]
|
|
24
|
-
Homepage = "https://writehuman.ai/api/docs"
|
|
25
|
-
Documentation = "https://writehuman.ai/api/docs"
|
|
26
|
-
Source = "https://github.com/WriteHuman-ai/rephrase-api/tree/main/sdk/python"
|
|
27
|
-
Changelog = "https://github.com/WriteHuman-ai/rephrase-api/blob/main/sdk/python/CHANGELOG.md"
|
|
28
|
-
|
|
29
|
-
[tool.setuptools.packages.find]
|
|
30
|
-
where = ["src"]
|
|
31
|
-
|
|
32
|
-
[tool.setuptools.package-data]
|
|
33
|
-
writehuman = ["py.typed"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "writehuman"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Official WriteHuman API client for Python: humanize AI text, detect AI authorship, read usage."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = ["httpx>=0.25,<1"]
|
|
13
|
+
keywords = ["writehuman", "humanize", "ai-detection", "api-client"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://writehuman.ai/api/docs"
|
|
25
|
+
Documentation = "https://writehuman.ai/api/docs"
|
|
26
|
+
Source = "https://github.com/WriteHuman-ai/rephrase-api/tree/main/sdk/python"
|
|
27
|
+
Changelog = "https://github.com/WriteHuman-ai/rephrase-api/blob/main/sdk/python/CHANGELOG.md"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
writehuman = ["py.typed"]
|
|
@@ -1,47 +1,51 @@
|
|
|
1
|
-
"""Official WriteHuman API client for Python.
|
|
2
|
-
|
|
3
|
-
from writehuman import WriteHuman
|
|
4
|
-
wh = WriteHuman("wh_live_...")
|
|
5
|
-
out = wh.humanize("Artificial intelligence has transformed numerous industries ...")
|
|
6
|
-
print(out["results"][0])
|
|
7
|
-
|
|
8
|
-
Docs: https://writehuman.ai/api/docs. Contract: https://api.writehuman.ai/openapi.json.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
__version__ = "0.
|
|
12
|
-
|
|
13
|
-
from .client import DEFAULT_BASE_URL, USER_AGENT, AsyncWriteHuman, WriteHuman # noqa: E402
|
|
14
|
-
from .errors import ( # noqa: E402
|
|
15
|
-
APIError,
|
|
16
|
-
AuthenticationError,
|
|
17
|
-
BillingError,
|
|
18
|
-
ClientTimeoutError,
|
|
19
|
-
ConnectionError,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
1
|
+
"""Official WriteHuman API client for Python.
|
|
2
|
+
|
|
3
|
+
from writehuman import WriteHuman
|
|
4
|
+
wh = WriteHuman("wh_live_...")
|
|
5
|
+
out = wh.humanize("Artificial intelligence has transformed numerous industries ...")
|
|
6
|
+
print(out["results"][0])
|
|
7
|
+
|
|
8
|
+
Docs: https://writehuman.ai/api/docs. Contract: https://api.writehuman.ai/openapi.json.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.0"
|
|
12
|
+
|
|
13
|
+
from .client import DEFAULT_BASE_URL, USER_AGENT, AsyncWriteHuman, WriteHuman # noqa: E402
|
|
14
|
+
from .errors import ( # noqa: E402
|
|
15
|
+
APIError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
BillingError,
|
|
18
|
+
ClientTimeoutError,
|
|
19
|
+
ConnectionError,
|
|
20
|
+
FeatureUnavailableError,
|
|
21
|
+
InsufficientBalanceError,
|
|
22
|
+
InvalidRequestError,
|
|
23
|
+
ModerationError,
|
|
24
|
+
NoSubscriptionError,
|
|
25
|
+
PlanRequiredError,
|
|
26
|
+
ProcessingError,
|
|
27
|
+
ProtocolError,
|
|
28
|
+
RateLimitError,
|
|
29
|
+
ReverifyRequiredError,
|
|
30
|
+
ServerTimeoutError,
|
|
31
|
+
ServiceUnavailableError,
|
|
32
|
+
WriteHumanError,
|
|
33
|
+
)
|
|
34
|
+
from .types import ( # noqa: E402
|
|
35
|
+
API_VERSION,
|
|
36
|
+
DetectResponse,
|
|
37
|
+
ErrorCode,
|
|
38
|
+
HumanizeResponse,
|
|
39
|
+
UsageResponse,
|
|
40
|
+
WordsRemaining,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"__version__", "API_VERSION", "DEFAULT_BASE_URL", "USER_AGENT",
|
|
45
|
+
"WriteHuman", "AsyncWriteHuman",
|
|
46
|
+
"WriteHumanError", "APIError", "AuthenticationError", "NoSubscriptionError", "InsufficientBalanceError",
|
|
47
|
+
"ReverifyRequiredError", "PlanRequiredError", "FeatureUnavailableError",
|
|
48
|
+
"InvalidRequestError", "ModerationError", "RateLimitError", "ServiceUnavailableError", "ServerTimeoutError",
|
|
49
|
+
"ProcessingError", "BillingError", "ProtocolError", "ClientTimeoutError", "ConnectionError",
|
|
50
|
+
"HumanizeResponse", "DetectResponse", "UsageResponse", "WordsRemaining", "ErrorCode",
|
|
51
|
+
]
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
"""The request/retry policy shared by the sync and async clients. Pure functions, no I/O,
|
|
2
|
-
so both clients are thin loops around it and the behaviour is identical. The policy is the
|
|
3
|
-
one sdk/conformance/README.md specifies and the conformance scenarios enforce."""
|
|
4
|
-
from typing import Any, Dict, Optional, Tuple
|
|
5
|
-
|
|
6
|
-
from .errors import WriteHumanError, error_from_body
|
|
7
|
-
|
|
8
|
-
RETRYABLE_STATUSES = frozenset({429, 503, 504})
|
|
9
|
-
|
|
10
|
-
# Total timeouts per operation, in seconds: the 30-day p99 of successful calls with room,
|
|
11
|
-
# under the API's own 170 s humanize deadline.
|
|
12
|
-
DEFAULT_TIMEOUTS: Dict[str, float] = {"humanize": 120.0, "detect": 30.0, "usage": 15.0}
|
|
13
|
-
DEFAULT_CONNECT_TIMEOUT = 10.0
|
|
14
|
-
DEFAULT_MAX_RETRIES = 2
|
|
15
|
-
DEFAULT_MAX_RETRY_AFTER = 60.0
|
|
16
|
-
DEFAULT_FALLBACK_RETRY_AFTER = 2.0
|
|
17
|
-
|
|
18
|
-
OPERATIONS = {
|
|
19
|
-
# name: (method, path, idempotent)
|
|
20
|
-
"humanize": ("POST", "/v1/humanize", False),
|
|
21
|
-
"detect": ("POST", "/v1/detect", False),
|
|
22
|
-
"usage": ("GET", "/v1/usage", True),
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def parse_retry_after(value: Optional[str]) -> Optional[float]:
|
|
27
|
-
if value is None:
|
|
28
|
-
return None
|
|
29
|
-
try:
|
|
30
|
-
return max(0.0, float(value))
|
|
31
|
-
except ValueError:
|
|
32
|
-
return None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class Policy:
|
|
36
|
-
def __init__(self, *, max_retries: int = DEFAULT_MAX_RETRIES, max_retry_after: float = DEFAULT_MAX_RETRY_AFTER,
|
|
37
|
-
fallback_retry_after: float = DEFAULT_FALLBACK_RETRY_AFTER):
|
|
38
|
-
self.max_retries = max(0, int(max_retries))
|
|
39
|
-
self.max_retry_after = float(max_retry_after)
|
|
40
|
-
self.fallback_retry_after = float(fallback_retry_after)
|
|
41
|
-
|
|
42
|
-
def wait_after_response(self, status: int, retry_after: Optional[float], attempt: int, *,
|
|
43
|
-
from_api: bool = True, idempotent: bool = False) -> Optional[float]:
|
|
44
|
-
"""Seconds to sleep before retrying a non-2xx response, or None to raise now.
|
|
45
|
-
Only 429/503/504 are retried, and only when the API itself answered (its error body):
|
|
46
|
-
the contract guarantees those left nothing charged. The same status from a load
|
|
47
|
-
balancer or proxy carries no such guarantee (a balancer 504 can follow a request the
|
|
48
|
-
server went on to finish and bill), so it is treated like a transport error."""
|
|
49
|
-
if status not in RETRYABLE_STATUSES:
|
|
50
|
-
return None
|
|
51
|
-
if not from_api:
|
|
52
|
-
return self.wait_after_transport_error(idempotent, attempt)
|
|
53
|
-
if attempt >= self.max_retries:
|
|
54
|
-
return None
|
|
55
|
-
if retry_after is None:
|
|
56
|
-
return self.fallback_retry_after
|
|
57
|
-
if retry_after > self.max_retry_after:
|
|
58
|
-
return None # a 15-minute lockout is for the caller to decide about, not to sleep through
|
|
59
|
-
return retry_after
|
|
60
|
-
|
|
61
|
-
def wait_after_transport_error(self, idempotent: bool, attempt: int) -> Optional[float]:
|
|
62
|
-
"""A timeout or dropped connection: retried only for the read-only usage call. A
|
|
63
|
-
billed POST may have been processed, so it is never resent automatically."""
|
|
64
|
-
if not idempotent or attempt >= self.max_retries:
|
|
65
|
-
return None
|
|
66
|
-
return self.fallback_retry_after
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def interpret(status: int, headers: Any, body_text: str) -> Tuple[Optional[dict], Optional[WriteHumanError], Optional[float]]:
|
|
70
|
-
"""Turn a response into (data, None, retry_after) on success or (None, error, retry_after)."""
|
|
71
|
-
import json
|
|
72
|
-
|
|
73
|
-
retry_after = parse_retry_after(headers.get("Retry-After") if hasattr(headers, "get") else None)
|
|
74
|
-
try:
|
|
75
|
-
data = json.loads(body_text) if body_text else None
|
|
76
|
-
except ValueError:
|
|
77
|
-
data = None
|
|
78
|
-
if 200 <= status < 300:
|
|
79
|
-
if isinstance(data, dict):
|
|
80
|
-
return data, None, retry_after
|
|
81
|
-
from .errors import ProtocolError
|
|
82
|
-
return None, ProtocolError(f"HTTP {status} with a body that is not JSON", status=status), retry_after
|
|
83
|
-
return None, error_from_body(status, data, retry_after), retry_after
|
|
1
|
+
"""The request/retry policy shared by the sync and async clients. Pure functions, no I/O,
|
|
2
|
+
so both clients are thin loops around it and the behaviour is identical. The policy is the
|
|
3
|
+
one sdk/conformance/README.md specifies and the conformance scenarios enforce."""
|
|
4
|
+
from typing import Any, Dict, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from .errors import WriteHumanError, error_from_body
|
|
7
|
+
|
|
8
|
+
RETRYABLE_STATUSES = frozenset({429, 503, 504})
|
|
9
|
+
|
|
10
|
+
# Total timeouts per operation, in seconds: the 30-day p99 of successful calls with room,
|
|
11
|
+
# under the API's own 170 s humanize deadline.
|
|
12
|
+
DEFAULT_TIMEOUTS: Dict[str, float] = {"humanize": 120.0, "detect": 30.0, "usage": 15.0}
|
|
13
|
+
DEFAULT_CONNECT_TIMEOUT = 10.0
|
|
14
|
+
DEFAULT_MAX_RETRIES = 2
|
|
15
|
+
DEFAULT_MAX_RETRY_AFTER = 60.0
|
|
16
|
+
DEFAULT_FALLBACK_RETRY_AFTER = 2.0
|
|
17
|
+
|
|
18
|
+
OPERATIONS = {
|
|
19
|
+
# name: (method, path, idempotent)
|
|
20
|
+
"humanize": ("POST", "/v1/humanize", False),
|
|
21
|
+
"detect": ("POST", "/v1/detect", False),
|
|
22
|
+
"usage": ("GET", "/v1/usage", True),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_retry_after(value: Optional[str]) -> Optional[float]:
|
|
27
|
+
if value is None:
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
return max(0.0, float(value))
|
|
31
|
+
except ValueError:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Policy:
|
|
36
|
+
def __init__(self, *, max_retries: int = DEFAULT_MAX_RETRIES, max_retry_after: float = DEFAULT_MAX_RETRY_AFTER,
|
|
37
|
+
fallback_retry_after: float = DEFAULT_FALLBACK_RETRY_AFTER):
|
|
38
|
+
self.max_retries = max(0, int(max_retries))
|
|
39
|
+
self.max_retry_after = float(max_retry_after)
|
|
40
|
+
self.fallback_retry_after = float(fallback_retry_after)
|
|
41
|
+
|
|
42
|
+
def wait_after_response(self, status: int, retry_after: Optional[float], attempt: int, *,
|
|
43
|
+
from_api: bool = True, idempotent: bool = False) -> Optional[float]:
|
|
44
|
+
"""Seconds to sleep before retrying a non-2xx response, or None to raise now.
|
|
45
|
+
Only 429/503/504 are retried, and only when the API itself answered (its error body):
|
|
46
|
+
the contract guarantees those left nothing charged. The same status from a load
|
|
47
|
+
balancer or proxy carries no such guarantee (a balancer 504 can follow a request the
|
|
48
|
+
server went on to finish and bill), so it is treated like a transport error."""
|
|
49
|
+
if status not in RETRYABLE_STATUSES:
|
|
50
|
+
return None
|
|
51
|
+
if not from_api:
|
|
52
|
+
return self.wait_after_transport_error(idempotent, attempt)
|
|
53
|
+
if attempt >= self.max_retries:
|
|
54
|
+
return None
|
|
55
|
+
if retry_after is None:
|
|
56
|
+
return self.fallback_retry_after
|
|
57
|
+
if retry_after > self.max_retry_after:
|
|
58
|
+
return None # a 15-minute lockout is for the caller to decide about, not to sleep through
|
|
59
|
+
return retry_after
|
|
60
|
+
|
|
61
|
+
def wait_after_transport_error(self, idempotent: bool, attempt: int) -> Optional[float]:
|
|
62
|
+
"""A timeout or dropped connection: retried only for the read-only usage call. A
|
|
63
|
+
billed POST may have been processed, so it is never resent automatically."""
|
|
64
|
+
if not idempotent or attempt >= self.max_retries:
|
|
65
|
+
return None
|
|
66
|
+
return self.fallback_retry_after
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def interpret(status: int, headers: Any, body_text: str) -> Tuple[Optional[dict], Optional[WriteHumanError], Optional[float]]:
|
|
70
|
+
"""Turn a response into (data, None, retry_after) on success or (None, error, retry_after)."""
|
|
71
|
+
import json
|
|
72
|
+
|
|
73
|
+
retry_after = parse_retry_after(headers.get("Retry-After") if hasattr(headers, "get") else None)
|
|
74
|
+
try:
|
|
75
|
+
data = json.loads(body_text) if body_text else None
|
|
76
|
+
except ValueError:
|
|
77
|
+
data = None
|
|
78
|
+
if 200 <= status < 300:
|
|
79
|
+
if isinstance(data, dict):
|
|
80
|
+
return data, None, retry_after
|
|
81
|
+
from .errors import ProtocolError
|
|
82
|
+
return None, ProtocolError(f"HTTP {status} with a body that is not JSON", status=status), retry_after
|
|
83
|
+
return None, error_from_body(status, data, retry_after), retry_after
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
wh = WriteHuman("wh_live_...")
|
|
5
5
|
out = wh.humanize("Artificial intelligence has transformed ...", tone="professional")
|
|
6
6
|
print(out["results"][0], out["words_remaining"]["total"])
|
|
7
|
+
out = wh.humanize("...", enhanced=True) # Premium plans: the enhanced model
|
|
7
8
|
|
|
8
9
|
from writehuman import AsyncWriteHuman
|
|
9
10
|
async with AsyncWriteHuman("wh_live_...") as wh:
|
|
@@ -70,12 +71,15 @@ class _Base:
|
|
|
70
71
|
return httpx.Timeout(total, connect=min(self.connect_timeout, total))
|
|
71
72
|
|
|
72
73
|
@staticmethod
|
|
73
|
-
def _humanize_body(text: str, tone: Optional[str], language: Optional[str]) -> Dict[str, Any]:
|
|
74
|
+
def _humanize_body(text: str, tone: Optional[str], language: Optional[str], enhanced: bool) -> Dict[str, Any]:
|
|
74
75
|
body: Dict[str, Any] = {"text": text}
|
|
75
76
|
if tone is not None:
|
|
76
77
|
body["tone"] = tone
|
|
77
78
|
if language is not None:
|
|
78
79
|
body["language"] = language
|
|
80
|
+
if enhanced:
|
|
81
|
+
# only when asked for, so a default request's body is unchanged
|
|
82
|
+
body["enhanced"] = True
|
|
79
83
|
return body
|
|
80
84
|
|
|
81
85
|
@staticmethod
|
|
@@ -105,8 +109,12 @@ class WriteHuman(_Base):
|
|
|
105
109
|
self.close()
|
|
106
110
|
|
|
107
111
|
def humanize(self, text: str, *, tone: Optional[str] = None, language: Optional[str] = None,
|
|
108
|
-
timeout: Optional[float] = None) -> HumanizeResponse:
|
|
109
|
-
|
|
112
|
+
enhanced: bool = False, timeout: Optional[float] = None) -> HumanizeResponse:
|
|
113
|
+
"""`enhanced=True` serves the request with the enhanced model (Premium plans and above;
|
|
114
|
+
the response's `model` says which model served it). On a Standard plan it raises
|
|
115
|
+
PlanRequiredError with `upgrade_url`; while the enhanced model is switched off it raises
|
|
116
|
+
FeatureUnavailableError. Nothing is charged for either."""
|
|
117
|
+
return self._call("humanize", self._humanize_body(text, tone, language, enhanced), timeout) # type: ignore[return-value]
|
|
110
118
|
|
|
111
119
|
def detect(self, text: str, *, language: Optional[str] = None, include_sentences: bool = False,
|
|
112
120
|
timeout: Optional[float] = None) -> DetectResponse:
|
|
@@ -182,8 +190,12 @@ class AsyncWriteHuman(_Base):
|
|
|
182
190
|
await self.aclose()
|
|
183
191
|
|
|
184
192
|
async def humanize(self, text: str, *, tone: Optional[str] = None, language: Optional[str] = None,
|
|
185
|
-
timeout: Optional[float] = None) -> HumanizeResponse:
|
|
186
|
-
|
|
193
|
+
enhanced: bool = False, timeout: Optional[float] = None) -> HumanizeResponse:
|
|
194
|
+
"""`enhanced=True` serves the request with the enhanced model (Premium plans and above;
|
|
195
|
+
the response's `model` says which model served it). On a Standard plan it raises
|
|
196
|
+
PlanRequiredError with `upgrade_url`; while the enhanced model is switched off it raises
|
|
197
|
+
FeatureUnavailableError. Nothing is charged for either."""
|
|
198
|
+
return await self._call("humanize", self._humanize_body(text, tone, language, enhanced), timeout) # type: ignore[return-value]
|
|
187
199
|
|
|
188
200
|
async def detect(self, text: str, *, language: Optional[str] = None, include_sentences: bool = False,
|
|
189
201
|
timeout: Optional[float] = None) -> DetectResponse:
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
status HTTP status, or None for client-side failures
|
|
6
6
|
retry_after seconds the API asked us to wait (from the last response), or None
|
|
7
7
|
words_remaining on InsufficientBalanceError: {"included", "topup", "total"}
|
|
8
|
+
upgrade_url on PlanRequiredError: where the account owner upgrades the plan
|
|
8
9
|
"""
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
@@ -17,13 +18,15 @@ class WriteHumanError(Exception):
|
|
|
17
18
|
kind = "api"
|
|
18
19
|
|
|
19
20
|
def __init__(self, message: str, *, status: Optional[int] = None, code: Optional[str] = None,
|
|
20
|
-
retry_after: Optional[float] = None, words_remaining: Optional[WordsRemaining] = None
|
|
21
|
+
retry_after: Optional[float] = None, words_remaining: Optional[WordsRemaining] = None,
|
|
22
|
+
upgrade_url: Optional[str] = None):
|
|
21
23
|
super().__init__(message)
|
|
22
24
|
self.message = message
|
|
23
25
|
self.status = status
|
|
24
26
|
self.code = code
|
|
25
27
|
self.retry_after = retry_after
|
|
26
28
|
self.words_remaining = words_remaining
|
|
29
|
+
self.upgrade_url = upgrade_url
|
|
27
30
|
|
|
28
31
|
def __repr__(self) -> str:
|
|
29
32
|
return f"{type(self).__name__}({self.message!r}, status={self.status}, code={self.code!r})"
|
|
@@ -49,6 +52,26 @@ class InsufficientBalanceError(WriteHumanError):
|
|
|
49
52
|
kind = "insufficient_balance"
|
|
50
53
|
|
|
51
54
|
|
|
55
|
+
class ReverifyRequiredError(WriteHumanError):
|
|
56
|
+
"""reverify_required (403): the account must be re-verified by email. The account owner
|
|
57
|
+
signs in at writehuman.ai to continue. Never retried."""
|
|
58
|
+
kind = "reverify_required"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PlanRequiredError(WriteHumanError):
|
|
62
|
+
"""enhanced_requires_premium (403): the request asked for a feature the plan does not
|
|
63
|
+
include (`enhanced=True` on a Standard plan). `upgrade_url` is where to upgrade; or send
|
|
64
|
+
the request without the feature. Nothing is charged. Never retried."""
|
|
65
|
+
kind = "plan_required"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FeatureUnavailableError(WriteHumanError):
|
|
69
|
+
"""enhanced_unavailable (403): the feature the request asked for is switched off on the
|
|
70
|
+
API for every account right now (`enhanced=True` while the enhanced model is off). Send
|
|
71
|
+
the request without it. Upgrading does not help. Nothing is charged. Never retried."""
|
|
72
|
+
kind = "feature_unavailable"
|
|
73
|
+
|
|
74
|
+
|
|
52
75
|
class InvalidRequestError(WriteHumanError):
|
|
53
76
|
"""invalid_request, forbidden_param, word_limit."""
|
|
54
77
|
kind = "invalid_request"
|
|
@@ -104,6 +127,9 @@ KIND_BY_CODE = {
|
|
|
104
127
|
"missing_api_key": "authentication", "invalid_api_key": "authentication",
|
|
105
128
|
"no_subscription": "no_subscription",
|
|
106
129
|
"insufficient_balance": "insufficient_balance",
|
|
130
|
+
"reverify_required": "reverify_required",
|
|
131
|
+
"enhanced_requires_premium": "plan_required",
|
|
132
|
+
"enhanced_unavailable": "feature_unavailable",
|
|
107
133
|
"invalid_request": "invalid_request", "forbidden_param": "invalid_request", "word_limit": "invalid_request",
|
|
108
134
|
"moderation_blocked": "moderation",
|
|
109
135
|
"rate_limited": "rate_limit", "auth_locked_out": "rate_limit", "queue_full": "rate_limit", "queue_timeout": "rate_limit",
|
|
@@ -114,8 +140,8 @@ KIND_BY_CODE = {
|
|
|
114
140
|
}
|
|
115
141
|
|
|
116
142
|
CLASS_BY_KIND = {cls.kind: cls for cls in (
|
|
117
|
-
APIError, AuthenticationError, NoSubscriptionError, InsufficientBalanceError,
|
|
118
|
-
ModerationError, RateLimitError, ServiceUnavailableError, ServerTimeoutError, ProcessingError, BillingError,
|
|
143
|
+
APIError, AuthenticationError, NoSubscriptionError, InsufficientBalanceError, ReverifyRequiredError,
|
|
144
|
+
PlanRequiredError, FeatureUnavailableError, InvalidRequestError, ModerationError, RateLimitError, ServiceUnavailableError, ServerTimeoutError, ProcessingError, BillingError,
|
|
119
145
|
)}
|
|
120
146
|
|
|
121
147
|
|
|
@@ -130,5 +156,7 @@ def error_from_body(status: int, body: object, retry_after: Optional[float]) ->
|
|
|
130
156
|
code: Optional[ErrorCode] = body["code"] # type: ignore[assignment]
|
|
131
157
|
cls = CLASS_BY_KIND.get(KIND_BY_CODE.get(code or "", "api"), APIError)
|
|
132
158
|
wr = body.get("words_remaining")
|
|
159
|
+
up = body.get("upgrade_url")
|
|
133
160
|
return cls(body["error"], status=status, code=code, retry_after=retry_after,
|
|
134
|
-
words_remaining=wr if isinstance(wr, dict) else None
|
|
161
|
+
words_remaining=wr if isinstance(wr, dict) else None,
|
|
162
|
+
upgrade_url=up if isinstance(up, str) else None)
|
|
@@ -1,155 +1,173 @@
|
|
|
1
|
-
# GENERATED from src/openapi.json by sdk/gen_types.py. Do not edit; run the generator.
|
|
2
|
-
# Contract version 1.
|
|
3
|
-
from typing import Any, List, Literal, Optional, TypedDict
|
|
4
|
-
|
|
5
|
-
API_VERSION = "v1.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class _HumanizeRequestOptional(TypedDict, total=False):
|
|
9
|
-
# Optional writing tone. Omit for default style. A value outside this list is ignored.
|
|
10
|
-
tone: Optional[Literal["professional", "academic", "blog", "casual", "creative", "scientific", "technical"]]
|
|
11
|
-
# Target language (e.g. `English`, `Spanish`, `Chinese (Mandarin)`). Auto-detected if omitted.
|
|
12
|
-
language: Optional[str]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Words remaining
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
1
|
+
# GENERATED from src/openapi.json by sdk/gen_types.py. Do not edit; run the generator.
|
|
2
|
+
# Contract version 1.2.
|
|
3
|
+
from typing import Any, List, Literal, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
API_VERSION = "v1.2"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _HumanizeRequestOptional(TypedDict, total=False):
|
|
9
|
+
# Optional writing tone. Omit for default style. A value outside this list is ignored.
|
|
10
|
+
tone: Optional[Literal["professional", "academic", "blog", "casual", "creative", "scientific", "technical"]]
|
|
11
|
+
# Target language (e.g. `English`, `Spanish`, `Chinese (Mandarin)`). Auto-detected if omitted.
|
|
12
|
+
language: Optional[str]
|
|
13
|
+
# Serve this request with our enhanced model. Premium plans and above; on a Standard plan `true` returns `403 enhanced_requires_premium`. Must be a JSON boolean.
|
|
14
|
+
enhanced: bool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HumanizeRequest(_HumanizeRequestOptional):
|
|
18
|
+
# The text to humanize.
|
|
19
|
+
text: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WordsRemaining(TypedDict):
|
|
23
|
+
# Words remaining in your monthly subscription allowance.
|
|
24
|
+
included: int
|
|
25
|
+
# Words remaining from purchased top-ups.
|
|
26
|
+
topup: int
|
|
27
|
+
# Total words available (included + topup).
|
|
28
|
+
total: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HumanizeResponse(TypedDict):
|
|
32
|
+
# Unique identifier for this humanization request.
|
|
33
|
+
id: str
|
|
34
|
+
# Unix timestamp (milliseconds) when the request was received.
|
|
35
|
+
created: int
|
|
36
|
+
# Humanized text variations, sorted by score (best first).
|
|
37
|
+
results: List[str]
|
|
38
|
+
# Human-likeness score per result (0–1, higher = more human). `null` on Standard plans; array on Premium.
|
|
39
|
+
scores: Optional[List[Optional[float]]]
|
|
40
|
+
# Number of words billed for this request.
|
|
41
|
+
input_words: int
|
|
42
|
+
words_remaining: "WordsRemaining"
|
|
43
|
+
# Which model served the request: `enhanced` when the request was sent with `enhanced: true`, else `standard`.
|
|
44
|
+
model: Literal["standard", "enhanced"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UsageResponse(TypedDict):
|
|
48
|
+
# Monthly word allowance for your plan.
|
|
49
|
+
included_words: int
|
|
50
|
+
# Words consumed in the current billing cycle.
|
|
51
|
+
words_used_this_cycle: int
|
|
52
|
+
# Remaining purchased top-up words.
|
|
53
|
+
topup_balance: int
|
|
54
|
+
# Your plan. Every Premium size reports `api_premium`.
|
|
55
|
+
tier: Literal["api_standard", "api_premium"]
|
|
56
|
+
# API version.
|
|
57
|
+
version: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _DetectRequestOptional(TypedDict, total=False):
|
|
61
|
+
# Language of the input (e.g. `English`, `Spanish`). Auto-detected if omitted. Non-English text is translated to English before scoring.
|
|
62
|
+
language: Optional[str]
|
|
63
|
+
# Include per-sentence AI probabilities in the response.
|
|
64
|
+
include_sentences: bool
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DetectRequest(_DetectRequestOptional):
|
|
68
|
+
# The text to score.
|
|
69
|
+
text: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _DetectResponseClassProbabilitiesOptional(TypedDict, total=False):
|
|
73
|
+
ai: Optional[float]
|
|
74
|
+
human: Optional[float]
|
|
75
|
+
mixed: Optional[float]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DetectResponseClassProbabilities(_DetectResponseClassProbabilitiesOptional):
|
|
79
|
+
"""Probabilities for each class."""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DetectResponseRequestMetaDocLength(TypedDict):
|
|
84
|
+
"""Length of the scored text."""
|
|
85
|
+
chars: Optional[int]
|
|
86
|
+
tokens: Optional[int]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DetectResponseRequestMeta(TypedDict):
|
|
90
|
+
# Length of the scored text.
|
|
91
|
+
doc_length: "DetectResponseRequestMetaDocLength"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DetectResponseSentencesItem(TypedDict):
|
|
95
|
+
text: Optional[str]
|
|
96
|
+
ai_prob: Optional[float]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class _DetectResponseOptional(TypedDict, total=False):
|
|
100
|
+
# Per-sentence AI probabilities. Present only when `include_sentences` is `true`.
|
|
101
|
+
sentences: List["DetectResponseSentencesItem"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DetectResponse(_DetectResponseOptional):
|
|
105
|
+
# Identifier for this detection scan.
|
|
106
|
+
scanId: Optional[str]
|
|
107
|
+
# Detector model version.
|
|
108
|
+
version: Optional[str]
|
|
109
|
+
# Detector build identifier.
|
|
110
|
+
versionID: Optional[str]
|
|
111
|
+
# Probability the text is AI-generated (`1` = AI — opposite orientation of `/v1/humanize` scores).
|
|
112
|
+
ai_probability: Optional[float]
|
|
113
|
+
# Overall label for the document.
|
|
114
|
+
predicted_class: Optional[Literal["ai", "human", "mixed"]]
|
|
115
|
+
# Probabilities for each class.
|
|
116
|
+
class_probabilities: "DetectResponseClassProbabilities"
|
|
117
|
+
request_meta: "DetectResponseRequestMeta"
|
|
118
|
+
# Detected (or supplied) language of the input.
|
|
119
|
+
language: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
ErrorCode = Literal[
|
|
123
|
+
"invalid_request",
|
|
124
|
+
"forbidden_param",
|
|
125
|
+
"word_limit",
|
|
126
|
+
"moderation_blocked",
|
|
127
|
+
"missing_api_key",
|
|
128
|
+
"invalid_api_key",
|
|
129
|
+
"no_subscription",
|
|
130
|
+
"insufficient_balance",
|
|
131
|
+
"reverify_required",
|
|
132
|
+
"enhanced_requires_premium",
|
|
133
|
+
"enhanced_unavailable",
|
|
134
|
+
"rate_limited",
|
|
135
|
+
"auth_locked_out",
|
|
136
|
+
"queue_full",
|
|
137
|
+
"queue_timeout",
|
|
138
|
+
"at_capacity",
|
|
139
|
+
"billing_unavailable",
|
|
140
|
+
"detector_unavailable",
|
|
141
|
+
"timeout",
|
|
142
|
+
"processing_failed",
|
|
143
|
+
"no_results",
|
|
144
|
+
"safety_limit",
|
|
145
|
+
"billing_failed",
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ErrorResponse(TypedDict):
|
|
150
|
+
# Human-readable error message.
|
|
151
|
+
error: str
|
|
152
|
+
code: "ErrorCode"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class _ErrorResponseWithBalanceOptional(TypedDict, total=False):
|
|
156
|
+
words_remaining: "WordsRemaining"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ErrorResponseWithBalance(_ErrorResponseWithBalanceOptional):
|
|
160
|
+
# Human-readable error message.
|
|
161
|
+
error: str
|
|
162
|
+
code: "ErrorCode"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class _ErrorResponseWithUpgradeOptional(TypedDict, total=False):
|
|
166
|
+
# Where to upgrade the plan. Present on `enhanced_requires_premium`.
|
|
167
|
+
upgrade_url: str
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ErrorResponseWithUpgrade(_ErrorResponseWithUpgradeOptional):
|
|
171
|
+
# Human-readable error message.
|
|
172
|
+
error: str
|
|
173
|
+
code: "ErrorCode"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: writehuman
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Official WriteHuman API client for Python: humanize AI text, detect AI authorship, read usage.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://writehuman.ai/api/docs
|
|
@@ -44,6 +44,16 @@ print(score["ai_probability"], score["predicted_class"])
|
|
|
44
44
|
print(wh.usage())
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Premium plans and above can ask for the enhanced model. On a Standard plan this raises
|
|
48
|
+
`PlanRequiredError` (nothing is charged) with `upgrade_url` set; while the enhanced model is
|
|
49
|
+
switched off on the API it raises `FeatureUnavailableError`, so send without `enhanced`. Every
|
|
50
|
+
humanize response's `model` (`"standard"` or `"enhanced"`) says which model served it:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
out = wh.humanize(text, enhanced=True)
|
|
54
|
+
print(out["model"]) # "enhanced"
|
|
55
|
+
```
|
|
56
|
+
|
|
47
57
|
Async, same methods:
|
|
48
58
|
|
|
49
59
|
```python
|
|
@@ -57,13 +67,16 @@ async with AsyncWriteHuman("wh_live_...") as wh:
|
|
|
57
67
|
|
|
58
68
|
Every failure raises a subclass of `WriteHumanError` with `code` (the API's stable error
|
|
59
69
|
code), `status`, `retry_after` (seconds, when the API sent one) and, on
|
|
60
|
-
`InsufficientBalanceError`, `words_remaining`.
|
|
70
|
+
`InsufficientBalanceError`, `words_remaining`; on `PlanRequiredError`, `upgrade_url`.
|
|
61
71
|
|
|
62
72
|
| exception | API `code` |
|
|
63
73
|
|---|---|
|
|
64
74
|
| `AuthenticationError` | `missing_api_key`, `invalid_api_key` |
|
|
65
75
|
| `NoSubscriptionError` | `no_subscription` |
|
|
66
76
|
| `InsufficientBalanceError` | `insufficient_balance` |
|
|
77
|
+
| `ReverifyRequiredError` | `reverify_required` (403: the account owner must re-verify by email at writehuman.ai; not retried) |
|
|
78
|
+
| `PlanRequiredError` | `enhanced_requires_premium` (403: `enhanced=True` on a Standard plan; `upgrade_url` says where to upgrade; nothing charged; not retried) |
|
|
79
|
+
| `FeatureUnavailableError` | `enhanced_unavailable` (403: the enhanced model is switched off for everyone; send without `enhanced`; nothing charged; not retried) |
|
|
67
80
|
| `InvalidRequestError` | `invalid_request`, `forbidden_param`, `word_limit` |
|
|
68
81
|
| `ModerationError` | `moderation_blocked` |
|
|
69
82
|
| `RateLimitError` | `rate_limited`, `auth_locked_out`, `queue_full`, `queue_timeout` |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|