hushscript 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. hushscript/__init__.py +97 -0
  2. hushscript/_client.py +687 -0
  3. hushscript/_clock.py +36 -0
  4. hushscript/_config.py +139 -0
  5. hushscript/_errors.py +563 -0
  6. hushscript/_guard.py +61 -0
  7. hushscript/_http.py +624 -0
  8. hushscript/_json.py +55 -0
  9. hushscript/_log.py +155 -0
  10. hushscript/_models.py +144 -0
  11. hushscript/_pacing.py +51 -0
  12. hushscript/_redact.py +140 -0
  13. hushscript/_response.py +67 -0
  14. hushscript/_retry.py +205 -0
  15. hushscript/_shape.py +197 -0
  16. hushscript/_shapes.py +248 -0
  17. hushscript/_version.py +1 -0
  18. hushscript/format.py +224 -0
  19. hushscript/generated/__init__.py +8 -0
  20. hushscript/generated/api/__init__.py +1 -0
  21. hushscript/generated/api/default/__init__.py +1 -0
  22. hushscript/generated/api/default/buy_minutes.py +248 -0
  23. hushscript/generated/api/default/claim_agent_account.py +238 -0
  24. hushscript/generated/api/default/complete_upload.py +238 -0
  25. hushscript/generated/api/default/create_agent_account_signup.py +261 -0
  26. hushscript/generated/api/default/create_setup_session.py +244 -0
  27. hushscript/generated/api/default/create_upload.py +225 -0
  28. hushscript/generated/api/default/get_account.py +169 -0
  29. hushscript/generated/api/default/get_job.py +198 -0
  30. hushscript/generated/api/default/get_org.py +169 -0
  31. hushscript/generated/api/default/get_transcript.py +226 -0
  32. hushscript/generated/api/default/get_transcription_context.py +173 -0
  33. hushscript/generated/api/default/get_upload_part_urls.py +246 -0
  34. hushscript/generated/api/default/heartbeat_upload.py +197 -0
  35. hushscript/generated/api/default/lift_transcript_hold.py +206 -0
  36. hushscript/generated/api/default/list_credit_ledger.py +229 -0
  37. hushscript/generated/api/default/list_deleted_transcripts.py +237 -0
  38. hushscript/generated/api/default/list_holds.py +233 -0
  39. hushscript/generated/api/default/list_jobs.py +249 -0
  40. hushscript/generated/api/default/list_org_audit.py +221 -0
  41. hushscript/generated/api/default/list_org_invoices.py +221 -0
  42. hushscript/generated/api/default/list_org_ledger.py +221 -0
  43. hushscript/generated/api/default/list_org_matters.py +247 -0
  44. hushscript/generated/api/default/list_org_members.py +221 -0
  45. hushscript/generated/api/default/list_org_transcripts.py +233 -0
  46. hushscript/generated/api/default/list_packs.py +169 -0
  47. hushscript/generated/api/default/list_saved_cards.py +169 -0
  48. hushscript/generated/api/default/list_transactions.py +225 -0
  49. hushscript/generated/api/default/list_transcripts.py +225 -0
  50. hushscript/generated/api/default/org_audit_csv.py +163 -0
  51. hushscript/generated/api/default/org_ledger_csv.py +163 -0
  52. hushscript/generated/api/default/org_usage_report.py +247 -0
  53. hushscript/generated/api/default/org_usage_report_csv.py +229 -0
  54. hushscript/generated/api/default/patch_org_member.py +237 -0
  55. hushscript/generated/api/default/place_transcript_hold.py +206 -0
  56. hushscript/generated/api/default/rotate_agent_credentials.py +177 -0
  57. hushscript/generated/api/default/schedule_org_custody.py +215 -0
  58. hushscript/generated/api/default/set_default_card.py +223 -0
  59. hushscript/generated/api/default/upload_part.py +249 -0
  60. hushscript/generated/catalog.py +138 -0
  61. hushscript/generated/client.py +282 -0
  62. hushscript/generated/errors.py +16 -0
  63. hushscript/generated/hints.py +28 -0
  64. hushscript/generated/models/__init__.py +193 -0
  65. hushscript/generated/models/account_response.py +114 -0
  66. hushscript/generated/models/active_hold.py +88 -0
  67. hushscript/generated/models/active_hold_kind.py +18 -0
  68. hushscript/generated/models/agent_claim_request.py +61 -0
  69. hushscript/generated/models/agent_claim_response.py +85 -0
  70. hushscript/generated/models/agent_credentials_rotate_response.py +101 -0
  71. hushscript/generated/models/agent_signup_request.py +118 -0
  72. hushscript/generated/models/agent_signup_request_agent.py +80 -0
  73. hushscript/generated/models/agent_signup_response.py +111 -0
  74. hushscript/generated/models/buy_minutes_body.py +89 -0
  75. hushscript/generated/models/card.py +132 -0
  76. hushscript/generated/models/cards_response.py +90 -0
  77. hushscript/generated/models/complete_upload_body.py +107 -0
  78. hushscript/generated/models/complete_upload_body_parts_type_0_item.py +69 -0
  79. hushscript/generated/models/context_dictionary.py +143 -0
  80. hushscript/generated/models/context_prompt_preset.py +118 -0
  81. hushscript/generated/models/create_setup_session_body.py +62 -0
  82. hushscript/generated/models/create_upload_body.py +254 -0
  83. hushscript/generated/models/create_upload_body_transcription_options_type_0.py +178 -0
  84. hushscript/generated/models/credit_ledger_entry.py +113 -0
  85. hushscript/generated/models/credit_ledger_response.py +90 -0
  86. hushscript/generated/models/deleted_transcript.py +148 -0
  87. hushscript/generated/models/deleted_transcript_cause.py +18 -0
  88. hushscript/generated/models/deleted_transcript_receipt_type_0.py +107 -0
  89. hushscript/generated/models/deleted_transcript_receipt_type_0_items_item.py +69 -0
  90. hushscript/generated/models/deleted_transcripts_response.py +90 -0
  91. hushscript/generated/models/dictionary_entry.py +123 -0
  92. hushscript/generated/models/dictionary_entry_category.py +37 -0
  93. hushscript/generated/models/error.py +155 -0
  94. hushscript/generated/models/error_details.py +94 -0
  95. hushscript/generated/models/get_upload_part_urls_response_200_type_0.py +108 -0
  96. hushscript/generated/models/heartbeat_upload_response_200.py +61 -0
  97. hushscript/generated/models/holds_response.py +90 -0
  98. hushscript/generated/models/job.py +309 -0
  99. hushscript/generated/models/job_failure_kind_type_1.py +16 -0
  100. hushscript/generated/models/job_failure_kind_type_2_type_1.py +16 -0
  101. hushscript/generated/models/job_failure_kind_type_3_type_1.py +16 -0
  102. hushscript/generated/models/job_list.py +90 -0
  103. hushscript/generated/models/lift_transcript_hold_response_200.py +61 -0
  104. hushscript/generated/models/list_jobs_active.py +17 -0
  105. hushscript/generated/models/list_org_matters_status.py +16 -0
  106. hushscript/generated/models/options_summary.py +135 -0
  107. hushscript/generated/models/org_audit_entry.py +105 -0
  108. hushscript/generated/models/org_audit_response.py +90 -0
  109. hushscript/generated/models/org_invoice.py +175 -0
  110. hushscript/generated/models/org_invoices_response.py +90 -0
  111. hushscript/generated/models/org_ledger_entry.py +155 -0
  112. hushscript/generated/models/org_ledger_response.py +90 -0
  113. hushscript/generated/models/org_matter.py +79 -0
  114. hushscript/generated/models/org_matter_status.py +16 -0
  115. hushscript/generated/models/org_matters_response.py +75 -0
  116. hushscript/generated/models/org_member.py +123 -0
  117. hushscript/generated/models/org_members_response.py +90 -0
  118. hushscript/generated/models/org_summary_response.py +482 -0
  119. hushscript/generated/models/org_transcript.py +103 -0
  120. hushscript/generated/models/org_transcripts_response.py +90 -0
  121. hushscript/generated/models/org_usage_report_csv_group_by.py +17 -0
  122. hushscript/generated/models/org_usage_report_group_by.py +17 -0
  123. hushscript/generated/models/org_usage_response.py +112 -0
  124. hushscript/generated/models/org_usage_response_group_by.py +17 -0
  125. hushscript/generated/models/org_usage_row.py +86 -0
  126. hushscript/generated/models/pack.py +107 -0
  127. hushscript/generated/models/packs_response.py +75 -0
  128. hushscript/generated/models/patch_org_member_body.py +102 -0
  129. hushscript/generated/models/patch_org_member_response_200.py +61 -0
  130. hushscript/generated/models/place_transcript_hold_response_200.py +61 -0
  131. hushscript/generated/models/presigned_part_url.py +69 -0
  132. hushscript/generated/models/purchase_response.py +76 -0
  133. hushscript/generated/models/schedule_org_custody_body.py +66 -0
  134. hushscript/generated/models/schedule_org_custody_body_custody.py +17 -0
  135. hushscript/generated/models/schedule_org_custody_response_200.py +69 -0
  136. hushscript/generated/models/set_default_card_body.py +61 -0
  137. hushscript/generated/models/set_default_card_response_200.py +61 -0
  138. hushscript/generated/models/setup_session_response.py +77 -0
  139. hushscript/generated/models/transaction.py +183 -0
  140. hushscript/generated/models/transactions_response.py +98 -0
  141. hushscript/generated/models/transcript_detail.py +194 -0
  142. hushscript/generated/models/transcript_detail_speaker_names.py +47 -0
  143. hushscript/generated/models/transcript_envelope.py +89 -0
  144. hushscript/generated/models/transcript_envelope_translations.py +62 -0
  145. hushscript/generated/models/transcript_language.py +75 -0
  146. hushscript/generated/models/transcript_list.py +90 -0
  147. hushscript/generated/models/transcript_list_row.py +152 -0
  148. hushscript/generated/models/transcript_list_row_insights_state.py +20 -0
  149. hushscript/generated/models/transcription_context_response.py +95 -0
  150. hushscript/generated/models/upload_complete_response.py +100 -0
  151. hushscript/generated/models/upload_create_response.py +148 -0
  152. hushscript/generated/models/upload_create_response_funding.py +16 -0
  153. hushscript/generated/models/upload_create_response_upload_type_0.py +108 -0
  154. hushscript/generated/models/upload_limits.py +86 -0
  155. hushscript/generated/models/upload_part_response_200.py +69 -0
  156. hushscript/generated/models/utterance.py +85 -0
  157. hushscript/generated/scopes.py +42 -0
  158. hushscript/generated/types.py +54 -0
  159. hushscript/money.py +79 -0
  160. hushscript/paginate.py +78 -0
  161. hushscript/py.typed +0 -0
  162. hushscript/resources/__init__.py +1 -0
  163. hushscript/resources/_params.py +33 -0
  164. hushscript/resources/account.py +121 -0
  165. hushscript/resources/agent.py +421 -0
  166. hushscript/resources/billing.py +567 -0
  167. hushscript/resources/jobs.py +105 -0
  168. hushscript/resources/transcripts.py +209 -0
  169. hushscript/transcribe/__init__.py +1 -0
  170. hushscript/transcribe/apipeline.py +234 -0
  171. hushscript/transcribe/aupload.py +197 -0
  172. hushscript/transcribe/heartbeat.py +78 -0
  173. hushscript/transcribe/pipeline.py +312 -0
  174. hushscript/transcribe/preflight.py +272 -0
  175. hushscript/transcribe/probe.py +185 -0
  176. hushscript/transcribe/resume.py +115 -0
  177. hushscript/transcribe/segment.py +23 -0
  178. hushscript/transcribe/session.py +34 -0
  179. hushscript/transcribe/sniff.py +30 -0
  180. hushscript/transcribe/source.py +218 -0
  181. hushscript/transcribe/upload.py +498 -0
  182. hushscript/transcribe/wait.py +124 -0
  183. hushscript/transcript.py +197 -0
  184. hushscript-1.0.0.dist-info/METADATA +502 -0
  185. hushscript-1.0.0.dist-info/RECORD +187 -0
  186. hushscript-1.0.0.dist-info/WHEEL +4 -0
  187. hushscript-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,502 @@
