pinecall-runtime 0.1.6__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 (328) hide show
  1. pinecall/__init__.py +0 -0
  2. pinecall/channels/__init__.py +0 -0
  3. pinecall/channels/_chooser.py +45 -0
  4. pinecall/channels/offers.py +219 -0
  5. pinecall/channels/rooms.py +176 -0
  6. pinecall/channels/routes.py +275 -0
  7. pinecall/channels/telephony/__init__.py +0 -0
  8. pinecall/channels/telephony/_twilio.py +349 -0
  9. pinecall/channels/telephony/carrier.py +229 -0
  10. pinecall/channels/telephony/carrier_catalog.py +92 -0
  11. pinecall/channels/telephony/carriers.csv +41 -0
  12. pinecall/channels/telephony/dialing.py +419 -0
  13. pinecall/channels/telephony/firewall.py +58 -0
  14. pinecall/channels/telephony/hand_over.py +78 -0
  15. pinecall/channels/telephony/number_path.py +176 -0
  16. pinecall/channels/telephony/numbers.py +530 -0
  17. pinecall/channels/telephony/origination.py +77 -0
  18. pinecall/channels/telephony/sip.py +448 -0
  19. pinecall/channels/whatsapp.py +413 -0
  20. pinecall/cli/__init__.py +0 -0
  21. pinecall/cli/_archive.py +85 -0
  22. pinecall/cli/_drift.py +38 -0
  23. pinecall/cli/_facts.py +67 -0
  24. pinecall/cli/_load.py +433 -0
  25. pinecall/cli/_operator.py +626 -0
  26. pinecall/cli/_sessions.py +164 -0
  27. pinecall/cli/_traceback.py +95 -0
  28. pinecall/cli/_usage.py +34 -0
  29. pinecall/cli/main.py +521 -0
  30. pinecall/domain/__init__.py +0 -0
  31. pinecall/domain/agent.py +446 -0
  32. pinecall/domain/call.py +152 -0
  33. pinecall/domain/country_codes.py +32 -0
  34. pinecall/domain/errors.py +151 -0
  35. pinecall/domain/names.py +115 -0
  36. pinecall/domain/org.py +120 -0
  37. pinecall/domain/person.py +208 -0
  38. pinecall/domain/scope.py +21 -0
  39. pinecall/evals/__init__.py +0 -0
  40. pinecall/evals/_evidence.py +158 -0
  41. pinecall/evals/calibration.py +112 -0
  42. pinecall/evals/callers.py +160 -0
  43. pinecall/evals/case.py +327 -0
  44. pinecall/evals/checks.py +437 -0
  45. pinecall/evals/compliance.py +197 -0
  46. pinecall/evals/dataset.py +451 -0
  47. pinecall/evals/goldens.py +331 -0
  48. pinecall/evals/judges.py +525 -0
  49. pinecall/evals/runs.py +252 -0
  50. pinecall/evals/spoken.py +356 -0
  51. pinecall/fleet/__init__.py +0 -0
  52. pinecall/fleet/client.py +569 -0
  53. pinecall/fleet/demand.py +51 -0
  54. pinecall/fleet/heartbeat.py +194 -0
  55. pinecall/fleet/measures.py +157 -0
  56. pinecall/fleet/roster.py +232 -0
  57. pinecall/fleet/worlds.py +44 -0
  58. pinecall/gateway/__init__.py +0 -0
  59. pinecall/gateway/_call_setup.py +53 -0
  60. pinecall/gateway/_deps.py +556 -0
  61. pinecall/gateway/_gateway.py +75 -0
  62. pinecall/gateway/_served.py +396 -0
  63. pinecall/gateway/_sockets.py +444 -0
  64. pinecall/gateway/_streams.py +83 -0
  65. pinecall/gateway/_text_calls.py +182 -0
  66. pinecall/gateway/api/__init__.py +0 -0
  67. pinecall/gateway/api/accounts.py +324 -0
  68. pinecall/gateway/api/agents.py +150 -0
  69. pinecall/gateway/api/apps.py +478 -0
  70. pinecall/gateway/api/box.py +658 -0
  71. pinecall/gateway/api/calibration.py +58 -0
  72. pinecall/gateway/api/callbacks.py +105 -0
  73. pinecall/gateway/api/calls.py +655 -0
  74. pinecall/gateway/api/chat.py +223 -0
  75. pinecall/gateway/api/dataset.py +163 -0
  76. pinecall/gateway/api/desk.py +113 -0
  77. pinecall/gateway/api/entries.py +77 -0
  78. pinecall/gateway/api/evals.py +647 -0
  79. pinecall/gateway/api/fleet.py +73 -0
  80. pinecall/gateway/api/hosting.py +241 -0
  81. pinecall/gateway/api/judges.py +97 -0
  82. pinecall/gateway/api/keys.py +110 -0
  83. pinecall/gateway/api/line.py +142 -0
  84. pinecall/gateway/api/members.py +197 -0
  85. pinecall/gateway/api/metrics.py +226 -0
  86. pinecall/gateway/api/numbers.py +476 -0
  87. pinecall/gateway/api/ops.py +428 -0
  88. pinecall/gateway/api/org.py +283 -0
  89. pinecall/gateway/api/personas.py +157 -0
  90. pinecall/gateway/api/pipeline.py +206 -0
  91. pinecall/gateway/api/prompts.py +50 -0
  92. pinecall/gateway/api/providers.py +240 -0
  93. pinecall/gateway/api/recordings.py +89 -0
  94. pinecall/gateway/api/relay.py +169 -0
  95. pinecall/gateway/api/retrieval.py +403 -0
  96. pinecall/gateway/api/runner.py +109 -0
  97. pinecall/gateway/api/sessions.py +102 -0
  98. pinecall/gateway/api/settings.py +463 -0
  99. pinecall/gateway/api/signup.py +164 -0
  100. pinecall/gateway/api/sso_login.py +168 -0
  101. pinecall/gateway/api/threads.py +187 -0
  102. pinecall/gateway/api/usage.py +303 -0
  103. pinecall/gateway/api/visitors.py +222 -0
  104. pinecall/gateway/api/whatsapp.py +48 -0
  105. pinecall/gateway/api/widget.py +49 -0
  106. pinecall/gateway/app.py +504 -0
  107. pinecall/gateway/calls/__init__.py +0 -0
  108. pinecall/gateway/calls/binding.py +63 -0
  109. pinecall/gateway/calls/commands.py +18 -0
  110. pinecall/gateway/calls/inbox.py +100 -0
  111. pinecall/gateway/calls/known.py +23 -0
  112. pinecall/gateway/calls/owners.py +87 -0
  113. pinecall/gateway/calls/pump.py +33 -0
  114. pinecall/gateway/calls/serving.py +118 -0
  115. pinecall/gateway/calls/threads.py +367 -0
  116. pinecall/gateway/dispatching/__init__.py +0 -0
  117. pinecall/gateway/dispatching/arrivals.py +58 -0
  118. pinecall/gateway/dispatching/sweep.py +43 -0
  119. pinecall/gateway/ending/__init__.py +0 -0
  120. pinecall/gateway/ending/reaper.py +160 -0
  121. pinecall/gateway/ending/seal.py +312 -0
  122. pinecall/gateway/ending/stranded.py +117 -0
  123. pinecall/log/__init__.py +0 -0
  124. pinecall/log/_histogram.py +74 -0
  125. pinecall/log/_relay.py +259 -0
  126. pinecall/log/_writer.py +454 -0
  127. pinecall/log/days.py +154 -0
  128. pinecall/log/drift.py +550 -0
  129. pinecall/log/facts.py +358 -0
  130. pinecall/log/inbox.py +169 -0
  131. pinecall/log/lists.py +155 -0
  132. pinecall/log/logs.py +581 -0
  133. pinecall/log/openings.py +71 -0
  134. pinecall/log/private.py +135 -0
  135. pinecall/log/queries.py +229 -0
  136. pinecall/log/readers.py +207 -0
  137. pinecall/log/reduce.py +659 -0
  138. pinecall/log/refold.py +217 -0
  139. pinecall/log/store.py +370 -0
  140. pinecall/log/usage.py +242 -0
  141. pinecall/postgres/__init__.py +0 -0
  142. pinecall/postgres/migrate.py +202 -0
  143. pinecall/postgres/migrations/0001_schema.sql +591 -0
  144. pinecall/postgres/migrations/0002_worlds.sql +15 -0
  145. pinecall/postgres/migrations/0003_lent.sql +4 -0
  146. pinecall/postgres/migrations/0004_carriers.sql +23 -0
  147. pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
  148. pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
  149. pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
  150. pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
  151. pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
  152. pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
  153. pinecall/postgres/migrations/0011_erasure.sql +32 -0
  154. pinecall/postgres/migrations/0012_org_policy.sql +10 -0
  155. pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
  156. pinecall/postgres/migrations/0014_consents.sql +24 -0
  157. pinecall/postgres/migrations/0015_disclosure.sql +7 -0
  158. pinecall/postgres/migrations/0016_call_records.sql +21 -0
  159. pinecall/postgres/migrations/0017_reads.sql +17 -0
  160. pinecall/postgres/migrations/0018_hosting.sql +41 -0
  161. pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
  162. pinecall/postgres/migrations/0020_written_once.sql +7 -0
  163. pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
  164. pinecall/postgres/migrations/0022_sealed.sql +39 -0
  165. pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
  166. pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
  167. pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
  168. pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
  169. pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
  170. pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
  171. pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
  172. pinecall/postgres/migrations/0060_drift.sql +62 -0
  173. pinecall/postgres/migrations/0061_canaries.sql +24 -0
  174. pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
  175. pinecall/postgres/migrations/0063_prompts.sql +16 -0
  176. pinecall/postgres/migrations/0075_call_private.sql +14 -0
  177. pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
  178. pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
  179. pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
  180. pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
  181. pinecall/postgres/migrations/0080_call_openings.sql +20 -0
  182. pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
  183. pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
  184. pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
  185. pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
  186. pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
  187. pinecall/postgres/migrations/0086_route_origin.sql +17 -0
  188. pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
  189. pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
  190. pinecall/postgres/migrations/0089_offers.sql +21 -0
  191. pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
  192. pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
  193. pinecall/postgres/migrations/0092_key_parent.sql +6 -0
  194. pinecall/postgres/migrations/0093_code_tries.sql +10 -0
  195. pinecall/postgres/migrations/0094_route_approval.sql +12 -0
  196. pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
  197. pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
  198. pinecall/postgres/migrations/0097_case_status.sql +19 -0
  199. pinecall/postgres/pool.py +190 -0
  200. pinecall/process/__init__.py +0 -0
  201. pinecall/process/_objects.py +97 -0
  202. pinecall/process/box_settings.py +23 -0
  203. pinecall/process/connections.py +124 -0
  204. pinecall/process/metrics.py +127 -0
  205. pinecall/process/recordings.py +323 -0
  206. pinecall/process/resolver.py +41 -0
  207. pinecall/process/sealed_audio.py +173 -0
  208. pinecall/process/settings.py +534 -0
  209. pinecall/process/shared.py +158 -0
  210. pinecall/process/signal.py +416 -0
  211. pinecall/providers/__init__.py +0 -0
  212. pinecall/providers/build.py +570 -0
  213. pinecall/providers/catalog.py +266 -0
  214. pinecall/providers/credentials.py +230 -0
  215. pinecall/providers/declared.py +69 -0
  216. pinecall/providers/prices.py +240 -0
  217. pinecall/providers/tried.py +104 -0
  218. pinecall/providers/voices.py +174 -0
  219. pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
  220. pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
  221. pinecall/public/console/favicon.png +0 -0
  222. pinecall/public/console/index.html +20 -0
  223. pinecall/public/console/logo-mark.png +0 -0
  224. pinecall/public/console/pinecall-logo-dark.png +0 -0
  225. pinecall/public/console/pinecall-logo.png +0 -0
  226. pinecall/public/console/pinecall-mark.png +0 -0
  227. pinecall/public/console/sw.js +37 -0
  228. pinecall/public/widget/pinecall-widget.js +853 -0
  229. pinecall/py.typed +0 -0
  230. pinecall/retrieval/__init__.py +0 -0
  231. pinecall/retrieval/_search.py +186 -0
  232. pinecall/retrieval/_sections.py +70 -0
  233. pinecall/retrieval/embed.py +340 -0
  234. pinecall/retrieval/extraction.py +403 -0
  235. pinecall/retrieval/knowledge.py +659 -0
  236. pinecall/retrieval/lookups.py +192 -0
  237. pinecall/retrieval/memory.py +603 -0
  238. pinecall/runner/__init__.py +0 -0
  239. pinecall/runner/_kube.py +459 -0
  240. pinecall/runner/_plan.py +157 -0
  241. pinecall/runner/main.py +306 -0
  242. pinecall/session/__init__.py +0 -0
  243. pinecall/session/_agent.py +141 -0
  244. pinecall/session/_hearing.py +101 -0
  245. pinecall/session/_livekit.py +195 -0
  246. pinecall/session/_prompt.py +147 -0
  247. pinecall/session/_recording.py +237 -0
  248. pinecall/session/call.py +367 -0
  249. pinecall/session/clock.py +40 -0
  250. pinecall/session/hold.py +239 -0
  251. pinecall/session/room.py +626 -0
  252. pinecall/session/session.py +690 -0
  253. pinecall/session/text.py +144 -0
  254. pinecall/session/tools.py +483 -0
  255. pinecall/session/voice.py +118 -0
  256. pinecall/session/widget.py +201 -0
  257. pinecall/tenancy/__init__.py +0 -0
  258. pinecall/tenancy/admission.py +204 -0
  259. pinecall/tenancy/agents.py +164 -0
  260. pinecall/tenancy/canary.py +126 -0
  261. pinecall/tenancy/carrier_networks.py +156 -0
  262. pinecall/tenancy/carriers.py +265 -0
  263. pinecall/tenancy/codes.py +196 -0
  264. pinecall/tenancy/consents.py +167 -0
  265. pinecall/tenancy/dial_policy.py +405 -0
  266. pinecall/tenancy/disclosure.py +41 -0
  267. pinecall/tenancy/erasure.py +204 -0
  268. pinecall/tenancy/export.py +167 -0
  269. pinecall/tenancy/hosted_running.py +166 -0
  270. pinecall/tenancy/hosting.py +475 -0
  271. pinecall/tenancy/judges.py +126 -0
  272. pinecall/tenancy/keys.py +467 -0
  273. pinecall/tenancy/knocks.py +40 -0
  274. pinecall/tenancy/letters.py +378 -0
  275. pinecall/tenancy/mail.py +458 -0
  276. pinecall/tenancy/org_secrets.py +123 -0
  277. pinecall/tenancy/orgs.py +78 -0
  278. pinecall/tenancy/people.py +543 -0
  279. pinecall/tenancy/personas.py +145 -0
  280. pinecall/tenancy/policy.py +65 -0
  281. pinecall/tenancy/prompts.py +48 -0
  282. pinecall/tenancy/reads.py +73 -0
  283. pinecall/tenancy/recording_keys.py +41 -0
  284. pinecall/tenancy/remembered.py +148 -0
  285. pinecall/tenancy/retention.py +86 -0
  286. pinecall/tenancy/scopes.py +406 -0
  287. pinecall/tenancy/signin.py +622 -0
  288. pinecall/tenancy/spend.py +69 -0
  289. pinecall/tenancy/sso.py +431 -0
  290. pinecall/tenancy/sso_domains.py +132 -0
  291. pinecall/tenancy/throttle.py +88 -0
  292. pinecall/tenancy/tokens.py +400 -0
  293. pinecall/tenancy/traceback.py +70 -0
  294. pinecall/tenancy/usage.py +136 -0
  295. pinecall/tenancy/vault.py +204 -0
  296. pinecall/tenancy/words.py +144 -0
  297. pinecall/wire/__init__.py +0 -0
  298. pinecall/wire/commands.py +350 -0
  299. pinecall/wire/events.py +655 -0
  300. pinecall/wire/frames.py +65 -0
  301. pinecall/wire/metrics.py +287 -0
  302. pinecall/wire/parts.py +320 -0
  303. pinecall/wire/rest/__init__.py +0 -0
  304. pinecall/wire/rest/accounts.py +512 -0
  305. pinecall/wire/rest/agents.py +172 -0
  306. pinecall/wire/rest/calls.py +397 -0
  307. pinecall/wire/rest/evals.py +453 -0
  308. pinecall/wire/rest/fleet.py +69 -0
  309. pinecall/wire/rest/hosting.py +174 -0
  310. pinecall/wire/rest/numbers.py +280 -0
  311. pinecall/wire/rest/ops.py +342 -0
  312. pinecall/wire/rest/providers.py +83 -0
  313. pinecall/wire/rest/retrieval.py +301 -0
  314. pinecall/wire/rest/settings.py +184 -0
  315. pinecall/wire/rest/usage.py +245 -0
  316. pinecall/wire/room.py +61 -0
  317. pinecall/wire/scores.py +49 -0
  318. pinecall/wire/state.py +243 -0
  319. pinecall/worker/__init__.py +0 -0
  320. pinecall/worker/_job.py +655 -0
  321. pinecall/worker/_recorder.py +82 -0
  322. pinecall/worker/_traces.py +48 -0
  323. pinecall/worker/main.py +307 -0
  324. pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
  325. pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
  326. pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
  327. pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
  328. pinecall_runtime-0.1.6.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,658 @@
