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,138 @@
1
+ # Generated by scripts/generate.py from openapi.json and the conformance hints.json. Do not edit.
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Final
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class ErrorCatalogEntry:
9
+ code: int
10
+ status: int
11
+ meaning: str
12
+
13
+
14
+ ERROR_CATALOG: Final[dict[str, ErrorCatalogEntry]] = {
15
+ "bad_request": ErrorCatalogEntry(1000, 400, "The request body could not be parsed, or a required field was missing. Check the request against this operation's schema and resend."),
16
+ "invalid_email": ErrorCatalogEntry(1001, 400, "The email address failed format or safety validation (address lists, spoofed display names, control characters and quoted local parts are all rejected). Send a single plain address."),
17
+ "invalid_password": ErrorCatalogEntry(1002, 400, "The password did not meet the length bounds this API enforces. Choose a password between the documented minimum and maximum byte length."),
18
+ "missing_token": ErrorCatalogEntry(1003, 400, "No longer emitted: GET /api/auth/verify without a token now redirects to the app with ?error=link_expired instead of returning this JSON error. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it."),
19
+ "invalid_locale": ErrorCatalogEntry(1004, 400, "The requested locale is not one this account or API supports. Use one of the documented locale codes."),
20
+ "bad_part_number": ErrorCatalogEntry(1005, 400, "The part number is outside the plan for this upload (below 1 or past the last part for the declared size). Recompute the part count from the declared size and part size."),
21
+ "no_payment_method": ErrorCatalogEntry(1006, 400, "No payment method was available to complete this action. Add or select a saved payment method before retrying."),
22
+ "malformed_event": ErrorCatalogEntry(1007, 400, "An inbound webhook or event payload could not be parsed or verified. This is not actionable from a normal API client."),
23
+ "invalid_filename": ErrorCatalogEntry(1020, 422, "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: The uploaded file's name failed validation. Rename the file to remove unsupported characters and retry."),
24
+ "duration_out_of_range": ErrorCatalogEntry(1021, 422, "The declared or verified duration is shorter than 15 seconds or longer than this API's maximum accepted length of 10 hours. Trim or split the recording before uploading."),
25
+ "size_out_of_range": ErrorCatalogEntry(1022, 422, "The declared byte size is zero or exceeds this API's maximum accepted upload size. Compress or split the file before uploading."),
26
+ "part_too_large": ErrorCatalogEntry(1023, 422, "One uploaded part exceeded the maximum part size this API accepts. Resend that part split into smaller chunks."),
27
+ "size_exceeded": ErrorCatalogEntry(1024, 422, "The bytes received for this upload would exceed the size declared at creation. Start a new upload with the correct declared size."),
28
+ "no_parts": ErrorCatalogEntry(1025, 422, "The upload was marked complete with no parts recorded. Upload at least one part before calling complete."),
29
+ "invalid_title": ErrorCatalogEntry(1026, 422, "The provided title exceeded the maximum length or contained disallowed content. Shorten or sanitize the title and retry."),
30
+ "invalid_speaker_names": ErrorCatalogEntry(1027, 422, "The speaker-name mapping was not valid JSON or exceeded the allowed shape. Send a flat object of speaker label to display name."),
31
+ "too_many_names": ErrorCatalogEntry(1028, 422, "The speaker-name mapping named more speakers than this API allows on one transcript. Remove entries before retrying."),
32
+ "invalid_clip": ErrorCatalogEntry(1029, 422, "The preview clip could not be read as audio. Send a supported audio format within the preview size and duration limits."),
33
+ "clip_too_long": ErrorCatalogEntry(1030, 422, "The preview clip exceeds the maximum preview duration. Trim the clip and retry."),
34
+ "unsupported_format": ErrorCatalogEntry(1031, 422, "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: The audio container or codec could not be probed by this API. Re-encode to one of the documented supported formats."),
35
+ "unknown_pack": ErrorCatalogEntry(1032, 422, "The requested pack id does not exist in this account's catalogue. Read GET /v1/billing/packs for the current, valid pack ids."),
36
+ "invalid_transcription_options": ErrorCatalogEntry(1033, 422, "The transcription_options object failed validation (an unsupported language, an invalid combination of options, or an over-limit field). Re-check each option against its documented bounds."),
37
+ "invalid_edit": ErrorCatalogEntry(1034, 422, "The submitted transcript edit was not valid for the current body (bad utterance shape or an out-of-range index). Reload the current body before editing."),
38
+ "too_many_tags": ErrorCatalogEntry(1035, 422, "The request would leave more tags on the transcript than this API allows. Remove existing tags before adding new ones."),
39
+ "no_speech": ErrorCatalogEntry(1036, 422, "No speech was detected in the submitted audio. Confirm the source file actually contains an audio track with speech."),
40
+ "url_invalid": ErrorCatalogEntry(1037, 400, "The supplied source URL is not a well-formed https URL. Provide a direct https link to the audio or video file."),
41
+ "url_blocked": ErrorCatalogEntry(1038, 422, "The supplied URL resolves to a host or address this API refuses to fetch (internal ranges, this API's own storage, or a disallowed host). Use a public, third-party hosted URL instead."),
42
+ "url_needs_app": ErrorCatalogEntry(1039, 422, "This URL source needs interactive authentication this API cannot perform on the caller's behalf. Download the file yourself and upload the bytes directly instead."),
43
+ "invalid_idempotency_key": ErrorCatalogEntry(1040, 422, "The supplied idempotency_key was outside the accepted length bounds. Send a key between the documented minimum and maximum length, or omit it."),
44
+ "upload_incomplete": ErrorCatalogEntry(1041, 422, "The assembled object was shorter than the size declared at creation, so the hold was released and nothing was billed. Re-upload every part and call complete again."),
45
+ "url_target_busy": ErrorCatalogEntry(1042, 429, "This API is already fetching from that target host as fast as it will, so the fetch was refused before it was attempted. This bounds the target site, not the caller: a different source URL will go through immediately, and the same one will after a short wait."),
46
+ "policy_version_stale": ErrorCatalogEntry(1043, 422, "accept_terms_version did not match the server's current policy version. The server records its own value, never the caller's, so retry with the current version returned by this same error."),
47
+ "unauthorized": ErrorCatalogEntry(2000, 401, "The bearer token is missing, malformed, expired, or revoked. This is also the answer for a revoked, malformed, or unknown Personal Access Token, so a caller holding a PAT should mint a fresh one rather than retry the same one. Obtain a fresh session token or Personal Access Token and retry with it in the Authorization header."),
48
+ "invalid_credentials": ErrorCatalogEntry(2001, 401, "The submitted email/password pair did not match an account. Check the credentials or use the password-reset flow."),
49
+ "turnstile_failed": ErrorCatalogEntry(2010, 403, "The Cloudflare Turnstile challenge could not be verified. Re-run the challenge on the client and resend with a fresh token."),
50
+ "country_restricted": ErrorCatalogEntry(2011, 403, "This action is not available from the caller's detected jurisdiction. This is a policy decision, not a transient failure; retrying will not help."),
51
+ "signup_not_allowed": ErrorCatalogEntry(2012, 403, "New account creation is currently closed for this signup path. Retry later or use an already-provisioned account."),
52
+ "account_suspended": ErrorCatalogEntry(2013, 403, "The account is suspended, most commonly due to an open payment dispute. Resolve the dispute or contact support before retrying."),
53
+ "bad_signature": ErrorCatalogEntry(2020, 400, "A cryptographic signature on this request could not be verified. Not reachable from this public API; indicates a caller configuration bug elsewhere in the stack."),
54
+ "org_required": ErrorCatalogEntry(2021, 403, "This action requires the caller to belong to an organization, and the bearer does not. Join or create an organization first. PATCH /api/org also answers it, writing nothing, when the patch names a field whose right the caller lacks: the billing contact, tax identity and auto_renew need the billing right, every other field the admin right. POST /api/org/invites and PATCH /api/org/members/:user_id answer it too when they hand out or take away the billing right (an invite carrying an explicit is_billing true; a patch naming is_billing) for a caller who does not hold it, except that while no member of the workspace holds the right any admin may grant it (a revoke always needs it). A workspace member removal answers it too when the target holds the billing right and the caller neither holds it nor is that member (a member may always leave; removing a holder needs the right, so the recovery rule above is reachable only after the last holder leaves, revokes their own right, or deletes their account). An invite that leaves is_billing out defaults it to the caller's own right for role admin, so an admin without the billing right inviting an admin sends a plain admin invite and is not refused."),
55
+ "pat_forbidden": ErrorCatalogEntry(2022, 403, "This route is not in the Personal Access Token scope map at all, so no token, regardless of scope, can reach it. Use a session bearer for this call instead."),
56
+ "pat_scope_missing": ErrorCatalogEntry(2023, 403, "The Personal Access Token does not carry the scope this route requires (see x-required-scope on the operation). Mint or use a token that includes it."),
57
+ "step_up_required": ErrorCatalogEntry(2024, 403, "Deleting an account needs proof it is the holder asking. DELETE /api/me carried neither current_password nor email_code, or sent current_password for a password set or changed in the last 7 days, or one the requesting session itself set. Send the current password (set more than 7 days ago, not by this session), or a code sent by POST /api/me/deletion-code."),
58
+ "card_required": ErrorCatalogEntry(3000, 402, "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: This purchase needs a card on file and the account has none. Add a card through the account's payment settings before retrying."),
59
+ "insufficient_balance": ErrorCatalogEntry(3001, 402, "The account's balance cannot cover the requested seconds. The error body's needed_seconds and balance_seconds report the shortfall; top up before retrying."),
60
+ "card_declined": ErrorCatalogEntry(3002, 402, "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: The card on file was declined when charged. Use a different payment method or contact the card issuer."),
61
+ "org_insufficient_balance": ErrorCatalogEntry(3003, 402, "The organization's shared minute pool cannot cover the requested seconds. The error body's org_balance_seconds reports the pool's current balance; an org admin must top it up."),
62
+ "not_found": ErrorCatalogEntry(4000, 404, "No resource exists at this id for the authenticated caller. Confirm the id and that it belongs to this account."),
63
+ "wrong_state": ErrorCatalogEntry(4001, 409, "The target resource is not in a state this action allows (for example, an upload that already finished). Read its current state before retrying."),
64
+ "email_exists": ErrorCatalogEntry(4002, 409, "An account already exists for this email address. Sign in instead of registering again."),
65
+ "account_has_active_jobs": ErrorCatalogEntry(4003, 409, "This action cannot proceed while the account has active transcription jobs. Wait for them to finish, or cancel them, then retry."),
66
+ "ai_already_generated": ErrorCatalogEntry(4004, 409, "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: An Insights bundle already exists for this transcript and this call did not ask for a paid regeneration. Pass a mode to request one."),
67
+ "promo_already_redeemed": ErrorCatalogEntry(4005, 409, "This promo code has already been redeemed on this account. Each code may be redeemed once per account."),
68
+ "org_already_member": ErrorCatalogEntry(4006, 409, "Creating an organization: the caller founded another organization that is still unpaid and either is still a member of it or created it less than 7 days ago (one unpaid founded organization at a time; leaving it does not lift the bound inside the 7 days and does after them; belonging to other organizations does not block it). Accepting an invite: the account is already a member of that organization."),
69
+ "org_plan_inactive": ErrorCatalogEntry(4007, 409, "The organization has no active plan, so its pool cannot fund this action. An org admin must renew the plan."),
70
+ "org_seat_limit": ErrorCatalogEntry(4008, 409, "The organization is already at its seat limit. Remove a member or increase the plan's seat count before inviting another."),
71
+ "org_wrong_region": ErrorCatalogEntry(4009, 409, "The target account's data region does not match the organization's region. Cross-region membership is not supported; the user needs an account in the org's region."),
72
+ "link_expired": ErrorCatalogEntry(4010, 410, "The magic link or similar time-boxed link has expired. Request a new one."),
73
+ "expired": ErrorCatalogEntry(4011, 410, "The referenced token or session has expired. Request a fresh one and retry."),
74
+ "invalid_code": ErrorCatalogEntry(4012, 410, "The submitted one-time code does not match what was issued. Re-check the code or request a new one."),
75
+ "no_pending": ErrorCatalogEntry(4013, 404, "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: There is no pending operation of this kind to act on (for example, no pending export). Start the operation before trying to read its result."),
76
+ "promo_unavailable": ErrorCatalogEntry(4014, 410, "This promo code is not currently redeemable (expired, not yet active, or exhausted). Check the promotion's terms."),
77
+ "promo_card_required": ErrorCatalogEntry(4015, 409, "This promo requires a verified card on file before it can be redeemed. Add a card first."),
78
+ "invoice_not_found": ErrorCatalogEntry(4016, 404, "No invoice exists at this id for the caller's organization. Confirm the invoice id from GET /api/org/invoices."),
79
+ "org_retention_too_loose": ErrorCatalogEntry(4017, 409, "The requested retention setting exceeds the organization's configured maximum. Request a value at or below the org's ceiling."),
80
+ "org_plan_invoice_open": ErrorCatalogEntry(4018, 409, "The organization already has an open, unpaid plan invoice. Settle or wait for it to be superseded before requesting another."),
81
+ "pat_exchange_invalid": ErrorCatalogEntry(4019, 410, "The OAuth handoff exchange code is invalid, already used, or expired. Restart the handoff flow to obtain a new code."),
82
+ "pat_invalid": ErrorCatalogEntry(4020, 422, "The POST /api/pats request body failed validation: an empty or oversized name, an unknown or empty scope list, created_via outside app or oauth, or an invalid client_label. Fix the request body and retry. A bearer token that is itself malformed, revoked, or unknown never reaches this code; that case answers unauthorized (2000) instead."),
83
+ "pat_purchase_requires_app": ErrorCatalogEntry(4021, 403, "This purchase needs full Checkout, which this API cannot open or let the caller complete. Either the request had no quick_payment_method_id, in which case resend with one set to a card already on file (see GET /v1/billing/cards), or the saved card will not go through as attempted (declined, needs a different payment method, or was canceled), in which case retrying the same card changes nothing: fix or replace it, then retry with a brand new idempotency_key, or complete the purchase from the Hushscript app. A card whose issuer wants an interactive challenge is not a decline and answers 4023 instead."),
84
+ "pat_limit": ErrorCatalogEntry(4022, 409, "The account already holds the maximum number of Personal Access Tokens. Revoke an unused token before minting another."),
85
+ "pat_purchase_requires_authentication": ErrorCatalogEntry(4023, 403, "The saved card is fine, but the issuer wants a challenge (3DS) before this charge can go through. This API cannot open or let the caller complete that challenge, so the purchase is refused rather than left pending. Complete the purchase from the Hushscript app instead, where the holder can go through authentication."),
86
+ "org_signups_closed": ErrorCatalogEntry(4024, 403, "Creating a new organization is closed in this environment. Signups are open in every environment today, so this answers only where they were deliberately closed again. Existing organizations keep working normally; every other org route, including renewals, pack purchases, invites, and admin reads, stays open."),
87
+ "org_plan_frozen": ErrorCatalogEntry(4025, 402, "The organization's plan lapsed and its 30 day grace window has also passed, so no new organization funded upload is admitted. A job already running still settles normally. Paying a plan renewal clears the freeze right away."),
88
+ "org_pool_expired": ErrorCatalogEntry(4026, 402, "The organization's shared minute pool is present but its 365-day rolling clock has lapsed with no purchase, grant, or completed job to renew it. A pack purchase, an included plan grant, or completing another org-funded job all restart the clock for another year."),
89
+ "org_member_cap_exceeded": ErrorCatalogEntry(4027, 402, "This pool member has used their whole monthly share of the workspace's pooled seconds. The workspace pool itself may still have a balance; this is the per-member cap, separate from a plain insufficient-balance refusal."),
90
+ "org_renewal_payment_failed": ErrorCatalogEntry(4028, 402, "The daily renewal lane charged the organization's stored renewal card and Stripe answered with a structured failure. The invoice stays issued, the failure count goes up by one, a renewal-failed email carrying a fresh Checkout link for that same invoice is sent (flagged as the final attempt once the count reaches the cap of 3), and the lapsed term falls into the ordinary grace window. Once the cap is reached the lane stops retrying this organization automatically -- no further charge attempts, no further emails -- until the open invoice is paid by hand, which resets the count. GET /api/org exposes this as a derived renewal_capped boolean."),
91
+ "org_no_default_payment_method": ErrorCatalogEntry(4029, 409, "The chosen payment method is not one of the caller's own saved, fast checkout eligible cards, the caller has no Stripe customer at all, or auto renew was switched on with no renewal card stored yet. All three answer identically so this route can never be used to learn whether a given payment method id exists."),
92
+ "org_refund_window_closed": ErrorCatalogEntry(4030, 409, "The operator's signed fee-refund command came more than 30 days after the workspace's plan was first paid for, so the plan fee refund window has closed. No customer route returns this code; the fee refund exists only as that operator command. Packs are never refundable and never reach this check."),
93
+ "org_refund_not_eligible": ErrorCatalogEntry(4031, 409, "The target invoice of the operator's signed fee-refund command is not a paid, unrefunded plan invoice of this workspace, or the workspace already took its one refund. No customer route returns this code; the fee refund exists only as that operator command. Packs are never refundable regardless of this check."),
94
+ "org_refund_currency_mismatch": ErrorCatalogEntry(4032, 409, "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. The signed admin fee-refund command answers its own currency_mismatch refusal instead. It used to mean: The Stripe charge settled in a currency different from the invoice's currency, so a partial refund amount cannot be computed. Contact support for a manual refund."),
95
+ "org_tax_id_invalid": ErrorCatalogEntry(4033, 400, "The tax id or its label does not match a shape this system accepts. This is a shape check only, never a registry lookup against a tax authority."),
96
+ "agent_signup_pending": ErrorCatalogEntry(4035, 409, "A signup already exists for this contact_email and has not yet been claimed, refunded, or expired. Wait for it to resolve or use a different contact address."),
97
+ "agent_claim_invalid": ErrorCatalogEntry(4036, 401, "The claim secret did not match, or the signup does not exist, has already been claimed, was refunded, or has expired. These cases answer identically on purpose. A confirmed-but-unpaid Checkout Session answers wrong_state (4001) instead once the secret itself has checked out."),
98
+ "agent_pack_locked": ErrorCatalogEntry(4037, 403, "An agent account's pack tier does not allow the requested pack yet. A fresh agent account is limited to the small packs; a larger pack unlocks only once enough prior payments have settled past the dispute window. Reached on POST /v1/agent/accounts (signup, always tier 0) and POST /v1/billing/purchase for an agent caller. The response carries details.allowed_packs, the pack ids currently buyable; retry with one of those. Never reached by a human account."),
99
+ "agent_purchase_capped": ErrorCatalogEntry(4038, 429, "An agent account has reached its per-credential spend cap for the current rolling window: 3 purchases, paid or declined, in the trailing 24 hours. The cap is counted per account across every credential the account has ever held, not per token, so rotating a credential does not reset it. Reached on POST /v1/billing/purchase for an agent caller, and for a human account's personal access token caller, which has its own cap of the same size counted the same way. A human purchasing through the app is never capped. The response carries details.limit, details.window_seconds and details.retry_after_seconds, and a Retry-After header naming the same wait in seconds."),
100
+ "org_last_admin": ErrorCatalogEntry(4039, 409, "A rights change or a member removal would leave the workspace with no member carrying is_admin. This is refused by a count check evaluated inside the update itself, not by a pre-read, so two admins concurrently dropping each other's flag cannot both succeed."),
101
+ "org_rights_invalid": ErrorCatalogEntry(4040, 422, "The member rights update carried no changed field, or monthly_cap_seconds fell outside the allowed range for a pooled member cap."),
102
+ "org_custody_unsupported": ErrorCatalogEntry(4043, 409, "The requested custody change is refused: the workspace is already firm custody, a custody switch is already scheduled, or the request asks to move to private custody, which is not reversible once transcripts carry an org_id."),
103
+ "org_reviewer_required": ErrorCatalogEntry(4044, 403, "The route needs the reviewer right. Carrying the admin flag alone never satisfies it, since the reviewer right is kept deliberately separate."),
104
+ "org_custodian_required": ErrorCatalogEntry(4045, 409, "Offboarding a member from a firm-custody workspace named no custodian to receive that member's work product."),
105
+ "org_custodian_invalid": ErrorCatalogEntry(4046, 422, "The named custodian is not a reviewer member of this workspace, or is the member being removed. The admin right does not qualify on its own."),
106
+ "transcript_legal_hold": ErrorCatalogEntry(4047, 409, "Deletion, an edit (body, translation, cleaned text, revert, retranslate), an AI-output delete or paid regeneration, a retention change or a re-file to another matter was refused because the transcript is under a legal hold, placed by a workspace admin or derived from its matter."),
107
+ "org_not_member": ErrorCatalogEntry(4048, 403, "The request named a workspace that the caller does not belong to."),
108
+ "org_matter_code_required": ErrorCatalogEntry(4049, 422, "The workspace sets matter_code_policy to required and the upload carried no matter_code. Send one, or ask a workspace admin to relax the policy."),
109
+ "org_matter_code_invalid": ErrorCatalogEntry(4050, 422, "matter_code or client_code is longer than 64 characters after trimming. Both are free text fields checked on length alone, and a blank or whitespace-only value is accepted and stored as no code. For a workspace (org) upload, client_code is validated here but the stored value always comes from the resolved matter's client, never from this field; a personal (no org) upload's client_code is stored exactly as sent."),
110
+ "org_usage_range_invalid": ErrorCatalogEntry(4051, 422, "GET /api/org/usage: from or to is missing, not a positive unix timestamp, reversed, or spans more than 366 days."),
111
+ "org_usage_group_invalid": ErrorCatalogEntry(4052, 422, "GET /api/org/usage: group_by is present and is not member, matter or client."),
112
+ "org_rebill_rate_invalid": ErrorCatalogEntry(4053, 422, "PATCH /api/org: rebill_rate_cents_per_hour is negative or above 100000. The rate is report only and never charges anyone."),
113
+ "org_asset_forbidden": ErrorCatalogEntry(4054, 403, "A workspace dictionary or prompt preset was created, edited or deleted by a member who holds neither the admin nor the reviewer right."),
114
+ "org_asset_limit": ErrorCatalogEntry(4055, 409, "The workspace already owns the maximum number of dictionaries or prompt presets. Delete one first before creating another."),
115
+ "org_letterhead_invalid": ErrorCatalogEntry(4056, 422, "PATCH /api/org: letterhead failed shape or length validation. Send null to clear the letterhead instead."),
116
+ "org_matter_hold_forbidden": ErrorCatalogEntry(4057, 403, "POST or DELETE /api/org/holds/matter from a workspace that is not in firm custody, or from a member without the reviewer right."),
117
+ "org_matter_hold_empty": ErrorCatalogEntry(4058, 404, "POST /api/org/holds/matter: no workspace transcript carries that matter code, so there was nothing to hold."),
118
+ "org_refund_term_renewed": ErrorCatalogEntry(4061, 409, "The workspace holds another paid plan term besides the invoice being refunded, so the operator's signed fee-refund command refuses and the refund is handled manually through support. No customer route returns this code; the fee refund exists only as that operator command."),
119
+ "org_self_grant_refused": ErrorCatalogEntry(4062, 409, "The member rights update targeted the caller's own row and asked for a right the caller does not already hold. Self-revocation, a self-targeted monthly cap change and a self-grant of the reviewer right are still allowed; a self-grant of the billing right is allowed only while no member of the workspace holds it. Otherwise another admin has to grant the right."),
120
+ "org_matter_not_found": ErrorCatalogEntry(4063, 404, "The matter id named in the request does not exist in this workspace."),
121
+ "org_matter_code_taken": ErrorCatalogEntry(4064, 409, "Another matter in this workspace already uses that matter code."),
122
+ "org_matter_closed": ErrorCatalogEntry(4065, 409, "That matter is closed and cannot take new work."),
123
+ "org_matter_limit": ErrorCatalogEntry(4066, 409, "This workspace has reached its configured matter limit."),
124
+ "org_client_not_found": ErrorCatalogEntry(4067, 404, "The client id named in the request does not exist in this workspace."),
125
+ "org_client_code_taken": ErrorCatalogEntry(4068, 409, "Another client in this workspace already uses that client code."),
126
+ "org_client_closed": ErrorCatalogEntry(4069, 409, "That client is closed, so its matters cannot take new work either."),
127
+ "org_client_limit": ErrorCatalogEntry(4070, 409, "This workspace has reached its configured client limit."),
128
+ "deletion_code_invalid": ErrorCatalogEntry(4071, 422, "The email_code sent to DELETE /api/me is wrong, expired, already used, or has had five wrong guesses. Request a new code with POST /api/me/deletion-code."),
129
+ "rate_limited": ErrorCatalogEntry(5000, 429, "The caller has been rate limited. PAT-authenticated traffic carries a Retry-After header naming the wait in seconds; other rate-limited routes may not."),
130
+ "too_many_active_jobs": ErrorCatalogEntry(5001, 429, "The account already has the maximum number of concurrent pipeline jobs in flight. Wait for one to finish before starting another."),
131
+ "ai_cap_reached": ErrorCatalogEntry(5002, 503, "The Insights provider's own capacity is temporarily exhausted. Retry the generation after a short delay."),
132
+ "preview_daily_limit": ErrorCatalogEntry(5003, 429, "The caller's IP has reached the daily limit for the anonymous preview endpoint. Retry after the daily window resets, or authenticate to use the ordinary upload flow instead."),
133
+ "provider_error": ErrorCatalogEntry(6001, 502, "The transcription provider returned an error or could not be reached. This is usually transient; retry after a delay."),
134
+ "stripe_error": ErrorCatalogEntry(6002, 502, "The payment provider returned an error or could not be reached. This is usually transient; retry after a delay."),
135
+ "email_error": ErrorCatalogEntry(6003, 502, "The transactional email provider could not be reached or refused the message. Not directly actionable by an API caller; the underlying action may still have succeeded."),
136
+ "internal_error": ErrorCatalogEntry(9000, 500, "An unexpected server-side failure occurred. Retry later; if it persists, it is a bug on the backend, not the request."),
137
+ "transcript_data_missing": ErrorCatalogEntry(9001, 500, "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: The transcript's stored body could not be found or read where this action expected it. Treat as a transient storage fault and retry, or contact support if it persists."),
138
+ }
@@ -0,0 +1,282 @@
1
+ import ssl
2
+ from typing import Any
3
+
4
+ from typing_extensions import Self
5
+
6
+ import httpx
7
+ from attrs import define, evolve, field
8
+
9
+
10
+ @define
11
+ class Client:
12
+ """A class for keeping track of data related to the API
13
+
14
+ The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
15
+
16
+ ``base_url``: The base URL for the API, all requests are made to a relative path to this URL
17
+
18
+ ``cookies``: A dictionary of cookies to be sent with every request
19
+
20
+ ``headers``: A dictionary of headers to be sent with every request
21
+
22
+ ``timeout``: The maximum amount of a time a request can take. API functions will raise
23
+ httpx.TimeoutException if this is exceeded.
24
+
25
+ ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
26
+ but can be set to False for testing purposes.
27
+
28
+ ``follow_redirects``: Whether or not to follow redirects. Default value is False.
29
+
30
+ ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
31
+
32
+
33
+ Attributes:
34
+ raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
35
+ status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
36
+ argument to the constructor.
37
+ """
38
+
39
+ raise_on_unexpected_status: bool = field(default=False, kw_only=True)
40
+ _base_url: str = field(alias="base_url")
41
+ _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
42
+ _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
43
+ _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
44
+ _verify_ssl: str | bool | ssl.SSLContext = field(
45
+ default=True, kw_only=True, alias="verify_ssl"
46
+ )
47
+ _follow_redirects: bool = field(
48
+ default=False, kw_only=True, alias="follow_redirects"
49
+ )
50
+ _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
51
+ _client: httpx.Client | None = field(default=None, init=False)
52
+ _async_client: httpx.AsyncClient | None = field(default=None, init=False)
53
+
54
+ def with_headers(self, headers: dict[str, str]) -> "Client":
55
+ """Get a new client matching this one with additional headers"""
56
+ if self._client is not None:
57
+ self._client.headers.update(headers)
58
+ if self._async_client is not None:
59
+ self._async_client.headers.update(headers)
60
+ return evolve(self, headers={**self._headers, **headers})
61
+
62
+ def with_cookies(self, cookies: dict[str, str]) -> "Client":
63
+ """Get a new client matching this one with additional cookies"""
64
+ if self._client is not None:
65
+ self._client.cookies.update(cookies)
66
+ if self._async_client is not None:
67
+ self._async_client.cookies.update(cookies)
68
+ return evolve(self, cookies={**self._cookies, **cookies})
69
+
70
+ def with_timeout(self, timeout: httpx.Timeout) -> "Client":
71
+ """Get a new client matching this one with a new timeout configuration"""
72
+ if self._client is not None:
73
+ self._client.timeout = timeout
74
+ if self._async_client is not None:
75
+ self._async_client.timeout = timeout
76
+ return evolve(self, timeout=timeout)
77
+
78
+ def set_httpx_client(self, client: httpx.Client) -> Self:
79
+ """Manually set the underlying httpx.Client
80
+
81
+ **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
82
+ """
83
+ self._client = client
84
+ return self
85
+
86
+ def get_httpx_client(self) -> httpx.Client:
87
+ """Get the underlying httpx.Client, constructing a new one if not previously set"""
88
+ if self._client is None:
89
+ self._client = httpx.Client(
90
+ base_url=self._base_url,
91
+ cookies=self._cookies,
92
+ headers=self._headers,
93
+ timeout=self._timeout,
94
+ verify=self._verify_ssl,
95
+ follow_redirects=self._follow_redirects,
96
+ **self._httpx_args,
97
+ )
98
+ return self._client
99
+
100
+ def __enter__(self) -> Self:
101
+ """Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
102
+ self.get_httpx_client().__enter__()
103
+ return self
104
+
105
+ def __exit__(self, *args: Any, **kwargs: Any) -> None:
106
+ """Exit a context manager for internal httpx.Client (see httpx docs)"""
107
+ self.get_httpx_client().__exit__(*args, **kwargs)
108
+
109
+ def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self:
110
+ """Manually set the underlying httpx.AsyncClient
111
+
112
+ **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
113
+ """
114
+ self._async_client = async_client
115
+ return self
116
+
117
+ def get_async_httpx_client(self) -> httpx.AsyncClient:
118
+ """Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
119
+ if self._async_client is None:
120
+ self._async_client = httpx.AsyncClient(
121
+ base_url=self._base_url,
122
+ cookies=self._cookies,
123
+ headers=self._headers,
124
+ timeout=self._timeout,
125
+ verify=self._verify_ssl,
126
+ follow_redirects=self._follow_redirects,
127
+ **self._httpx_args,
128
+ )
129
+ return self._async_client
130
+
131
+ async def __aenter__(self) -> Self:
132
+ """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
133
+ await self.get_async_httpx_client().__aenter__()
134
+ return self
135
+
136
+ async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
137
+ """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
138
+ await self.get_async_httpx_client().__aexit__(*args, **kwargs)
139
+
140
+
141
+ @define
142
+ class AuthenticatedClient:
143
+ """A Client which has been authenticated for use on secured endpoints
144
+
145
+ The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
146
+
147
+ ``base_url``: The base URL for the API, all requests are made to a relative path to this URL
148
+
149
+ ``cookies``: A dictionary of cookies to be sent with every request
150
+
151
+ ``headers``: A dictionary of headers to be sent with every request
152
+
153
+ ``timeout``: The maximum amount of a time a request can take. API functions will raise
154
+ httpx.TimeoutException if this is exceeded.
155
+
156
+ ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
157
+ but can be set to False for testing purposes.
158
+
159
+ ``follow_redirects``: Whether or not to follow redirects. Default value is False.
160
+
161
+ ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
162
+
163
+
164
+ Attributes:
165
+ raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
166
+ status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
167
+ argument to the constructor.
168
+ token: The token to use for authentication
169
+ prefix: The prefix to use for the Authorization header
170
+ auth_header_name: The name of the Authorization header
171
+ """
172
+
173
+ raise_on_unexpected_status: bool = field(default=False, kw_only=True)
174
+ _base_url: str = field(alias="base_url")
175
+ _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
176
+ _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
177
+ _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
178
+ _verify_ssl: str | bool | ssl.SSLContext = field(
179
+ default=True, kw_only=True, alias="verify_ssl"
180
+ )
181
+ _follow_redirects: bool = field(
182
+ default=False, kw_only=True, alias="follow_redirects"
183
+ )
184
+ _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
185
+ _client: httpx.Client | None = field(default=None, init=False)
186
+ _async_client: httpx.AsyncClient | None = field(default=None, init=False)
187
+
188
+ token: str
189
+ prefix: str = "Bearer"
190
+ auth_header_name: str = "Authorization"
191
+
192
+ def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient":
193
+ """Get a new client matching this one with additional headers"""
194
+ if self._client is not None:
195
+ self._client.headers.update(headers)
196
+ if self._async_client is not None:
197
+ self._async_client.headers.update(headers)
198
+ return evolve(self, headers={**self._headers, **headers})
199
+
200
+ def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient":
201
+ """Get a new client matching this one with additional cookies"""
202
+ if self._client is not None:
203
+ self._client.cookies.update(cookies)
204
+ if self._async_client is not None:
205
+ self._async_client.cookies.update(cookies)
206
+ return evolve(self, cookies={**self._cookies, **cookies})
207
+
208
+ def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
209
+ """Get a new client matching this one with a new timeout configuration"""
210
+ if self._client is not None:
211
+ self._client.timeout = timeout
212
+ if self._async_client is not None:
213
+ self._async_client.timeout = timeout
214
+ return evolve(self, timeout=timeout)
215
+
216
+ def set_httpx_client(self, client: httpx.Client) -> Self:
217
+ """Manually set the underlying httpx.Client
218
+
219
+ **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
220
+ """
221
+ self._client = client
222
+ return self
223
+
224
+ def get_httpx_client(self) -> httpx.Client:
225
+ """Get the underlying httpx.Client, constructing a new one if not previously set"""
226
+ if self._client is None:
227
+ self._headers[self.auth_header_name] = (
228
+ f"{self.prefix} {self.token}" if self.prefix else self.token
229
+ )
230
+ self._client = httpx.Client(
231
+ base_url=self._base_url,
232
+ cookies=self._cookies,
233
+ headers=self._headers,
234
+ timeout=self._timeout,
235
+ verify=self._verify_ssl,
236
+ follow_redirects=self._follow_redirects,
237
+ **self._httpx_args,
238
+ )
239
+ return self._client
240
+
241
+ def __enter__(self) -> Self:
242
+ """Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
243
+ self.get_httpx_client().__enter__()
244
+ return self
245
+
246
+ def __exit__(self, *args: Any, **kwargs: Any) -> None:
247
+ """Exit a context manager for internal httpx.Client (see httpx docs)"""
248
+ self.get_httpx_client().__exit__(*args, **kwargs)
249
+
250
+ def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self:
251
+ """Manually set the underlying httpx.AsyncClient
252
+
253
+ **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
254
+ """
255
+ self._async_client = async_client
256
+ return self
257
+
258
+ def get_async_httpx_client(self) -> httpx.AsyncClient:
259
+ """Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
260
+ if self._async_client is None:
261
+ self._headers[self.auth_header_name] = (
262
+ f"{self.prefix} {self.token}" if self.prefix else self.token
263
+ )
264
+ self._async_client = httpx.AsyncClient(
265
+ base_url=self._base_url,
266
+ cookies=self._cookies,
267
+ headers=self._headers,
268
+ timeout=self._timeout,
269
+ verify=self._verify_ssl,
270
+ follow_redirects=self._follow_redirects,
271
+ **self._httpx_args,
272
+ )
273
+ return self._async_client
274
+
275
+ async def __aenter__(self) -> Self:
276
+ """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
277
+ await self.get_async_httpx_client().__aenter__()
278
+ return self
279
+
280
+ async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
281
+ """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
282
+ await self.get_async_httpx_client().__aexit__(*args, **kwargs)
@@ -0,0 +1,16 @@
1
+ """Contains shared errors types that can be raised from API functions"""
2
+
3
+
4
+ class UnexpectedStatus(Exception):
5
+ """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True"""
6
+
7
+ def __init__(self, status_code: int, content: bytes):
8
+ self.status_code = status_code
9
+ self.content = content
10
+
11
+ super().__init__(
12
+ f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}"
13
+ )
14
+
15
+
16
+ __all__ = ["UnexpectedStatus"]
@@ -0,0 +1,28 @@
1
+ # Generated by scripts/generate.py from openapi.json and the conformance hints.json. Do not edit.
2
+
3
+ from typing import Final
4
+
5
+ HINTS: Final[dict[str, str]] = {
6
+ "unauthorized": "Your token is invalid, revoked or expired. Create a new one in the Hushscript app under Account → MCP. Changing your password revokes all tokens.",
7
+ "pat_scope_missing": "This call needs the \"{scope}\" permission. Create a token that includes it. For billing, the token also needs \"Let this token buy minutes\" turned on when you create it, and \"Let connected apps buy minutes\" must be on in the app under Account → MCP.",
8
+ "insufficient_balance": "Not enough minutes: {needed} needed, {balance} available. Buy a pack with the SDK's buy method or in the app.",
9
+ "rate_limited": "Too many requests. The SDK already waited and retried; slow down or try again in {retryAfter} s.",
10
+ "too_many_active_jobs": "50 jobs are already in progress. Wait for some to finish.",
11
+ "agent_purchase_capped": "This account has used its 3 purchase attempts for the last 24 hours. Try again in {retryAfter} s.",
12
+ "agent_pack_locked": "New agent accounts can buy {allowedPacks} until earlier purchases settle.",
13
+ "pat_purchase_requires_app": "The saved card was declined or can't be charged automatically. Buy this pack in the Hushscript app.",
14
+ "pat_purchase_requires_authentication": "Your bank wants to confirm this payment. Buy this pack in the Hushscript app.",
15
+ "no_default_card": "No saved card. Open the add-card link from the SDK's billing methods in a browser, or add a card in the app.",
16
+ "unsupported_media": "Supported: MP3, M4A/MP4/MOV, WAV, FLAC, OGG/Opus. Convert it first, e.g. ffmpeg -i input -vn -c:a libopus output.opus",
17
+ "duration_unreadable": "Could not read the file's duration. Pass durationSeconds.",
18
+ "duration_out_of_range": "Files must be between 15 seconds and 10 hours long.",
19
+ "size_out_of_range": "Files must be at most 5 GB.",
20
+ "daily_limit": "This upload would pass your daily transcription limit ({used} of {max} min used in the last 24 h).",
21
+ "policy_version_stale": "Accept the current terms version: {currentVersion}.",
22
+ "wrong_state": "The job has already moved on. Fetch the job to see where it is.",
23
+ "resume_file_mismatch": "This is not the file the upload started with. Pass the original file, or start a new transcription.",
24
+ "resume_file_required": "The upload was not finished. Pass the original file to resume it.",
25
+ "resume_job_missing": "This job no longer exists. Start a new transcription.",
26
+ "stream_size_mismatch": "The stream did not match the size you passed. Pass the exact size in bytes.",
27
+ "default_card_required": "You have more than one saved card and none is the default. Set a default card with the SDK's billing methods or in the app.",
28
+ }