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
hushscript/_client.py ADDED
@@ -0,0 +1,687 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ from contextlib import asynccontextmanager, contextmanager
5
+ from typing import TYPE_CHECKING, BinaryIO
6
+
7
+ import httpx
8
+ from typing_extensions import Self
9
+
10
+ from ._clock import Clock, SystemClock
11
+ from ._config import PART_TIMEOUT, Config, resolve_config, resolve_tokenless_config
12
+ from ._errors import ConfigurationError
13
+ from ._http import AsyncRetryingTransport, RetryingTransport, TransportSettings
14
+ from ._log import RequestLog
15
+ from ._pacing import TokenBucket
16
+ from ._redact import REDACTED, Secret
17
+ from ._response import parsed
18
+ from ._version import __version__
19
+ from .generated.client import AuthenticatedClient
20
+ from .resources.account import Account, Balance
21
+ from .resources.agent import (
22
+ Agent,
23
+ AgentAccount,
24
+ AgentInfo,
25
+ AgentSignup,
26
+ AsyncAgent,
27
+ check_accept_terms,
28
+ claim_body,
29
+ claim_path,
30
+ claimed,
31
+ remember_claim_secret,
32
+ signed_up,
33
+ signup_body,
34
+ )
35
+ from .resources.billing import AsyncBilling, Billing, Purchase
36
+ from .resources.jobs import AsyncJobs, Jobs
37
+ from .resources.transcripts import AsyncTranscripts, Transcripts
38
+ from .transcribe import apipeline, pipeline
39
+ from .transcribe.pipeline import Callbacks
40
+ from .transcribe.preflight import Options
41
+ from .transcribe.session import AsyncSession, SyncSession
42
+ from .transcribe.wait import DEFAULT_WAIT_TIMEOUT
43
+
44
+ if TYPE_CHECKING:
45
+ import os
46
+ from collections.abc import AsyncGenerator, Callable, Generator, Sequence
47
+ from types import TracebackType
48
+
49
+ from ._models import Progress
50
+ from .transcript import Transcript
51
+
52
+
53
+ def user_agent() -> str:
54
+ return f"hushscript-python/{__version__} python/{platform.python_version()}"
55
+
56
+
57
+ class _SharedTransport(httpx.BaseTransport):
58
+ """Gives a derived httpx client the hardened transport without letting it close it."""
59
+
60
+ def __init__(self, inner: httpx.BaseTransport) -> None:
61
+ self._inner = inner
62
+
63
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
64
+ try:
65
+ return self._inner.handle_request(request)
66
+ finally:
67
+ del request
68
+
69
+ def close(self) -> None:
70
+ """The owning `Hushscript` closes the transport."""
71
+
72
+
73
+ class _SharedAsyncTransport(httpx.AsyncBaseTransport):
74
+ def __init__(self, inner: httpx.AsyncBaseTransport) -> None:
75
+ self._inner = inner
76
+
77
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
78
+ try:
79
+ return await self._inner.handle_async_request(request)
80
+ finally:
81
+ del request
82
+
83
+ async def aclose(self) -> None:
84
+ """The owning `AsyncHushscript` closes the transport."""
85
+
86
+
87
+ class _ApiClient(AuthenticatedClient):
88
+ """The generated client on the hardened httpx client.
89
+
90
+ It holds no token: the transport adds the Authorization header per request, so a rotated token
91
+ is picked up and nothing here can show it. The generated `with_*` methods would build a plain
92
+ httpx client without the retrying transport, so they are replaced by ones that keep it.
93
+ """
94
+
95
+ _sync_transport: httpx.BaseTransport | None = None
96
+ _async_transport: httpx.AsyncBaseTransport | None = None
97
+
98
+ def __repr__(self) -> str:
99
+ return f"AuthenticatedClient(base_url={self._base_url!r}, token={REDACTED!r})"
100
+
101
+ def bind_sync(self, client: httpx.Client, transport: httpx.BaseTransport) -> None:
102
+ self.set_httpx_client(client)
103
+ self._sync_transport = transport
104
+
105
+ def bind_async(self, client: httpx.AsyncClient, transport: httpx.AsyncBaseTransport) -> None:
106
+ self.set_async_httpx_client(client)
107
+ self._async_transport = transport
108
+
109
+ def get_httpx_client(self) -> httpx.Client:
110
+ if self._client is None:
111
+ raise ConfigurationError("This is an async client; use get_async_httpx_client().")
112
+ return self._client
113
+
114
+ def get_async_httpx_client(self) -> httpx.AsyncClient:
115
+ if self._async_client is None:
116
+ raise ConfigurationError("This is a sync client; use get_httpx_client().")
117
+ return self._async_client
118
+
119
+ def with_timeout(self, timeout: httpx.Timeout) -> _ApiClient:
120
+ return self._derive(timeout=timeout)
121
+
122
+ def with_headers(self, headers: dict[str, str]) -> _ApiClient:
123
+ return self._derive(headers=headers)
124
+
125
+ def with_cookies(self, cookies: dict[str, str]) -> _ApiClient:
126
+ return self._derive(cookies=cookies)
127
+
128
+ def _derive(
129
+ self,
130
+ *,
131
+ timeout: httpx.Timeout | None = None,
132
+ headers: dict[str, str] | None = None,
133
+ cookies: dict[str, str] | None = None,
134
+ ) -> _ApiClient:
135
+ derived = _ApiClient(base_url=self._base_url, token="", timeout=timeout or self._timeout)
136
+ if self._client is not None and self._sync_transport is not None:
137
+ derived.bind_sync(
138
+ httpx.Client(
139
+ base_url=self._client.base_url,
140
+ headers=_merged_headers(self._client.headers, headers),
141
+ cookies=_merged_cookies(self._client.cookies, cookies),
142
+ timeout=timeout or self._client.timeout,
143
+ transport=_SharedTransport(self._sync_transport),
144
+ ),
145
+ self._sync_transport,
146
+ )
147
+ if self._async_client is not None and self._async_transport is not None:
148
+ derived.bind_async(
149
+ httpx.AsyncClient(
150
+ base_url=self._async_client.base_url,
151
+ headers=_merged_headers(self._async_client.headers, headers),
152
+ cookies=_merged_cookies(self._async_client.cookies, cookies),
153
+ timeout=timeout or self._async_client.timeout,
154
+ transport=_SharedAsyncTransport(self._async_transport),
155
+ ),
156
+ self._async_transport,
157
+ )
158
+ return derived
159
+
160
+
161
+ def _merged_headers(current: httpx.Headers, extra: dict[str, str] | None) -> httpx.Headers:
162
+ merged = httpx.Headers(current)
163
+ merged.update(extra or {})
164
+ return merged
165
+
166
+
167
+ def _merged_cookies(current: httpx.Cookies, extra: dict[str, str] | None) -> httpx.Cookies:
168
+ merged = httpx.Cookies(current)
169
+ merged.update(extra or {})
170
+ return merged
171
+
172
+
173
+ def _sync_inner() -> httpx.BaseTransport:
174
+ return httpx.HTTPTransport()
175
+
176
+
177
+ def _async_inner() -> httpx.AsyncBaseTransport:
178
+ return httpx.AsyncHTTPTransport()
179
+
180
+
181
+ def _api_client(config: Config) -> _ApiClient:
182
+ return _ApiClient(base_url=config.base_url, token="", timeout=httpx.Timeout(config.timeout))
183
+
184
+
185
+ def _settings(config: Config, clock: Clock, bucket: TokenBucket) -> TransportSettings:
186
+ return TransportSettings(
187
+ clock=clock,
188
+ log=RequestLog(config.log_level),
189
+ credentials=config.credentials,
190
+ max_retries=config.max_retries,
191
+ base_url=httpx.URL(config.base_url),
192
+ timeout=config.timeout,
193
+ bucket=bucket,
194
+ )
195
+
196
+
197
+ # Signed direct-to-R2 part URLs are not API requests: this base URL matches no request, so the
198
+ # client built on it never sends the token and the part class of the call is honored.
199
+ _R2_ORIGIN = httpx.URL("https://r2.invalid")
200
+
201
+
202
+ def _r2_settings(config: Config, clock: Clock) -> TransportSettings:
203
+ return TransportSettings(
204
+ clock=clock,
205
+ log=RequestLog(config.log_level),
206
+ credentials=None,
207
+ max_retries=config.max_retries,
208
+ base_url=_R2_ORIGIN,
209
+ timeout=PART_TIMEOUT,
210
+ )
211
+
212
+
213
+ @contextmanager
214
+ def _tokenless_http(config: Config, clock: Clock | None) -> Generator[httpx.Client]:
215
+ """A client for the calls that need no token: it sends none, and never retries."""
216
+ chosen = clock or SystemClock()
217
+ settings = _settings(config, chosen, TokenBucket(chosen))
218
+ with httpx.Client(
219
+ base_url=config.base_url,
220
+ timeout=config.timeout,
221
+ headers={"User-Agent": user_agent()},
222
+ transport=RetryingTransport(_sync_inner(), settings),
223
+ follow_redirects=False,
224
+ ) as http:
225
+ yield http
226
+
227
+
228
+ @asynccontextmanager
229
+ async def _tokenless_async_http(
230
+ config: Config, clock: Clock | None
231
+ ) -> AsyncGenerator[httpx.AsyncClient]:
232
+ chosen = clock or SystemClock()
233
+ settings = _settings(config, chosen, TokenBucket(chosen))
234
+ async with httpx.AsyncClient(
235
+ base_url=config.base_url,
236
+ timeout=config.timeout,
237
+ headers={"User-Agent": user_agent()},
238
+ transport=AsyncRetryingTransport(_async_inner(), settings),
239
+ follow_redirects=False,
240
+ ) as http:
241
+ yield http
242
+
243
+
244
+ def _options( # noqa: PLR0913 the keyword surface of `transcribe()`, validated before any request
245
+ *,
246
+ title: str | None,
247
+ language: str | None,
248
+ speakers: bool,
249
+ translate_to: Sequence[str] | None,
250
+ keyterms: Sequence[str] | None,
251
+ prompt: str | None,
252
+ medical: bool,
253
+ multichannel: bool,
254
+ auto_insights: bool | None,
255
+ matter_code: str | None,
256
+ duration_seconds: float | None,
257
+ idempotency_key: str | None,
258
+ concurrency: int,
259
+ wait_timeout: float,
260
+ ) -> Options:
261
+ return Options(
262
+ title=title,
263
+ language=language,
264
+ speakers=speakers,
265
+ translate_to=tuple(translate_to or ()),
266
+ keyterms=tuple(keyterms or ()),
267
+ prompt=prompt,
268
+ medical=medical,
269
+ multichannel=multichannel,
270
+ auto_insights=auto_insights,
271
+ matter_code=matter_code,
272
+ duration_seconds=duration_seconds,
273
+ idempotency_key=idempotency_key,
274
+ concurrency=concurrency,
275
+ wait_timeout=wait_timeout,
276
+ )
277
+
278
+
279
+ class Hushscript:
280
+ """The synchronous Hushscript client. `api` is the generated low-level client on the same
281
+ retrying transport."""
282
+
283
+ api: AuthenticatedClient
284
+
285
+ def __init__( # the public surface of spec section 3
286
+ self,
287
+ token: str | None = None,
288
+ *,
289
+ base_url: str | None = None,
290
+ timeout: float = 60,
291
+ max_retries: int = 3,
292
+ log_level: str | None = None,
293
+ clock: Clock | None = None,
294
+ ) -> None:
295
+ self._config = resolve_config(
296
+ token, base_url=base_url, timeout=timeout, max_retries=max_retries, log_level=log_level
297
+ )
298
+ self._clock: Clock = clock or SystemClock()
299
+ bucket = TokenBucket(self._clock)
300
+ transport = RetryingTransport(_sync_inner(), _settings(self._config, self._clock, bucket))
301
+ self._http = httpx.Client(
302
+ base_url=self._config.base_url,
303
+ timeout=self._config.timeout,
304
+ headers={"User-Agent": user_agent()},
305
+ transport=transport,
306
+ follow_redirects=False,
307
+ )
308
+ self._r2 = httpx.Client(
309
+ timeout=PART_TIMEOUT,
310
+ headers={"User-Agent": user_agent()},
311
+ transport=RetryingTransport(_sync_inner(), _r2_settings(self._config, self._clock)),
312
+ follow_redirects=False,
313
+ )
314
+ api = _api_client(self._config)
315
+ api.bind_sync(self._http, transport)
316
+ self.api = api
317
+ self._session = SyncSession(
318
+ clock=self._clock,
319
+ credentials=self._config.credentials,
320
+ log=RequestLog(self._config.log_level),
321
+ http=self._http,
322
+ r2=self._r2,
323
+ )
324
+ self.jobs = Jobs(self._session)
325
+ self.transcripts = Transcripts(self._session)
326
+ self.billing = Billing(self._session)
327
+ self.agent = Agent(self._session)
328
+
329
+ def balance(self) -> Balance:
330
+ """What the account can spend. Net of holds: `held_seconds` says how much of it is already
331
+ committed, and is not to be subtracted again."""
332
+ response = self._http.get("/v1/account")
333
+ return parsed(response, self._config.credentials, Balance.from_account)
334
+
335
+ def account(self) -> Account:
336
+ """The account: balance, limits, card on file and data region."""
337
+ response = self._http.get("/v1/account")
338
+ return parsed(response, self._config.credentials, Account.from_body)
339
+
340
+ def buy(
341
+ self,
342
+ pack: str,
343
+ *,
344
+ acknowledge_no_refund: bool = False,
345
+ idempotency_key: str | None = None,
346
+ ) -> Purchase:
347
+ """Buys a minute pack with the saved card; it is `hs.billing.buy`."""
348
+ return self.billing.buy(
349
+ pack, acknowledge_no_refund=acknowledge_no_refund, idempotency_key=idempotency_key
350
+ )
351
+
352
+ @staticmethod
353
+ def agent_signup( # noqa: PLR0913 the keyword surface of spec 6.6
354
+ *,
355
+ email: str,
356
+ pack: str,
357
+ accept_terms: str,
358
+ agent: AgentInfo,
359
+ data_region: str | None = None,
360
+ dry_run: bool = False,
361
+ base_url: str | None = None,
362
+ timeout: float = 60,
363
+ log_level: str | None = None,
364
+ clock: Clock | None = None,
365
+ ) -> AgentSignup:
366
+ """Opens an agent account. It needs no token (HUSHSCRIPT_TOKEN is not read or sent): the
367
+ agent then has the checkout paid at `checkout_url` and claims the account with
368
+ `agent_claim`. `accept_terms`, the policy version being accepted, is never defaulted. The
369
+ request is never retried; a lost answer is an `OutcomeUnknownError`."""
370
+ check_accept_terms(accept_terms)
371
+ config = resolve_tokenless_config(base_url=base_url, timeout=timeout, log_level=log_level)
372
+ body = signup_body(
373
+ email=email,
374
+ pack=pack,
375
+ accept_terms=accept_terms,
376
+ agent=agent,
377
+ data_region=data_region,
378
+ dry_run=dry_run,
379
+ )
380
+ with _tokenless_http(config, clock) as http:
381
+ return signed_up(http.post("/v1/agent/accounts", json=body), config.credentials)
382
+
383
+ @staticmethod
384
+ def agent_claim(
385
+ signup_id: str,
386
+ claim_secret: str,
387
+ *,
388
+ base_url: str | None = None,
389
+ timeout: float = 60,
390
+ log_level: str | None = None,
391
+ clock: Clock | None = None,
392
+ ) -> AgentAccount:
393
+ """Claims a paid agent signup and returns the account's token, which the server shows
394
+ once: write it down before doing anything else. It answers `wrong_state` (a
395
+ `ConflictError`) until the checkout is paid, and is never retried: a lost answer, or one
396
+ without a usable token, is an `OutcomeUnknownError` whose hint says what to check."""
397
+ path = claim_path(signup_id)
398
+ secret = Secret(claim_secret)
399
+ del claim_secret # no frame of the SDK holds the secret as text from here on (R81)
400
+ config = resolve_tokenless_config(base_url=base_url, timeout=timeout, log_level=log_level)
401
+ remember_claim_secret(config.credentials, secret)
402
+ with _tokenless_http(config, clock) as http:
403
+ response = http.post(path, json=claim_body(secret))
404
+ return claimed(response, config.credentials)
405
+
406
+ def transcribe( # noqa: PLR0913 the keyword surface of spec 6.1
407
+ self,
408
+ file: str | os.PathLike[str] | bytes | BinaryIO,
409
+ *,
410
+ title: str | None = None,
411
+ language: str | None = None,
412
+ speakers: bool = True,
413
+ translate_to: Sequence[str] | None = None,
414
+ keyterms: Sequence[str] | None = None,
415
+ prompt: str | None = None,
416
+ medical: bool = False,
417
+ multichannel: bool = False,
418
+ auto_insights: bool | None = None,
419
+ matter_code: str | None = None,
420
+ duration_seconds: float | None = None,
421
+ size: int | None = None,
422
+ idempotency_key: str | None = None,
423
+ concurrency: int = 4,
424
+ wait_timeout: float = DEFAULT_WAIT_TIMEOUT,
425
+ on_progress: Callable[[Progress], None] | None = None,
426
+ on_resume_token: Callable[[str], None] | None = None,
427
+ ) -> Transcript:
428
+ """Uploads `file`, waits for the transcript and returns it (spec 8). A non-seekable
429
+ stream needs `size` and `duration_seconds`.
430
+
431
+ Key terms and the prompt are trimmed first: blank terms are dropped, and an empty
432
+ list or a blank prompt is not sent. `keyterms` takes up to 1000 unique terms of at most
433
+ 6 words and 128 characters each. `prompt` takes up to 2000 characters; with key terms
434
+ set, the 2000 covers the prompt and the key terms the API wraps around it (203
435
+ characters of fixed text, each term plus 2, and 1 between terms)."""
436
+ options = _options(
437
+ title=title,
438
+ language=language,
439
+ speakers=speakers,
440
+ translate_to=translate_to,
441
+ keyterms=keyterms,
442
+ prompt=prompt,
443
+ medical=medical,
444
+ multichannel=multichannel,
445
+ auto_insights=auto_insights,
446
+ matter_code=matter_code,
447
+ duration_seconds=duration_seconds,
448
+ idempotency_key=idempotency_key,
449
+ concurrency=concurrency,
450
+ wait_timeout=wait_timeout,
451
+ )
452
+ callbacks = Callbacks(on_progress=on_progress, on_resume_token=on_resume_token)
453
+ return pipeline.run_transcribe(self._session, file, options, size, callbacks)
454
+
455
+ def resume(
456
+ self,
457
+ token: str,
458
+ file: str | os.PathLike[str] | bytes | BinaryIO | None = None,
459
+ *,
460
+ concurrency: int = 4,
461
+ wait_timeout: float = DEFAULT_WAIT_TIMEOUT,
462
+ on_progress: Callable[[Progress], None] | None = None,
463
+ ) -> Transcript:
464
+ """Picks a transcription up from the token of an earlier call (spec 7.3). `file` is needed
465
+ only while the job is still uploading."""
466
+ options = Options(concurrency=concurrency, wait_timeout=wait_timeout)
467
+ return pipeline.run_resume(
468
+ self._session, token, file, options, Callbacks(on_progress=on_progress)
469
+ )
470
+
471
+ def close(self) -> None:
472
+ self._http.close()
473
+ self._r2.close()
474
+
475
+ def __enter__(self) -> Self:
476
+ return self
477
+
478
+ def __exit__(
479
+ self,
480
+ exc_type: type[BaseException] | None,
481
+ exc: BaseException | None,
482
+ traceback: TracebackType | None,
483
+ ) -> None:
484
+ self.close()
485
+
486
+ def __repr__(self) -> str:
487
+ return f"Hushscript(base_url={self._config.base_url!r})"
488
+
489
+
490
+ class AsyncHushscript:
491
+ """The asyncio client with the same surface as `Hushscript`."""
492
+
493
+ api: AuthenticatedClient
494
+
495
+ def __init__( # the public surface of spec section 3
496
+ self,
497
+ token: str | None = None,
498
+ *,
499
+ base_url: str | None = None,
500
+ timeout: float = 60,
501
+ max_retries: int = 3,
502
+ log_level: str | None = None,
503
+ clock: Clock | None = None,
504
+ ) -> None:
505
+ self._config = resolve_config(
506
+ token, base_url=base_url, timeout=timeout, max_retries=max_retries, log_level=log_level
507
+ )
508
+ self._clock: Clock = clock or SystemClock()
509
+ bucket = TokenBucket(self._clock)
510
+ transport = AsyncRetryingTransport(
511
+ _async_inner(), _settings(self._config, self._clock, bucket)
512
+ )
513
+ self._http = httpx.AsyncClient(
514
+ base_url=self._config.base_url,
515
+ timeout=self._config.timeout,
516
+ headers={"User-Agent": user_agent()},
517
+ transport=transport,
518
+ follow_redirects=False,
519
+ )
520
+ self._r2 = httpx.AsyncClient(
521
+ timeout=PART_TIMEOUT,
522
+ headers={"User-Agent": user_agent()},
523
+ transport=AsyncRetryingTransport(
524
+ _async_inner(), _r2_settings(self._config, self._clock)
525
+ ),
526
+ follow_redirects=False,
527
+ )
528
+ api = _api_client(self._config)
529
+ api.bind_async(self._http, transport)
530
+ self.api = api
531
+ self._session = AsyncSession(
532
+ clock=self._clock,
533
+ credentials=self._config.credentials,
534
+ log=RequestLog(self._config.log_level),
535
+ http=self._http,
536
+ r2=self._r2,
537
+ )
538
+ self.jobs = AsyncJobs(self._session)
539
+ self.transcripts = AsyncTranscripts(self._session)
540
+ self.billing = AsyncBilling(self._session)
541
+ self.agent = AsyncAgent(self._session)
542
+
543
+ async def balance(self) -> Balance:
544
+ response = await self._http.get("/v1/account")
545
+ return parsed(response, self._config.credentials, Balance.from_account)
546
+
547
+ async def account(self) -> Account:
548
+ response = await self._http.get("/v1/account")
549
+ return parsed(response, self._config.credentials, Account.from_body)
550
+
551
+ async def buy(
552
+ self,
553
+ pack: str,
554
+ *,
555
+ acknowledge_no_refund: bool = False,
556
+ idempotency_key: str | None = None,
557
+ ) -> Purchase:
558
+ return await self.billing.buy(
559
+ pack, acknowledge_no_refund=acknowledge_no_refund, idempotency_key=idempotency_key
560
+ )
561
+
562
+ @staticmethod
563
+ async def agent_signup( # noqa: PLR0913 the keyword surface of spec 6.6
564
+ *,
565
+ email: str,
566
+ pack: str,
567
+ accept_terms: str,
568
+ agent: AgentInfo,
569
+ data_region: str | None = None,
570
+ dry_run: bool = False,
571
+ base_url: str | None = None,
572
+ timeout: float = 60,
573
+ log_level: str | None = None,
574
+ clock: Clock | None = None,
575
+ ) -> AgentSignup:
576
+ check_accept_terms(accept_terms)
577
+ config = resolve_tokenless_config(base_url=base_url, timeout=timeout, log_level=log_level)
578
+ body = signup_body(
579
+ email=email,
580
+ pack=pack,
581
+ accept_terms=accept_terms,
582
+ agent=agent,
583
+ data_region=data_region,
584
+ dry_run=dry_run,
585
+ )
586
+ async with _tokenless_async_http(config, clock) as http:
587
+ response = await http.post("/v1/agent/accounts", json=body)
588
+ return signed_up(response, config.credentials)
589
+
590
+ @staticmethod
591
+ async def agent_claim(
592
+ signup_id: str,
593
+ claim_secret: str,
594
+ *,
595
+ base_url: str | None = None,
596
+ timeout: float = 60,
597
+ log_level: str | None = None,
598
+ clock: Clock | None = None,
599
+ ) -> AgentAccount:
600
+ path = claim_path(signup_id)
601
+ secret = Secret(claim_secret)
602
+ del claim_secret # no frame of the SDK holds the secret as text from here on (R81)
603
+ config = resolve_tokenless_config(base_url=base_url, timeout=timeout, log_level=log_level)
604
+ remember_claim_secret(config.credentials, secret)
605
+ async with _tokenless_async_http(config, clock) as http:
606
+ response = await http.post(path, json=claim_body(secret))
607
+ return claimed(response, config.credentials)
608
+
609
+ async def transcribe( # noqa: PLR0913 the keyword surface of spec 6.1
610
+ self,
611
+ file: str | os.PathLike[str] | bytes | BinaryIO,
612
+ *,
613
+ title: str | None = None,
614
+ language: str | None = None,
615
+ speakers: bool = True,
616
+ translate_to: Sequence[str] | None = None,
617
+ keyterms: Sequence[str] | None = None,
618
+ prompt: str | None = None,
619
+ medical: bool = False,
620
+ multichannel: bool = False,
621
+ auto_insights: bool | None = None,
622
+ matter_code: str | None = None,
623
+ duration_seconds: float | None = None,
624
+ size: int | None = None,
625
+ idempotency_key: str | None = None,
626
+ concurrency: int = 4,
627
+ wait_timeout: float = DEFAULT_WAIT_TIMEOUT,
628
+ on_progress: Callable[[Progress], None] | None = None,
629
+ on_resume_token: Callable[[str], None] | None = None,
630
+ ) -> Transcript:
631
+ """The async twin of `Hushscript.transcribe()`.
632
+
633
+ Key terms and the prompt are trimmed first: blank terms are dropped, and an empty
634
+ list or a blank prompt is not sent. `keyterms` takes up to 1000 unique terms of at most
635
+ 6 words and 128 characters each. `prompt` takes up to 2000 characters; with key terms
636
+ set, the 2000 covers the prompt and the key terms the API wraps around it (203
637
+ characters of fixed text, each term plus 2, and 1 between terms)."""
638
+ options = _options(
639
+ title=title,
640
+ language=language,
641
+ speakers=speakers,
642
+ translate_to=translate_to,
643
+ keyterms=keyterms,
644
+ prompt=prompt,
645
+ medical=medical,
646
+ multichannel=multichannel,
647
+ auto_insights=auto_insights,
648
+ matter_code=matter_code,
649
+ duration_seconds=duration_seconds,
650
+ idempotency_key=idempotency_key,
651
+ concurrency=concurrency,
652
+ wait_timeout=wait_timeout,
653
+ )
654
+ callbacks = Callbacks(on_progress=on_progress, on_resume_token=on_resume_token)
655
+ return await apipeline.run_transcribe(self._session, file, options, size, callbacks)
656
+
657
+ async def resume(
658
+ self,
659
+ token: str,
660
+ file: str | os.PathLike[str] | bytes | BinaryIO | None = None,
661
+ *,
662
+ concurrency: int = 4,
663
+ wait_timeout: float = DEFAULT_WAIT_TIMEOUT,
664
+ on_progress: Callable[[Progress], None] | None = None,
665
+ ) -> Transcript:
666
+ options = Options(concurrency=concurrency, wait_timeout=wait_timeout)
667
+ return await apipeline.run_resume(
668
+ self._session, token, file, options, Callbacks(on_progress=on_progress)
669
+ )
670
+
671
+ async def aclose(self) -> None:
672
+ await self._http.aclose()
673
+ await self._r2.aclose()
674
+
675
+ async def __aenter__(self) -> Self:
676
+ return self
677
+
678
+ async def __aexit__(
679
+ self,
680
+ exc_type: type[BaseException] | None,
681
+ exc: BaseException | None,
682
+ traceback: TracebackType | None,
683
+ ) -> None:
684
+ await self.aclose()
685
+
686
+ def __repr__(self) -> str:
687
+ return f"AsyncHushscript(base_url={self._config.base_url!r})"