1
+ Metadata-Version: 2.5
2
+ Name: hushscript
3
+ Version: 1.0.0
4
+ Summary: The official Python SDK for the Hushscript transcription API.
5
+ Project-URL: Homepage, https://hushscript.com
6
+ Project-URL: Source, https://github.com/Hushscript/hushscript-sdk-python
7
+ Author-email: Hushscript <support@hushscript.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: anyio<5,>=4
22
+ Requires-Dist: async-timeout<6,>=5
23
+ Requires-Dist: attrs>=23.2
24
+ Requires-Dist: exceptiongroup>=1.2; python_version < '3.11'
25
+ Requires-Dist: httpx<0.29,>=0.28.1
26
+ Requires-Dist: mutagen<2,>=1.47
27
+ Requires-Dist: typing-extensions>=4.12
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Hushscript Python SDK
31
+
32
+ The official Python SDK for [Hushscript](https://hushscript.com), a privacy-first, pay-as-you-go
33
+ transcription service. It uploads a file, waits for the transcript and hands it back, and it handles
34
+ the failures in between so you do not have to.
35
+
36
+ ## Why Hushscript
37
+
38
+ - **No subscription.** You buy prepaid minute packs, and nothing recurs. Current prices are on the
39
+ [pricing page](https://hushscript.com/pricing).
40
+ - **The audio you send isn't kept.** The audio sent for transcription isn't kept after processing. Stored
41
+ transcripts are encrypted at rest, and you choose how long they stay.
42
+ - **Free speaker labels and AI insights.** Speaker labels carry no extra charge (medical mode and
43
+ translation cost more). The first all-in AI insights run on a transcript (summaries, action items,
44
+ quotes, chapters) is free in the Hushscript app.
45
+ - **Built for specialist recordings.** Key terms, prompts and multichannel audio in
46
+ around 99 languages, plus a medical mode for English, Spanish, French and German.
47
+ - **An SDK you can read.** It is MIT licensed, official, typed, sends no telemetry, and is tested
48
+ against a [conformance suite](https://github.com/Hushscript/hushscript-sdk-conformance) shared with
49
+ the other Hushscript SDKs.
50
+
51
+ ## What you can do with it
52
+
53
+ - Transcribe a file from a path, `bytes` or a binary file object, with progress callbacks and
54
+ retries that cannot charge you twice.
55
+ - Set the language or let it be detected, turn speaker labels on or off, and pass `keyterms` and a
56
+ `prompt` to steer the vocabulary.
57
+ - Use medical mode, multichannel audio, or translate into other languages (`translate_to`) in the
58
+ same call.
59
+ - Export SRT, WebVTT and plain text that match the Hushscript app's exports byte for byte, including
60
+ translations and the AI-cleaned body.
61
+ - Resume an interrupted job with its `resume_token` instead of paying twice.
62
+ - Check your balance, list transcripts and jobs, and buy minute packs from code (off until you
63
+ allow it).
64
+ - Let an agent sign up, pay at a hosted checkout and claim its own token, with no human account.
65
+
66
+ [Website](https://hushscript.com) · [Pricing](https://hushscript.com/pricing) · [SDK docs](https://hushscript.com/sdks) · [API reference](https://hushscript.com/developers)
67
+
68
+ ## Quick start
69
+
70
+ ```sh
71
+ pip install hushscript
72
+ export HUSHSCRIPT_TOKEN=hsr1:... # Account → MCP in the Hushscript app
73
+ ```
74
+
75
+ ```python
76
+ from pathlib import Path
77
+
78
+ from hushscript import Hushscript
79
+
80
+ with Hushscript() as hs: # reads HUSHSCRIPT_TOKEN
81
+ transcript = hs.transcribe("meeting.mp3")
82
+ print(transcript.text)
83
+ Path("meeting.srt").write_text(transcript.to_srt(), encoding="utf-8")
84
+ ```
85
+
86
+ Every call has an asyncio twin on `AsyncHushscript` with the same names (`await` the calls and use
87
+ `async for` on the lists); the examples below use the synchronous client.
88
+
89
+ Async calls preserve `asyncio.CancelledError`. Some operations attach recovery information in
90
+ the exception's `__notes__`. On Python 3.10, awaiting a canceled task can raise a new
91
+ `CancelledError`; follow its `__context__` chain and read `__notes__` there too.
92
+
93
+ ## Inputs
94
+
95
+ `transcribe()` and `resume()` take a path, `bytes` or a binary file object. A seekable file is
96
+ probed and read like a path; a non-seekable one (a pipe, a socket) is read once, in order, one part
97
+ at a time, so it needs its exact `size` and `duration_seconds`:
98
+
99
+ ```python
100
+ transcript = hs.transcribe(sys.stdin.buffer, size=size_in_bytes, duration_seconds=1800)
101
+ ```
102
+
103
+ ## Key terms and prompt
104
+
105
+ `keyterms` and `prompt` steer the transcript toward your vocabulary. Both are trimmed first: blank
106
+ terms are dropped, and an empty list or a blank prompt is not sent. The limits are checked before
107
+ any upload, and a breach raises `PreflightError` with code `invalid_option` naming the limit and the
108
+ offending term:
109
+
110
+ - `keyterms`: up to 1000 unique terms, each at most 6 words and 128 characters.
111
+ - `prompt`: at most 2000 characters.
112
+ - Both together: the API wraps them into one prompt, and the 2000 limit covers that whole text,
113
+ which is 203 characters of fixed text plus the prompt, each term plus 2, and 1 between terms.
114
+
115
+ Lengths count Unicode code points, and words are split on Unicode white space.
116
+
117
+ ```python
118
+ hs.transcribe("call.mp3", keyterms=["Hushscript", "Dr. Okafor"], prompt="A support call.")
119
+ ```
120
+
121
+ ## Check my balance
122
+
123
+ ```python
124
+ balance = hs.balance()
125
+ print(f"{balance.minutes} minutes to spend")
126
+ ```
127
+
128
+ `balance.minutes` is what you can spend now. Minutes held for jobs that are still running are
129
+ already taken out of it, so do not subtract `held_seconds` again.
130
+
131
+ ## List my transcripts
132
+
133
+ ```python
134
+ for summary in hs.transcripts.list():
135
+ print(summary.id, summary.title)
136
+ ```
137
+
138
+ The list holds finished transcripts only. A recording that is still processing is a job, so look for
139
+ it in `hs.jobs.list(active=True)`. `hs.jobs.wait(job_id)` waits for one, and
140
+ `hs.transcripts.get(transcript_id)` returns the full `Transcript` once it is done.
141
+
142
+ `hs.transcripts.page(cursor=...)` returns one page. A page that hands back the cursor it was sent
143
+ raises `HushscriptError` with the code `pagination_loop`, from `page` and from the list iterators
144
+ alike.
145
+
146
+ ## Buy a pack
147
+
148
+ ```python
149
+ for pack in hs.billing.packs():
150
+ print(pack.id, f"{pack.minutes} minutes", pack.price.formatted)
151
+
152
+ purchase = hs.buy("300min", acknowledge_no_refund=True)
153
+ print(f"{purchase.minutes} minutes to spend now")
154
+ ```
155
+
156
+ The packs are `45min`, `300min`, `900min`, `1800min` and `6000min`. Prices depend on your country,
157
+ so read them from `pack.price` and never hard-code one.
158
+
159
+ `buy()` charges your saved default card, or the only card that can be charged when there is no
160
+ default, and returns a `Purchase`: the `seconds` and `minutes` from the purchase response, without a
161
+ follow-up request. A lost or unusable response can still raise an error after payment; retry with
162
+ the `idempotency_key` attached to that error. With no usable card it
163
+ raises `PaymentMethodRequiredError` and sends nothing: `no_default_card` when there is none (add one
164
+ at the URL from `hs.billing.add_card_url()` or in the app), `default_card_required` when several
165
+ cards could be charged and none is the default (pick one with `hs.billing.set_default_card(card_id)`).
166
+
167
+ Show your buyer this notice before you call it, then pass `acknowledge_no_refund=True`:
168
+
169
+ > Minutes are added to your account instantly after payment. All purchases are final and
170
+ > non-refundable.
171
+
172
+ The flag records that the notice was shown. It does not gate the charge, and without it `buy()`
173
+ still goes ahead.
174
+
175
+ ### Allow buying first
176
+
177
+ Buying from code is off until you allow it in three places. If any is missing, the call fails with
178
+ `pat_scope_missing`.
179
+
180
+ 1. The token has the permission labeled "Buy minutes and manage your saved payment methods" (the
181
+ `billing` scope), ticked when you create it.
182
+ 2. The token has "Let this token buy minutes" turned on, on the same "Create a new token" form.
183
+ 3. "Let connected apps buy minutes" is on for the whole account.
184
+
185
+ All of it is in the app under Account → MCP.
186
+
187
+ ### When a purchase does not go through
188
+
189
+ If the saved card was declined or cannot be charged without you, the call fails with
190
+ `pat_purchase_requires_app` (code 4021, status 403). If the bank wants to confirm the payment, it
191
+ fails with `pat_purchase_requires_authentication` (code 4023, status 403). In both cases nothing was
192
+ charged: buy that pack in the app. A key that met 4021 is spent, so the next `buy()` call makes a
193
+ new one.
194
+
195
+ A purchase is retried at most once, on a network error, a 502 or a 503, with the same idempotency
196
+ key, so those retries cannot charge you twice. When `buy()` still gives up after the request was
197
+ sent, the card may have been charged: this includes network failures, answers the SDK cannot
198
+ use, a 401, a 429, any 403 other than codes 4021 and 4023, and any 5xx response. Every such error carries `idempotency_key`; ask again with it and the purchase can
199
+ never happen twice:
200
+
201
+ ```python
202
+ from hushscript import HushscriptError
203
+
204
+ try:
205
+ purchase = hs.buy("300min", acknowledge_no_refund=True)
206
+ except HushscriptError as error:
207
+ refused = error.status in {400, 402, 404, 422} or (
208
+ error.status == 403 and error.numeric_code in {4021, 4023}
209
+ )
210
+ if error.idempotency_key is None or refused:
211
+ raise # only these refusals charged nothing; any other failure keeps the key for a retry
212
+ purchase = hs.buy("300min", acknowledge_no_refund=True, idempotency_key=error.idempotency_key)
213
+ ```
214
+
215
+ A key you pass yourself must be 1 to 128 characters from `A-Za-z0-9_-`. Do not reuse a key for a
216
+ different pack.
217
+
218
+ ## Export subtitles
219
+
220
+ ```python
221
+ Path("meeting.srt").write_text(transcript.to_srt(), encoding="utf-8")
222
+ Path("meeting.vtt").write_text(transcript.to_vtt("es"), encoding="utf-8")
223
+ Path("meeting.txt").write_text(transcript.to_text(timestamps=True), encoding="utf-8")
224
+ ```
225
+
226
+ The output is byte for byte what the Hushscript app exports. `to_srt` and `to_vtt` wrap lines into
227
+ caption cues by default; pass `captions=False` for one cue per utterance.
228
+
229
+ ## Agents: sign up without a human account
230
+
231
+ An agent can open an account, have it paid for at a hosted checkout page, and claim its own token.
232
+ The first two calls need no token yet, so they are static.
233
+
234
+ ```python
235
+ import os
236
+ import tempfile
237
+ from pathlib import Path
238
+
239
+ from hushscript import AgentInfo, Hushscript
240
+
241
+ TOKEN_FILE = Path("agent-token")
242
+
243
+
244
+ def keep(token: str) -> None:
245
+ """Writes the token to a private temporary file and renames it into place: the file is never
246
+ half a token, and an older token is not truncated before the new one is safely on disk."""
247
+ descriptor, temporary = tempfile.mkstemp(dir=TOKEN_FILE.parent, prefix=".token-") # mode 0600
248
+ try:
249
+ with os.fdopen(descriptor, "w", encoding="utf-8") as file:
250
+ file.write(f"{token}\n")
251
+ file.flush()
252
+ os.fsync(file.fileno())
253
+ os.replace(temporary, TOKEN_FILE)
254
+ except BaseException:
255
+ Path(temporary).unlink(missing_ok=True)
256
+ raise
257
+
258
+
259
+ signup = Hushscript.agent_signup(
260
+ email="agent@example.com",
261
+ pack="45min",
262
+ accept_terms="<the policy version you accept>",
263
+ agent=AgentInfo(name="My agent", platform="custom"),
264
+ )
265
+ print(f"Pay {signup.price.formatted} at {signup.checkout_url}")
266
+
267
+ # After the checkout is paid:
268
+ account = Hushscript.agent_claim(signup.signup_id, signup.claim_secret)
269
+ keep(account.token) # before anything else can fail
270
+ with Hushscript(account.token) as hs:
271
+ rotated = hs.agent.rotate_token() # revokes the claimed token; the client sends the new one
272
+ keep(rotated.token)
273
+ ```
274
+
275
+ - `accept_terms` is never filled in for you; an empty one raises `ValidationError` with the code
276
+ `accept_terms_required` before any request. A wrong version fails with `policy_version_stale`, and
277
+ the error's `hint` names the current one.
278
+ - The claim answers `wrong_state` (a `ConflictError`) until the checkout is paid.
279
+ - New agent accounts can buy only `45min` and `300min` until earlier purchases settle.
280
+ - Keep `claim_secret` and every token: the server shows each of them once, and a rotation revokes
281
+ the token it replaces, so lose the new one and the account is out of reach. A claim or a rotation
282
+ whose answer is lost, or comes back without a usable token, raises `OutcomeUnknownError` and is
283
+ never retried, because a retry could not succeed; its `hint` says what to check. When the answer
284
+ did carry something that looked like a token, `error.received_token` holds it, so you can store it
285
+ before anything else. It is kept out of the error's text, repr, pickle and logs.
286
+ - `dry_run=True` validates a signup without opening a checkout.
287
+
288
+ ## Errors
289
+
290
+ Every error the SDK raises extends `HushscriptError`, so one `except` covers everything:
291
+
292
+ ```python
293
+ from hushscript import HushscriptError, InsufficientBalanceError
294
+
295
+ try:
296
+ hs.transcribe("meeting.mp3")
297
+ except InsufficientBalanceError as error:
298
+ print(error.hint)
299
+ except HushscriptError as error:
300
+ print(error.code, error.hint or error.message)
301
+ ```
302
+
303
+ An error carries `code` (a slug such as `insufficient_balance`), `numeric_code`, `status`,
304
+ `message`, `hint` (what to do next, or `None`), `details` and `retry_after`. The classes:
305
+
306
+ ```
307
+ HushscriptError
308
+ ├─ ConfigurationError
309
+ ├─ PreflightError no credit was held
310
+ │ ├─ UnsupportedMediaError
311
+ │ ├─ FileLimitError
312
+ │ ├─ DailyLimitError
313
+ │ └─ ResumeMismatchError
314
+ ├─ ApiError has status
315
+ │ ├─ AuthenticationError 401
316
+ │ ├─ PermissionDeniedError 403
317
+ │ ├─ InsufficientBalanceError 402
318
+ │ ├─ NotFoundError 404
319
+ │ ├─ ConflictError 409
320
+ │ ├─ ValidationError 400, 422
321
+ │ └─ RateLimitError 429
322
+ ├─ PaymentMethodRequiredError
323
+ ├─ UploadError
324
+ ├─ JobFailedError
325
+ ├─ WaitTimeoutError has resume_token
326
+ ├─ AbortError has resume_token once a job exists
327
+ ├─ NetworkError
328
+ └─ OutcomeUnknownError
329
+ ```
330
+
331
+ An error raised after an upload started, or after a purchase was sent, carries `idempotency_key`;
332
+ once the job exists it carries `resume_token` too. Pass the token to `hs.resume(token, file)` to
333
+ carry on without a second charge.
334
+
335
+ Every error that wraps another ends its message with the original's type and its message, tokens and
336
+ signed-URL queries removed, for example `(ConnectError: refused)`. Where the original comes from the
337
+ HTTP or parsing layer (`network_error`, `outcome_unknown`, `unexpected_error`, `bad_response`), the
338
+ `__cause__` is a stand-in with that text, not the original: an httpx error keeps the request it sent
339
+ and a JSON error the body it read. Where it comes from a local file or media read
340
+ (`file_unreadable`, `source_read_failed`, `duration_unreadable`, a resume token that does not
341
+ decode), the cause is the original error itself. A failure of the SDK itself that is not a transport
342
+ failure is `unexpected_error`; its message says whether the request was sent when the SDK knows. A
343
+ purchase that provably failed before its request was sent carries no `idempotency_key` and says so:
344
+ nothing was charged.
345
+
346
+ ## What the SDK does for you
347
+
348
+ - **Checks before it spends.** It sniffs the format from the file's bytes, probes the duration,
349
+ and refuses a file the server would refuse, or one your daily limit cannot cover,
350
+ before any minutes are held.
351
+ - **Keeps retries from charging twice.** Every upload and purchase carries an idempotency key, and
352
+ retries reuse it. A `resume_token` replays the same job instead of starting a new one, and the
353
+ key on a failed purchase does the same for `buy()`.
354
+ - **Retries what is safe.** Reads, heartbeats and part uploads retry on network errors, 429 and 5xx
355
+ with jittered backoff, and honor `Retry-After` up to 60 seconds. A purchase retries once on a
356
+ network error, 502 or 503. A claim, a rotation and a signup never retry.
357
+ - **Paces uploads.** The API allows four upload creates a minute. A loop over a folder waits as
358
+ needed, and `on_progress` keeps reporting.
359
+ - **Keeps the upload alive.** It uploads parts in parallel with bounded memory, falls back to the
360
+ proxied route when direct upload fails, refreshes expired part URLs and sends a heartbeat every
361
+ two minutes.
362
+ - **Waits and fetches.** It polls the job until it is done, raises `JobFailedError` if it fails,
363
+ and gives you a `resume_token` if the wait times out. The job keeps running.
364
+ - **Preserves unknown API fields.** Friendly results and nested result models expose them in
365
+ `extra`, keeping the API's key spelling. The top-level mapping is read-only; nested values are
366
+ independent copies, so modifying returned nested data does not change the result.
367
+ - **Redacts secrets.** The token, a received token and every signed upload URL stay out of every
368
+ error, log line, `repr`, pickle and copy. The results that hand you a secret
369
+ (`AgentSignup.claim_secret`, `AgentAccount.token`, `RotatedToken.token`) keep it readable as a
370
+ field, but `repr`, `str` and `pprint` show `[redacted]` in its place; `dataclasses.asdict`, `json`
371
+ and `pickle` are your own export and carry the value. Once an HTTP call has returned or raised, no
372
+ local of an SDK frame and no attribute of an SDK object reachable from one holds the token, a
373
+ signed URL or a claim secret as plain text; the SDK keeps them in `Secret` and `Credentials`
374
+ holders that print nothing. Once a call is over, the request httpx keeps on the response and on
375
+ its errors is stripped in place: no `Authorization` header, no query string, no body; a streamed
376
+ response's request is stripped when its body is read to the end or closed. The body of a claim,
377
+ rotation or signup answer is dropped once read. While a call runs, httpx's own frames hold the
378
+ request as sent; a tool that captures local variables at that moment (for example a profiler, or
379
+ Sentry with `include_local_variables` on an event raised from inside the call) can see them there.
380
+
381
+ ## Supported formats
382
+
383
+ MP3, M4A, MP4 and MOV (the audio track of a video is used), WAV, FLAC, and OGG with Vorbis or Opus.
384
+ Files can be up to 5 GB and from 15 seconds to 10 hours. The SDK does not convert media. For
385
+ anything else, convert it first:
386
+
387
+ ```sh
388
+ ffmpeg -i input -vn -c:a libopus output.opus
389
+ ```
390
+
391
+ ## Configuration
392
+
393
+ | Option | Env var | Default |
394
+ | ------------------------ | --------------------- | -------------------------------------------------------------------- |
395
+ | `token` (first argument) | `HUSHSCRIPT_TOKEN` | none |
396
+ | `base_url` | `HUSHSCRIPT_BASE_URL` | `https://api.hushscript.com` |
397
+ | `timeout` | none | 60 s per request |
398
+ | `max_retries` | none | 3 |
399
+ | `log_level` | `HUSHSCRIPT_LOG` | `off` (`debug` logs one metadata line per request to standard error) |
400
+ | `clock` | none | `SystemClock`; pass a `Clock` in tests |
401
+
402
+ An explicit option beats its environment variable. A `None`, empty or whitespace-only option, and
403
+ an empty or whitespace-only environment variable, count as not set: the option falls back to the
404
+ environment variable, then to the default. A token that is still missing raises `ConfigurationError`
405
+ with the setup hint. The base URL must be `https://`, or `http://` to `localhost`, `127.0.0.1` or
406
+ `[::1]`, with no path. Each upload part has its own 300 s timeout, which you cannot change.
407
+
408
+ ## Privacy
409
+
410
+ The SDK sends no telemetry. The only client identification is the `User-Agent` header
411
+ (`hushscript-python/<version> python/<version>`). Logging is off by default;
412
+ `HUSHSCRIPT_LOG=debug` (or `log_level="debug"`) writes one line per request with the method, path,
413
+ status and timing, and never a header, body, title or transcript text.
414
+
415
+ ## The low-level API
416
+
417
+ Every `/v1` operation is available as `hs.api`, the generated client on the same retries, timeouts
418
+ and redaction:
419
+
420
+ ```python
421
+ from hushscript.generated.api.default import get_account
422
+
423
+ response = get_account.sync_detailed(client=hs.api)
424
+ ```
425
+
426
+ An answer the generated code cannot read (a body that is not JSON, or lacks a field) raises
427
+ `HushscriptError` with code `bad_response`, or the error for its status, never a bare
428
+ `JSONDecodeError` or `KeyError`. A failure after a purchase went out carries its `idempotency_key`.
429
+
430
+ ## Server only
431
+
432
+ The SDK is for servers and scripts. The API does not accept browser origins, and a token shipped in
433
+ client-side code is a leaked token.
434
+
435
+ ## Pricing
436
+
437
+ Personal accounts buy prepaid minute packs. The price of a pack depends on your country, and your
438
+ minutes stay valid for a year after your last transcription or purchase. A transcription uses the
439
+ minutes of the recording; medical mode and each translation target use more. Current prices are on
440
+ [hushscript.com/pricing](https://hushscript.com/pricing).
441
+
442
+ ## Requirements
443
+
444
+ Python 3.10 or newer. CI covers 3.10, 3.11, 3.12, 3.13, 3.14, and 3.15.
445
+ The 3.15 image builds final Python 3.15.0 from its checksum-pinned official source.
446
+ `just build <version>` installs the built wheel with only runtime dependencies and imports it. The package is typed (`py.typed`).
447
+
448
+ ## Links
449
+
450
+ - Documentation: <https://hushscript.com/sdks>
451
+ - REST API reference: <https://hushscript.com/developers>
452
+ - Package on PyPI: <https://pypi.org/project/hushscript/>
453
+ - Source: <https://github.com/Hushscript/hushscript-sdk-python>
454
+ - Other Hushscript SDKs: [TypeScript](https://github.com/Hushscript/hushscript-sdk-typescript), [Go](https://github.com/Hushscript/hushscript-sdk-go), [PHP](https://github.com/Hushscript/hushscript-sdk-php), [CLI](https://github.com/Hushscript/hushscript-cli)
455
+
456
+ ## Releases
457
+
458
+ Releases come from release-please. The package is published to PyPI by `publish.yml` through PyPI
459
+ trusted publishing (OIDC) with PEP 740 attestations: no PyPI token is stored in GitHub. The build job
460
+ has no secrets and no `id-token`; the publish job runs behind the `release` environment, checks the
461
+ digest of every artifact the build job produced, and only then attests and uploads. Before the first
462
+ release the owner adds a pending trusted publisher for the project `hushscript` on pypi.org (owner
463
+ `Hushscript`, this repository, workflow `publish.yml`, environment `release`).
464
+
465
+ ## Development
466
+
467
+ Everything runs in containers; nothing touches a host toolchain. You need Docker with Compose v2 and
468
+ [just](https://github.com/casey/just).
469
+
470
+ ```sh
471
+ just ci # lint, tests, conformance, examples and build for each version from 3.10 through 3.15
472
+ just test 3.14 # unit tests with coverage on one interpreter
473
+ just lint # every static check
474
+ just examples # every program in examples/ against the conformance mock
475
+ just build # sdist and wheel, twine and wheel-contents checks, install on the default interpreter (3.10)
476
+ just regen # regenerate openapi.json and src/hushscript/generated from the production spec
477
+ ```
478
+
479
+ The conformance mock is built from the `hushscript-sdk-conformance` repository at the tag or commit in
480
+ `conformance-ref`; `just ci` fails when a scenario is skipped (`CONFORMANCE_STRICT=1`). To use a local
481
+ checkout instead:
482
+
483
+ ```sh
484
+ CONFORMANCE_CONTEXT=../hushscript-sdk-conformance CONFORMANCE_DIR=../hushscript-sdk-conformance just ci
485
+ ```
486
+
487
+ `just test` reads the conformance repo's fixtures (`fixtures/hint-rendering.json`) from
488
+ `CONFORMANCE_DIR`, which defaults to `../hushscript-sdk-conformance`; the hint-rendering test fails
489
+ without them.
490
+
491
+ Runtime dependencies in `pyproject.toml` are compatible ranges on purpose: a library that pins exact
492
+ versions breaks the resolvers of the projects that install it. Everything else is pinned: the dev
493
+ and tool dependencies, the build backend and the container images are locked in `uv.lock` and in
494
+ `pyproject.toml`, and `just ci` tests exactly that set.
495
+
496
+ ## Security
497
+
498
+ See [SECURITY.md](SECURITY.md).
499
+
500
+ ## License
501
+
502
+ MIT