1
+ """The box's own settings and floor: providers, admission, mail, brand, routes, fleet, events."""
2
+
3
+ import asyncio
4
+ import time
5
+ from collections import Counter
6
+ from collections.abc import AsyncIterator
7
+ from typing import Annotated
8
+
9
+ from fastapi import APIRouter, Depends, Header, Query, Request
10
+ from fastapi.responses import StreamingResponse
11
+
12
+ from pinecall.channels import offers, routes
13
+ from pinecall.channels.telephony import carrier_catalog, firewall, numbers, origination, sip
14
+ from pinecall.channels.telephony.carrier_catalog import KnownCarrier
15
+ from pinecall.channels.telephony.numbers import NumberImport
16
+ from pinecall.domain.call import Route
17
+ from pinecall.domain.errors import Conflict, NotAvailable, NotFound
18
+ from pinecall.domain.names import PRODUCTION, Env, parse_e164
19
+ from pinecall.domain.scope import Scope
20
+ from pinecall.fleet import worlds
21
+ from pinecall.fleet.roster import STALE_AFTER_S
22
+ from pinecall.fleet.worlds import Fleets
23
+ from pinecall.gateway import _streams
24
+ from pinecall.gateway._deps import GatewayDep, OperatorDep, operator, public_url
25
+ from pinecall.gateway._gateway import Gateway
26
+ from pinecall.gateway._streams import frame, paced, streamed, wants_sse
27
+ from pinecall.gateway.api import hosting as hosting_doors
28
+ from pinecall.gateway.api.org import mailbox_of
29
+ from pinecall.gateway.api.providers import credentials_of, installed_vendor
30
+ from pinecall.gateway.api.sso_login import NO_BOX_WIDE
31
+ from pinecall.gateway.api.usage import usage_row_response, usage_totals
32
+ from pinecall.log import usage
33
+ from pinecall.log.reduce import totals_by_org
34
+ from pinecall.log.store import DEFAULT_LIMIT, Store
35
+ from pinecall.providers import catalog
36
+ from pinecall.providers.catalog import Providers
37
+ from pinecall.tenancy import (
38
+ admission,
39
+ carrier_networks,
40
+ hosted_running,
41
+ letters,
42
+ mail,
43
+ orgs,
44
+ vault,
45
+ )
46
+ from pinecall.tenancy.admission import Admission
47
+ from pinecall.tenancy.carrier_networks import NetworkState
48
+ from pinecall.tenancy.mail import MailboxStatus
49
+ from pinecall.wire.frames import Entry
50
+ from pinecall.wire.rest.accounts import (
51
+ BrandRow,
52
+ OrgMailRequest,
53
+ SendTestLetterRequest,
54
+ SendTestLetterResponse,
55
+ )
56
+ from pinecall.wire.rest.hosting import ServedPage
57
+ from pinecall.wire.rest.ops import (
58
+ AdmitCarrierRequest,
59
+ BoxCarrier,
60
+ BoxCarriers,
61
+ BoxEvent,
62
+ BoxFence,
63
+ BoxMailResponse,
64
+ BoxNumber,
65
+ BoxProvider,
66
+ BoxSignInResponse,
67
+ CarrierNetworkRow,
68
+ FenceOpening,
69
+ FleetListed,
70
+ NumberCameIn,
71
+ PutBrandRequest,
72
+ PutSignInRequest,
73
+ RepointedTrunk,
74
+ RepointRequest,
75
+ RouteRequest,
76
+ RouteRow,
77
+ )
78
+ from pinecall.wire.rest.providers import ProviderKeyRequest, VendorsResponse
79
+ from pinecall.wire.rest.usage import BoxUsagePage
80
+
81
+ router = APIRouter(dependencies=[Depends(operator)])
82
+
83
+
84
+ NOTHING_STORED = (
85
+ "this box has no stored mail server: what it posts through, if anything, is PINECALL_SMTP_URL"
86
+ )
87
+
88
+
89
+ NOTHING_TO_TEST = (
90
+ "this box has no mail server: wire one at PUT /v1/ops/mail, or set PINECALL_SMTP_URL"
91
+ )
92
+
93
+
94
+ NO_SUCH_ORG = "no org named {named}: by id or by slug"
95
+
96
+
97
+ NO_BOX_KEY = "this box holds no key for {vendor}"
98
+
99
+
100
+ NO_SUCH_ROUTE = "no route for {number} in org {slug}"
101
+
102
+
103
+ NOTHING_WAITS = "no number {number} of org {slug} waits for approval"
104
+
105
+
106
+ NO_SUCH_WORKER = "no worker named {worker} has knocked at this gateway"
107
+
108
+
109
+ GOOGLE_CALLBACK = "/v1/login/google/callback"
110
+
111
+
112
+ # The usage stream polls: a summary is a row of the store, not a topic.
113
+ POLL_S = 1.0
114
+
115
+
116
+ # ── what the box runs, and what a new org is given ──
117
+
118
+
119
+ # The console's box screens read and write these rows whole; code holds no list of them.
120
+ @router.get("/v1/ops/providers")
121
+ async def get_providers(gateway: GatewayDep) -> Providers:
122
+ """The providers row: defaults, models, voices, tuning, rates, the judge, the embedder."""
123
+ return await catalog.providers(gateway.connections.pool)
124
+
125
+
126
+ @router.put("/v1/ops/providers")
127
+ async def put_providers(body: Providers, gateway: GatewayDep) -> Providers:
128
+ """The providers row replaced whole; a vendor not installed or not doing its stage refused."""
129
+ await catalog.configure(gateway.connections.pool, body)
130
+ return body
131
+
132
+
133
+ # Offering a vendor is holding its key: the box's credentials, sealed, never read back.
134
+ @router.get("/v1/ops/provider-keys")
135
+ async def box_vendors(gateway: GatewayDep) -> VendorsResponse:
136
+ """The vendors the box holds a key for, never the key."""
137
+ connections = gateway.connections
138
+ credentials = await vault.box_credentials(connections.pool, connections.vault)
139
+ return VendorsResponse(vendors=sorted(credentials))
140
+
141
+
142
+ @router.put("/v1/ops/provider-keys/{vendor}", status_code=204)
143
+ async def put_box_key(vendor: str, body: ProviderKeyRequest, gateway: GatewayDep) -> None:
144
+ """The box's own credentials for a vendor; orgs it lends to run on them from the next call."""
145
+ connections = gateway.connections
146
+ await vault.put_box_credentials(
147
+ connections.pool, connections.vault, installed_vendor(vendor), credentials_of(body)
148
+ )
149
+
150
+
151
+ @router.delete("/v1/ops/provider-keys/{vendor}", status_code=204)
152
+ async def drop_box_key(vendor: str, gateway: GatewayDep) -> None:
153
+ """The box stops offering the vendor on its key; 404 when it held none."""
154
+ named = installed_vendor(vendor)
155
+ if not await vault.drop_box_credentials(gateway.connections.pool, named):
156
+ raise NotFound(NO_BOX_KEY.format(vendor=named))
157
+
158
+
159
+ @router.get("/v1/ops/admission")
160
+ async def get_admission(gateway: GatewayDep) -> Admission:
161
+ """What a newborn org is given in each world; nothing limited on a box that never said."""
162
+ return await admission.admission(gateway.connections.pool)
163
+
164
+
165
+ # Read at the moment an org is made: the orgs already made keep their quotas.
166
+ @router.put("/v1/ops/admission")
167
+ async def put_admission(body: Admission, gateway: GatewayDep) -> Admission:
168
+ """What a newborn org is given, replaced whole."""
169
+ await admission.set_admission(gateway.connections.pool, body)
170
+ return body
171
+
172
+
173
+ @router.get("/v1/ops/hosted-usage")
174
+ async def box_hosted_usage(
175
+ gateway: GatewayDep, month: Annotated[str | None, Query()] = None
176
+ ) -> ServedPage:
177
+ """The time every org's apps served, both worlds, per UTC day, one month: what bills them."""
178
+ since, until = hosting_doors.month_of(month)
179
+ rows = await hosted_running.served(gateway.connections.pool, since, until)
180
+ return hosting_doors.served_page(since, until, rows)
181
+
182
+
183
+ @router.get("/v1/ops/fleets")
184
+ async def get_fleets(gateway: GatewayDep) -> Fleets:
185
+ """The fleet of workers each world's calls are dispatched to."""
186
+ return await worlds.fleets(gateway.connections.pool)
187
+
188
+
189
+ @router.put("/v1/ops/fleets")
190
+ async def put_fleets(body: Fleets, gateway: GatewayDep) -> Fleets:
191
+ """The fleet of each world, replaced whole; the next dispatch reads it."""
192
+ await worlds.set_fleets(gateway.connections.pool, body)
193
+ return body
194
+
195
+
196
+ # ── the box's mailbox ──
197
+
198
+
199
+ @router.get("/v1/ops/mail")
200
+ async def get_box_mail(gateway: GatewayDep) -> BoxMailResponse:
201
+ """The mailbox the box posts through, where it came from, and how its last letter went."""
202
+ return _box_mail_row(await _box_mail(gateway))
203
+
204
+
205
+ # Sends nothing: the test door does, and a person watches it.
206
+ @router.put("/v1/ops/mail")
207
+ async def put_box_mail(body: OrgMailRequest, gateway: GatewayDep) -> BoxMailResponse:
208
+ """The box's mailbox, stored over the environment's; its letters go through it next."""
209
+ connections = gateway.connections
210
+ await mail.put_box_mail(connections.pool, connections.vault, mailbox_of(body))
211
+ return _box_mail_row(await _box_mail(gateway))
212
+
213
+
214
+ @router.delete("/v1/ops/mail", status_code=204)
215
+ async def drop_box_mail(gateway: GatewayDep) -> None:
216
+ """Forget the stored mailbox, back to the environment's; 404 when none was stored."""
217
+ if not await mail.drop_box_mail(gateway.connections.pool):
218
+ raise NotFound(NOTHING_STORED)
219
+
220
+
221
+ # The one door of the box that waits on SMTP: a person is watching.
222
+ @router.post("/v1/ops/mail/test")
223
+ async def send_box_test_letter(
224
+ body: SendTestLetterRequest, gateway: GatewayDep
225
+ ) -> SendTestLetterResponse:
226
+ """One test letter through the box's mailbox, waited for."""
227
+ to = mail.address_of(body.to)
228
+ if await gateway.outbox.mailbox_for(None) is None:
229
+ raise Conflict(NOTHING_TO_TEST)
230
+ letter = letters.probe_letter(to, await letters.brand_of(gateway.connections.pool))
231
+ error = await gateway.outbox.sent(None, letter)
232
+ return SendTestLetterResponse(sent=error is None, error=error)
233
+
234
+
235
+ # ── sign-in across orgs ──
236
+
237
+
238
+ # Box-wide sign-in is not in this version: the doors stand, say so, and name the URI to register.
239
+ @router.get("/v1/ops/signin")
240
+ async def get_box_signin(request: Request, gateway: GatewayDep) -> BoxSignInResponse:
241
+ """Every provider the box could offer every org's people: none wired in this version."""
242
+ redirect = f"{public_url(request, gateway)}{GOOGLE_CALLBACK}"
243
+ return BoxSignInResponse(
244
+ google=BoxProvider(configured=False, client_id=None, redirect_uri=redirect)
245
+ )
246
+
247
+
248
+ @router.put("/v1/ops/signin/google")
249
+ async def put_google_signin(_body: PutSignInRequest) -> BoxProvider:
250
+ """Refused: box-wide Google sign-in is not in this version."""
251
+ raise NotAvailable(NO_BOX_WIDE)
252
+
253
+
254
+ @router.delete("/v1/ops/signin/google", status_code=204)
255
+ async def drop_google_signin() -> None:
256
+ """Refused: box-wide Google sign-in is not in this version."""
257
+ raise NotAvailable(NO_BOX_WIDE)
258
+
259
+
260
+ # ── the brand ──
261
+
262
+
263
+ @router.get("/v1/ops/brand")
264
+ async def get_brand(gateway: GatewayDep) -> BrandRow:
265
+ """What the box's letters and sign-in page are called and painted with."""
266
+ return _brand_row(await letters.brand_of(gateway.connections.pool))
267
+
268
+
269
+ @router.put("/v1/ops/brand")
270
+ async def put_brand(body: PutBrandRequest, gateway: GatewayDep) -> BrandRow:
271
+ """The brand changed field by field: one left out stays, an empty one goes to the default."""
272
+ pool = gateway.connections.pool
273
+ brand = letters.apply_brand(
274
+ await letters.brand_of(pool), name=body.name, logo_url=body.logo_url, accent=body.accent
275
+ )
276
+ await letters.put_brand(pool, brand)
277
+ return _brand_row(brand)
278
+
279
+
280
+ # ── every org's floor, and every org's meter ──
281
+
282
+
283
+ # Live only, no cursor: frame ids are per-log seqs that interleave, so a reconnect starts now.
284
+ @router.get("/v1/ops/events", response_model=None)
285
+ async def stream_box_events(gateway: GatewayDep) -> StreamingResponse:
286
+ """Every org's floor at once, each frame saying whose."""
287
+ return streamed(_owned(gateway, await gateway.logs.box_reader()), gateway.closing)
288
+
289
+
290
+ @router.get("/v1/ops/usage", response_model=None)
291
+ async def box_usage(
292
+ gateway: GatewayDep,
293
+ after: Annotated[int, Query(ge=0)] = 0,
294
+ limit: Annotated[int, Query(ge=1, le=DEFAULT_LIMIT)] = DEFAULT_LIMIT,
295
+ org: Annotated[str | None, Query()] = None,
296
+ accept: Annotated[str | None, Header()] = None,
297
+ ) -> StreamingResponse | BoxUsagePage:
298
+ """Every org's metered rows after the cursor, totals per org; or the same as a stream."""
299
+ pool, store = gateway.connections.pool, gateway.logs.store
300
+ only = None
301
+ if org is not None:
302
+ found = await orgs.find(pool, org)
303
+ only = org if found is None else found.id
304
+ if wants_sse(accept):
305
+ return streamed(_metered_stream(store, after, only), gateway.closing)
306
+ page = await usage.metered_page(store, after=after, limit=limit, org=only)
307
+ return BoxUsagePage(
308
+ rows=[usage_row_response(row) for row in page.rows],
309
+ totals={org: usage_totals(used) for org, used in totals_by_org(page.rows).items()},
310
+ next=page.next,
311
+ )
312
+
313
+
314
+ # ── the routes ──
315
+
316
+
317
+ @router.get("/v1/ops/routes")
318
+ async def list_box_routes(
319
+ gateway: GatewayDep, org: Annotated[str, Query()], env: Annotated[Env, Query()] = PRODUCTION
320
+ ) -> list[RouteRow]:
321
+ """Every number the org answers at in the world, oldest first."""
322
+ found = await _org_id(gateway, org)
323
+ return [
324
+ _route_row(route) for route in await routes.of_org(gateway.connections.pool, found, env)
325
+ ]
326
+
327
+
328
+ # One row per number per org: a number added again moves. It is admitted on the SFU at once;
329
+ # the carrier is never touched, so it rings only where the carrier already points it here.
330
+ @router.post("/v1/ops/routes")
331
+ async def add_box_route(body: RouteRequest, gateway: GatewayDep) -> RouteRow:
332
+ """A number answered by this org's agent, in this world, on this channel."""
333
+ scope = Scope(org=await _org_id(gateway, body.org), env=body.env)
334
+ wanted = NumberImport(scope, body.agent, body.number, channel=body.channel)
335
+ typed = await numbers.type_route(gateway.connections, wanted)
336
+ return _route_row(typed.route)
337
+
338
+
339
+ @router.delete("/v1/ops/routes/{number}", status_code=204)
340
+ async def drop_box_route(number: str, gateway: GatewayDep, org: Annotated[str, Query()]) -> None:
341
+ """The org's route at the number forgotten, and its admission; 404 for a number nobody typed."""
342
+ found = await _org_id(gateway, org)
343
+ route = await routes.of_number(gateway.connections.pool, found, number)
344
+ if route is None:
345
+ raise NotFound(NO_SUCH_ROUTE.format(number=number, slug=org))
346
+ await numbers.release(gateway.connections, found, number, route.env)
347
+
348
+
349
+ # What a number would do if it rang now: whose it is, how it came, and whether anyone picks up.
350
+ @router.get("/v1/ops/numbers")
351
+ async def list_box_numbers(gateway: GatewayDep) -> list[BoxNumber]:
352
+ """Every number the box answers at, of every org and world, by number."""
353
+ pool = gateway.connections.pool
354
+ slugs = {org.id: org.slug for org in await orgs.listed(pool)}
355
+ found = await routes.on_the_box(pool)
356
+ running = {
357
+ (route.org, route.env): {
358
+ agent.slug
359
+ for agent in gateway.sockets.holding(
360
+ Scope(org=route.org, env=route.env), every_corner=True
361
+ )
362
+ }
363
+ for route in (row.route for row in found)
364
+ }
365
+ return [
366
+ BoxNumber(
367
+ number=row.route.number or "",
368
+ channel=row.route.channel,
369
+ org=slugs.get(row.route.org, row.route.org),
370
+ env=row.route.env,
371
+ agent=row.route.agent,
372
+ came_in=_came_in(row),
373
+ running=row.route.agent in running[(row.route.org, row.route.env)],
374
+ via=row.via,
375
+ approved=row.approved,
376
+ )
377
+ for row in found
378
+ ]
379
+
380
+
381
+ # A number an org hooked proves nothing of whose it is: the operator says it is the org's, and a
382
+ # call to it opens from then on. Refused, it is let go of (DELETE /v1/ops/routes/{number}).
383
+ @router.post("/v1/ops/numbers/{number}/approve", status_code=204)
384
+ async def approve_box_number(
385
+ number: str, gateway: GatewayDep, operating: OperatorDep, org: Annotated[str, Query()]
386
+ ) -> None:
387
+ """Approve that a number the org hooked is the org's."""
388
+ found = await _org_id(gateway, org)
389
+ pool = gateway.connections.pool
390
+ if not await routes.approve(pool, found, parse_e164(number), operating):
391
+ raise NotFound(NOTHING_WAITS.format(number=number, slug=org))
392
+
393
+
394
+ # ── the carriers ──
395
+
396
+
397
+ @router.get("/v1/ops/carriers")
398
+ async def list_box_carriers(gateway: GatewayDep) -> BoxCarriers:
399
+ """The catalog, each carrier admitted or not with the numbers it brings, and the fence now."""
400
+ pool = gateway.connections.pool
401
+ admitted = await carrier_catalog.admitted(pool)
402
+ through = Counter(_comes_through(record) for record in await routes.on_the_box(pool))
403
+ found = await firewall.openings(pool)
404
+ return BoxCarriers(
405
+ carriers=[
406
+ _box_carrier(carrier, admitted, through[carrier.kind])
407
+ for carrier in carrier_catalog.known().values()
408
+ ],
409
+ fence=BoxFence(
410
+ openings=[
411
+ FenceOpening(network=opening.network, reason=opening.reason) for opening in found
412
+ ],
413
+ networks=firewall.every_network(found),
414
+ ),
415
+ )
416
+
417
+
418
+ # On, every org sees it under Add a number, and its networks join the fence's list for the cloud's
419
+ # firewall (`fence export`, then Terraform).
420
+ @router.put("/v1/ops/carriers/{kind}")
421
+ async def admit_box_carrier(
422
+ kind: str, body: AdmitCarrierRequest, gateway: GatewayDep
423
+ ) -> BoxCarrier:
424
+ """Admit a carrier of the catalog, or stop admitting it; Twilio is admitted always."""
425
+ pool = gateway.connections.pool
426
+ admitted = await carrier_catalog.admit(pool, kind, on=body.admitted)
427
+ through = Counter(_comes_through(record) for record in await routes.on_the_box(pool))
428
+ return _box_carrier(carrier_catalog.known_carrier(kind), admitted, through[kind])
429
+
430
+
431
+ # Once, when the box's SIP moves off its names or a name is retired: the runtime never touches a
432
+ # carrier on its own.
433
+ @router.post("/v1/ops/sip/repoint")
434
+ async def repoint_trunks(
435
+ gateway: GatewayDep, body: RepointRequest | None = None
436
+ ) -> list[RepointedTrunk]:
437
+ """Send every Twilio trunk at a world's name, or a former one, to that world's SIP name."""
438
+ return [
439
+ RepointedTrunk(
440
+ account=moved.account,
441
+ trunk=moved.trunk,
442
+ world=moved.world,
443
+ was=moved.was,
444
+ now=moved.now,
445
+ )
446
+ for moved in await origination.repointed(
447
+ gateway.connections, None if body is None else body.former
448
+ )
449
+ ]
450
+
451
+
452
+ @router.get("/v1/ops/carrier-networks")
453
+ async def list_carrier_networks(
454
+ gateway: GatewayDep, state: Annotated[NetworkState | None, Query()] = None
455
+ ) -> list[CarrierNetworkRow]:
456
+ """Every network an org asked 5060 to open to, or those in one state, oldest first."""
457
+ pool = gateway.connections.pool
458
+ slugs = {org.id: org.slug for org in await orgs.listed(pool)}
459
+ return [_network_row(ask, slugs) for ask in await carrier_networks.listed(pool, state)]
460
+
461
+
462
+ @router.post("/v1/ops/carrier-networks/{ask}/approve")
463
+ async def approve_carrier_network(
464
+ ask: int, gateway: GatewayDep, operating: OperatorDep
465
+ ) -> CarrierNetworkRow:
466
+ """Put the network on the firewall's list, and admit the org's numbers it fences."""
467
+ return await _decided(gateway, ask, "approved", operating)
468
+
469
+
470
+ @router.post("/v1/ops/carrier-networks/{ask}/refuse")
471
+ async def refuse_carrier_network(
472
+ ask: int, gateway: GatewayDep, operating: OperatorDep
473
+ ) -> CarrierNetworkRow:
474
+ """Keep the network off the fence's list; numbers it alone fenced are let go of on the SFU."""
475
+ return await _decided(gateway, ask, "refused", operating)
476
+
477
+
478
+ # ── the fleet ──
479
+
480
+
481
+ @router.get("/v1/ops/fleet")
482
+ async def list_fleet(gateway: GatewayDep) -> FleetListed:
483
+ """Every worker heard from, of both fleets, and each fleet summed over the ones up."""
484
+ now = time.time()
485
+ named = await worlds.fleets(gateway.connections.pool)
486
+ fleets = list(dict.fromkeys((named.production, named.sandbox)))
487
+ waiting = await offers.waiting(gateway.connections.pool, now - offers.WAITING_COUNTED_S)
488
+ totals = [gateway.roster.totals(fleet, now) for fleet in fleets]
489
+ return FleetListed(
490
+ now=now,
491
+ stale_after_s=STALE_AFTER_S,
492
+ workers=[seat for fleet in fleets for seat in gateway.roster.of(fleet, now)],
493
+ totals=[
494
+ total.model_copy(update={"waiting": waiting.get(total.fleet, 0)}) for total in totals
495
+ ],
496
+ )
497
+
498
+
499
+ # A cordon is told on the worker's next heartbeat: it takes no new call, finishes, and leaves.
500
+ @router.post("/v1/ops/fleet/{worker}/cordon", status_code=204)
501
+ async def cordon_worker(
502
+ worker: str, gateway: GatewayDep, fleet: Annotated[str | None, Query()] = None
503
+ ) -> None:
504
+ """Cordon a worker of a fleet; the fleet is found by the worker's name when not named."""
505
+ if not gateway.roster.cordon(_fleet_of(gateway, fleet, worker), worker):
506
+ raise NotFound(NO_SUCH_WORKER.format(worker=worker))
507
+
508
+
509
+ @router.delete("/v1/ops/fleet/{worker}/cordon", status_code=204)
510
+ async def uncordon_worker(
511
+ worker: str, gateway: GatewayDep, fleet: Annotated[str | None, Query()] = None
512
+ ) -> None:
513
+ """Take a worker's cordon back, when it has not left yet."""
514
+ if not gateway.roster.cordon(_fleet_of(gateway, fleet, worker), worker, on=False):
515
+ raise NotFound(NO_SUCH_WORKER.format(worker=worker))
516
+
517
+
518
+ def _box_mail_row(kept: MailboxStatus | None) -> BoxMailResponse:
519
+ """The box's mailbox as the doors send it; the password never."""
520
+ return BoxMailResponse.model_validate(
521
+ {
522
+ "configured": kept is not None,
523
+ "source": None if kept is None else kept.source,
524
+ "host": None if kept is None else kept.mailbox.host,
525
+ "port": None if kept is None else kept.mailbox.port,
526
+ "security": None if kept is None else kept.mailbox.security,
527
+ "username": None if kept is None else kept.mailbox.username,
528
+ "from": None if kept is None else kept.mailbox.sender,
529
+ "verified_at": None
530
+ if kept is None or kept.verified_at is None
531
+ else kept.verified_at.isoformat(),
532
+ "last_error": None if kept is None else kept.last_error,
533
+ }
534
+ )
535
+
536
+
537
+ def _brand_row(brand: letters.Brand) -> BrandRow:
538
+ """The brand as the doors send it."""
539
+ return BrandRow(name=brand.name, logo_url=brand.logo_url, accent=brand.accent)
540
+
541
+
542
+ def _route_row(route: Route) -> RouteRow:
543
+ """A route as the operator lists it."""
544
+ return RouteRow(
545
+ org=route.org,
546
+ number=route.number or "",
547
+ agent=route.agent,
548
+ channel=route.channel,
549
+ env=route.env,
550
+ managed=route.managed,
551
+ )
552
+
553
+
554
+ def _came_in(row: routes.RouteRecord) -> NumberCameIn:
555
+ """How the number reached the box: the kind of the org's account it came from, or its origin."""
556
+ match row.origin, row.carrier:
557
+ case "imported", "twilio" | "sip" | "whatsapp" as kind:
558
+ return kind
559
+ case origin, _:
560
+ return origin
561
+
562
+
563
+ def _box_carrier(carrier: KnownCarrier, admitted: frozenset[str], numbers: int) -> BoxCarrier:
564
+ """A carrier of the catalog as the doors send it."""
565
+ return BoxCarrier(
566
+ kind=carrier.kind,
567
+ name=carrier.name,
568
+ control=carrier.control,
569
+ networks=list(carrier.networks),
570
+ source=carrier.source,
571
+ read_on=carrier.read_on.isoformat(),
572
+ admitted=carrier.kind in admitted,
573
+ fixed=carrier.kind == carrier_catalog.BOX_CARRIER,
574
+ numbers=numbers,
575
+ )
576
+
577
+
578
+ # A number with no account, no carrier named and no networks of its own is fenced to Twilio's.
579
+ def _comes_through(record: routes.RouteRecord) -> str | None:
580
+ """The catalog carrier a number reaches the box through, if any."""
581
+ if record.via is not None:
582
+ return record.via
583
+ if record.carrier is not None:
584
+ return record.carrier
585
+ if record.route.managed or record.origin in {"hooked", "typed"}:
586
+ return carrier_catalog.BOX_CARRIER
587
+ return None
588
+
589
+
590
+ def _network_row(ask: carrier_networks.NetworkAsk, slugs: dict[str, str]) -> CarrierNetworkRow:
591
+ """A network asked for as the doors send it, the org by its slug."""
592
+ return CarrierNetworkRow(
593
+ id=ask.id,
594
+ org=slugs.get(ask.org, ask.org),
595
+ source=ask.source,
596
+ network=ask.network,
597
+ state=ask.state,
598
+ asked_at=ask.asked_at,
599
+ decided_by=ask.decided_by,
600
+ decided_at=ask.decided_at,
601
+ )
602
+
603
+
604
+ async def _decided(
605
+ gateway: Gateway, ask: int, state: NetworkState, operating: str
606
+ ) -> CarrierNetworkRow:
607
+ """The operator's answer kept, and the org's trunks made to follow it."""
608
+ pool = gateway.connections.pool
609
+ decided = await carrier_networks.decide(pool, ask, state, operating)
610
+ await sip.readmit(gateway.connections, decided.org)
611
+ slugs = {org.id: org.slug for org in await orgs.listed(pool)}
612
+ return _network_row(decided, slugs)
613
+
614
+
615
+ async def _box_mail(gateway: Gateway) -> MailboxStatus | None:
616
+ connections = gateway.connections
617
+ return await mail.box_mail_of(connections.pool, connections.vault, gateway.outbox.environment)
618
+
619
+
620
+ async def _org_id(gateway: Gateway, named: str) -> str:
621
+ org = await orgs.find(gateway.connections.pool, named)
622
+ if org is None:
623
+ raise NotFound(NO_SUCH_ORG.format(named=named))
624
+ return org.id
625
+
626
+
627
+ def _fleet_of(gateway: Gateway, fleet: str | None, worker: str) -> str:
628
+ if fleet is not None:
629
+ return fleet
630
+ return next((named for named, name in gateway.roster.seats if name == worker), "")
631
+
632
+
633
+ # Entries whose log nobody claimed yet carry no org, and are left out.
634
+ async def _owned(gateway: Gateway, entries: AsyncIterator[Entry]) -> AsyncIterator[str]:
635
+ yield f"retry: {_streams.RETRY_MS}\n\n"
636
+ async for entry in paced(entries):
637
+ if entry is None:
638
+ yield _streams.PING
639
+ continue
640
+ claimant = await gateway.logs.claimant_of(entry)
641
+ if claimant is None:
642
+ continue
643
+ event = BoxEvent(org=claimant.org, env=claimant.env, entry=entry)
644
+ yield frame(entry.type, event.written(), seq=entry.seq)
645
+
646
+
647
+ async def _metered_stream(store: Store, after: int, only: str | None) -> AsyncIterator[str]:
648
+ yield f"retry: {_streams.RETRY_MS}\n\n"
649
+ cursor = after
650
+ while True:
651
+ page = await usage.metered_page(store, after=cursor, limit=DEFAULT_LIMIT, org=only)
652
+ for row in page.rows:
653
+ yield frame("usage", usage_row_response(row).written(), seq=row.cursor)
654
+ if page.next is not None:
655
+ cursor = page.next
656
+ continue
657
+ yield _streams.PING
658
+ await asyncio.sleep(POLL_S)