writehuman 0.0.1__tar.gz → 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,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: writehuman
3
+ Version: 0.1.0
4
+ Summary: Official WriteHuman API client for Python: humanize AI text, detect AI authorship, read usage.
5
+ License: MIT
6
+ Project-URL: Homepage, https://writehuman.ai/api/docs
7
+ Project-URL: Documentation, https://writehuman.ai/api/docs
8
+ Project-URL: Source, https://github.com/WriteHuman-ai/rephrase-api/tree/main/sdk/python
9
+ Project-URL: Changelog, https://github.com/WriteHuman-ai/rephrase-api/blob/main/sdk/python/CHANGELOG.md
10
+ Keywords: writehuman,humanize,ai-detection,api-client
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx<1,>=0.25
20
+
21
+ # writehuman
22
+
23
+ Official WriteHuman API client for Python. Humanize AI-written text, score text for AI
24
+ authorship, and read your usage, with typed errors and a retry policy that follows the
25
+ API's own `Retry-After` and never resends a billed request.
26
+
27
+ ```
28
+ pip install writehuman
29
+ ```
30
+
31
+ ```python
32
+ from writehuman import WriteHuman, InsufficientBalanceError, RateLimitError
33
+
34
+ wh = WriteHuman("wh_live_your_api_key_here")
35
+
36
+ out = wh.humanize("Artificial intelligence has transformed numerous industries over the past decade.",
37
+ tone="professional")
38
+ print(out["results"][0])
39
+ print(out["words_remaining"]["total"], "words left")
40
+
41
+ score = wh.detect("Your text to check here.", include_sentences=True)
42
+ print(score["ai_probability"], score["predicted_class"])
43
+
44
+ print(wh.usage())
45
+ ```
46
+
47
+ Async, same methods:
48
+
49
+ ```python
50
+ from writehuman import AsyncWriteHuman
51
+
52
+ async with AsyncWriteHuman("wh_live_...") as wh:
53
+ out = await wh.humanize("...")
54
+ ```
55
+
56
+ ## Errors
57
+
58
+ Every failure raises a subclass of `WriteHumanError` with `code` (the API's stable error
59
+ code), `status`, `retry_after` (seconds, when the API sent one) and, on
60
+ `InsufficientBalanceError`, `words_remaining`.
61
+
62
+ | exception | API `code` |
63
+ |---|---|
64
+ | `AuthenticationError` | `missing_api_key`, `invalid_api_key` |
65
+ | `NoSubscriptionError` | `no_subscription` |
66
+ | `InsufficientBalanceError` | `insufficient_balance` |
67
+ | `InvalidRequestError` | `invalid_request`, `forbidden_param`, `word_limit` |
68
+ | `ModerationError` | `moderation_blocked` |
69
+ | `RateLimitError` | `rate_limited`, `auth_locked_out`, `queue_full`, `queue_timeout` |
70
+ | `ServiceUnavailableError` | `at_capacity`, `billing_unavailable`, `detector_unavailable` |
71
+ | `ServerTimeoutError` | `timeout` (the API's 504: nothing charged) |
72
+ | `ProcessingError` | `processing_failed`, `no_results`, `safety_limit` |
73
+ | `BillingError` | `billing_failed` (some words may have been charged; check `usage()`) |
74
+ | `APIError` | any code this version does not know (`code` is preserved) |
75
+ | `ProtocolError`, `ClientTimeoutError`, `ConnectionError` | no API body |
76
+
77
+ ```python
78
+ try:
79
+ wh.humanize(text)
80
+ except InsufficientBalanceError as e:
81
+ print("out of words:", e.words_remaining)
82
+ except RateLimitError as e:
83
+ print("slow down; the API asked for", e.retry_after, "seconds")
84
+ ```
85
+
86
+ ## Retries and timeouts
87
+
88
+ - `429`, `503` and `504` are retried after exactly the `Retry-After` the API sends, up to
89
+ `max_retries` (default 2). A `Retry-After` above `max_retry_after` (default 60 s, for
90
+ example an authentication lockout) is not slept through: the error is raised at once with
91
+ `retry_after` set. A missing `Retry-After` waits `fallback_retry_after` (default 2 s).
92
+ - Nothing else is retried. `humanize` and `detect` are billed and non-idempotent, so a client
93
+ timeout, a dropped connection, or a 429/503/504 that did not come from the API (a load
94
+ balancer or proxy answering without the API's error body) raises immediately rather than
95
+ risking a second charge. `usage` is read-only and may retry those.
96
+ - Timeouts are total deadlines through the whole response, not per-read limits (`AsyncWriteHuman`
97
+ enforces them exactly; `WriteHuman` checks after every chunk of the body, so it can overrun
98
+ by at most one network read). `timeout=` sets all operations (float) or per operation
99
+ (`{"humanize": 120, "detect": 30, "usage": 15}`, the defaults); `connect_timeout` defaults
100
+ to 10 s. Every method takes a per-call `timeout=`.
101
+
102
+ ```python
103
+ wh = WriteHuman(key, timeout={"humanize": 60}, max_retries=3)
104
+ ```
105
+
106
+ ## Types
107
+
108
+ `HumanizeResponse`, `DetectResponse`, `UsageResponse`, `WordsRemaining` and `ErrorCode`
109
+ are generated from the API's OpenAPI document and exported for type checkers. The package
110
+ ships `py.typed`.
111
+
112
+ This client passes the WriteHuman SDK conformance suite (`sdk/conformance` in the API
113
+ repository), as does the TypeScript client; both implement the same documented policy.
@@ -0,0 +1,93 @@
1
+ # writehuman
2
+
3
+ Official WriteHuman API client for Python. Humanize AI-written text, score text for AI
4
+ authorship, and read your usage, with typed errors and a retry policy that follows the
5
+ API's own `Retry-After` and never resends a billed request.
6
+
7
+ ```
8
+ pip install writehuman
9
+ ```
10
+
11
+ ```python
12
+ from writehuman import WriteHuman, InsufficientBalanceError, RateLimitError
13
+
14
+ wh = WriteHuman("wh_live_your_api_key_here")
15
+
16
+ out = wh.humanize("Artificial intelligence has transformed numerous industries over the past decade.",
17
+ tone="professional")
18
+ print(out["results"][0])
19
+ print(out["words_remaining"]["total"], "words left")
20
+
21
+ score = wh.detect("Your text to check here.", include_sentences=True)
22
+ print(score["ai_probability"], score["predicted_class"])
23
+
24
+ print(wh.usage())
25
+ ```
26
+
27
+ Async, same methods:
28
+
29
+ ```python
30
+ from writehuman import AsyncWriteHuman
31
+
32
+ async with AsyncWriteHuman("wh_live_...") as wh:
33
+ out = await wh.humanize("...")
34
+ ```
35
+
36
+ ## Errors
37
+
38
+ Every failure raises a subclass of `WriteHumanError` with `code` (the API's stable error
39
+ code), `status`, `retry_after` (seconds, when the API sent one) and, on
40
+ `InsufficientBalanceError`, `words_remaining`.
41
+
42
+ | exception | API `code` |
43
+ |---|---|
44
+ | `AuthenticationError` | `missing_api_key`, `invalid_api_key` |
45
+ | `NoSubscriptionError` | `no_subscription` |
46
+ | `InsufficientBalanceError` | `insufficient_balance` |
47
+ | `InvalidRequestError` | `invalid_request`, `forbidden_param`, `word_limit` |
48
+ | `ModerationError` | `moderation_blocked` |
49
+ | `RateLimitError` | `rate_limited`, `auth_locked_out`, `queue_full`, `queue_timeout` |
50
+ | `ServiceUnavailableError` | `at_capacity`, `billing_unavailable`, `detector_unavailable` |
51
+ | `ServerTimeoutError` | `timeout` (the API's 504: nothing charged) |
52
+ | `ProcessingError` | `processing_failed`, `no_results`, `safety_limit` |
53
+ | `BillingError` | `billing_failed` (some words may have been charged; check `usage()`) |
54
+ | `APIError` | any code this version does not know (`code` is preserved) |
55
+ | `ProtocolError`, `ClientTimeoutError`, `ConnectionError` | no API body |
56
+
57
+ ```python
58
+ try:
59
+ wh.humanize(text)
60
+ except InsufficientBalanceError as e:
61
+ print("out of words:", e.words_remaining)
62
+ except RateLimitError as e:
63
+ print("slow down; the API asked for", e.retry_after, "seconds")
64
+ ```
65
+
66
+ ## Retries and timeouts
67
+
68
+ - `429`, `503` and `504` are retried after exactly the `Retry-After` the API sends, up to
69
+ `max_retries` (default 2). A `Retry-After` above `max_retry_after` (default 60 s, for
70
+ example an authentication lockout) is not slept through: the error is raised at once with
71
+ `retry_after` set. A missing `Retry-After` waits `fallback_retry_after` (default 2 s).
72
+ - Nothing else is retried. `humanize` and `detect` are billed and non-idempotent, so a client
73
+ timeout, a dropped connection, or a 429/503/504 that did not come from the API (a load
74
+ balancer or proxy answering without the API's error body) raises immediately rather than
75
+ risking a second charge. `usage` is read-only and may retry those.
76
+ - Timeouts are total deadlines through the whole response, not per-read limits (`AsyncWriteHuman`
77
+ enforces them exactly; `WriteHuman` checks after every chunk of the body, so it can overrun
78
+ by at most one network read). `timeout=` sets all operations (float) or per operation
79
+ (`{"humanize": 120, "detect": 30, "usage": 15}`, the defaults); `connect_timeout` defaults
80
+ to 10 s. Every method takes a per-call `timeout=`.
81
+
82
+ ```python
83
+ wh = WriteHuman(key, timeout={"humanize": 60}, max_retries=3)
84
+ ```
85
+
86
+ ## Types
87
+
88
+ `HumanizeResponse`, `DetectResponse`, `UsageResponse`, `WordsRemaining` and `ErrorCode`
89
+ are generated from the API's OpenAPI document and exported for type checkers. The package
90
+ ships `py.typed`.
91
+
92
+ This client passes the WriteHuman SDK conformance suite (`sdk/conformance` in the API
93
+ repository), as does the TypeScript client; both implement the same documented policy.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "writehuman"
7
+ version = "0.1.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"]
@@ -0,0 +1,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.1.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
+ InsufficientBalanceError,
21
+ InvalidRequestError,
22
+ ModerationError,
23
+ NoSubscriptionError,
24
+ ProcessingError,
25
+ ProtocolError,
26
+ RateLimitError,
27
+ ServerTimeoutError,
28
+ ServiceUnavailableError,
29
+ WriteHumanError,
30
+ )
31
+ from .types import ( # noqa: E402
32
+ API_VERSION,
33
+ DetectResponse,
34
+ ErrorCode,
35
+ HumanizeResponse,
36
+ UsageResponse,
37
+ WordsRemaining,
38
+ )
39
+
40
+ __all__ = [
41
+ "__version__", "API_VERSION", "DEFAULT_BASE_URL", "USER_AGENT",
42
+ "WriteHuman", "AsyncWriteHuman",
43
+ "WriteHumanError", "APIError", "AuthenticationError", "NoSubscriptionError", "InsufficientBalanceError",
44
+ "InvalidRequestError", "ModerationError", "RateLimitError", "ServiceUnavailableError", "ServerTimeoutError",
45
+ "ProcessingError", "BillingError", "ProtocolError", "ClientTimeoutError", "ConnectionError",
46
+ "HumanizeResponse", "DetectResponse", "UsageResponse", "WordsRemaining", "ErrorCode",
47
+ ]
@@ -0,0 +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
@@ -0,0 +1,232 @@
1
+ """Sync and async clients for the WriteHuman API.
2
+
3
+ from writehuman import WriteHuman
4
+ wh = WriteHuman("wh_live_...")
5
+ out = wh.humanize("Artificial intelligence has transformed ...", tone="professional")
6
+ print(out["results"][0], out["words_remaining"]["total"])
7
+
8
+ from writehuman import AsyncWriteHuman
9
+ async with AsyncWriteHuman("wh_live_...") as wh:
10
+ score = await wh.detect("...", include_sentences=True)
11
+
12
+ Both follow the policy in sdk/conformance/README.md and pass the same conformance suite.
13
+ """
14
+ import asyncio
15
+ import time
16
+ from typing import Any, Dict, Optional, Tuple, Union
17
+
18
+ import httpx
19
+
20
+ from . import __version__
21
+ from ._policy import (
22
+ DEFAULT_CONNECT_TIMEOUT,
23
+ DEFAULT_FALLBACK_RETRY_AFTER,
24
+ DEFAULT_MAX_RETRIES,
25
+ DEFAULT_MAX_RETRY_AFTER,
26
+ DEFAULT_TIMEOUTS,
27
+ OPERATIONS,
28
+ Policy,
29
+ interpret,
30
+ )
31
+ from .errors import ClientTimeoutError, ConnectionError, ProtocolError, WriteHumanError
32
+ from .types import DetectResponse, HumanizeResponse, UsageResponse
33
+
34
+ DEFAULT_BASE_URL = "https://api.writehuman.ai"
35
+ USER_AGENT = f"writehuman-python/{__version__}"
36
+
37
+ Timeouts = Union[float, Dict[str, float], None]
38
+
39
+
40
+ def _timeouts(timeout: Timeouts) -> Dict[str, float]:
41
+ if timeout is None:
42
+ return dict(DEFAULT_TIMEOUTS)
43
+ if isinstance(timeout, (int, float)):
44
+ return {k: float(timeout) for k in DEFAULT_TIMEOUTS}
45
+ return {**DEFAULT_TIMEOUTS, **{k: float(v) for k, v in timeout.items()}}
46
+
47
+
48
+ class _Base:
49
+ def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE_URL, timeout: Timeouts = None,
50
+ connect_timeout: float = DEFAULT_CONNECT_TIMEOUT, max_retries: int = DEFAULT_MAX_RETRIES,
51
+ max_retry_after: float = DEFAULT_MAX_RETRY_AFTER,
52
+ fallback_retry_after: float = DEFAULT_FALLBACK_RETRY_AFTER):
53
+ if not api_key or not isinstance(api_key, str):
54
+ raise ValueError("api_key is required")
55
+ self.api_key = api_key
56
+ self.base_url = base_url.rstrip("/")
57
+ self.timeouts = _timeouts(timeout)
58
+ self.connect_timeout = float(connect_timeout)
59
+ self.policy = Policy(max_retries=max_retries, max_retry_after=max_retry_after,
60
+ fallback_retry_after=fallback_retry_after)
61
+
62
+ def _headers(self) -> Dict[str, str]:
63
+ return {"Authorization": f"Bearer {self.api_key}", "User-Agent": USER_AGENT, "Accept": "application/json"}
64
+
65
+ def _total(self, operation: str, override: Optional[float]) -> float:
66
+ return float(override) if override is not None else self.timeouts[operation]
67
+
68
+ def _httpx_timeout(self, operation: str, override: Optional[float]) -> httpx.Timeout:
69
+ total = self._total(operation, override)
70
+ return httpx.Timeout(total, connect=min(self.connect_timeout, total))
71
+
72
+ @staticmethod
73
+ def _humanize_body(text: str, tone: Optional[str], language: Optional[str]) -> Dict[str, Any]:
74
+ body: Dict[str, Any] = {"text": text}
75
+ if tone is not None:
76
+ body["tone"] = tone
77
+ if language is not None:
78
+ body["language"] = language
79
+ return body
80
+
81
+ @staticmethod
82
+ def _detect_body(text: str, language: Optional[str], include_sentences: bool) -> Dict[str, Any]:
83
+ body: Dict[str, Any] = {"text": text, "include_sentences": bool(include_sentences)}
84
+ if language is not None:
85
+ body["language"] = language
86
+ return body
87
+
88
+
89
+ class WriteHuman(_Base):
90
+ """Synchronous client. Thread-safe for concurrent calls (httpx.Client is)."""
91
+
92
+ def __init__(self, api_key: str, *, http_client: Optional[httpx.Client] = None, **options: Any):
93
+ super().__init__(api_key, **options)
94
+ self._client = http_client or httpx.Client()
95
+ self._owns_client = http_client is None
96
+
97
+ def close(self) -> None:
98
+ if self._owns_client:
99
+ self._client.close()
100
+
101
+ def __enter__(self) -> "WriteHuman":
102
+ return self
103
+
104
+ def __exit__(self, *exc: Any) -> None:
105
+ self.close()
106
+
107
+ def humanize(self, text: str, *, tone: Optional[str] = None, language: Optional[str] = None,
108
+ timeout: Optional[float] = None) -> HumanizeResponse:
109
+ return self._call("humanize", self._humanize_body(text, tone, language), timeout) # type: ignore[return-value]
110
+
111
+ def detect(self, text: str, *, language: Optional[str] = None, include_sentences: bool = False,
112
+ timeout: Optional[float] = None) -> DetectResponse:
113
+ return self._call("detect", self._detect_body(text, language, include_sentences), timeout) # type: ignore[return-value]
114
+
115
+ def usage(self, *, timeout: Optional[float] = None) -> UsageResponse:
116
+ return self._call("usage", None, timeout) # type: ignore[return-value]
117
+
118
+ def _call(self, operation: str, body: Optional[dict], timeout: Optional[float]) -> dict:
119
+ method, path, idempotent = OPERATIONS[operation]
120
+ url = self.base_url + path
121
+ attempt = 0
122
+ while True:
123
+ try:
124
+ status, headers, text = self._send(method, url, body, operation, timeout)
125
+ except httpx.TimeoutException as exc:
126
+ wait = self.policy.wait_after_transport_error(idempotent, attempt)
127
+ if wait is None:
128
+ raise ClientTimeoutError(f"{operation}: no response within the client timeout") from exc
129
+ attempt += 1
130
+ time.sleep(wait)
131
+ continue
132
+ except httpx.TransportError as exc:
133
+ wait = self.policy.wait_after_transport_error(idempotent, attempt)
134
+ if wait is None:
135
+ raise ConnectionError(f"{operation}: could not reach {self.base_url}: {exc}") from exc
136
+ attempt += 1
137
+ time.sleep(wait)
138
+ continue
139
+ data, err, retry_after = interpret(status, headers, text)
140
+ if err is None:
141
+ return data # type: ignore[return-value]
142
+ wait = self.policy.wait_after_response(status, retry_after, attempt,
143
+ from_api=not isinstance(err, ProtocolError),
144
+ idempotent=idempotent)
145
+ if wait is None:
146
+ raise err
147
+ attempt += 1
148
+ time.sleep(wait)
149
+
150
+ def _send(self, method: str, url: str, body: Optional[dict], operation: str,
151
+ timeout: Optional[float]) -> Tuple[int, Any, str]:
152
+ """One attempt. The timeout is a total deadline through the whole body: httpx's own
153
+ timeouts bound each read, so the body is streamed and the elapsed time is checked
154
+ after every chunk; a response can overrun the deadline by at most one network read."""
155
+ deadline = time.monotonic() + self._total(operation, timeout)
156
+ with self._client.stream(method, url, headers=self._headers(), json=body,
157
+ timeout=self._httpx_timeout(operation, timeout)) as resp:
158
+ chunks = []
159
+ for chunk in resp.iter_bytes():
160
+ chunks.append(chunk)
161
+ if time.monotonic() > deadline:
162
+ raise httpx.ReadTimeout("total client timeout exceeded while reading the response")
163
+ return resp.status_code, resp.headers, b"".join(chunks).decode(resp.encoding or "utf-8", "replace")
164
+
165
+
166
+ class AsyncWriteHuman(_Base):
167
+ """Asynchronous client, same policy and same results as WriteHuman."""
168
+
169
+ def __init__(self, api_key: str, *, http_client: Optional[httpx.AsyncClient] = None, **options: Any):
170
+ super().__init__(api_key, **options)
171
+ self._client = http_client or httpx.AsyncClient()
172
+ self._owns_client = http_client is None
173
+
174
+ async def aclose(self) -> None:
175
+ if self._owns_client:
176
+ await self._client.aclose()
177
+
178
+ async def __aenter__(self) -> "AsyncWriteHuman":
179
+ return self
180
+
181
+ async def __aexit__(self, *exc: Any) -> None:
182
+ await self.aclose()
183
+
184
+ async def humanize(self, text: str, *, tone: Optional[str] = None, language: Optional[str] = None,
185
+ timeout: Optional[float] = None) -> HumanizeResponse:
186
+ return await self._call("humanize", self._humanize_body(text, tone, language), timeout) # type: ignore[return-value]
187
+
188
+ async def detect(self, text: str, *, language: Optional[str] = None, include_sentences: bool = False,
189
+ timeout: Optional[float] = None) -> DetectResponse:
190
+ return await self._call("detect", self._detect_body(text, language, include_sentences), timeout) # type: ignore[return-value]
191
+
192
+ async def usage(self, *, timeout: Optional[float] = None) -> UsageResponse:
193
+ return await self._call("usage", None, timeout) # type: ignore[return-value]
194
+
195
+ async def _call(self, operation: str, body: Optional[dict], timeout: Optional[float]) -> dict:
196
+ method, path, idempotent = OPERATIONS[operation]
197
+ url = self.base_url + path
198
+ attempt = 0
199
+ while True:
200
+ try:
201
+ # total deadline through the whole body (httpx's own timeouts bound each read)
202
+ resp = await asyncio.wait_for(
203
+ self._client.request(method, url, headers=self._headers(), json=body,
204
+ timeout=self._httpx_timeout(operation, timeout)),
205
+ timeout=self._total(operation, timeout))
206
+ except (httpx.TimeoutException, asyncio.TimeoutError) as exc:
207
+ wait = self.policy.wait_after_transport_error(idempotent, attempt)
208
+ if wait is None:
209
+ raise ClientTimeoutError(f"{operation}: no response within the client timeout") from exc
210
+ attempt += 1
211
+ await asyncio.sleep(wait)
212
+ continue
213
+ except httpx.TransportError as exc:
214
+ wait = self.policy.wait_after_transport_error(idempotent, attempt)
215
+ if wait is None:
216
+ raise ConnectionError(f"{operation}: could not reach {self.base_url}: {exc}") from exc
217
+ attempt += 1
218
+ await asyncio.sleep(wait)
219
+ continue
220
+ data, err, retry_after = interpret(resp.status_code, resp.headers, resp.text)
221
+ if err is None:
222
+ return data # type: ignore[return-value]
223
+ wait = self.policy.wait_after_response(resp.status_code, retry_after, attempt,
224
+ from_api=not isinstance(err, ProtocolError),
225
+ idempotent=idempotent)
226
+ if wait is None:
227
+ raise err
228
+ attempt += 1
229
+ await asyncio.sleep(wait)
230
+
231
+
232
+ __all__ = ["WriteHuman", "AsyncWriteHuman", "WriteHumanError", "DEFAULT_BASE_URL", "USER_AGENT"]
@@ -0,0 +1,134 @@
1
+ """Typed errors. Every error carries the attributes the conformance suite reads:
2
+
3
+ kind abstract type (see sdk/conformance/README.md); one class per kind
4
+ code the API's `code`, or None when there was no API body
5
+ status HTTP status, or None for client-side failures
6
+ retry_after seconds the API asked us to wait (from the last response), or None
7
+ words_remaining on InsufficientBalanceError: {"included", "topup", "total"}
8
+ """
9
+ from typing import Optional
10
+
11
+ from .types import ErrorCode, WordsRemaining
12
+
13
+
14
+ class WriteHumanError(Exception):
15
+ """Base of every error this library raises."""
16
+
17
+ kind = "api"
18
+
19
+ 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
+ super().__init__(message)
22
+ self.message = message
23
+ self.status = status
24
+ self.code = code
25
+ self.retry_after = retry_after
26
+ self.words_remaining = words_remaining
27
+
28
+ def __repr__(self) -> str:
29
+ return f"{type(self).__name__}({self.message!r}, status={self.status}, code={self.code!r})"
30
+
31
+
32
+ class APIError(WriteHumanError):
33
+ """A well-formed error whose `code` this version of the library does not know."""
34
+ kind = "api"
35
+
36
+
37
+ class AuthenticationError(WriteHumanError):
38
+ """missing_api_key, invalid_api_key."""
39
+ kind = "authentication"
40
+
41
+
42
+ class NoSubscriptionError(WriteHumanError):
43
+ """The key is valid but the account has no active API subscription."""
44
+ kind = "no_subscription"
45
+
46
+
47
+ class InsufficientBalanceError(WriteHumanError):
48
+ """Out of words. `words_remaining` says what is left."""
49
+ kind = "insufficient_balance"
50
+
51
+
52
+ class InvalidRequestError(WriteHumanError):
53
+ """invalid_request, forbidden_param, word_limit."""
54
+ kind = "invalid_request"
55
+
56
+
57
+ class ModerationError(WriteHumanError):
58
+ """Content moderation refused the input."""
59
+ kind = "moderation"
60
+
61
+
62
+ class RateLimitError(WriteHumanError):
63
+ """rate_limited, auth_locked_out, queue_full, queue_timeout. Retried per Retry-After."""
64
+ kind = "rate_limit"
65
+
66
+
67
+ class ServiceUnavailableError(WriteHumanError):
68
+ """at_capacity, billing_unavailable, detector_unavailable. Retried per Retry-After."""
69
+ kind = "service_unavailable"
70
+
71
+
72
+ class ServerTimeoutError(WriteHumanError):
73
+ """The API's own deadline (504 `timeout`): nothing charged, retried per Retry-After."""
74
+ kind = "server_timeout"
75
+
76
+
77
+ class ProcessingError(WriteHumanError):
78
+ """processing_failed, no_results, safety_limit. Never retried."""
79
+ kind = "processing"
80
+
81
+
82
+ class BillingError(WriteHumanError):
83
+ """billing_failed: the result was produced but the usage write failed part-way; some
84
+ words may have been charged. Never retried; check usage()."""
85
+ kind = "billing"
86
+
87
+
88
+ class ProtocolError(WriteHumanError):
89
+ """The response was not the JSON the contract promises."""
90
+ kind = "protocol"
91
+
92
+
93
+ class ClientTimeoutError(WriteHumanError):
94
+ """No response within the client timeout. Not retried on humanize/detect (billed)."""
95
+ kind = "client_timeout"
96
+
97
+
98
+ class ConnectionError(WriteHumanError): # noqa: A001 — mirrors the TypeScript name on purpose
99
+ """The connection failed. Not retried on humanize/detect (billed)."""
100
+ kind = "connection"
101
+
102
+
103
+ KIND_BY_CODE = {
104
+ "missing_api_key": "authentication", "invalid_api_key": "authentication",
105
+ "no_subscription": "no_subscription",
106
+ "insufficient_balance": "insufficient_balance",
107
+ "invalid_request": "invalid_request", "forbidden_param": "invalid_request", "word_limit": "invalid_request",
108
+ "moderation_blocked": "moderation",
109
+ "rate_limited": "rate_limit", "auth_locked_out": "rate_limit", "queue_full": "rate_limit", "queue_timeout": "rate_limit",
110
+ "at_capacity": "service_unavailable", "billing_unavailable": "service_unavailable", "detector_unavailable": "service_unavailable",
111
+ "timeout": "server_timeout",
112
+ "processing_failed": "processing", "no_results": "processing", "safety_limit": "processing",
113
+ "billing_failed": "billing",
114
+ }
115
+
116
+ CLASS_BY_KIND = {cls.kind: cls for cls in (
117
+ APIError, AuthenticationError, NoSubscriptionError, InsufficientBalanceError, InvalidRequestError,
118
+ ModerationError, RateLimitError, ServiceUnavailableError, ServerTimeoutError, ProcessingError, BillingError,
119
+ )}
120
+
121
+
122
+ def error_from_body(status: int, body: object, retry_after: Optional[float]) -> WriteHumanError:
123
+ """Build the typed error for a non-2xx response."""
124
+ # The API's error body always has a string `error` and a string `code`. Anything else (an
125
+ # HTML page, or JSON like {"error": "Gateway timeout"} with no code) came from a load
126
+ # balancer or proxy, which gives no guarantee about whether the request was processed.
127
+ if not isinstance(body, dict) or not isinstance(body.get("error"), str) or not isinstance(body.get("code"), str):
128
+ return ProtocolError(f"HTTP {status} with a body that is not the API's error shape", status=status,
129
+ retry_after=retry_after)
130
+ code: Optional[ErrorCode] = body["code"] # type: ignore[assignment]
131
+ cls = CLASS_BY_KIND.get(KIND_BY_CODE.get(code or "", "api"), APIError)
132
+ wr = body.get("words_remaining")
133
+ return cls(body["error"], status=status, code=code, retry_after=retry_after,
134
+ words_remaining=wr if isinstance(wr, dict) else None)
File without changes
@@ -0,0 +1,155 @@
1
+ # GENERATED from src/openapi.json by sdk/gen_types.py. Do not edit; run the generator.
2
+ # Contract version 1.1.
3
+ from typing import Any, List, Literal, Optional, TypedDict
4
+
5
+ API_VERSION = "v1.1"
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
+ class HumanizeRequest(_HumanizeRequestOptional):
16
+ # The text to humanize.
17
+ text: str
18
+
19
+
20
+ class WordsRemaining(TypedDict):
21
+ # Words remaining in your monthly subscription allowance.
22
+ included: int
23
+ # Words remaining from purchased top-ups.
24
+ topup: int
25
+ # Total words available (included + topup).
26
+ total: int
27
+
28
+
29
+ class HumanizeResponse(TypedDict):
30
+ # Unique identifier for this humanization request.
31
+ id: str
32
+ # Unix timestamp (milliseconds) when the request was received.
33
+ created: int
34
+ # Humanized text variations, sorted by score (best first).
35
+ results: List[str]
36
+ # Human-likeness score per result (0–1, higher = more human). `null` on Standard plans; array on Premium.
37
+ scores: Optional[List[Optional[float]]]
38
+ # Number of words billed for this request.
39
+ input_words: int
40
+ words_remaining: "WordsRemaining"
41
+
42
+
43
+ class UsageResponse(TypedDict):
44
+ # Monthly word allowance for your plan.
45
+ included_words: int
46
+ # Words consumed in the current billing cycle.
47
+ words_used_this_cycle: int
48
+ # Remaining purchased top-up words.
49
+ topup_balance: int
50
+ # Your plan. Every Premium size reports `api_premium`.
51
+ tier: Literal["api_standard", "api_premium"]
52
+ # API version.
53
+ version: str
54
+
55
+
56
+ class _DetectRequestOptional(TypedDict, total=False):
57
+ # Language of the input (e.g. `English`, `Spanish`). Auto-detected if omitted. Non-English text is translated to English before scoring.
58
+ language: Optional[str]
59
+ # Include per-sentence AI probabilities in the response.
60
+ include_sentences: bool
61
+
62
+
63
+ class DetectRequest(_DetectRequestOptional):
64
+ # The text to score.
65
+ text: str
66
+
67
+
68
+ class _DetectResponseClassProbabilitiesOptional(TypedDict, total=False):
69
+ ai: Optional[float]
70
+ human: Optional[float]
71
+ mixed: Optional[float]
72
+
73
+
74
+ class DetectResponseClassProbabilities(_DetectResponseClassProbabilitiesOptional):
75
+ """Probabilities for each class."""
76
+ pass
77
+
78
+
79
+ class DetectResponseRequestMetaDocLength(TypedDict):
80
+ """Length of the scored text."""
81
+ chars: Optional[int]
82
+ tokens: Optional[int]
83
+
84
+
85
+ class DetectResponseRequestMeta(TypedDict):
86
+ # Length of the scored text.
87
+ doc_length: "DetectResponseRequestMetaDocLength"
88
+
89
+
90
+ class DetectResponseSentencesItem(TypedDict):
91
+ text: Optional[str]
92
+ ai_prob: Optional[float]
93
+
94
+
95
+ class _DetectResponseOptional(TypedDict, total=False):
96
+ # Per-sentence AI probabilities. Present only when `include_sentences` is `true`.
97
+ sentences: List["DetectResponseSentencesItem"]
98
+
99
+
100
+ class DetectResponse(_DetectResponseOptional):
101
+ # Identifier for this detection scan.
102
+ scanId: Optional[str]
103
+ # Detector model version.
104
+ version: Optional[str]
105
+ # Detector build identifier.
106
+ versionID: Optional[str]
107
+ # Probability the text is AI-generated (`1` = AI — opposite orientation of `/v1/humanize` scores).
108
+ ai_probability: Optional[float]
109
+ # Overall label for the document.
110
+ predicted_class: Optional[Literal["ai", "human", "mixed"]]
111
+ # Probabilities for each class.
112
+ class_probabilities: "DetectResponseClassProbabilities"
113
+ request_meta: "DetectResponseRequestMeta"
114
+ # Detected (or supplied) language of the input.
115
+ language: str
116
+
117
+
118
+ ErrorCode = Literal[
119
+ "invalid_request",
120
+ "forbidden_param",
121
+ "word_limit",
122
+ "moderation_blocked",
123
+ "missing_api_key",
124
+ "invalid_api_key",
125
+ "no_subscription",
126
+ "insufficient_balance",
127
+ "rate_limited",
128
+ "auth_locked_out",
129
+ "queue_full",
130
+ "queue_timeout",
131
+ "at_capacity",
132
+ "billing_unavailable",
133
+ "detector_unavailable",
134
+ "timeout",
135
+ "processing_failed",
136
+ "no_results",
137
+ "safety_limit",
138
+ "billing_failed",
139
+ ]
140
+
141
+
142
+ class ErrorResponse(TypedDict):
143
+ # Human-readable error message.
144
+ error: str
145
+ code: "ErrorCode"
146
+
147
+
148
+ class _ErrorResponseWithBalanceOptional(TypedDict, total=False):
149
+ words_remaining: "WordsRemaining"
150
+
151
+
152
+ class ErrorResponseWithBalance(_ErrorResponseWithBalanceOptional):
153
+ # Human-readable error message.
154
+ error: str
155
+ code: "ErrorCode"
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: writehuman
3
+ Version: 0.1.0
4
+ Summary: Official WriteHuman API client for Python: humanize AI text, detect AI authorship, read usage.
5
+ License: MIT
6
+ Project-URL: Homepage, https://writehuman.ai/api/docs
7
+ Project-URL: Documentation, https://writehuman.ai/api/docs
8
+ Project-URL: Source, https://github.com/WriteHuman-ai/rephrase-api/tree/main/sdk/python
9
+ Project-URL: Changelog, https://github.com/WriteHuman-ai/rephrase-api/blob/main/sdk/python/CHANGELOG.md
10
+ Keywords: writehuman,humanize,ai-detection,api-client
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx<1,>=0.25
20
+
21
+ # writehuman
22
+
23
+ Official WriteHuman API client for Python. Humanize AI-written text, score text for AI
24
+ authorship, and read your usage, with typed errors and a retry policy that follows the
25
+ API's own `Retry-After` and never resends a billed request.
26
+
27
+ ```
28
+ pip install writehuman
29
+ ```
30
+
31
+ ```python
32
+ from writehuman import WriteHuman, InsufficientBalanceError, RateLimitError
33
+
34
+ wh = WriteHuman("wh_live_your_api_key_here")
35
+
36
+ out = wh.humanize("Artificial intelligence has transformed numerous industries over the past decade.",
37
+ tone="professional")
38
+ print(out["results"][0])
39
+ print(out["words_remaining"]["total"], "words left")
40
+
41
+ score = wh.detect("Your text to check here.", include_sentences=True)
42
+ print(score["ai_probability"], score["predicted_class"])
43
+
44
+ print(wh.usage())
45
+ ```
46
+
47
+ Async, same methods:
48
+
49
+ ```python
50
+ from writehuman import AsyncWriteHuman
51
+
52
+ async with AsyncWriteHuman("wh_live_...") as wh:
53
+ out = await wh.humanize("...")
54
+ ```
55
+
56
+ ## Errors
57
+
58
+ Every failure raises a subclass of `WriteHumanError` with `code` (the API's stable error
59
+ code), `status`, `retry_after` (seconds, when the API sent one) and, on
60
+ `InsufficientBalanceError`, `words_remaining`.
61
+
62
+ | exception | API `code` |
63
+ |---|---|
64
+ | `AuthenticationError` | `missing_api_key`, `invalid_api_key` |
65
+ | `NoSubscriptionError` | `no_subscription` |
66
+ | `InsufficientBalanceError` | `insufficient_balance` |
67
+ | `InvalidRequestError` | `invalid_request`, `forbidden_param`, `word_limit` |
68
+ | `ModerationError` | `moderation_blocked` |
69
+ | `RateLimitError` | `rate_limited`, `auth_locked_out`, `queue_full`, `queue_timeout` |
70
+ | `ServiceUnavailableError` | `at_capacity`, `billing_unavailable`, `detector_unavailable` |
71
+ | `ServerTimeoutError` | `timeout` (the API's 504: nothing charged) |
72
+ | `ProcessingError` | `processing_failed`, `no_results`, `safety_limit` |
73
+ | `BillingError` | `billing_failed` (some words may have been charged; check `usage()`) |
74
+ | `APIError` | any code this version does not know (`code` is preserved) |
75
+ | `ProtocolError`, `ClientTimeoutError`, `ConnectionError` | no API body |
76
+
77
+ ```python
78
+ try:
79
+ wh.humanize(text)
80
+ except InsufficientBalanceError as e:
81
+ print("out of words:", e.words_remaining)
82
+ except RateLimitError as e:
83
+ print("slow down; the API asked for", e.retry_after, "seconds")
84
+ ```
85
+
86
+ ## Retries and timeouts
87
+
88
+ - `429`, `503` and `504` are retried after exactly the `Retry-After` the API sends, up to
89
+ `max_retries` (default 2). A `Retry-After` above `max_retry_after` (default 60 s, for
90
+ example an authentication lockout) is not slept through: the error is raised at once with
91
+ `retry_after` set. A missing `Retry-After` waits `fallback_retry_after` (default 2 s).
92
+ - Nothing else is retried. `humanize` and `detect` are billed and non-idempotent, so a client
93
+ timeout, a dropped connection, or a 429/503/504 that did not come from the API (a load
94
+ balancer or proxy answering without the API's error body) raises immediately rather than
95
+ risking a second charge. `usage` is read-only and may retry those.
96
+ - Timeouts are total deadlines through the whole response, not per-read limits (`AsyncWriteHuman`
97
+ enforces them exactly; `WriteHuman` checks after every chunk of the body, so it can overrun
98
+ by at most one network read). `timeout=` sets all operations (float) or per operation
99
+ (`{"humanize": 120, "detect": 30, "usage": 15}`, the defaults); `connect_timeout` defaults
100
+ to 10 s. Every method takes a per-call `timeout=`.
101
+
102
+ ```python
103
+ wh = WriteHuman(key, timeout={"humanize": 60}, max_retries=3)
104
+ ```
105
+
106
+ ## Types
107
+
108
+ `HumanizeResponse`, `DetectResponse`, `UsageResponse`, `WordsRemaining` and `ErrorCode`
109
+ are generated from the API's OpenAPI document and exported for type checkers. The package
110
+ ships `py.typed`.
111
+
112
+ This client passes the WriteHuman SDK conformance suite (`sdk/conformance` in the API
113
+ repository), as does the TypeScript client; both implement the same documented policy.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/writehuman/__init__.py
4
+ src/writehuman/_policy.py
5
+ src/writehuman/client.py
6
+ src/writehuman/errors.py
7
+ src/writehuman/py.typed
8
+ src/writehuman/types.py
9
+ src/writehuman.egg-info/PKG-INFO
10
+ src/writehuman.egg-info/SOURCES.txt
11
+ src/writehuman.egg-info/dependency_links.txt
12
+ src/writehuman.egg-info/requires.txt
13
+ src/writehuman.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ httpx<1,>=0.25
writehuman-0.0.1/PKG-INFO DELETED
@@ -1,19 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: writehuman
3
- Version: 0.0.1
4
- Summary: Official WriteHuman API client for Python (placeholder release; the client ships in 0.1).
5
- License: MIT
6
- Project-URL: Homepage, https://writehuman.ai/api/docs
7
- Project-URL: Source, https://github.com/WriteHuman-ai/rephrase-api/tree/main/sdk/python
8
- Keywords: writehuman,humanize,ai-detection,api-client
9
- Classifier: Development Status :: 1 - Planning
10
- Classifier: Programming Language :: Python :: 3
11
- Requires-Python: >=3.9
12
- Description-Content-Type: text/markdown
13
-
14
- # writehuman
15
-
16
- Official WriteHuman API client for Python.
17
-
18
- This is a placeholder release that reserves the package name. The client ships in 0.1.
19
- Until then, call the REST API directly: https://writehuman.ai/api/docs
@@ -1,6 +0,0 @@
1
- # writehuman
2
-
3
- Official WriteHuman API client for Python.
4
-
5
- This is a placeholder release that reserves the package name. The client ships in 0.1.
6
- Until then, call the REST API directly: https://writehuman.ai/api/docs
@@ -1,20 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=68"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "writehuman"
7
- version = "0.0.1"
8
- description = "Official WriteHuman API client for Python (placeholder release; the client ships in 0.1)."
9
- readme = "README.md"
10
- license = { text = "MIT" }
11
- requires-python = ">=3.9"
12
- keywords = ["writehuman", "humanize", "ai-detection", "api-client"]
13
- classifiers = ["Development Status :: 1 - Planning", "Programming Language :: Python :: 3"]
14
-
15
- [project.urls]
16
- Homepage = "https://writehuman.ai/api/docs"
17
- Source = "https://github.com/WriteHuman-ai/rephrase-api/tree/main/sdk/python"
18
-
19
- [tool.setuptools.packages.find]
20
- where = ["src"]
@@ -1,3 +0,0 @@
1
- """WriteHuman API client. Placeholder release; the client ships in 0.1."""
2
-
3
- __version__ = "0.0.1"
@@ -1,19 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: writehuman
3
- Version: 0.0.1
4
- Summary: Official WriteHuman API client for Python (placeholder release; the client ships in 0.1).
5
- License: MIT
6
- Project-URL: Homepage, https://writehuman.ai/api/docs
7
- Project-URL: Source, https://github.com/WriteHuman-ai/rephrase-api/tree/main/sdk/python
8
- Keywords: writehuman,humanize,ai-detection,api-client
9
- Classifier: Development Status :: 1 - Planning
10
- Classifier: Programming Language :: Python :: 3
11
- Requires-Python: >=3.9
12
- Description-Content-Type: text/markdown
13
-
14
- # writehuman
15
-
16
- Official WriteHuman API client for Python.
17
-
18
- This is a placeholder release that reserves the package name. The client ships in 0.1.
19
- Until then, call the REST API directly: https://writehuman.ai/api/docs
@@ -1,7 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- src/writehuman/__init__.py
4
- src/writehuman.egg-info/PKG-INFO
5
- src/writehuman.egg-info/SOURCES.txt
6
- src/writehuman.egg-info/dependency_links.txt
7
- src/writehuman.egg-info/top_level.txt
File without changes