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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"]