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.
- hushscript/__init__.py +97 -0
- hushscript/_client.py +687 -0
- hushscript/_clock.py +36 -0
- hushscript/_config.py +139 -0
- hushscript/_errors.py +563 -0
- hushscript/_guard.py +61 -0
- hushscript/_http.py +624 -0
- hushscript/_json.py +55 -0
- hushscript/_log.py +155 -0
- hushscript/_models.py +144 -0
- hushscript/_pacing.py +51 -0
- hushscript/_redact.py +140 -0
- hushscript/_response.py +67 -0
- hushscript/_retry.py +205 -0
- hushscript/_shape.py +197 -0
- hushscript/_shapes.py +248 -0
- hushscript/_version.py +1 -0
- hushscript/format.py +224 -0
- hushscript/generated/__init__.py +8 -0
- hushscript/generated/api/__init__.py +1 -0
- hushscript/generated/api/default/__init__.py +1 -0
- hushscript/generated/api/default/buy_minutes.py +248 -0
- hushscript/generated/api/default/claim_agent_account.py +238 -0
- hushscript/generated/api/default/complete_upload.py +238 -0
- hushscript/generated/api/default/create_agent_account_signup.py +261 -0
- hushscript/generated/api/default/create_setup_session.py +244 -0
- hushscript/generated/api/default/create_upload.py +225 -0
- hushscript/generated/api/default/get_account.py +169 -0
- hushscript/generated/api/default/get_job.py +198 -0
- hushscript/generated/api/default/get_org.py +169 -0
- hushscript/generated/api/default/get_transcript.py +226 -0
- hushscript/generated/api/default/get_transcription_context.py +173 -0
- hushscript/generated/api/default/get_upload_part_urls.py +246 -0
- hushscript/generated/api/default/heartbeat_upload.py +197 -0
- hushscript/generated/api/default/lift_transcript_hold.py +206 -0
- hushscript/generated/api/default/list_credit_ledger.py +229 -0
- hushscript/generated/api/default/list_deleted_transcripts.py +237 -0
- hushscript/generated/api/default/list_holds.py +233 -0
- hushscript/generated/api/default/list_jobs.py +249 -0
- hushscript/generated/api/default/list_org_audit.py +221 -0
- hushscript/generated/api/default/list_org_invoices.py +221 -0
- hushscript/generated/api/default/list_org_ledger.py +221 -0
- hushscript/generated/api/default/list_org_matters.py +247 -0
- hushscript/generated/api/default/list_org_members.py +221 -0
- hushscript/generated/api/default/list_org_transcripts.py +233 -0
- hushscript/generated/api/default/list_packs.py +169 -0
- hushscript/generated/api/default/list_saved_cards.py +169 -0
- hushscript/generated/api/default/list_transactions.py +225 -0
- hushscript/generated/api/default/list_transcripts.py +225 -0
- hushscript/generated/api/default/org_audit_csv.py +163 -0
- hushscript/generated/api/default/org_ledger_csv.py +163 -0
- hushscript/generated/api/default/org_usage_report.py +247 -0
- hushscript/generated/api/default/org_usage_report_csv.py +229 -0
- hushscript/generated/api/default/patch_org_member.py +237 -0
- hushscript/generated/api/default/place_transcript_hold.py +206 -0
- hushscript/generated/api/default/rotate_agent_credentials.py +177 -0
- hushscript/generated/api/default/schedule_org_custody.py +215 -0
- hushscript/generated/api/default/set_default_card.py +223 -0
- hushscript/generated/api/default/upload_part.py +249 -0
- hushscript/generated/catalog.py +138 -0
- hushscript/generated/client.py +282 -0
- hushscript/generated/errors.py +16 -0
- hushscript/generated/hints.py +28 -0
- hushscript/generated/models/__init__.py +193 -0
- hushscript/generated/models/account_response.py +114 -0
- hushscript/generated/models/active_hold.py +88 -0
- hushscript/generated/models/active_hold_kind.py +18 -0
- hushscript/generated/models/agent_claim_request.py +61 -0
- hushscript/generated/models/agent_claim_response.py +85 -0
- hushscript/generated/models/agent_credentials_rotate_response.py +101 -0
- hushscript/generated/models/agent_signup_request.py +118 -0
- hushscript/generated/models/agent_signup_request_agent.py +80 -0
- hushscript/generated/models/agent_signup_response.py +111 -0
- hushscript/generated/models/buy_minutes_body.py +89 -0
- hushscript/generated/models/card.py +132 -0
- hushscript/generated/models/cards_response.py +90 -0
- hushscript/generated/models/complete_upload_body.py +107 -0
- hushscript/generated/models/complete_upload_body_parts_type_0_item.py +69 -0
- hushscript/generated/models/context_dictionary.py +143 -0
- hushscript/generated/models/context_prompt_preset.py +118 -0
- hushscript/generated/models/create_setup_session_body.py +62 -0
- hushscript/generated/models/create_upload_body.py +254 -0
- hushscript/generated/models/create_upload_body_transcription_options_type_0.py +178 -0
- hushscript/generated/models/credit_ledger_entry.py +113 -0
- hushscript/generated/models/credit_ledger_response.py +90 -0
- hushscript/generated/models/deleted_transcript.py +148 -0
- hushscript/generated/models/deleted_transcript_cause.py +18 -0
- hushscript/generated/models/deleted_transcript_receipt_type_0.py +107 -0
- hushscript/generated/models/deleted_transcript_receipt_type_0_items_item.py +69 -0
- hushscript/generated/models/deleted_transcripts_response.py +90 -0
- hushscript/generated/models/dictionary_entry.py +123 -0
- hushscript/generated/models/dictionary_entry_category.py +37 -0
- hushscript/generated/models/error.py +155 -0
- hushscript/generated/models/error_details.py +94 -0
- hushscript/generated/models/get_upload_part_urls_response_200_type_0.py +108 -0
- hushscript/generated/models/heartbeat_upload_response_200.py +61 -0
- hushscript/generated/models/holds_response.py +90 -0
- hushscript/generated/models/job.py +309 -0
- hushscript/generated/models/job_failure_kind_type_1.py +16 -0
- hushscript/generated/models/job_failure_kind_type_2_type_1.py +16 -0
- hushscript/generated/models/job_failure_kind_type_3_type_1.py +16 -0
- hushscript/generated/models/job_list.py +90 -0
- hushscript/generated/models/lift_transcript_hold_response_200.py +61 -0
- hushscript/generated/models/list_jobs_active.py +17 -0
- hushscript/generated/models/list_org_matters_status.py +16 -0
- hushscript/generated/models/options_summary.py +135 -0
- hushscript/generated/models/org_audit_entry.py +105 -0
- hushscript/generated/models/org_audit_response.py +90 -0
- hushscript/generated/models/org_invoice.py +175 -0
- hushscript/generated/models/org_invoices_response.py +90 -0
- hushscript/generated/models/org_ledger_entry.py +155 -0
- hushscript/generated/models/org_ledger_response.py +90 -0
- hushscript/generated/models/org_matter.py +79 -0
- hushscript/generated/models/org_matter_status.py +16 -0
- hushscript/generated/models/org_matters_response.py +75 -0
- hushscript/generated/models/org_member.py +123 -0
- hushscript/generated/models/org_members_response.py +90 -0
- hushscript/generated/models/org_summary_response.py +482 -0
- hushscript/generated/models/org_transcript.py +103 -0
- hushscript/generated/models/org_transcripts_response.py +90 -0
- hushscript/generated/models/org_usage_report_csv_group_by.py +17 -0
- hushscript/generated/models/org_usage_report_group_by.py +17 -0
- hushscript/generated/models/org_usage_response.py +112 -0
- hushscript/generated/models/org_usage_response_group_by.py +17 -0
- hushscript/generated/models/org_usage_row.py +86 -0
- hushscript/generated/models/pack.py +107 -0
- hushscript/generated/models/packs_response.py +75 -0
- hushscript/generated/models/patch_org_member_body.py +102 -0
- hushscript/generated/models/patch_org_member_response_200.py +61 -0
- hushscript/generated/models/place_transcript_hold_response_200.py +61 -0
- hushscript/generated/models/presigned_part_url.py +69 -0
- hushscript/generated/models/purchase_response.py +76 -0
- hushscript/generated/models/schedule_org_custody_body.py +66 -0
- hushscript/generated/models/schedule_org_custody_body_custody.py +17 -0
- hushscript/generated/models/schedule_org_custody_response_200.py +69 -0
- hushscript/generated/models/set_default_card_body.py +61 -0
- hushscript/generated/models/set_default_card_response_200.py +61 -0
- hushscript/generated/models/setup_session_response.py +77 -0
- hushscript/generated/models/transaction.py +183 -0
- hushscript/generated/models/transactions_response.py +98 -0
- hushscript/generated/models/transcript_detail.py +194 -0
- hushscript/generated/models/transcript_detail_speaker_names.py +47 -0
- hushscript/generated/models/transcript_envelope.py +89 -0
- hushscript/generated/models/transcript_envelope_translations.py +62 -0
- hushscript/generated/models/transcript_language.py +75 -0
- hushscript/generated/models/transcript_list.py +90 -0
- hushscript/generated/models/transcript_list_row.py +152 -0
- hushscript/generated/models/transcript_list_row_insights_state.py +20 -0
- hushscript/generated/models/transcription_context_response.py +95 -0
- hushscript/generated/models/upload_complete_response.py +100 -0
- hushscript/generated/models/upload_create_response.py +148 -0
- hushscript/generated/models/upload_create_response_funding.py +16 -0
- hushscript/generated/models/upload_create_response_upload_type_0.py +108 -0
- hushscript/generated/models/upload_limits.py +86 -0
- hushscript/generated/models/upload_part_response_200.py +69 -0
- hushscript/generated/models/utterance.py +85 -0
- hushscript/generated/scopes.py +42 -0
- hushscript/generated/types.py +54 -0
- hushscript/money.py +79 -0
- hushscript/paginate.py +78 -0
- hushscript/py.typed +0 -0
- hushscript/resources/__init__.py +1 -0
- hushscript/resources/_params.py +33 -0
- hushscript/resources/account.py +121 -0
- hushscript/resources/agent.py +421 -0
- hushscript/resources/billing.py +567 -0
- hushscript/resources/jobs.py +105 -0
- hushscript/resources/transcripts.py +209 -0
- hushscript/transcribe/__init__.py +1 -0
- hushscript/transcribe/apipeline.py +234 -0
- hushscript/transcribe/aupload.py +197 -0
- hushscript/transcribe/heartbeat.py +78 -0
- hushscript/transcribe/pipeline.py +312 -0
- hushscript/transcribe/preflight.py +272 -0
- hushscript/transcribe/probe.py +185 -0
- hushscript/transcribe/resume.py +115 -0
- hushscript/transcribe/segment.py +23 -0
- hushscript/transcribe/session.py +34 -0
- hushscript/transcribe/sniff.py +30 -0
- hushscript/transcribe/source.py +218 -0
- hushscript/transcribe/upload.py +498 -0
- hushscript/transcribe/wait.py +124 -0
- hushscript/transcript.py +197 -0
- hushscript-1.0.0.dist-info/METADATA +502 -0
- hushscript-1.0.0.dist-info/RECORD +187 -0
- hushscript-1.0.0.dist-info/WHEEL +4 -0
- hushscript-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import re
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Final, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
from hushscript._errors import (
|
|
10
|
+
HushscriptError,
|
|
11
|
+
PaymentMethodRequiredError,
|
|
12
|
+
add_note,
|
|
13
|
+
is_unsent,
|
|
14
|
+
raise_for_response,
|
|
15
|
+
unexpected_error,
|
|
16
|
+
)
|
|
17
|
+
from hushscript._json import ExtraFields, as_dict, as_list, extra_fields
|
|
18
|
+
from hushscript._response import BadBodyError, parsed
|
|
19
|
+
from hushscript._shape import (
|
|
20
|
+
as_flag,
|
|
21
|
+
as_text,
|
|
22
|
+
as_whole,
|
|
23
|
+
check_shape,
|
|
24
|
+
maybe_text,
|
|
25
|
+
to_datetime,
|
|
26
|
+
)
|
|
27
|
+
from hushscript._shapes import CARDS, HOLDS, LEDGER, PACKS, PURCHASE, SETUP_SESSION, TRANSACTIONS
|
|
28
|
+
from hushscript.money import Money, to_money
|
|
29
|
+
from hushscript.paginate import Page, apaginate, paginate
|
|
30
|
+
from hushscript.resources._params import list_params, path_id
|
|
31
|
+
from hushscript.resources.account import minutes_of
|
|
32
|
+
from hushscript.transcribe.preflight import invalid_option
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
|
|
36
|
+
from datetime import datetime
|
|
37
|
+
|
|
38
|
+
from hushscript._redact import Credentials
|
|
39
|
+
from hushscript.transcribe.session import AsyncSession, SyncSession
|
|
40
|
+
|
|
41
|
+
PURCHASE_KEY: Final = re.compile(r"[A-Za-z0-9_-]{1,128}")
|
|
42
|
+
OK: Final = "ok"
|
|
43
|
+
T = TypeVar("T")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class Pack(ExtraFields):
|
|
48
|
+
"""A minute pack as the account can see it."""
|
|
49
|
+
|
|
50
|
+
id: str
|
|
51
|
+
"""`45min`, `300min`, `900min`, `1800min` or `6000min`."""
|
|
52
|
+
seconds: int
|
|
53
|
+
minutes: int
|
|
54
|
+
"""`seconds / 60`, rounded toward zero."""
|
|
55
|
+
price: Money
|
|
56
|
+
"""In the account's own currency, as the API states it: never hard-code one."""
|
|
57
|
+
bonus_minutes: int
|
|
58
|
+
"""What an available referral bonus would add to this purchase; 0 when none."""
|
|
59
|
+
available: bool
|
|
60
|
+
"""False only for an agent account's tier-locked pack."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class Card(ExtraFields):
|
|
65
|
+
"""A saved payment method."""
|
|
66
|
+
|
|
67
|
+
id: str
|
|
68
|
+
type: str
|
|
69
|
+
brand: str
|
|
70
|
+
last4: str
|
|
71
|
+
exp_month: int
|
|
72
|
+
exp_year: int
|
|
73
|
+
"""`exp_month` and `exp_year` are 0 when the method has no expiry."""
|
|
74
|
+
email: str | None
|
|
75
|
+
is_default: bool
|
|
76
|
+
fast_checkout_eligible: bool
|
|
77
|
+
"""Whether `buy()` may charge it without the buyer present."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class Cards(ExtraFields):
|
|
82
|
+
"""The saved payment methods and which one is the default."""
|
|
83
|
+
|
|
84
|
+
default_id: str | None
|
|
85
|
+
cards: list[Card]
|
|
86
|
+
|
|
87
|
+
__hash__ = None # type: ignore[assignment] # holds a list, so it is unhashable by design
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class Purchase(ExtraFields):
|
|
92
|
+
"""What a purchase left the account with, read from the purchase answer alone."""
|
|
93
|
+
|
|
94
|
+
seconds: int
|
|
95
|
+
"""The balance after the purchase settled: spendable now, holds already deducted."""
|
|
96
|
+
minutes: int
|
|
97
|
+
"""`seconds / 60`, rounded toward zero."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class Transaction(ExtraFields):
|
|
102
|
+
"""One purchase, as the API reports it."""
|
|
103
|
+
|
|
104
|
+
id: int
|
|
105
|
+
seconds: int
|
|
106
|
+
amount_cents: int
|
|
107
|
+
amount_refunded_cents: int
|
|
108
|
+
currency: str
|
|
109
|
+
status: str
|
|
110
|
+
created_at: datetime
|
|
111
|
+
payment_method_type: str | None
|
|
112
|
+
payment_method_brand: str | None
|
|
113
|
+
payment_method_last4: str | None
|
|
114
|
+
receipt_number: str | None
|
|
115
|
+
"""The public receipt reference, when there is one."""
|
|
116
|
+
initiated_via: str | None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class LedgerEntry(ExtraFields):
|
|
121
|
+
"""One movement of the credit balance."""
|
|
122
|
+
|
|
123
|
+
id: int
|
|
124
|
+
delta_seconds: int
|
|
125
|
+
kind: str
|
|
126
|
+
job_id: str | None
|
|
127
|
+
note: str | None
|
|
128
|
+
created_at: datetime
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True, slots=True)
|
|
132
|
+
class Hold(ExtraFields):
|
|
133
|
+
"""Seconds reserved by work still running; they are already deducted from the balance."""
|
|
134
|
+
|
|
135
|
+
id: str
|
|
136
|
+
kind: str
|
|
137
|
+
"""`transcription`, `translation`, `insights` or `qa`; a kind the SDK does not know yet passes
|
|
138
|
+
through."""
|
|
139
|
+
seconds: int
|
|
140
|
+
created_at: datetime
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def pack_list(body: object) -> list[Pack]:
|
|
144
|
+
check_shape(body, PACKS, "pack list response")
|
|
145
|
+
packs: list[Pack] = []
|
|
146
|
+
for row in as_list((as_dict(body) or {})["packs"]) or []:
|
|
147
|
+
item = as_dict(row) or {}
|
|
148
|
+
seconds = as_whole(item["seconds"])
|
|
149
|
+
packs.append(
|
|
150
|
+
Pack(
|
|
151
|
+
_extra=extra_fields(
|
|
152
|
+
item,
|
|
153
|
+
"id",
|
|
154
|
+
"seconds",
|
|
155
|
+
"amount",
|
|
156
|
+
"currency",
|
|
157
|
+
"referral_bonus_seconds",
|
|
158
|
+
"available",
|
|
159
|
+
),
|
|
160
|
+
id=as_text(item["id"]),
|
|
161
|
+
seconds=seconds,
|
|
162
|
+
minutes=minutes_of(seconds),
|
|
163
|
+
price=to_money(as_whole(item["amount"]), as_text(item["currency"])),
|
|
164
|
+
bonus_minutes=minutes_of(as_whole(item["referral_bonus_seconds"])),
|
|
165
|
+
available=item.get("available") is not False,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
return packs
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def card_list(body: object) -> Cards:
|
|
172
|
+
check_shape(body, CARDS, "card list response")
|
|
173
|
+
fields = as_dict(body) or {}
|
|
174
|
+
cards = [
|
|
175
|
+
Card(
|
|
176
|
+
_extra=extra_fields(
|
|
177
|
+
item,
|
|
178
|
+
"id",
|
|
179
|
+
"type",
|
|
180
|
+
"brand",
|
|
181
|
+
"last4",
|
|
182
|
+
"exp_month",
|
|
183
|
+
"exp_year",
|
|
184
|
+
"email",
|
|
185
|
+
"is_default",
|
|
186
|
+
"fast_checkout_eligible",
|
|
187
|
+
),
|
|
188
|
+
id=as_text(item["id"]),
|
|
189
|
+
type=as_text(item["type"]),
|
|
190
|
+
brand=as_text(item["brand"]),
|
|
191
|
+
last4=as_text(item["last4"]),
|
|
192
|
+
exp_month=as_whole(item["exp_month"]),
|
|
193
|
+
exp_year=as_whole(item["exp_year"]),
|
|
194
|
+
email=maybe_text(item.get("email")),
|
|
195
|
+
is_default=as_flag(item["is_default"]),
|
|
196
|
+
fast_checkout_eligible=as_flag(item["fast_checkout_eligible"]),
|
|
197
|
+
)
|
|
198
|
+
for item in (as_dict(row) or {} for row in as_list(fields["methods"]) or [])
|
|
199
|
+
]
|
|
200
|
+
return Cards(
|
|
201
|
+
_extra=extra_fields(fields, "default_id", "methods"),
|
|
202
|
+
default_id=maybe_text(fields["default_id"]),
|
|
203
|
+
cards=cards,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def purchase_of(body: object) -> Purchase:
|
|
208
|
+
check_shape(body, PURCHASE, "purchase response")
|
|
209
|
+
fields = as_dict(body) or {}
|
|
210
|
+
if fields["status"] != OK:
|
|
211
|
+
raise BadBodyError("purchase response (status: expected ok)")
|
|
212
|
+
seconds = as_whole(fields["balance_seconds"])
|
|
213
|
+
return Purchase(
|
|
214
|
+
_extra=extra_fields(fields, "status", "balance_seconds"),
|
|
215
|
+
seconds=seconds,
|
|
216
|
+
minutes=minutes_of(seconds),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def setup_url(body: object) -> str:
|
|
221
|
+
check_shape(body, SETUP_SESSION, "setup session response")
|
|
222
|
+
url = as_text((as_dict(body) or {})["checkout_url"])
|
|
223
|
+
if not url:
|
|
224
|
+
raise BadBodyError("setup session response (checkout_url: expected a URL)")
|
|
225
|
+
return url
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def transaction_page(body: object) -> Page[Transaction]:
|
|
229
|
+
check_shape(body, TRANSACTIONS, "transaction list response")
|
|
230
|
+
fields = as_dict(body) or {}
|
|
231
|
+
items = [
|
|
232
|
+
Transaction(
|
|
233
|
+
_extra=extra_fields(
|
|
234
|
+
item,
|
|
235
|
+
"id",
|
|
236
|
+
"seconds",
|
|
237
|
+
"amount_cents",
|
|
238
|
+
"amount_refunded_cents",
|
|
239
|
+
"currency",
|
|
240
|
+
"status",
|
|
241
|
+
"created_at",
|
|
242
|
+
"payment_method_type",
|
|
243
|
+
"payment_method_brand",
|
|
244
|
+
"payment_method_last4",
|
|
245
|
+
"receipt_number",
|
|
246
|
+
"initiated_via",
|
|
247
|
+
),
|
|
248
|
+
id=as_whole(item["id"]),
|
|
249
|
+
seconds=as_whole(item["seconds"]),
|
|
250
|
+
amount_cents=as_whole(item["amount_cents"]),
|
|
251
|
+
amount_refunded_cents=as_whole(item["amount_refunded_cents"]),
|
|
252
|
+
currency=as_text(item["currency"]),
|
|
253
|
+
status=as_text(item["status"]),
|
|
254
|
+
created_at=to_datetime(item["created_at"]),
|
|
255
|
+
payment_method_type=maybe_text(item.get("payment_method_type")),
|
|
256
|
+
payment_method_brand=maybe_text(item.get("payment_method_brand")),
|
|
257
|
+
payment_method_last4=maybe_text(item.get("payment_method_last4")),
|
|
258
|
+
receipt_number=maybe_text(item.get("receipt_number")),
|
|
259
|
+
initiated_via=maybe_text(item.get("initiated_via")),
|
|
260
|
+
)
|
|
261
|
+
for item in (as_dict(row) or {} for row in as_list(fields["transactions"]) or [])
|
|
262
|
+
]
|
|
263
|
+
return Page(
|
|
264
|
+
_extra=extra_fields(fields, "transactions", "next_cursor"),
|
|
265
|
+
items=items,
|
|
266
|
+
next_cursor=maybe_text(fields["next_cursor"]),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def ledger_page(body: object) -> Page[LedgerEntry]:
|
|
271
|
+
check_shape(body, LEDGER, "ledger response")
|
|
272
|
+
fields = as_dict(body) or {}
|
|
273
|
+
items = [
|
|
274
|
+
LedgerEntry(
|
|
275
|
+
_extra=extra_fields(
|
|
276
|
+
item, "id", "delta_seconds", "kind", "job_id", "note", "created_at"
|
|
277
|
+
),
|
|
278
|
+
id=as_whole(item["id"]),
|
|
279
|
+
delta_seconds=as_whole(item["delta_seconds"]),
|
|
280
|
+
kind=as_text(item["kind"]),
|
|
281
|
+
job_id=maybe_text(item.get("job_id")),
|
|
282
|
+
note=maybe_text(item.get("note")),
|
|
283
|
+
created_at=to_datetime(item["created_at"]),
|
|
284
|
+
)
|
|
285
|
+
for item in (as_dict(row) or {} for row in as_list(fields["entries"]) or [])
|
|
286
|
+
]
|
|
287
|
+
return Page(
|
|
288
|
+
_extra=extra_fields(fields, "entries", "next_cursor"),
|
|
289
|
+
items=items,
|
|
290
|
+
next_cursor=maybe_text(fields["next_cursor"]),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def hold_page(body: object) -> Page[Hold]:
|
|
295
|
+
check_shape(body, HOLDS, "hold list response")
|
|
296
|
+
fields = as_dict(body) or {}
|
|
297
|
+
items = [
|
|
298
|
+
Hold(
|
|
299
|
+
_extra=extra_fields(item, "id", "kind", "seconds", "created_at"),
|
|
300
|
+
id=as_text(item["id"]),
|
|
301
|
+
kind=as_text(item["kind"]),
|
|
302
|
+
seconds=as_whole(item["seconds"]),
|
|
303
|
+
created_at=to_datetime(item["created_at"]),
|
|
304
|
+
)
|
|
305
|
+
for item in (as_dict(row) or {} for row in as_list(fields["holds"]) or [])
|
|
306
|
+
]
|
|
307
|
+
return Page(
|
|
308
|
+
_extra=extra_fields(fields, "holds", "next_cursor"),
|
|
309
|
+
items=items,
|
|
310
|
+
next_cursor=maybe_text(fields["next_cursor"]),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def purchase_key(idempotency_key: object) -> str:
|
|
315
|
+
"""The key a purchase goes out with: the caller's (1 to 128 of `A-Za-z0-9_-`, spec 7.3) or a
|
|
316
|
+
fresh one."""
|
|
317
|
+
if idempotency_key is None:
|
|
318
|
+
return str(uuid.uuid4())
|
|
319
|
+
if not isinstance(idempotency_key, str) or not PURCHASE_KEY.fullmatch(idempotency_key):
|
|
320
|
+
raise invalid_option(
|
|
321
|
+
"idempotency_key must be 1 to 128 characters from A-Z, a-z, 0-9, _ and -."
|
|
322
|
+
)
|
|
323
|
+
return idempotency_key
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def check_pack(pack: object) -> str:
|
|
327
|
+
if not isinstance(pack, str) or not pack:
|
|
328
|
+
raise invalid_option("pack is a pack id such as 300min.")
|
|
329
|
+
return pack
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def choose_card(cards: Cards) -> str:
|
|
333
|
+
"""The card `buy()` charges: the default, else the only one that can be charged. With none
|
|
334
|
+
usable it raises before anything is sent."""
|
|
335
|
+
if cards.default_id:
|
|
336
|
+
return cards.default_id
|
|
337
|
+
eligible = [card.id for card in cards.cards if card.fast_checkout_eligible]
|
|
338
|
+
if len(eligible) == 1:
|
|
339
|
+
return eligible[0]
|
|
340
|
+
if not eligible:
|
|
341
|
+
raise PaymentMethodRequiredError(
|
|
342
|
+
"The account has no saved card that can be charged.", code="no_default_card"
|
|
343
|
+
)
|
|
344
|
+
raise PaymentMethodRequiredError(
|
|
345
|
+
"The account has no default card and several that can be charged.",
|
|
346
|
+
code="default_card_required",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def purchase_body(
|
|
351
|
+
pack: str, key: str, card_id: str, *, acknowledge_no_refund: bool
|
|
352
|
+
) -> dict[str, object]:
|
|
353
|
+
return {
|
|
354
|
+
"pack": pack,
|
|
355
|
+
"idempotency_key": key,
|
|
356
|
+
"quick_payment_method_id": card_id,
|
|
357
|
+
"policy_ack": acknowledge_no_refund is True,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _interrupted(interrupted: BaseException, key: str) -> None:
|
|
362
|
+
add_note(
|
|
363
|
+
interrupted,
|
|
364
|
+
f"The purchase request was sent, so the card may have been charged. Idempotency key: {key}",
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def sent_purchase(key: str, credentials: Credentials, send: Callable[[], T]) -> T:
|
|
369
|
+
"""From the moment the purchase request goes out the card may have been charged, so every error
|
|
370
|
+
carries the key (spec 6.5), whatever its kind; an interrupt is never converted, and carries it
|
|
371
|
+
as a note. A failure of the SDK's own is raised once the handler has ended, so the unredacted
|
|
372
|
+
original is on neither the cause nor the `__context__` of what the caller gets."""
|
|
373
|
+
try:
|
|
374
|
+
return send()
|
|
375
|
+
except (asyncio.CancelledError, KeyboardInterrupt) as interrupted:
|
|
376
|
+
_interrupted(interrupted, key)
|
|
377
|
+
raise
|
|
378
|
+
except HushscriptError as error:
|
|
379
|
+
if not is_unsent(error): # R103: provably never sent, so nothing was charged
|
|
380
|
+
error.idempotency_key = key
|
|
381
|
+
raise
|
|
382
|
+
except Exception as failure: # noqa: BLE001 # R61: nothing but an SDK error leaves
|
|
383
|
+
failed = unexpected_error(failure, credentials)
|
|
384
|
+
failed.idempotency_key = key
|
|
385
|
+
raise failed
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
async def sent_purchase_async(
|
|
389
|
+
key: str, credentials: Credentials, send: Callable[[], Awaitable[T]]
|
|
390
|
+
) -> T:
|
|
391
|
+
"""`sent_purchase` for the asyncio client."""
|
|
392
|
+
try:
|
|
393
|
+
return await send()
|
|
394
|
+
except (asyncio.CancelledError, KeyboardInterrupt) as interrupted:
|
|
395
|
+
_interrupted(interrupted, key)
|
|
396
|
+
raise
|
|
397
|
+
except HushscriptError as error:
|
|
398
|
+
if not is_unsent(error): # R103: provably never sent, so nothing was charged
|
|
399
|
+
error.idempotency_key = key
|
|
400
|
+
raise
|
|
401
|
+
except Exception as failure: # noqa: BLE001 # R61: nothing but an SDK error leaves
|
|
402
|
+
failed = unexpected_error(failure, credentials)
|
|
403
|
+
failed.idempotency_key = key
|
|
404
|
+
raise failed
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def check_card_id(card_id: object) -> str:
|
|
408
|
+
path_id(card_id)
|
|
409
|
+
return cast("str", card_id)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class Billing:
|
|
413
|
+
"""`hs.billing`: packs, cards, purchases and the billing history."""
|
|
414
|
+
|
|
415
|
+
def __init__(self, session: SyncSession) -> None:
|
|
416
|
+
self._session = session
|
|
417
|
+
|
|
418
|
+
def packs(self) -> list[Pack]:
|
|
419
|
+
"""The minute packs the account can buy, priced in its own currency. Needs the billing
|
|
420
|
+
scope."""
|
|
421
|
+
return parsed(
|
|
422
|
+
self._session.http.get("/v1/billing/packs"), self._session.credentials, pack_list
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def cards(self) -> Cards:
|
|
426
|
+
"""The saved payment methods and the default one."""
|
|
427
|
+
return parsed(
|
|
428
|
+
self._session.http.get("/v1/billing/cards"), self._session.credentials, card_list
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
def set_default_card(self, card_id: str) -> None:
|
|
432
|
+
"""Makes a saved card the default one."""
|
|
433
|
+
body = {"payment_method_id": check_card_id(card_id)}
|
|
434
|
+
raise_for_response(
|
|
435
|
+
self._session.http.post("/v1/billing/cards/default", json=body),
|
|
436
|
+
credentials=self._session.credentials,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def add_card_url(self) -> str:
|
|
440
|
+
"""The hosted page where a card is saved: open it in a browser. The request carries a key,
|
|
441
|
+
so its one retry after a 502 lands on the same page."""
|
|
442
|
+
response = self._session.http.post(
|
|
443
|
+
"/v1/billing/setup-session", json={"idempotency_key": str(uuid.uuid4())}
|
|
444
|
+
)
|
|
445
|
+
return parsed(response, self._session.credentials, setup_url)
|
|
446
|
+
|
|
447
|
+
def buy(
|
|
448
|
+
self,
|
|
449
|
+
pack: str,
|
|
450
|
+
*,
|
|
451
|
+
acknowledge_no_refund: bool = False,
|
|
452
|
+
idempotency_key: str | None = None,
|
|
453
|
+
) -> Purchase:
|
|
454
|
+
"""Buys a pack with the default card, or the only card that can be charged when there is no
|
|
455
|
+
default, and returns the balance the purchase answer reports, without a follow-up request.
|
|
456
|
+
A lost or unusable response can still raise an error after payment. Every error raised after
|
|
457
|
+
the purchase request was sent carries `idempotency_key`; pass it back with no chance of a
|
|
458
|
+
second charge. `acknowledge_no_refund` records that the no-refund notice was shown to the
|
|
459
|
+
buyer; it is evidence for disputes and never gates the charge."""
|
|
460
|
+
pack = check_pack(pack)
|
|
461
|
+
key = purchase_key(idempotency_key)
|
|
462
|
+
card_id = choose_card(self.cards())
|
|
463
|
+
body = purchase_body(pack, key, card_id, acknowledge_no_refund=acknowledge_no_refund)
|
|
464
|
+
|
|
465
|
+
def send() -> Purchase:
|
|
466
|
+
response = self._session.http.post("/v1/billing/purchase", json=body)
|
|
467
|
+
return parsed(response, self._session.credentials, purchase_of)
|
|
468
|
+
|
|
469
|
+
return sent_purchase(key, self._session.credentials, send)
|
|
470
|
+
|
|
471
|
+
def transactions(self) -> Iterator[Transaction]:
|
|
472
|
+
"""Every purchase, newest first, fetching pages as it goes."""
|
|
473
|
+
return paginate(self._transactions)
|
|
474
|
+
|
|
475
|
+
def ledger(self) -> Iterator[LedgerEntry]:
|
|
476
|
+
"""Every movement of the credit balance, newest first: holds, settles, releases, purchases
|
|
477
|
+
and refunds."""
|
|
478
|
+
return paginate(self._ledger)
|
|
479
|
+
|
|
480
|
+
def holds(self) -> Iterator[Hold]:
|
|
481
|
+
"""The seconds reserved by work in flight, newest first."""
|
|
482
|
+
return paginate(self._holds)
|
|
483
|
+
|
|
484
|
+
def _transactions(self, cursor: str | None) -> Page[Transaction]:
|
|
485
|
+
response = self._session.http.get(
|
|
486
|
+
"/v1/billing/transactions", params=list_params(cursor, None)
|
|
487
|
+
)
|
|
488
|
+
return parsed(response, self._session.credentials, transaction_page)
|
|
489
|
+
|
|
490
|
+
def _ledger(self, cursor: str | None) -> Page[LedgerEntry]:
|
|
491
|
+
response = self._session.http.get("/v1/billing/ledger", params=list_params(cursor, None))
|
|
492
|
+
return parsed(response, self._session.credentials, ledger_page)
|
|
493
|
+
|
|
494
|
+
def _holds(self, cursor: str | None) -> Page[Hold]:
|
|
495
|
+
response = self._session.http.get("/v1/billing/holds", params=list_params(cursor, None))
|
|
496
|
+
return parsed(response, self._session.credentials, hold_page)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class AsyncBilling:
|
|
500
|
+
"""`hs.billing` on the asyncio client; the lists are async iterators."""
|
|
501
|
+
|
|
502
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
503
|
+
self._session = session
|
|
504
|
+
|
|
505
|
+
async def packs(self) -> list[Pack]:
|
|
506
|
+
response = await self._session.http.get("/v1/billing/packs")
|
|
507
|
+
return parsed(response, self._session.credentials, pack_list)
|
|
508
|
+
|
|
509
|
+
async def cards(self) -> Cards:
|
|
510
|
+
response = await self._session.http.get("/v1/billing/cards")
|
|
511
|
+
return parsed(response, self._session.credentials, card_list)
|
|
512
|
+
|
|
513
|
+
async def set_default_card(self, card_id: str) -> None:
|
|
514
|
+
body = {"payment_method_id": check_card_id(card_id)}
|
|
515
|
+
response = await self._session.http.post("/v1/billing/cards/default", json=body)
|
|
516
|
+
raise_for_response(response, credentials=self._session.credentials)
|
|
517
|
+
|
|
518
|
+
async def add_card_url(self) -> str:
|
|
519
|
+
response = await self._session.http.post(
|
|
520
|
+
"/v1/billing/setup-session", json={"idempotency_key": str(uuid.uuid4())}
|
|
521
|
+
)
|
|
522
|
+
return parsed(response, self._session.credentials, setup_url)
|
|
523
|
+
|
|
524
|
+
async def buy(
|
|
525
|
+
self,
|
|
526
|
+
pack: str,
|
|
527
|
+
*,
|
|
528
|
+
acknowledge_no_refund: bool = False,
|
|
529
|
+
idempotency_key: str | None = None,
|
|
530
|
+
) -> Purchase:
|
|
531
|
+
pack = check_pack(pack)
|
|
532
|
+
key = purchase_key(idempotency_key)
|
|
533
|
+
card_id = choose_card(await self.cards())
|
|
534
|
+
body = purchase_body(pack, key, card_id, acknowledge_no_refund=acknowledge_no_refund)
|
|
535
|
+
|
|
536
|
+
async def send() -> Purchase:
|
|
537
|
+
response = await self._session.http.post("/v1/billing/purchase", json=body)
|
|
538
|
+
return parsed(response, self._session.credentials, purchase_of)
|
|
539
|
+
|
|
540
|
+
return await sent_purchase_async(key, self._session.credentials, send)
|
|
541
|
+
|
|
542
|
+
def transactions(self) -> AsyncIterator[Transaction]:
|
|
543
|
+
return apaginate(self._transactions)
|
|
544
|
+
|
|
545
|
+
def ledger(self) -> AsyncIterator[LedgerEntry]:
|
|
546
|
+
return apaginate(self._ledger)
|
|
547
|
+
|
|
548
|
+
def holds(self) -> AsyncIterator[Hold]:
|
|
549
|
+
return apaginate(self._holds)
|
|
550
|
+
|
|
551
|
+
async def _transactions(self, cursor: str | None) -> Page[Transaction]:
|
|
552
|
+
response = await self._session.http.get(
|
|
553
|
+
"/v1/billing/transactions", params=list_params(cursor, None)
|
|
554
|
+
)
|
|
555
|
+
return parsed(response, self._session.credentials, transaction_page)
|
|
556
|
+
|
|
557
|
+
async def _ledger(self, cursor: str | None) -> Page[LedgerEntry]:
|
|
558
|
+
response = await self._session.http.get(
|
|
559
|
+
"/v1/billing/ledger", params=list_params(cursor, None)
|
|
560
|
+
)
|
|
561
|
+
return parsed(response, self._session.credentials, ledger_page)
|
|
562
|
+
|
|
563
|
+
async def _holds(self, cursor: str | None) -> Page[Hold]:
|
|
564
|
+
response = await self._session.http.get(
|
|
565
|
+
"/v1/billing/holds", params=list_params(cursor, None)
|
|
566
|
+
)
|
|
567
|
+
return parsed(response, self._session.credentials, hold_page)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from hushscript._json import as_dict, as_list, extra_fields
|
|
6
|
+
from hushscript._models import Job
|
|
7
|
+
from hushscript._response import parsed
|
|
8
|
+
from hushscript._shape import check_shape, maybe_text
|
|
9
|
+
from hushscript._shapes import JOB_LIST
|
|
10
|
+
from hushscript.paginate import Page, apaginate, paginate
|
|
11
|
+
from hushscript.resources._params import path_id
|
|
12
|
+
from hushscript.transcribe import apipeline, pipeline
|
|
13
|
+
from hushscript.transcribe.pipeline import Callbacks
|
|
14
|
+
from hushscript.transcribe.preflight import check_wait_timeout
|
|
15
|
+
from hushscript.transcribe.wait import DEFAULT_WAIT_TIMEOUT
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import AsyncIterator, Callable, Iterator
|
|
19
|
+
|
|
20
|
+
from hushscript._models import Progress
|
|
21
|
+
from hushscript.transcribe.session import AsyncSession, SyncSession
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def job_page(body: object) -> Page[Job]:
|
|
25
|
+
check_shape(body, JOB_LIST, "job list response")
|
|
26
|
+
fields = as_dict(body) or {}
|
|
27
|
+
items = [Job.from_body(row) for row in as_list(fields["jobs"]) or []]
|
|
28
|
+
return Page(
|
|
29
|
+
_extra=extra_fields(fields, "jobs", "next_cursor"),
|
|
30
|
+
items=items,
|
|
31
|
+
next_cursor=maybe_text(fields["next_cursor"]),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def job_params(cursor: str | None, *, active: bool) -> dict[str, str]:
|
|
36
|
+
params: dict[str, str] = {}
|
|
37
|
+
if active:
|
|
38
|
+
params["active"] = "true"
|
|
39
|
+
if cursor is not None:
|
|
40
|
+
params["cursor"] = cursor
|
|
41
|
+
return params
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Jobs:
|
|
45
|
+
"""`hs.jobs`: the account's transcription jobs."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, session: SyncSession) -> None:
|
|
48
|
+
self._session = session
|
|
49
|
+
|
|
50
|
+
def list(self, *, active: bool = False) -> Iterator[Job]:
|
|
51
|
+
"""Every job, newest first, fetching pages as it goes; `active=True` keeps only the jobs
|
|
52
|
+
that have not finished."""
|
|
53
|
+
return paginate(lambda cursor: self._page(cursor, active=active))
|
|
54
|
+
|
|
55
|
+
def get(self, job_id: str) -> Job:
|
|
56
|
+
return pipeline.get_job(self._session, path_id(job_id))
|
|
57
|
+
|
|
58
|
+
def wait(
|
|
59
|
+
self,
|
|
60
|
+
job_id: str,
|
|
61
|
+
*,
|
|
62
|
+
wait_timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
63
|
+
on_progress: Callable[[Progress], None] | None = None,
|
|
64
|
+
) -> Job:
|
|
65
|
+
"""Polls until the job is `done` or `failed` (spec 8.6); WaitTimeoutError after
|
|
66
|
+
`wait_timeout` seconds. A failed job is returned, not raised."""
|
|
67
|
+
check_wait_timeout(wait_timeout)
|
|
68
|
+
return pipeline.wait_job(
|
|
69
|
+
self._session, path_id(job_id), wait_timeout, Callbacks(on_progress=on_progress), None
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _page(self, cursor: str | None, *, active: bool) -> Page[Job]:
|
|
73
|
+
response = self._session.http.get("/v1/jobs", params=job_params(cursor, active=active))
|
|
74
|
+
return parsed(response, self._session.credentials, job_page)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AsyncJobs:
|
|
78
|
+
"""`hs.jobs` on the asyncio client; `list` is an async iterator."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
81
|
+
self._session = session
|
|
82
|
+
|
|
83
|
+
def list(self, *, active: bool = False) -> AsyncIterator[Job]:
|
|
84
|
+
return apaginate(lambda cursor: self._page(cursor, active=active))
|
|
85
|
+
|
|
86
|
+
async def get(self, job_id: str) -> Job:
|
|
87
|
+
return await apipeline.get_job(self._session, path_id(job_id))
|
|
88
|
+
|
|
89
|
+
async def wait(
|
|
90
|
+
self,
|
|
91
|
+
job_id: str,
|
|
92
|
+
*,
|
|
93
|
+
wait_timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
94
|
+
on_progress: Callable[[Progress], None] | None = None,
|
|
95
|
+
) -> Job:
|
|
96
|
+
check_wait_timeout(wait_timeout)
|
|
97
|
+
return await apipeline.wait_job(
|
|
98
|
+
self._session, path_id(job_id), wait_timeout, Callbacks(on_progress=on_progress), None
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def _page(self, cursor: str | None, *, active: bool) -> Page[Job]:
|
|
102
|
+
response = await self._session.http.get(
|
|
103
|
+
"/v1/jobs", params=job_params(cursor, active=active)
|
|
104
|
+
)
|
|
105
|
+
return parsed(response, self._session.credentials, job_page)
|