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.
- pinecall/__init__.py +0 -0
- pinecall/channels/__init__.py +0 -0
- pinecall/channels/_chooser.py +45 -0
- pinecall/channels/offers.py +219 -0
- pinecall/channels/rooms.py +176 -0
- pinecall/channels/routes.py +275 -0
- pinecall/channels/telephony/__init__.py +0 -0
- pinecall/channels/telephony/_twilio.py +349 -0
- pinecall/channels/telephony/carrier.py +229 -0
- pinecall/channels/telephony/carrier_catalog.py +92 -0
- pinecall/channels/telephony/carriers.csv +41 -0
- pinecall/channels/telephony/dialing.py +419 -0
- pinecall/channels/telephony/firewall.py +58 -0
- pinecall/channels/telephony/hand_over.py +78 -0
- pinecall/channels/telephony/number_path.py +176 -0
- pinecall/channels/telephony/numbers.py +530 -0
- pinecall/channels/telephony/origination.py +77 -0
- pinecall/channels/telephony/sip.py +448 -0
- pinecall/channels/whatsapp.py +413 -0
- pinecall/cli/__init__.py +0 -0
- pinecall/cli/_archive.py +85 -0
- pinecall/cli/_drift.py +38 -0
- pinecall/cli/_facts.py +67 -0
- pinecall/cli/_load.py +433 -0
- pinecall/cli/_operator.py +626 -0
- pinecall/cli/_sessions.py +164 -0
- pinecall/cli/_traceback.py +95 -0
- pinecall/cli/_usage.py +34 -0
- pinecall/cli/main.py +521 -0
- pinecall/domain/__init__.py +0 -0
- pinecall/domain/agent.py +446 -0
- pinecall/domain/call.py +152 -0
- pinecall/domain/country_codes.py +32 -0
- pinecall/domain/errors.py +151 -0
- pinecall/domain/names.py +115 -0
- pinecall/domain/org.py +120 -0
- pinecall/domain/person.py +208 -0
- pinecall/domain/scope.py +21 -0
- pinecall/evals/__init__.py +0 -0
- pinecall/evals/_evidence.py +158 -0
- pinecall/evals/calibration.py +112 -0
- pinecall/evals/callers.py +160 -0
- pinecall/evals/case.py +327 -0
- pinecall/evals/checks.py +437 -0
- pinecall/evals/compliance.py +197 -0
- pinecall/evals/dataset.py +451 -0
- pinecall/evals/goldens.py +331 -0
- pinecall/evals/judges.py +525 -0
- pinecall/evals/runs.py +252 -0
- pinecall/evals/spoken.py +356 -0
- pinecall/fleet/__init__.py +0 -0
- pinecall/fleet/client.py +569 -0
- pinecall/fleet/demand.py +51 -0
- pinecall/fleet/heartbeat.py +194 -0
- pinecall/fleet/measures.py +157 -0
- pinecall/fleet/roster.py +232 -0
- pinecall/fleet/worlds.py +44 -0
- pinecall/gateway/__init__.py +0 -0
- pinecall/gateway/_call_setup.py +53 -0
- pinecall/gateway/_deps.py +556 -0
- pinecall/gateway/_gateway.py +75 -0
- pinecall/gateway/_served.py +396 -0
- pinecall/gateway/_sockets.py +444 -0
- pinecall/gateway/_streams.py +83 -0
- pinecall/gateway/_text_calls.py +182 -0
- pinecall/gateway/api/__init__.py +0 -0
- pinecall/gateway/api/accounts.py +324 -0
- pinecall/gateway/api/agents.py +150 -0
- pinecall/gateway/api/apps.py +478 -0
- pinecall/gateway/api/box.py +658 -0
- pinecall/gateway/api/calibration.py +58 -0
- pinecall/gateway/api/callbacks.py +105 -0
- pinecall/gateway/api/calls.py +655 -0
- pinecall/gateway/api/chat.py +223 -0
- pinecall/gateway/api/dataset.py +163 -0
- pinecall/gateway/api/desk.py +113 -0
- pinecall/gateway/api/entries.py +77 -0
- pinecall/gateway/api/evals.py +647 -0
- pinecall/gateway/api/fleet.py +73 -0
- pinecall/gateway/api/hosting.py +241 -0
- pinecall/gateway/api/judges.py +97 -0
- pinecall/gateway/api/keys.py +110 -0
- pinecall/gateway/api/line.py +142 -0
- pinecall/gateway/api/members.py +197 -0
- pinecall/gateway/api/metrics.py +226 -0
- pinecall/gateway/api/numbers.py +476 -0
- pinecall/gateway/api/ops.py +428 -0
- pinecall/gateway/api/org.py +283 -0
- pinecall/gateway/api/personas.py +157 -0
- pinecall/gateway/api/pipeline.py +206 -0
- pinecall/gateway/api/prompts.py +50 -0
- pinecall/gateway/api/providers.py +240 -0
- pinecall/gateway/api/recordings.py +89 -0
- pinecall/gateway/api/relay.py +169 -0
- pinecall/gateway/api/retrieval.py +403 -0
- pinecall/gateway/api/runner.py +109 -0
- pinecall/gateway/api/sessions.py +102 -0
- pinecall/gateway/api/settings.py +463 -0
- pinecall/gateway/api/signup.py +164 -0
- pinecall/gateway/api/sso_login.py +168 -0
- pinecall/gateway/api/threads.py +187 -0
- pinecall/gateway/api/usage.py +303 -0
- pinecall/gateway/api/visitors.py +222 -0
- pinecall/gateway/api/whatsapp.py +48 -0
- pinecall/gateway/api/widget.py +49 -0
- pinecall/gateway/app.py +504 -0
- pinecall/gateway/calls/__init__.py +0 -0
- pinecall/gateway/calls/binding.py +63 -0
- pinecall/gateway/calls/commands.py +18 -0
- pinecall/gateway/calls/inbox.py +100 -0
- pinecall/gateway/calls/known.py +23 -0
- pinecall/gateway/calls/owners.py +87 -0
- pinecall/gateway/calls/pump.py +33 -0
- pinecall/gateway/calls/serving.py +118 -0
- pinecall/gateway/calls/threads.py +367 -0
- pinecall/gateway/dispatching/__init__.py +0 -0
- pinecall/gateway/dispatching/arrivals.py +58 -0
- pinecall/gateway/dispatching/sweep.py +43 -0
- pinecall/gateway/ending/__init__.py +0 -0
- pinecall/gateway/ending/reaper.py +160 -0
- pinecall/gateway/ending/seal.py +312 -0
- pinecall/gateway/ending/stranded.py +117 -0
- pinecall/log/__init__.py +0 -0
- pinecall/log/_histogram.py +74 -0
- pinecall/log/_relay.py +259 -0
- pinecall/log/_writer.py +454 -0
- pinecall/log/days.py +154 -0
- pinecall/log/drift.py +550 -0
- pinecall/log/facts.py +358 -0
- pinecall/log/inbox.py +169 -0
- pinecall/log/lists.py +155 -0
- pinecall/log/logs.py +581 -0
- pinecall/log/openings.py +71 -0
- pinecall/log/private.py +135 -0
- pinecall/log/queries.py +229 -0
- pinecall/log/readers.py +207 -0
- pinecall/log/reduce.py +659 -0
- pinecall/log/refold.py +217 -0
- pinecall/log/store.py +370 -0
- pinecall/log/usage.py +242 -0
- pinecall/postgres/__init__.py +0 -0
- pinecall/postgres/migrate.py +202 -0
- pinecall/postgres/migrations/0001_schema.sql +591 -0
- pinecall/postgres/migrations/0002_worlds.sql +15 -0
- pinecall/postgres/migrations/0003_lent.sql +4 -0
- pinecall/postgres/migrations/0004_carriers.sql +23 -0
- pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
- pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
- pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
- pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
- pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
- pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
- pinecall/postgres/migrations/0011_erasure.sql +32 -0
- pinecall/postgres/migrations/0012_org_policy.sql +10 -0
- pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
- pinecall/postgres/migrations/0014_consents.sql +24 -0
- pinecall/postgres/migrations/0015_disclosure.sql +7 -0
- pinecall/postgres/migrations/0016_call_records.sql +21 -0
- pinecall/postgres/migrations/0017_reads.sql +17 -0
- pinecall/postgres/migrations/0018_hosting.sql +41 -0
- pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
- pinecall/postgres/migrations/0020_written_once.sql +7 -0
- pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
- pinecall/postgres/migrations/0022_sealed.sql +39 -0
- pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
- pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
- pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
- pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
- pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
- pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
- pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
- pinecall/postgres/migrations/0060_drift.sql +62 -0
- pinecall/postgres/migrations/0061_canaries.sql +24 -0
- pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
- pinecall/postgres/migrations/0063_prompts.sql +16 -0
- pinecall/postgres/migrations/0075_call_private.sql +14 -0
- pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
- pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
- pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
- pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
- pinecall/postgres/migrations/0080_call_openings.sql +20 -0
- pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
- pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
- pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
- pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
- pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
- pinecall/postgres/migrations/0086_route_origin.sql +17 -0
- pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
- pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
- pinecall/postgres/migrations/0089_offers.sql +21 -0
- pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
- pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
- pinecall/postgres/migrations/0092_key_parent.sql +6 -0
- pinecall/postgres/migrations/0093_code_tries.sql +10 -0
- pinecall/postgres/migrations/0094_route_approval.sql +12 -0
- pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
- pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
- pinecall/postgres/migrations/0097_case_status.sql +19 -0
- pinecall/postgres/pool.py +190 -0
- pinecall/process/__init__.py +0 -0
- pinecall/process/_objects.py +97 -0
- pinecall/process/box_settings.py +23 -0
- pinecall/process/connections.py +124 -0
- pinecall/process/metrics.py +127 -0
- pinecall/process/recordings.py +323 -0
- pinecall/process/resolver.py +41 -0
- pinecall/process/sealed_audio.py +173 -0
- pinecall/process/settings.py +534 -0
- pinecall/process/shared.py +158 -0
- pinecall/process/signal.py +416 -0
- pinecall/providers/__init__.py +0 -0
- pinecall/providers/build.py +570 -0
- pinecall/providers/catalog.py +266 -0
- pinecall/providers/credentials.py +230 -0
- pinecall/providers/declared.py +69 -0
- pinecall/providers/prices.py +240 -0
- pinecall/providers/tried.py +104 -0
- pinecall/providers/voices.py +174 -0
- pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
- pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
- pinecall/public/console/favicon.png +0 -0
- pinecall/public/console/index.html +20 -0
- pinecall/public/console/logo-mark.png +0 -0
- pinecall/public/console/pinecall-logo-dark.png +0 -0
- pinecall/public/console/pinecall-logo.png +0 -0
- pinecall/public/console/pinecall-mark.png +0 -0
- pinecall/public/console/sw.js +37 -0
- pinecall/public/widget/pinecall-widget.js +853 -0
- pinecall/py.typed +0 -0
- pinecall/retrieval/__init__.py +0 -0
- pinecall/retrieval/_search.py +186 -0
- pinecall/retrieval/_sections.py +70 -0
- pinecall/retrieval/embed.py +340 -0
- pinecall/retrieval/extraction.py +403 -0
- pinecall/retrieval/knowledge.py +659 -0
- pinecall/retrieval/lookups.py +192 -0
- pinecall/retrieval/memory.py +603 -0
- pinecall/runner/__init__.py +0 -0
- pinecall/runner/_kube.py +459 -0
- pinecall/runner/_plan.py +157 -0
- pinecall/runner/main.py +306 -0
- pinecall/session/__init__.py +0 -0
- pinecall/session/_agent.py +141 -0
- pinecall/session/_hearing.py +101 -0
- pinecall/session/_livekit.py +195 -0
- pinecall/session/_prompt.py +147 -0
- pinecall/session/_recording.py +237 -0
- pinecall/session/call.py +367 -0
- pinecall/session/clock.py +40 -0
- pinecall/session/hold.py +239 -0
- pinecall/session/room.py +626 -0
- pinecall/session/session.py +690 -0
- pinecall/session/text.py +144 -0
- pinecall/session/tools.py +483 -0
- pinecall/session/voice.py +118 -0
- pinecall/session/widget.py +201 -0
- pinecall/tenancy/__init__.py +0 -0
- pinecall/tenancy/admission.py +204 -0
- pinecall/tenancy/agents.py +164 -0
- pinecall/tenancy/canary.py +126 -0
- pinecall/tenancy/carrier_networks.py +156 -0
- pinecall/tenancy/carriers.py +265 -0
- pinecall/tenancy/codes.py +196 -0
- pinecall/tenancy/consents.py +167 -0
- pinecall/tenancy/dial_policy.py +405 -0
- pinecall/tenancy/disclosure.py +41 -0
- pinecall/tenancy/erasure.py +204 -0
- pinecall/tenancy/export.py +167 -0
- pinecall/tenancy/hosted_running.py +166 -0
- pinecall/tenancy/hosting.py +475 -0
- pinecall/tenancy/judges.py +126 -0
- pinecall/tenancy/keys.py +467 -0
- pinecall/tenancy/knocks.py +40 -0
- pinecall/tenancy/letters.py +378 -0
- pinecall/tenancy/mail.py +458 -0
- pinecall/tenancy/org_secrets.py +123 -0
- pinecall/tenancy/orgs.py +78 -0
- pinecall/tenancy/people.py +543 -0
- pinecall/tenancy/personas.py +145 -0
- pinecall/tenancy/policy.py +65 -0
- pinecall/tenancy/prompts.py +48 -0
- pinecall/tenancy/reads.py +73 -0
- pinecall/tenancy/recording_keys.py +41 -0
- pinecall/tenancy/remembered.py +148 -0
- pinecall/tenancy/retention.py +86 -0
- pinecall/tenancy/scopes.py +406 -0
- pinecall/tenancy/signin.py +622 -0
- pinecall/tenancy/spend.py +69 -0
- pinecall/tenancy/sso.py +431 -0
- pinecall/tenancy/sso_domains.py +132 -0
- pinecall/tenancy/throttle.py +88 -0
- pinecall/tenancy/tokens.py +400 -0
- pinecall/tenancy/traceback.py +70 -0
- pinecall/tenancy/usage.py +136 -0
- pinecall/tenancy/vault.py +204 -0
- pinecall/tenancy/words.py +144 -0
- pinecall/wire/__init__.py +0 -0
- pinecall/wire/commands.py +350 -0
- pinecall/wire/events.py +655 -0
- pinecall/wire/frames.py +65 -0
- pinecall/wire/metrics.py +287 -0
- pinecall/wire/parts.py +320 -0
- pinecall/wire/rest/__init__.py +0 -0
- pinecall/wire/rest/accounts.py +512 -0
- pinecall/wire/rest/agents.py +172 -0
- pinecall/wire/rest/calls.py +397 -0
- pinecall/wire/rest/evals.py +453 -0
- pinecall/wire/rest/fleet.py +69 -0
- pinecall/wire/rest/hosting.py +174 -0
- pinecall/wire/rest/numbers.py +280 -0
- pinecall/wire/rest/ops.py +342 -0
- pinecall/wire/rest/providers.py +83 -0
- pinecall/wire/rest/retrieval.py +301 -0
- pinecall/wire/rest/settings.py +184 -0
- pinecall/wire/rest/usage.py +245 -0
- pinecall/wire/room.py +61 -0
- pinecall/wire/scores.py +49 -0
- pinecall/wire/state.py +243 -0
- pinecall/worker/__init__.py +0 -0
- pinecall/worker/_job.py +655 -0
- pinecall/worker/_recorder.py +82 -0
- pinecall/worker/_traces.py +48 -0
- pinecall/worker/main.py +307 -0
- pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
- pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
- pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
- pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
- 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)
|