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,86 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any, TypeVar
5
+
6
+ from attrs import define as _attrs_define
7
+ from attrs import field as _attrs_field
8
+
9
+ T = TypeVar("T", bound="UploadLimits")
10
+
11
+
12
+ @_attrs_define
13
+ class UploadLimits:
14
+ """The caller's current upload admission limits, shared by every response that carries them.
15
+
16
+ Attributes:
17
+ max_daily_seconds (int): The account's daily transcription-minutes cap, in seconds.
18
+ max_active_jobs (int): The maximum number of concurrent pipeline jobs this account may have in flight.
19
+ active_jobs (int): How many pipeline jobs are currently in flight.
20
+ used_today_seconds (int): Billable seconds already used today, in the account's own rolling day.
21
+ """
22
+
23
+ max_daily_seconds: int
24
+ max_active_jobs: int
25
+ active_jobs: int
26
+ used_today_seconds: int
27
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ max_daily_seconds = self.max_daily_seconds
31
+
32
+ max_active_jobs = self.max_active_jobs
33
+
34
+ active_jobs = self.active_jobs
35
+
36
+ used_today_seconds = self.used_today_seconds
37
+
38
+ field_dict: dict[str, Any] = {}
39
+ field_dict.update(self.additional_properties)
40
+ field_dict.update(
41
+ {
42
+ "max_daily_seconds": max_daily_seconds,
43
+ "max_active_jobs": max_active_jobs,
44
+ "active_jobs": active_jobs,
45
+ "used_today_seconds": used_today_seconds,
46
+ }
47
+ )
48
+
49
+ return field_dict
50
+
51
+ @classmethod
52
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
53
+ d = dict(src_dict)
54
+ max_daily_seconds = d.pop("max_daily_seconds")
55
+
56
+ max_active_jobs = d.pop("max_active_jobs")
57
+
58
+ active_jobs = d.pop("active_jobs")
59
+
60
+ used_today_seconds = d.pop("used_today_seconds")
61
+
62
+ upload_limits = cls(
63
+ max_daily_seconds=max_daily_seconds,
64
+ max_active_jobs=max_active_jobs,
65
+ active_jobs=active_jobs,
66
+ used_today_seconds=used_today_seconds,
67
+ )
68
+
69
+ upload_limits.additional_properties = d
70
+ return upload_limits
71
+
72
+ @property
73
+ def additional_keys(self) -> list[str]:
74
+ return list(self.additional_properties.keys())
75
+
76
+ def __getitem__(self, key: str) -> Any:
77
+ return self.additional_properties[key]
78
+
79
+ def __setitem__(self, key: str, value: Any) -> None:
80
+ self.additional_properties[key] = value
81
+
82
+ def __delitem__(self, key: str) -> None:
83
+ del self.additional_properties[key]
84
+
85
+ def __contains__(self, key: str) -> bool:
86
+ return key in self.additional_properties
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any, TypeVar
5
+
6
+ from attrs import define as _attrs_define
7
+ from attrs import field as _attrs_field
8
+
9
+ T = TypeVar("T", bound="UploadPartResponse200")
10
+
11
+
12
+ @_attrs_define
13
+ class UploadPartResponse200:
14
+ """
15
+ Attributes:
16
+ ok (bool):
17
+ part (int):
18
+ """
19
+
20
+ ok: bool
21
+ part: int
22
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
23
+
24
+ def to_dict(self) -> dict[str, Any]:
25
+ ok = self.ok
26
+
27
+ part = self.part
28
+
29
+ field_dict: dict[str, Any] = {}
30
+ field_dict.update(self.additional_properties)
31
+ field_dict.update(
32
+ {
33
+ "ok": ok,
34
+ "part": part,
35
+ }
36
+ )
37
+
38
+ return field_dict
39
+
40
+ @classmethod
41
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
42
+ d = dict(src_dict)
43
+ ok = d.pop("ok")
44
+
45
+ part = d.pop("part")
46
+
47
+ upload_part_response_200 = cls(
48
+ ok=ok,
49
+ part=part,
50
+ )
51
+
52
+ upload_part_response_200.additional_properties = d
53
+ return upload_part_response_200
54
+
55
+ @property
56
+ def additional_keys(self) -> list[str]:
57
+ return list(self.additional_properties.keys())
58
+
59
+ def __getitem__(self, key: str) -> Any:
60
+ return self.additional_properties[key]
61
+
62
+ def __setitem__(self, key: str, value: Any) -> None:
63
+ self.additional_properties[key] = value
64
+
65
+ def __delitem__(self, key: str) -> None:
66
+ del self.additional_properties[key]
67
+
68
+ def __contains__(self, key: str) -> bool:
69
+ return key in self.additional_properties
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any, TypeVar
5
+
6
+ from attrs import define as _attrs_define
7
+ from attrs import field as _attrs_field
8
+
9
+ T = TypeVar("T", bound="Utterance")
10
+
11
+
12
+ @_attrs_define
13
+ class Utterance:
14
+ """
15
+ Attributes:
16
+ speaker (str):
17
+ start_ms (int):
18
+ end_ms (int):
19
+ text (str):
20
+ """
21
+
22
+ speaker: str
23
+ start_ms: int
24
+ end_ms: int
25
+ text: str
26
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
27
+
28
+ def to_dict(self) -> dict[str, Any]:
29
+ speaker = self.speaker
30
+
31
+ start_ms = self.start_ms
32
+
33
+ end_ms = self.end_ms
34
+
35
+ text = self.text
36
+
37
+ field_dict: dict[str, Any] = {}
38
+ field_dict.update(self.additional_properties)
39
+ field_dict.update(
40
+ {
41
+ "speaker": speaker,
42
+ "start_ms": start_ms,
43
+ "end_ms": end_ms,
44
+ "text": text,
45
+ }
46
+ )
47
+
48
+ return field_dict
49
+
50
+ @classmethod
51
+ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
52
+ d = dict(src_dict)
53
+ speaker = d.pop("speaker")
54
+
55
+ start_ms = d.pop("start_ms")
56
+
57
+ end_ms = d.pop("end_ms")
58
+
59
+ text = d.pop("text")
60
+
61
+ utterance = cls(
62
+ speaker=speaker,
63
+ start_ms=start_ms,
64
+ end_ms=end_ms,
65
+ text=text,
66
+ )
67
+
68
+ utterance.additional_properties = d
69
+ return utterance
70
+
71
+ @property
72
+ def additional_keys(self) -> list[str]:
73
+ return list(self.additional_properties.keys())
74
+
75
+ def __getitem__(self, key: str) -> Any:
76
+ return self.additional_properties[key]
77
+
78
+ def __setitem__(self, key: str, value: Any) -> None:
79
+ self.additional_properties[key] = value
80
+
81
+ def __delitem__(self, key: str) -> None:
82
+ del self.additional_properties[key]
83
+
84
+ def __contains__(self, key: str) -> bool:
85
+ return key in self.additional_properties
@@ -0,0 +1,42 @@
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
+ OPERATION_SCOPES: Final[dict[str, str]] = {
6
+ "DELETE /v1/org/transcripts/{id}/hold": "org:write",
7
+ "GET /v1/account": "account:read",
8
+ "GET /v1/billing/cards": "billing",
9
+ "GET /v1/billing/holds": "account:read",
10
+ "GET /v1/billing/ledger": "account:read",
11
+ "GET /v1/billing/packs": "billing",
12
+ "GET /v1/billing/transactions": "account:read",
13
+ "GET /v1/jobs": "transcribe",
14
+ "GET /v1/jobs/{id}": "transcribe",
15
+ "GET /v1/org": "org:read",
16
+ "GET /v1/org/audit": "org:read",
17
+ "GET /v1/org/audit.csv": "org:read",
18
+ "GET /v1/org/invoices": "org:read",
19
+ "GET /v1/org/ledger": "org:read",
20
+ "GET /v1/org/ledger.csv": "org:read",
21
+ "GET /v1/org/matters": "org:read",
22
+ "GET /v1/org/members": "org:read",
23
+ "GET /v1/org/transcripts": "org:read",
24
+ "GET /v1/org/usage": "org:read",
25
+ "GET /v1/org/usage.csv": "org:read",
26
+ "GET /v1/transcription-context": "context",
27
+ "GET /v1/transcripts": "transcripts:read",
28
+ "GET /v1/transcripts/deleted": "transcripts:read",
29
+ "GET /v1/transcripts/{id}": "transcripts:read",
30
+ "GET /v1/uploads/{job}/part-urls": "transcribe",
31
+ "PATCH /v1/org/members/{user_id}": "org:write",
32
+ "POST /v1/agent/credentials/rotate": "pat:rotate",
33
+ "POST /v1/billing/cards/default": "billing",
34
+ "POST /v1/billing/purchase": "billing",
35
+ "POST /v1/billing/setup-session": "billing",
36
+ "POST /v1/org/custody": "org:write",
37
+ "POST /v1/org/transcripts/{id}/hold": "org:write",
38
+ "POST /v1/uploads": "transcribe",
39
+ "POST /v1/uploads/{job}/complete": "transcribe",
40
+ "POST /v1/uploads/{job}/heartbeat": "transcribe",
41
+ "PUT /v1/uploads/{job}/parts/{n}": "transcribe",
42
+ }
@@ -0,0 +1,54 @@
1
+ """Contains some shared types for properties"""
2
+
3
+ from collections.abc import Mapping, MutableMapping
4
+ from http import HTTPStatus
5
+ from typing import IO, BinaryIO, Generic, Literal, TypeVar
6
+
7
+ from attrs import define
8
+
9
+
10
+ class Unset:
11
+ def __bool__(self) -> Literal[False]:
12
+ return False
13
+
14
+
15
+ UNSET: Unset = Unset()
16
+
17
+ # The types that `httpx.Client(files=)` can accept, copied from that library.
18
+ FileContent = IO[bytes] | bytes | str
19
+ FileTypes = (
20
+ # (filename, file (or bytes), content_type)
21
+ tuple[str | None, FileContent, str | None]
22
+ # (filename, file (or bytes), content_type, headers)
23
+ | tuple[str | None, FileContent, str | None, Mapping[str, str]]
24
+ )
25
+ RequestFiles = list[tuple[str, FileTypes]]
26
+
27
+
28
+ @define
29
+ class File:
30
+ """Contains information for file uploads"""
31
+
32
+ payload: BinaryIO
33
+ file_name: str | None = None
34
+ mime_type: str | None = None
35
+
36
+ def to_tuple(self) -> FileTypes:
37
+ """Return a tuple representation that httpx will accept for multipart/form-data"""
38
+ return self.file_name, self.payload, self.mime_type
39
+
40
+
41
+ T = TypeVar("T")
42
+
43
+
44
+ @define
45
+ class Response(Generic[T]):
46
+ """A response from an endpoint"""
47
+
48
+ status_code: HTTPStatus
49
+ content: bytes
50
+ headers: MutableMapping[str, str]
51
+ parsed: T | None
52
+
53
+
54
+ __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"]
hushscript/money.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Final
5
+
6
+ from ._json import ExtraFields
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class Money(ExtraFields):
11
+ """A price as the API states it, with a display string in the `en` locale.
12
+
13
+ The amount digits and the grouping (minor-unit digits from Stripe's table, thousands grouped
14
+ with commas) are the same in every Hushscript SDK. The rest of `formatted` is this SDK's own: it
15
+ writes `$`, `€`, `£` or `¥` for USD, EUR, GBP and JPY, and for every other known currency the
16
+ code and a no-break space before the number (`KRW 1,234`), where the TypeScript SDK takes its
17
+ symbols from Intl and may print another. Compare `amount` and `currency`, never `formatted`,
18
+ across SDKs.
19
+ """
20
+
21
+ amount: int
22
+ """In the currency's minor units, from the API."""
23
+ currency: str
24
+ """The ISO 4217 code as the API sends it."""
25
+ formatted: str
26
+ """`amount` divided by Stripe's minor-unit digits for the currency, for example `$5.99`."""
27
+
28
+
29
+ # Stripe's minor-unit digits, which the API's amounts use (spec 6.5). They come from this table and
30
+ # never from locale data: ICU disagrees for IDR, HUF, ISK and others.
31
+ _ZERO_DIGITS: Final = frozenset(
32
+ "BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF".split() # noqa: SIM905
33
+ )
34
+ _THREE_DIGITS: Final = frozenset("BHD JOD KWD OMR TND".split()) # noqa: SIM905
35
+ # The ISO 4217 codes printed with the code in front; any other string is shown plainly, as
36
+ # "5.99 ZZZ", and never fails.
37
+ _KNOWN: Final = frozenset(
38
+ """
39
+ AED AFN ALL AMD ANG AOA ARS AUD AWG AZN BAM BBD BDT BGN BHD BIF BMD BND BOB BRL BSD
40
+ BTN BWP BYN BZD CAD CDF CHF CLP CNY COP CRC CUP CVE CZK DJF DKK DOP DZD EGP ERN ETB
41
+ EUR FJD FKP GBP GEL GHS GIP GMD GNF GTQ GYD HKD HNL HTG HUF IDR ILS INR IQD IRR ISK
42
+ JMD JOD JPY KES KGS KHR KMF KPW KRW KWD KYD KZT LAK LBP LKR LRD LSL LYD MAD MDL MGA
43
+ MKD MMK MNT MOP MRU MUR MVR MWK MXN MYR MZN NAD NGN NIO NOK NPR NZD OMR PAB PEN PGK
44
+ PHP PKR PLN PYG QAR RON RSD RUB RWF SAR SBD SCR SDG SEK SGD SHP SLE SOS SRD SSP STN
45
+ SVC SYP SZL THB TJS TMT TND TOP TRY TTD TWD TZS UAH UGX USD UYU UZS VES VND VUV WST
46
+ XAF XCD XOF XPF YER ZAR ZMW ZWG
47
+ """.split() # noqa: SIM905
48
+ )
49
+ _SYMBOLS: Final = {"USD": "$", "EUR": "€", "GBP": "£", "JPY": "¥"}
50
+ _NO_BREAK_SPACE: Final = chr(0xA0)
51
+ _ZERO_DIGIT_COUNT: Final = 0
52
+ _THREE_DIGIT_COUNT: Final = 3
53
+ _DEFAULT_DIGITS: Final = 2
54
+
55
+
56
+ def minor_unit_digits(code: str) -> int:
57
+ if code in _ZERO_DIGITS:
58
+ return _ZERO_DIGIT_COUNT
59
+ return _THREE_DIGIT_COUNT if code in _THREE_DIGITS else _DEFAULT_DIGITS
60
+
61
+
62
+ def to_money(amount: int, currency: str) -> Money:
63
+ """The `Money` of an API amount and currency; an unknown currency is shown as the plain amount
64
+ and the upper-cased code with 2 digits, and never raises."""
65
+ code = currency.upper()
66
+ digits = minor_unit_digits(code)
67
+ magnitude = abs(amount)
68
+ whole, fraction = divmod(magnitude, 10**digits)
69
+ known = code in _KNOWN
70
+ number = f"{whole:,}" if known else str(whole)
71
+ if digits:
72
+ number += f".{fraction:0{digits}d}"
73
+ sign = "-" if amount < 0 else ""
74
+ if not known:
75
+ return Money(amount, currency, f"{sign}{number} {code}")
76
+ symbol = _SYMBOLS.get(code)
77
+ if symbol is not None:
78
+ return Money(amount, currency, f"{sign}{symbol}{number}")
79
+ return Money(amount, currency, f"{sign}{code}{_NO_BREAK_SPACE}{number}")
hushscript/paginate.py ADDED
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Generic, TypeVar, cast
6
+
7
+ from ._errors import HushscriptError
8
+ from ._json import ExtraFields
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class Page(ExtraFields, Generic[T]):
18
+ """One page of a list. `next_cursor` is opaque and None on the last page."""
19
+
20
+ items: list[T]
21
+ next_cursor: str | None
22
+
23
+ __hash__ = None # type: ignore[assignment] # holds a list, so it is unhashable by design
24
+
25
+
26
+ if sys.version_info < (3, 11):
27
+ _page_setattr = cast("Callable[[object, str, object], None]", vars(Page)["__setattr__"])
28
+
29
+ def _page_setattr_compat(self: object, name: str, value: object) -> None:
30
+ # Python 3.10's generic constructor only ignores AttributeError for type metadata.
31
+ if name == "__orig_class__":
32
+ raise AttributeError(name)
33
+ _page_setattr(self, name, value)
34
+
35
+ Page.__setattr__ = _page_setattr_compat # type: ignore[method-assign]
36
+
37
+
38
+ def pagination_loop() -> HushscriptError:
39
+ return HushscriptError(
40
+ "The API returned the same page cursor it was sent; stopping instead of repeating it.",
41
+ code="pagination_loop",
42
+ )
43
+
44
+
45
+ def check_cursor_advanced(sent: str | None, page: Page[T]) -> None:
46
+ """R89: a page that hands back the cursor it was asked with would repeat forever."""
47
+ if page.next_cursor and page.next_cursor == sent:
48
+ raise pagination_loop()
49
+
50
+
51
+ def paginate(fetch: Callable[[str | None], Page[T]]) -> Iterator[T]:
52
+ """Walks a cursor-paged list one page at a time, yielding every item in order. The walk is lazy:
53
+ closing the iterator (or breaking out of a loop over it) sends no further request. A page that
54
+ hands back the cursor it was asked with would repeat forever, so it raises `pagination_loop`
55
+ once its items have been yielded."""
56
+ cursor: str | None = None
57
+ while True:
58
+ sent = cursor
59
+ page = fetch(sent)
60
+ yield from page.items
61
+ check_cursor_advanced(sent, page)
62
+ cursor = page.next_cursor or None
63
+ if cursor is None:
64
+ return
65
+
66
+
67
+ async def apaginate(fetch: Callable[[str | None], Awaitable[Page[T]]]) -> AsyncIterator[T]:
68
+ """`paginate` on the event loop; a cancellation between pages ends the walk there."""
69
+ cursor: str | None = None
70
+ while True:
71
+ sent = cursor
72
+ page = await fetch(sent)
73
+ for item in page.items:
74
+ yield item
75
+ check_cursor_advanced(sent, page)
76
+ cursor = page.next_cursor or None
77
+ if cursor is None:
78
+ return
hushscript/py.typed ADDED
File without changes
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import quote
4
+
5
+ from hushscript.transcribe.preflight import invalid_option
6
+
7
+
8
+ def check_page_size(page_size: object) -> None:
9
+ if page_size is None:
10
+ return
11
+ if isinstance(page_size, bool) or not isinstance(page_size, int) or page_size < 1:
12
+ raise invalid_option("page_size must be a whole number of 1 or more.")
13
+
14
+
15
+ def path_id(resource_id: object) -> str:
16
+ """An id as one percent-encoded path segment. An empty or non-string id, `.` and `..` (which a
17
+ client or proxy would resolve to another route) and text that cannot be encoded as UTF-8 (a
18
+ lone surrogate) are `invalid_option`, before any request."""
19
+ if not isinstance(resource_id, str) or resource_id in {"", ".", ".."}:
20
+ raise invalid_option("An id is a non-empty string other than . and ..")
21
+ try:
22
+ return quote(resource_id, safe="")
23
+ except UnicodeEncodeError:
24
+ raise invalid_option("An id must be text that encodes as UTF-8.") from None
25
+
26
+
27
+ def list_params(cursor: str | None, page_size: int | None) -> dict[str, str | int]:
28
+ params: dict[str, str | int] = {}
29
+ if page_size is not None:
30
+ params["limit"] = page_size
31
+ if cursor is not None:
32
+ params["cursor"] = cursor
33
+ return params
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ from hushscript._json import ExtraFields, as_dict, extra_fields
7
+ from hushscript._shape import as_flag, as_text, as_whole, check_shape, maybe_whole, to_datetime
8
+ from hushscript._shapes import ACCOUNT, BALANCE
9
+
10
+ if TYPE_CHECKING:
11
+ from datetime import datetime
12
+
13
+
14
+ def _expiry(fields: dict[str, object]) -> datetime | None:
15
+ expires = fields.get("credits_expire_at")
16
+ return None if expires is None else to_datetime(expires)
17
+
18
+
19
+ def minutes_of(seconds: int) -> int:
20
+ """`seconds / 60` rounded toward zero, exact for any size of int."""
21
+ return seconds // 60 if seconds >= 0 else -(-seconds // 60)
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class Balance(ExtraFields):
26
+ seconds: int
27
+ """Spendable now, already net of holds (not "balance minus held"); may be negative
28
+ (clawback debt)."""
29
+ minutes: int
30
+ """`seconds / 60`, rounded toward zero."""
31
+ held_seconds: int
32
+ """Committed to in-flight jobs, and already deducted from `seconds`."""
33
+ expires_at: datetime | None
34
+ purchases_remaining: int | None
35
+ """Agent accounts only, else None."""
36
+
37
+ @classmethod
38
+ def from_account(cls, body: object) -> Balance:
39
+ check_shape(body, BALANCE, "account response")
40
+ fields = as_dict(body) or {}
41
+ seconds = as_whole(fields["balance_seconds"])
42
+ return cls(
43
+ _extra=extra_fields(
44
+ fields,
45
+ "balance_seconds",
46
+ "held_seconds",
47
+ "credits_expire_at",
48
+ "purchases_remaining_in_window",
49
+ ),
50
+ seconds=seconds,
51
+ minutes=minutes_of(seconds),
52
+ held_seconds=as_whole(fields["held_seconds"]),
53
+ expires_at=_expiry(fields),
54
+ purchases_remaining=maybe_whole(fields.get("purchases_remaining_in_window")),
55
+ )
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class UploadLimits(ExtraFields):
60
+ """The caller's upload admission limits. Unknown API keys remain available in `extra`."""
61
+
62
+ max_daily_seconds: int
63
+ """The daily transcription cap, in seconds."""
64
+ max_active_jobs: int
65
+ """The most pipeline jobs in flight at once."""
66
+ active_jobs: int
67
+ """Pipeline jobs in flight now."""
68
+ used_today_seconds: int
69
+ """Billable seconds used today."""
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class Account(ExtraFields):
74
+ balance_seconds: int
75
+ """Net of holds and clawback debt; may be negative."""
76
+ held_seconds: int
77
+ """Committed to in-flight jobs, and already deducted from `balance_seconds`."""
78
+ credits_expire_at: datetime | None
79
+ card_on_file: bool
80
+ data_region: str
81
+ """For example `us`, `eu`, `apac` or `oc`."""
82
+ limits: UploadLimits
83
+ purchases_remaining_in_window: int | None
84
+ """Agent accounts only, else None."""
85
+
86
+ @classmethod
87
+ def from_body(cls, body: object) -> Account:
88
+ check_shape(body, ACCOUNT, "account response")
89
+ fields = as_dict(body) or {}
90
+ limits = as_dict(fields["limits"]) or {}
91
+ return cls(
92
+ _extra=extra_fields(
93
+ fields,
94
+ "balance_seconds",
95
+ "held_seconds",
96
+ "credits_expire_at",
97
+ "card_on_file",
98
+ "data_region",
99
+ "limits",
100
+ "purchases_remaining_in_window",
101
+ ),
102
+ balance_seconds=as_whole(fields["balance_seconds"]),
103
+ held_seconds=as_whole(fields["held_seconds"]),
104
+ credits_expire_at=_expiry(fields),
105
+ card_on_file=as_flag(fields["card_on_file"]),
106
+ data_region=as_text(fields["data_region"]),
107
+ limits=UploadLimits(
108
+ _extra=extra_fields(
109
+ limits,
110
+ "max_daily_seconds",
111
+ "max_active_jobs",
112
+ "active_jobs",
113
+ "used_today_seconds",
114
+ ),
115
+ max_daily_seconds=as_whole(limits["max_daily_seconds"]),
116
+ max_active_jobs=as_whole(limits["max_active_jobs"]),
117
+ active_jobs=as_whole(limits["active_jobs"]),
118
+ used_today_seconds=as_whole(limits["used_today_seconds"]),
119
+ ),
120
+ purchases_remaining_in_window=maybe_whole(fields.get("purchases_remaining_in_window")),
121
+ )