supertext-sdk 0.6.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.
- supertext_sdk-0.6.0/LICENSE +21 -0
- supertext_sdk-0.6.0/PKG-INFO +263 -0
- supertext_sdk-0.6.0/README.md +241 -0
- supertext_sdk-0.6.0/pyproject.toml +54 -0
- supertext_sdk-0.6.0/setup.cfg +4 -0
- supertext_sdk-0.6.0/src/supertext/__init__.py +16 -0
- supertext_sdk-0.6.0/src/supertext/files.py +859 -0
- supertext_sdk-0.6.0/src/supertext/py.typed +0 -0
- supertext_sdk-0.6.0/src/supertext/transport.py +68 -0
- supertext_sdk-0.6.0/src/supertext/v1/__init__.py +62 -0
- supertext_sdk-0.6.0/src/supertext/v1/api_key_check.py +114 -0
- supertext_sdk-0.6.0/src/supertext/v1/client.py +401 -0
- supertext_sdk-0.6.0/src/supertext/v1/corrections.py +124 -0
- supertext_sdk-0.6.0/src/supertext/v1/errors.py +156 -0
- supertext_sdk-0.6.0/src/supertext/v1/html_document.py +135 -0
- supertext_sdk-0.6.0/src/supertext/v1/languages.py +289 -0
- supertext_sdk-0.6.0/src/supertext/version.py +5 -0
- supertext_sdk-0.6.0/src/supertext_sdk.egg-info/PKG-INFO +263 -0
- supertext_sdk-0.6.0/src/supertext_sdk.egg-info/SOURCES.txt +23 -0
- supertext_sdk-0.6.0/src/supertext_sdk.egg-info/dependency_links.txt +1 -0
- supertext_sdk-0.6.0/src/supertext_sdk.egg-info/top_level.txt +1 -0
- supertext_sdk-0.6.0/tests/test_api_key_check.py +130 -0
- supertext_sdk-0.6.0/tests/test_client.py +458 -0
- supertext_sdk-0.6.0/tests/test_files.py +138 -0
- supertext_sdk-0.6.0/tests/test_spec.py +253 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Supertext AG
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: supertext-sdk
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Client for the Supertext AI translation API: translate HTML and texts, with language codes, rate-limit retries and HTML packing handled for you.
|
|
5
|
+
Author: Supertext AG
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Supertext/Supertext-API-SDK-Guide
|
|
8
|
+
Project-URL: Documentation, https://github.com/Supertext/Supertext-API-SDK-Guide/tree/main/sdk/python
|
|
9
|
+
Project-URL: Issues, https://github.com/Supertext/Supertext-API-SDK-Guide/issues
|
|
10
|
+
Keywords: supertext,translation,machine translation,localization,i18n
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Software Development :: Internationalization
|
|
16
|
+
Classifier: Topic :: Software Development :: Localization
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Supertext SDK for Python
|
|
24
|
+
|
|
25
|
+
Client for the [Supertext](https://www.supertext.com) AI translation API: translate HTML and texts, with language codes, rate-limit retries and HTML packing handled for you.
|
|
26
|
+
|
|
27
|
+
- Python 3.11+, standard library only (no dependencies), fully typed.
|
|
28
|
+
- Packs what Supertext's CMS, PIM and shop plugins learned against the live API. Background: [integration guide](../../docs/integration-guide.md).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install supertext-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from supertext import v1 # or: from supertext.v1 import SupertextClient
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Everything that talks to the API lives in `supertext.v1` (API v1). A later API version gets `supertext.v2` next to it, so an integration moves when it's ready. The top-level package holds only `v1` and `SDK_VERSION`.
|
|
41
|
+
|
|
42
|
+
## API key
|
|
43
|
+
|
|
44
|
+
The client needs a Supertext AI API key. No Supertext account yet? Create one at https://www.supertext.com/person/en/account/signin. Generate your API key at https://www.supertext.com/en/integrations/api (supertext.com → Integrations → API; requires the Admin role).
|
|
45
|
+
|
|
46
|
+
Paste it as shown there, with or without the `Supertext-Auth-Key` prefix. Keep it out of your code, e.g. in the `SUPERTEXT_API_KEY` environment variable. `client.validate_api_key()` checks it without cost.
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
### Texts (fields of a page, product, …)
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import os
|
|
54
|
+
from supertext import v1
|
|
55
|
+
from supertext.v1 import Languages, Segment
|
|
56
|
+
|
|
57
|
+
client = v1.SupertextClient(os.environ["SUPERTEXT_API_KEY"], user_agent="MyCms-Supertext/1.0.0")
|
|
58
|
+
|
|
59
|
+
translations = client.translate_texts(
|
|
60
|
+
{
|
|
61
|
+
"title": "Hello & welcome", # plain text
|
|
62
|
+
"body": Segment("<p>Our <b>new</b> shop opens on Monday.</p>", html=True),
|
|
63
|
+
"teaser": Segment("Line 1\nLine 2"), # line breaks survive
|
|
64
|
+
},
|
|
65
|
+
target_lang=Languages.target("de_CH"), # "de-CH", see Languages below
|
|
66
|
+
source_lang="en", # leave out to let Supertext detect it
|
|
67
|
+
politeness="more",
|
|
68
|
+
)
|
|
69
|
+
translations["title"] # "Hallo und willkommen"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- The result has the same keys in the same order.
|
|
73
|
+
- Plain text is escaped on the way out and unescaped on the way back; HTML stays HTML.
|
|
74
|
+
- One segment per whole text (field, paragraph): Supertext translates each segment on its own, so never split a sentence at its formatting.
|
|
75
|
+
- Blank texts come back unchanged and aren't sent; if nothing is left, no request is made.
|
|
76
|
+
- Large inputs are split into several documents of at most `max_characters` (default 900,000) and translated one after the other.
|
|
77
|
+
|
|
78
|
+
### A whole HTML document
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
html = client.translate_html("<html><body><h1>Hello</h1></body></html>", "fr-CH")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Both calls run the whole protocol: upload, poll the status, download, and always delete the file at Supertext afterwards. The single steps are available too: `submit`, `get_status`, `wait_until_done`, `download`, `delete`, and `get_features` / `validate_api_key` / `check_api_key` (all cost-free `GET features`).
|
|
85
|
+
|
|
86
|
+
## Languages
|
|
87
|
+
|
|
88
|
+
The client sends the language codes **as given** (only trimmed): nothing is guessed or converted. An empty target raises `unsupported_language` without a request; an empty source is left out (Supertext detects the language).
|
|
89
|
+
|
|
90
|
+
Supertext's codes are its own: sources are bare languages (`en`, `de`, `gsw`), targets are what Supertext offers exactly, regional for German, English, French, Italian and Portuguese (`de-CH`, `en-US`, `pt-BR`), a script for Chinese and Serbian (`zh-Hans`, `sr-Latn`), a dialect for Swiss German (`gsw-u-sd-chzh`) and bare for the others (`es`, `nl`). A bare `de` as target is rejected (`invalid_language_pair`). Let the administrator map each CMS language to a Supertext language (a combo box per language from `Languages.target_options()`, `Languages.suggest_target(code)` preselected), and resolve the codes with `Languages` before calling. A language nobody mapped uses its default where one is obvious (`de` → `de-DE`, `en` → `en-US`, `zh` → `zh-Hans`, `zh-TW` → `zh-Hant`, `es-ES` → `es`; `Languages.DEFAULT_TARGETS`); anything else (`en-IE`, `gsw`) is `unsupported_language`:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from supertext.v1 import Languages, SupertextError
|
|
94
|
+
|
|
95
|
+
mapping = {"de": "de-CH", "no": "nb"} # CMS language → Supertext code (the integration's setting)
|
|
96
|
+
Languages.target(cms_locale, mapping) # "de" → "de-CH", "no" → "nb"
|
|
97
|
+
Languages.target("de_ch") # "de-CH": a CMS code equal to a Supertext code (case, _/-) is used
|
|
98
|
+
Languages.target("de") # "de-DE" (the default)
|
|
99
|
+
Languages.target("en-IE") # SupertextError unsupported_language (target_lang "en-IE"): map it
|
|
100
|
+
Languages.default_target("es-ES") # "es"; None when there is no default (or an exact match)
|
|
101
|
+
Languages.suggest_target("de_ch") # "de-CH": exact match, else the default, else None (preselect it)
|
|
102
|
+
Languages.target_options() # TARGETS sorted by name, for the combo box
|
|
103
|
+
Languages.source("en-US") # "en" (sources have no regions)
|
|
104
|
+
Languages.source("") # "" (auto-detect)
|
|
105
|
+
Languages.match_target("pt_br") # "pt-BR"; None when there is no exact match
|
|
106
|
+
Languages.supports_politeness("en-US") # False: hide the formal/informal choice; None if unknown
|
|
107
|
+
Languages.is_supported_target("es") # True
|
|
108
|
+
Languages.TARGETS # tuple of TargetLanguage(code, name, politeness), for a settings page
|
|
109
|
+
Languages.SOURCES # tuple of SourceLanguage(code, name)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `Languages.target(code, mapping=None)`: the mapping's value (sent trimmed, even if the SDK doesn't know it yet, so a language Supertext adds later works), else the exact match in `Languages.TARGETS`, else `default_target`, else `SupertextError(unsupported_language)` with `target_lang` set. Mapping keys are compared like codes (case, `_`/`-`, spaces).
|
|
113
|
+
- `Languages.source(code, mapping=None)`: the mapping's value, else `match_source` (exact, else the first subtag: `de_DE_formal` → `de`), else `unsupported_language` with `source_lang` set.
|
|
114
|
+
- In the combo box, show each name in the administrator's language where you can (e.g. Babel's `Locale(ui_locale).languages`) and the code next to it.
|
|
115
|
+
- The functions are also available on the module: `supertext.v1.languages.target(…)`.
|
|
116
|
+
|
|
117
|
+
The list is from the live API (October 2026). For the live list, `client.get_features()` returns the language pairs Supertext offers right now, without cost:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
for pair in client.get_features():
|
|
121
|
+
pair.source_lang, pair.target_lang, pair.features # "de", "en-US", ("supports_glossary",)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Entries without string `source_lang` / `target_lang` are skipped.
|
|
125
|
+
|
|
126
|
+
## Politeness
|
|
127
|
+
|
|
128
|
+
`politeness="more"` asks for formal address (Sie, vous, Lei), `"less"` for informal (du, tu), `"default"` (the default) lets Supertext decide and sends nothing. Not every target offers it (`Languages.supports_politeness`); for one that doesn't (e.g. `en-US`), the API answers `unsupported_feature`, so hide the choice there.
|
|
129
|
+
|
|
130
|
+
## Errors
|
|
131
|
+
|
|
132
|
+
Every failure raises `supertext.v1.SupertextError`. Decide what to show from `error.code` (constants in `supertext.v1.ErrorCode`); `error.message` is English, `error.status` the HTTP status if any, `error.detail` the API's own text. For `invalid_language_pair` and `unsupported_feature`, `error.source_lang` / `error.target_lang` carry what the API reported, and for `unsupported_feature` `error.feature` the option (e.g. `politeness`); for `unsupported_language` they carry the code that has no Supertext language. `error.api_error_code` is the API's `error_code`. The API key never appears in errors.
|
|
133
|
+
|
|
134
|
+
| `code` | What to tell the user |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `missing_api_key` | No API key is configured (nothing was sent). Show the account and API key links above. |
|
|
137
|
+
| `authentication_failure` | The API key is wrong, revoked or lacks access (HTTP 401/403). Show the links above. |
|
|
138
|
+
| `invalid_language_pair` | Supertext doesn't support this language pair (HTTP 400/404 `INVALID_LANGUAGE_PAIR`). Check the language settings. |
|
|
139
|
+
| `unsupported_language` | The language has no Supertext code (nothing was sent): `Languages.target`/`source` found no match or mapping, or the target was empty. Ask the administrator to map it. |
|
|
140
|
+
| `unsupported_feature` | The pair exists, but not with this option (HTTP 400 `FEATURE_ERROR`, e.g. politeness for `en-US`; `feature` names it). |
|
|
141
|
+
| `not_found` | The file is gone at Supertext (expired after 24 h or deleted). Try again. |
|
|
142
|
+
| `payload_too_large` | The content is too large; lower `max_characters` or split it. |
|
|
143
|
+
| `too_many_requests` | Too many requests, even after retrying. Try again shortly. |
|
|
144
|
+
| `service_unavailable` | Supertext is unavailable (HTTP 5xx). Try again later. |
|
|
145
|
+
| `unexpected_status` | Supertext answered with an unexpected HTTP status (see `status`/`detail`). |
|
|
146
|
+
| `transport_error` | Supertext couldn't be reached (network, DNS, TLS, request timeout). |
|
|
147
|
+
| `no_file_id` | The upload answer had no file id. Try again. |
|
|
148
|
+
| `translation_error` | Supertext couldn't translate the document. |
|
|
149
|
+
| `quota_exceeded` | The account's translation limit is used up. |
|
|
150
|
+
| `file_deleted` | The file was deleted at Supertext before the download. Try again. |
|
|
151
|
+
| `timeout` | The translation took longer than `timeout`. Try again, or raise `timeout`. |
|
|
152
|
+
| `incomplete_response` | The translation came back empty or with texts missing (`detail`: `missing: key1, key2`). |
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
try:
|
|
156
|
+
translations = client.translate_texts(texts, Languages.target(cms_locale, mapping))
|
|
157
|
+
except v1.SupertextError as error:
|
|
158
|
+
if error.code == v1.ErrorCode.AUTHENTICATION_FAILURE:
|
|
159
|
+
...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Corrections from reviewers
|
|
163
|
+
|
|
164
|
+
With Supertext Localization Review, point the client at its review service (`base_url`) and fetch the corrections reviewers made, for content you don't translate again. Use your own `data-st-id`s (build the document yourself, `translate_html`) so `segment_id` tells you the field; see the integration guide → *Corrections from reviewers*.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
corrections = client.pending_corrections("de-CH") # list[Correction], all pages
|
|
168
|
+
# write c.html where c.segment_id points, if the field still holds c.previous_html
|
|
169
|
+
client.acknowledge_corrections(corrections) # → int accepted
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Against Supertext's API these raise `not_found`.
|
|
173
|
+
|
|
174
|
+
## API key check
|
|
175
|
+
|
|
176
|
+
`client.check_api_key()` sends `GET features` (at most 10 s) and never raises (test vectors: `spec/api-key-check.json`). It returns a frozen `ApiKeyCheck` with `status` (`ok`, `invalid` for 401/403, `missing` for a blank key without a request, `unreachable` for no answer or 5xx, `error` for anything else), `checked_at` (`2026-10-13T08:00:00Z`), `http_status`, `error_code` (the API's, at most 100 characters) and `fingerprint` (`api_key_fingerprint(key)`, SHA-256 of the key, to notice a changed key; the key itself is never stored or sent). `to_dict()` gives the camelCase form (`status`, `checkedAt`, `httpStatus`, `errorCode`, `fingerprint`). Show the result on the settings page; for `invalid` or `missing`, show the links: No Supertext account yet? Create one at https://www.supertext.com/person/en/account/signin. Generate your API key at https://www.supertext.com/en/integrations/api (supertext.com → Integrations → API; requires the Admin role).
|
|
177
|
+
|
|
178
|
+
Version reports to the Supertext Commando Center and its ping endpoint are [Supertext Plugin Health](https://github.com/Supertext/Commando-Center-Supertext-Translation/tree/main/plugin-health), not this SDK. Pass it `SupertextClient(key).check_api_key()` and `api_key_fingerprint` as the key checker and `supertext.SDK_VERSION` as the SDK version.
|
|
179
|
+
|
|
180
|
+
## Options
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
v1.SupertextClient(
|
|
184
|
+
api_key,
|
|
185
|
+
base_url=v1.LIVE_BASE_URL, # "https://api.supertext.com/v1/"
|
|
186
|
+
poll_interval=2.0, # seconds between status polls
|
|
187
|
+
timeout=180.0, # seconds until a translation gives up (timeout)
|
|
188
|
+
request_timeout=30.0, # seconds per HTTP request (transport_error)
|
|
189
|
+
max_retries=4, # HTTP 429 retries (Retry-After, else 1/2/4/8 s + jitter; max 30 s)
|
|
190
|
+
user_agent=None, # appended to "supertext-sdk-python", e.g. "MyCms-Supertext/1.0.0"
|
|
191
|
+
transport=None, # callable(HttpRequest) -> HttpResponse; default: urllib
|
|
192
|
+
sleep=time.sleep, # injectable for tests
|
|
193
|
+
clock=time.monotonic, # injectable for tests
|
|
194
|
+
now=time.time, # wall clock for check_api_key(); injectable for tests
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
A blank key doesn't fail in the constructor, only on the first request. The transport (`supertext.transport`, re-exported from `supertext.v1`) is shared by all API versions. A custom `transport` raises `OSError` when there's no HTTP answer; the client turns that into `transport_error`.
|
|
199
|
+
|
|
200
|
+
## Translation files
|
|
201
|
+
|
|
202
|
+
`supertext.files` exports segments as an XLIFF 1.2 or HTML file, to be translated outside the API (a CAT tool, a translator, Supertext's web app), and imports the translated file again (test vectors: `spec/translation-file.json`). It doesn't call the API, so it isn't versioned with it.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from supertext import files
|
|
206
|
+
from supertext.files import FileJob, FileSegment
|
|
207
|
+
|
|
208
|
+
job = FileJob("MyCms-Supertext-Translation", "page:12", "de-CH", "fr-CH", [
|
|
209
|
+
FileSegment("title", "Käse & Brot"),
|
|
210
|
+
FileSegment("body", "<p>Unser <b>neuer</b> Laden</p>", html=True),
|
|
211
|
+
])
|
|
212
|
+
xliff = files.export_xliff([job]) # or files.export_html(job)
|
|
213
|
+
|
|
214
|
+
# Later, with the uploaded file:
|
|
215
|
+
for file in files.read(uploaded): # one per job (<file>) in an XLIFF file
|
|
216
|
+
current = collect_segments(file.reference) # the item's segments as they are now
|
|
217
|
+
for result in files.import_translations(file, current):
|
|
218
|
+
if result.status == "ok":
|
|
219
|
+
save(file.reference, file.target_language, result.key, result.text)
|
|
220
|
+
elif result.status == "source_changed": # translated, but the source changed since
|
|
221
|
+
ask_or_save(result)
|
|
222
|
+
else: # untranslated, invalid_markup, missing
|
|
223
|
+
report(result.key, result.status)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- `export_xliff` and `export_html` leave out blank segments; a job with nothing to translate, empty metadata or duplicate keys raises `TranslationFileError` (`code` `invalid_job`). `read` raises `too_large`, `invalid_file` or `missing_metadata`.
|
|
227
|
+
- Each unit carries a checksum of its source; `checksum(segment)` computes it. `source_changed` means the source differs from the exported one (or the file has no checksum): decide whether to keep the translation.
|
|
228
|
+
- Imported files are untrusted: `read` refuses DTDs and entity declarations (nothing is ever expanded) and files over `MAX_FILE_CHARACTERS` (10,000,000). Markup in the result comes only from the current source: XLIFF inline codes are replaced by the source's tags, and an HTML file's translation may only use the tags, attributes and comments the source has, with safe URLs; anything else is `invalid_markup`.
|
|
229
|
+
|
|
230
|
+
## Testing your integration
|
|
231
|
+
|
|
232
|
+
Use the stand-in API from this repository instead of the live API: it checks the request the way the live API does and puts the target code in front of every text (`[de-CH] …`).
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
node stand-in/stand-in.mjs # http://127.0.0.1:8765/v1/
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
client = v1.SupertextClient("any-key", base_url="http://127.0.0.1:8765/v1/", poll_interval=0.1)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
For unit tests, pass a fake `transport` (and `sleep`) instead.
|
|
243
|
+
|
|
244
|
+
## Developing this SDK
|
|
245
|
+
|
|
246
|
+
From `sdk/python`:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
pip install pytest mypy ruff
|
|
250
|
+
ruff check . && ruff format --check .
|
|
251
|
+
mypy --strict
|
|
252
|
+
pytest
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Conformance against the stand-in, from the repository root:
|
|
256
|
+
|
|
257
|
+
```sh
|
|
258
|
+
node stand-in/conformance.mjs -- python3 sdk/python/examples/conformance.py
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Supertext SDK for Python
|
|
2
|
+
|
|
3
|
+
Client for the [Supertext](https://www.supertext.com) AI translation API: translate HTML and texts, with language codes, rate-limit retries and HTML packing handled for you.
|
|
4
|
+
|
|
5
|
+
- Python 3.11+, standard library only (no dependencies), fully typed.
|
|
6
|
+
- Packs what Supertext's CMS, PIM and shop plugins learned against the live API. Background: [integration guide](../../docs/integration-guide.md).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pip install supertext-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from supertext import v1 # or: from supertext.v1 import SupertextClient
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Everything that talks to the API lives in `supertext.v1` (API v1). A later API version gets `supertext.v2` next to it, so an integration moves when it's ready. The top-level package holds only `v1` and `SDK_VERSION`.
|
|
19
|
+
|
|
20
|
+
## API key
|
|
21
|
+
|
|
22
|
+
The client needs a Supertext AI API key. No Supertext account yet? Create one at https://www.supertext.com/person/en/account/signin. Generate your API key at https://www.supertext.com/en/integrations/api (supertext.com → Integrations → API; requires the Admin role).
|
|
23
|
+
|
|
24
|
+
Paste it as shown there, with or without the `Supertext-Auth-Key` prefix. Keep it out of your code, e.g. in the `SUPERTEXT_API_KEY` environment variable. `client.validate_api_key()` checks it without cost.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
### Texts (fields of a page, product, …)
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import os
|
|
32
|
+
from supertext import v1
|
|
33
|
+
from supertext.v1 import Languages, Segment
|
|
34
|
+
|
|
35
|
+
client = v1.SupertextClient(os.environ["SUPERTEXT_API_KEY"], user_agent="MyCms-Supertext/1.0.0")
|
|
36
|
+
|
|
37
|
+
translations = client.translate_texts(
|
|
38
|
+
{
|
|
39
|
+
"title": "Hello & welcome", # plain text
|
|
40
|
+
"body": Segment("<p>Our <b>new</b> shop opens on Monday.</p>", html=True),
|
|
41
|
+
"teaser": Segment("Line 1\nLine 2"), # line breaks survive
|
|
42
|
+
},
|
|
43
|
+
target_lang=Languages.target("de_CH"), # "de-CH", see Languages below
|
|
44
|
+
source_lang="en", # leave out to let Supertext detect it
|
|
45
|
+
politeness="more",
|
|
46
|
+
)
|
|
47
|
+
translations["title"] # "Hallo und willkommen"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- The result has the same keys in the same order.
|
|
51
|
+
- Plain text is escaped on the way out and unescaped on the way back; HTML stays HTML.
|
|
52
|
+
- One segment per whole text (field, paragraph): Supertext translates each segment on its own, so never split a sentence at its formatting.
|
|
53
|
+
- Blank texts come back unchanged and aren't sent; if nothing is left, no request is made.
|
|
54
|
+
- Large inputs are split into several documents of at most `max_characters` (default 900,000) and translated one after the other.
|
|
55
|
+
|
|
56
|
+
### A whole HTML document
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
html = client.translate_html("<html><body><h1>Hello</h1></body></html>", "fr-CH")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Both calls run the whole protocol: upload, poll the status, download, and always delete the file at Supertext afterwards. The single steps are available too: `submit`, `get_status`, `wait_until_done`, `download`, `delete`, and `get_features` / `validate_api_key` / `check_api_key` (all cost-free `GET features`).
|
|
63
|
+
|
|
64
|
+
## Languages
|
|
65
|
+
|
|
66
|
+
The client sends the language codes **as given** (only trimmed): nothing is guessed or converted. An empty target raises `unsupported_language` without a request; an empty source is left out (Supertext detects the language).
|
|
67
|
+
|
|
68
|
+
Supertext's codes are its own: sources are bare languages (`en`, `de`, `gsw`), targets are what Supertext offers exactly, regional for German, English, French, Italian and Portuguese (`de-CH`, `en-US`, `pt-BR`), a script for Chinese and Serbian (`zh-Hans`, `sr-Latn`), a dialect for Swiss German (`gsw-u-sd-chzh`) and bare for the others (`es`, `nl`). A bare `de` as target is rejected (`invalid_language_pair`). Let the administrator map each CMS language to a Supertext language (a combo box per language from `Languages.target_options()`, `Languages.suggest_target(code)` preselected), and resolve the codes with `Languages` before calling. A language nobody mapped uses its default where one is obvious (`de` → `de-DE`, `en` → `en-US`, `zh` → `zh-Hans`, `zh-TW` → `zh-Hant`, `es-ES` → `es`; `Languages.DEFAULT_TARGETS`); anything else (`en-IE`, `gsw`) is `unsupported_language`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from supertext.v1 import Languages, SupertextError
|
|
72
|
+
|
|
73
|
+
mapping = {"de": "de-CH", "no": "nb"} # CMS language → Supertext code (the integration's setting)
|
|
74
|
+
Languages.target(cms_locale, mapping) # "de" → "de-CH", "no" → "nb"
|
|
75
|
+
Languages.target("de_ch") # "de-CH": a CMS code equal to a Supertext code (case, _/-) is used
|
|
76
|
+
Languages.target("de") # "de-DE" (the default)
|
|
77
|
+
Languages.target("en-IE") # SupertextError unsupported_language (target_lang "en-IE"): map it
|
|
78
|
+
Languages.default_target("es-ES") # "es"; None when there is no default (or an exact match)
|
|
79
|
+
Languages.suggest_target("de_ch") # "de-CH": exact match, else the default, else None (preselect it)
|
|
80
|
+
Languages.target_options() # TARGETS sorted by name, for the combo box
|
|
81
|
+
Languages.source("en-US") # "en" (sources have no regions)
|
|
82
|
+
Languages.source("") # "" (auto-detect)
|
|
83
|
+
Languages.match_target("pt_br") # "pt-BR"; None when there is no exact match
|
|
84
|
+
Languages.supports_politeness("en-US") # False: hide the formal/informal choice; None if unknown
|
|
85
|
+
Languages.is_supported_target("es") # True
|
|
86
|
+
Languages.TARGETS # tuple of TargetLanguage(code, name, politeness), for a settings page
|
|
87
|
+
Languages.SOURCES # tuple of SourceLanguage(code, name)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `Languages.target(code, mapping=None)`: the mapping's value (sent trimmed, even if the SDK doesn't know it yet, so a language Supertext adds later works), else the exact match in `Languages.TARGETS`, else `default_target`, else `SupertextError(unsupported_language)` with `target_lang` set. Mapping keys are compared like codes (case, `_`/`-`, spaces).
|
|
91
|
+
- `Languages.source(code, mapping=None)`: the mapping's value, else `match_source` (exact, else the first subtag: `de_DE_formal` → `de`), else `unsupported_language` with `source_lang` set.
|
|
92
|
+
- In the combo box, show each name in the administrator's language where you can (e.g. Babel's `Locale(ui_locale).languages`) and the code next to it.
|
|
93
|
+
- The functions are also available on the module: `supertext.v1.languages.target(…)`.
|
|
94
|
+
|
|
95
|
+
The list is from the live API (October 2026). For the live list, `client.get_features()` returns the language pairs Supertext offers right now, without cost:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
for pair in client.get_features():
|
|
99
|
+
pair.source_lang, pair.target_lang, pair.features # "de", "en-US", ("supports_glossary",)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Entries without string `source_lang` / `target_lang` are skipped.
|
|
103
|
+
|
|
104
|
+
## Politeness
|
|
105
|
+
|
|
106
|
+
`politeness="more"` asks for formal address (Sie, vous, Lei), `"less"` for informal (du, tu), `"default"` (the default) lets Supertext decide and sends nothing. Not every target offers it (`Languages.supports_politeness`); for one that doesn't (e.g. `en-US`), the API answers `unsupported_feature`, so hide the choice there.
|
|
107
|
+
|
|
108
|
+
## Errors
|
|
109
|
+
|
|
110
|
+
Every failure raises `supertext.v1.SupertextError`. Decide what to show from `error.code` (constants in `supertext.v1.ErrorCode`); `error.message` is English, `error.status` the HTTP status if any, `error.detail` the API's own text. For `invalid_language_pair` and `unsupported_feature`, `error.source_lang` / `error.target_lang` carry what the API reported, and for `unsupported_feature` `error.feature` the option (e.g. `politeness`); for `unsupported_language` they carry the code that has no Supertext language. `error.api_error_code` is the API's `error_code`. The API key never appears in errors.
|
|
111
|
+
|
|
112
|
+
| `code` | What to tell the user |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `missing_api_key` | No API key is configured (nothing was sent). Show the account and API key links above. |
|
|
115
|
+
| `authentication_failure` | The API key is wrong, revoked or lacks access (HTTP 401/403). Show the links above. |
|
|
116
|
+
| `invalid_language_pair` | Supertext doesn't support this language pair (HTTP 400/404 `INVALID_LANGUAGE_PAIR`). Check the language settings. |
|
|
117
|
+
| `unsupported_language` | The language has no Supertext code (nothing was sent): `Languages.target`/`source` found no match or mapping, or the target was empty. Ask the administrator to map it. |
|
|
118
|
+
| `unsupported_feature` | The pair exists, but not with this option (HTTP 400 `FEATURE_ERROR`, e.g. politeness for `en-US`; `feature` names it). |
|
|
119
|
+
| `not_found` | The file is gone at Supertext (expired after 24 h or deleted). Try again. |
|
|
120
|
+
| `payload_too_large` | The content is too large; lower `max_characters` or split it. |
|
|
121
|
+
| `too_many_requests` | Too many requests, even after retrying. Try again shortly. |
|
|
122
|
+
| `service_unavailable` | Supertext is unavailable (HTTP 5xx). Try again later. |
|
|
123
|
+
| `unexpected_status` | Supertext answered with an unexpected HTTP status (see `status`/`detail`). |
|
|
124
|
+
| `transport_error` | Supertext couldn't be reached (network, DNS, TLS, request timeout). |
|
|
125
|
+
| `no_file_id` | The upload answer had no file id. Try again. |
|
|
126
|
+
| `translation_error` | Supertext couldn't translate the document. |
|
|
127
|
+
| `quota_exceeded` | The account's translation limit is used up. |
|
|
128
|
+
| `file_deleted` | The file was deleted at Supertext before the download. Try again. |
|
|
129
|
+
| `timeout` | The translation took longer than `timeout`. Try again, or raise `timeout`. |
|
|
130
|
+
| `incomplete_response` | The translation came back empty or with texts missing (`detail`: `missing: key1, key2`). |
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
try:
|
|
134
|
+
translations = client.translate_texts(texts, Languages.target(cms_locale, mapping))
|
|
135
|
+
except v1.SupertextError as error:
|
|
136
|
+
if error.code == v1.ErrorCode.AUTHENTICATION_FAILURE:
|
|
137
|
+
...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Corrections from reviewers
|
|
141
|
+
|
|
142
|
+
With Supertext Localization Review, point the client at its review service (`base_url`) and fetch the corrections reviewers made, for content you don't translate again. Use your own `data-st-id`s (build the document yourself, `translate_html`) so `segment_id` tells you the field; see the integration guide → *Corrections from reviewers*.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
corrections = client.pending_corrections("de-CH") # list[Correction], all pages
|
|
146
|
+
# write c.html where c.segment_id points, if the field still holds c.previous_html
|
|
147
|
+
client.acknowledge_corrections(corrections) # → int accepted
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Against Supertext's API these raise `not_found`.
|
|
151
|
+
|
|
152
|
+
## API key check
|
|
153
|
+
|
|
154
|
+
`client.check_api_key()` sends `GET features` (at most 10 s) and never raises (test vectors: `spec/api-key-check.json`). It returns a frozen `ApiKeyCheck` with `status` (`ok`, `invalid` for 401/403, `missing` for a blank key without a request, `unreachable` for no answer or 5xx, `error` for anything else), `checked_at` (`2026-10-13T08:00:00Z`), `http_status`, `error_code` (the API's, at most 100 characters) and `fingerprint` (`api_key_fingerprint(key)`, SHA-256 of the key, to notice a changed key; the key itself is never stored or sent). `to_dict()` gives the camelCase form (`status`, `checkedAt`, `httpStatus`, `errorCode`, `fingerprint`). Show the result on the settings page; for `invalid` or `missing`, show the links: No Supertext account yet? Create one at https://www.supertext.com/person/en/account/signin. Generate your API key at https://www.supertext.com/en/integrations/api (supertext.com → Integrations → API; requires the Admin role).
|
|
155
|
+
|
|
156
|
+
Version reports to the Supertext Commando Center and its ping endpoint are [Supertext Plugin Health](https://github.com/Supertext/Commando-Center-Supertext-Translation/tree/main/plugin-health), not this SDK. Pass it `SupertextClient(key).check_api_key()` and `api_key_fingerprint` as the key checker and `supertext.SDK_VERSION` as the SDK version.
|
|
157
|
+
|
|
158
|
+
## Options
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
v1.SupertextClient(
|
|
162
|
+
api_key,
|
|
163
|
+
base_url=v1.LIVE_BASE_URL, # "https://api.supertext.com/v1/"
|
|
164
|
+
poll_interval=2.0, # seconds between status polls
|
|
165
|
+
timeout=180.0, # seconds until a translation gives up (timeout)
|
|
166
|
+
request_timeout=30.0, # seconds per HTTP request (transport_error)
|
|
167
|
+
max_retries=4, # HTTP 429 retries (Retry-After, else 1/2/4/8 s + jitter; max 30 s)
|
|
168
|
+
user_agent=None, # appended to "supertext-sdk-python", e.g. "MyCms-Supertext/1.0.0"
|
|
169
|
+
transport=None, # callable(HttpRequest) -> HttpResponse; default: urllib
|
|
170
|
+
sleep=time.sleep, # injectable for tests
|
|
171
|
+
clock=time.monotonic, # injectable for tests
|
|
172
|
+
now=time.time, # wall clock for check_api_key(); injectable for tests
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A blank key doesn't fail in the constructor, only on the first request. The transport (`supertext.transport`, re-exported from `supertext.v1`) is shared by all API versions. A custom `transport` raises `OSError` when there's no HTTP answer; the client turns that into `transport_error`.
|
|
177
|
+
|
|
178
|
+
## Translation files
|
|
179
|
+
|
|
180
|
+
`supertext.files` exports segments as an XLIFF 1.2 or HTML file, to be translated outside the API (a CAT tool, a translator, Supertext's web app), and imports the translated file again (test vectors: `spec/translation-file.json`). It doesn't call the API, so it isn't versioned with it.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from supertext import files
|
|
184
|
+
from supertext.files import FileJob, FileSegment
|
|
185
|
+
|
|
186
|
+
job = FileJob("MyCms-Supertext-Translation", "page:12", "de-CH", "fr-CH", [
|
|
187
|
+
FileSegment("title", "Käse & Brot"),
|
|
188
|
+
FileSegment("body", "<p>Unser <b>neuer</b> Laden</p>", html=True),
|
|
189
|
+
])
|
|
190
|
+
xliff = files.export_xliff([job]) # or files.export_html(job)
|
|
191
|
+
|
|
192
|
+
# Later, with the uploaded file:
|
|
193
|
+
for file in files.read(uploaded): # one per job (<file>) in an XLIFF file
|
|
194
|
+
current = collect_segments(file.reference) # the item's segments as they are now
|
|
195
|
+
for result in files.import_translations(file, current):
|
|
196
|
+
if result.status == "ok":
|
|
197
|
+
save(file.reference, file.target_language, result.key, result.text)
|
|
198
|
+
elif result.status == "source_changed": # translated, but the source changed since
|
|
199
|
+
ask_or_save(result)
|
|
200
|
+
else: # untranslated, invalid_markup, missing
|
|
201
|
+
report(result.key, result.status)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- `export_xliff` and `export_html` leave out blank segments; a job with nothing to translate, empty metadata or duplicate keys raises `TranslationFileError` (`code` `invalid_job`). `read` raises `too_large`, `invalid_file` or `missing_metadata`.
|
|
205
|
+
- Each unit carries a checksum of its source; `checksum(segment)` computes it. `source_changed` means the source differs from the exported one (or the file has no checksum): decide whether to keep the translation.
|
|
206
|
+
- Imported files are untrusted: `read` refuses DTDs and entity declarations (nothing is ever expanded) and files over `MAX_FILE_CHARACTERS` (10,000,000). Markup in the result comes only from the current source: XLIFF inline codes are replaced by the source's tags, and an HTML file's translation may only use the tags, attributes and comments the source has, with safe URLs; anything else is `invalid_markup`.
|
|
207
|
+
|
|
208
|
+
## Testing your integration
|
|
209
|
+
|
|
210
|
+
Use the stand-in API from this repository instead of the live API: it checks the request the way the live API does and puts the target code in front of every text (`[de-CH] …`).
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
node stand-in/stand-in.mjs # http://127.0.0.1:8765/v1/
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
client = v1.SupertextClient("any-key", base_url="http://127.0.0.1:8765/v1/", poll_interval=0.1)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
For unit tests, pass a fake `transport` (and `sleep`) instead.
|
|
221
|
+
|
|
222
|
+
## Developing this SDK
|
|
223
|
+
|
|
224
|
+
From `sdk/python`:
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
pip install pytest mypy ruff
|
|
228
|
+
ruff check . && ruff format --check .
|
|
229
|
+
mypy --strict
|
|
230
|
+
pytest
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Conformance against the stand-in, from the repository root:
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
node stand-in/conformance.mjs -- python3 sdk/python/examples/conformance.py
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "supertext-sdk"
|
|
7
|
+
version = "0.6.0"
|
|
8
|
+
description = "Client for the Supertext AI translation API: translate HTML and texts, with language codes, rate-limit retries and HTML packing handled for you."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "Supertext AG" }]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = []
|
|
14
|
+
keywords = ["supertext", "translation", "machine translation", "localization", "i18n"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Software Development :: Internationalization",
|
|
21
|
+
"Topic :: Software Development :: Localization",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/Supertext/Supertext-API-SDK-Guide"
|
|
27
|
+
Documentation = "https://github.com/Supertext/Supertext-API-SDK-Guide/tree/main/sdk/python"
|
|
28
|
+
Issues = "https://github.com/Supertext/Supertext-API-SDK-Guide/issues"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
supertext = ["py.typed"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
pythonpath = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.mypy]
|
|
41
|
+
strict = true
|
|
42
|
+
files = ["src", "tests", "examples"]
|
|
43
|
+
mypy_path = "src"
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 120
|
|
47
|
+
target-version = "py311"
|
|
48
|
+
|
|
49
|
+
[tool.ruff.format]
|
|
50
|
+
# Keep the README's aligned comments.
|
|
51
|
+
exclude = ["*.md"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Supertext SDK. Everything that talks to the API is versioned with it:
|
|
2
|
+
|
|
3
|
+
from supertext import v1 # or: from supertext.v1 import SupertextClient
|
|
4
|
+
|
|
5
|
+
client = v1.SupertextClient(api_key)
|
|
6
|
+
|
|
7
|
+
A later API version gets ``supertext.v2`` next to ``v1``. The HTTP layer every version shares is
|
|
8
|
+
:mod:`supertext.transport` (also re-exported from ``supertext.v1``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from . import v1
|
|
12
|
+
from .version import SDK_VERSION
|
|
13
|
+
|
|
14
|
+
__version__ = SDK_VERSION
|
|
15
|
+
|
|
16
|
+
__all__ = ["SDK_VERSION", "__version__", "v1"]
|