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