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,151 @@
|
|
|
1
|
+
"""The one error hierarchy: every error the runtime raises on purpose, with its HTTP status."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PinecallError(Exception):
|
|
5
|
+
"""Base of every deliberate error; the message is the sentence a person reads."""
|
|
6
|
+
|
|
7
|
+
status = 500
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeclarationRefused(PinecallError):
|
|
11
|
+
"""A value broke a contract; the message states the rule."""
|
|
12
|
+
|
|
13
|
+
status = 400
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotSignedIn(PinecallError):
|
|
17
|
+
"""No key, a wrong one, or wrong credentials."""
|
|
18
|
+
|
|
19
|
+
status = 401
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotAllowed(PinecallError):
|
|
23
|
+
"""A key that opens something, but not this."""
|
|
24
|
+
|
|
25
|
+
status = 403
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NotFound(PinecallError):
|
|
29
|
+
"""Nothing by that name in the caller's scope."""
|
|
30
|
+
|
|
31
|
+
status = 404
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Conflict(PinecallError):
|
|
35
|
+
"""A valid request against stored state that contradicts it."""
|
|
36
|
+
|
|
37
|
+
status = 409
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class QuotaExhausted(PinecallError):
|
|
41
|
+
"""A limit of the org is reached: which one, what was used and the limit, when it is one."""
|
|
42
|
+
|
|
43
|
+
status = 429
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self, sentence: str, *, quota: str | None = None, used: float = 0, limit: int = 0
|
|
47
|
+
) -> None:
|
|
48
|
+
"""The refusal, and the numbers the door writes into the agent's log."""
|
|
49
|
+
super().__init__(sentence)
|
|
50
|
+
self.quota = quota
|
|
51
|
+
self.used = used
|
|
52
|
+
self.limit = limit
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TooLarge(PinecallError):
|
|
56
|
+
"""A body over what the door takes."""
|
|
57
|
+
|
|
58
|
+
status = 413
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TooManyRequests(PinecallError):
|
|
62
|
+
"""The same name knocked too often in the last minute: a password, a code, a sign-up."""
|
|
63
|
+
|
|
64
|
+
status = 429
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Throttled(TooManyRequests):
|
|
68
|
+
"""An org's family of doors took its minute's requests; the answer says when to come back."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, sentence: str, *, retry_after_s: int) -> None:
|
|
71
|
+
"""The refusal, and the whole seconds until the minute turns."""
|
|
72
|
+
super().__init__(sentence)
|
|
73
|
+
self.retry_after_s = retry_after_s
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class UpstreamFailed(PinecallError):
|
|
77
|
+
"""A vendor, a carrier or an identity provider did not answer as it should."""
|
|
78
|
+
|
|
79
|
+
status = 502
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GatewayRefused(UpstreamFailed):
|
|
83
|
+
"""The worker's gateway did not answer, or answered with a refusal; `answered` is its status."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, sentence: str, *, answered: int | None = None) -> None:
|
|
86
|
+
"""The refusal, and the status the gateway answered with, None when it was unreachable."""
|
|
87
|
+
super().__init__(sentence)
|
|
88
|
+
self.answered = answered
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AppRefused(PinecallError):
|
|
92
|
+
"""A tenant's app refused what a console asked of it; its own status is the answer's."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, answered: int, sentence: str) -> None:
|
|
95
|
+
"""The app's status and its sentence, passed on as they came."""
|
|
96
|
+
super().__init__(sentence)
|
|
97
|
+
self.status = answered
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class NotAvailable(PinecallError):
|
|
101
|
+
"""This box lacks what the request needs: a key, the vault, an embedder, a domain."""
|
|
102
|
+
|
|
103
|
+
status = 503
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class EmbedderUnreachable(PinecallError):
|
|
107
|
+
"""The box's embedder did not answer, or answered something that is not its vectors."""
|
|
108
|
+
|
|
109
|
+
status = 503
|
|
110
|
+
|
|
111
|
+
# The sentence reaches a tenant (a lookup's error on its call's log): it names the vendor and
|
|
112
|
+
# never where the box asked it, which is the operator's to read in the process's log.
|
|
113
|
+
def __init__(self, vendor: str, url: str, words: str) -> None:
|
|
114
|
+
"""The embedder, where it was asked, and what it said or why it said nothing."""
|
|
115
|
+
super().__init__(f"{vendor} did not answer: {words}")
|
|
116
|
+
self.url = url
|
|
117
|
+
self.words = words
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class WrongWidth(PinecallError):
|
|
121
|
+
"""The embedder answered vectors of a width the vector columns do not hold."""
|
|
122
|
+
|
|
123
|
+
status = 503
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Two models' vectors can share a width and still not be comparable.
|
|
127
|
+
class WrongModel(PinecallError):
|
|
128
|
+
"""A base's vectors were written by another model than the one this box embeds with."""
|
|
129
|
+
|
|
130
|
+
status = 503
|
|
131
|
+
|
|
132
|
+
def __init__(self, base: str, pushed: str, embeds: str) -> None:
|
|
133
|
+
"""The base, the model that wrote it, and the model the box embeds with now."""
|
|
134
|
+
super().__init__(
|
|
135
|
+
f"base {base} was pushed with {pushed}; this box embeds with {embeds}: "
|
|
136
|
+
"push it again with `pinecall docs push`"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class SettingsRefused(PinecallError):
|
|
141
|
+
"""The process cannot start with the environment it was given."""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class StoreUnreachable(PinecallError):
|
|
145
|
+
"""The database cannot be reached or is not usable."""
|
|
146
|
+
|
|
147
|
+
status = 503
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class MigrationsRefused(PinecallError):
|
|
151
|
+
"""The database and the shipped migrations disagree."""
|
pinecall/domain/names.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""The words every part of the runtime speaks: worlds, channels, slugs, numbers, key scopes."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pinecall.domain.errors import DeclarationRefused
|
|
7
|
+
|
|
8
|
+
# The distribution on PyPI; the import stays `pinecall`. PyPI `pinecall` is the Python SDK.
|
|
9
|
+
DISTRIBUTION = "pinecall-runtime"
|
|
10
|
+
|
|
11
|
+
type Json = str | int | float | bool | list[Json] | dict[str, Json] | None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
type JsonObject = dict[str, Json]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# A lone secret, or the constructor's own keyword arguments (Azure's key and region, Google's
|
|
18
|
+
# service account, LiveKit's key pair).
|
|
19
|
+
type Credentials = str | JsonObject
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# A key belongs to one environment; its agents, numbers and calls are isolated to it. A shared
|
|
23
|
+
# "staging" is a sandbox agent registered by a machine key (no holder), not a third environment.
|
|
24
|
+
type Env = Literal["production", "sandbox"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# phone: SIP into a LiveKit room; web: the widget over WebRTC; whatsapp: text.
|
|
28
|
+
type Channel = Literal["phone", "web", "whatsapp"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# How the call is had: spoken (a room, a voice) or written (a text session); `web` is either.
|
|
32
|
+
type Medium = Literal["voice", "text"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
type Direction = Literal["inbound", "outbound"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# How a number's row was written: bought by the box, imported from an account of the org, hooked
|
|
39
|
+
# by the org from its own carrier, or typed by the box's operator.
|
|
40
|
+
type RouteOrigin = Literal["bought", "imported", "hooked", "typed"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# What one step of a call's way to an agent does now: works, waits on someone, or stops the call.
|
|
44
|
+
type StepState = Literal["ok", "waiting", "broken"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# e.g. clinica-norte: an agent's slug, and an org's.
|
|
48
|
+
A_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_E164 = re.compile(r"^\+[1-9]\d{1,14}$")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Checked on every address before a letter is sent: whitespace would allow header injection
|
|
55
|
+
# (RFC 5322 §2.2).
|
|
56
|
+
AN_ADDRESS = re.compile(r"^[^\s@<>,;]+@[^\s@<>,;]+\.[^\s@<>,;]+$")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
ENVS: tuple[Env, ...] = ("production", "sandbox")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
PRODUCTION: Env = "production"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
SANDBOX: Env = "sandbox"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
CHANNELS: tuple[Channel, ...] = ("phone", "web", "whatsapp")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
CHANNELS_WITH_A_NUMBER: tuple[Channel, ...] = ("phone", "whatsapp")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
THE_WIDGET: Channel = "web"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def other_world(world: Env) -> Env:
|
|
78
|
+
"""Return the world that is not this one."""
|
|
79
|
+
return SANDBOX if world == PRODUCTION else PRODUCTION
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_env(word: str) -> Env:
|
|
83
|
+
"""Return the word as an Env, raising DeclarationRefused when it is neither world."""
|
|
84
|
+
if word not in ENVS:
|
|
85
|
+
raise DeclarationRefused(f"a key opens one of {sorted(ENVS)}, not {word!r}")
|
|
86
|
+
return word
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_channel(word: str) -> Channel:
|
|
90
|
+
"""Return the word as a Channel, raising DeclarationRefused when it is none."""
|
|
91
|
+
if word not in CHANNELS:
|
|
92
|
+
raise DeclarationRefused(f"a call comes through one of {sorted(CHANNELS)}, not {word!r}")
|
|
93
|
+
return word
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_slug(word: str) -> str:
|
|
97
|
+
"""Return the slug, raising DeclarationRefused unless it is lowercase words joined by dashes."""
|
|
98
|
+
if not A_SLUG.match(word):
|
|
99
|
+
raise DeclarationRefused(f"a slug is lowercase words joined by dashes, not {word!r}")
|
|
100
|
+
return word
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def dialable(number: str) -> bool:
|
|
104
|
+
"""Return whether the number is in E.164 form."""
|
|
105
|
+
return _E164.match(number) is not None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_e164(number: str) -> str:
|
|
109
|
+
"""Return the trimmed number, raising DeclarationRefused when it is not E.164."""
|
|
110
|
+
data = number.strip()
|
|
111
|
+
if not dialable(data):
|
|
112
|
+
raise DeclarationRefused(
|
|
113
|
+
f"a number is written in E.164 form, like +59829001199, not {data!r}"
|
|
114
|
+
)
|
|
115
|
+
return data
|
pinecall/domain/org.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""An org and what it may do: its quotas, its admission, what the box lends it."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pinecall.domain.errors import DeclarationRefused
|
|
8
|
+
from pinecall.domain.names import parse_slug
|
|
9
|
+
|
|
10
|
+
# Names match the DB columns and the wire. The first four are flows (consumed or open now); the
|
|
11
|
+
# rest are stocks (rows kept), which lets a plan disable a feature with a zero; the budget is
|
|
12
|
+
# dollars a month, both worlds together.
|
|
13
|
+
type QuotaName = Literal[
|
|
14
|
+
"minutes",
|
|
15
|
+
"messages",
|
|
16
|
+
"agents",
|
|
17
|
+
"concurrent_calls",
|
|
18
|
+
"memory_facts",
|
|
19
|
+
"knowledge_chunks",
|
|
20
|
+
"numbers",
|
|
21
|
+
"seats",
|
|
22
|
+
"llm_tokens",
|
|
23
|
+
"hosted_apps",
|
|
24
|
+
"budget_usd",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Seeded by the schema; the first key is issued against it and CLI verbs fall back to it.
|
|
29
|
+
DEFAULT_ORG = "default"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Org:
|
|
34
|
+
"""An organisation: immutable id, editable slug, and display name."""
|
|
35
|
+
|
|
36
|
+
id: str
|
|
37
|
+
slug: str
|
|
38
|
+
name: str
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
if not self.id:
|
|
42
|
+
raise DeclarationRefused("an org has an id")
|
|
43
|
+
parse_slug(self.slug)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# None is no limit (the self-hosted default). Zero is a real limit that refuses everything.
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class Quotas:
|
|
49
|
+
"""An org's quota limits, plus its monthly budget and the box keys lent to it."""
|
|
50
|
+
|
|
51
|
+
minutes: int | None = None
|
|
52
|
+
messages: int | None = None
|
|
53
|
+
agents: int | None = None
|
|
54
|
+
concurrent_calls: int | None = None
|
|
55
|
+
memory_facts: int | None = None
|
|
56
|
+
knowledge_chunks: int | None = None
|
|
57
|
+
# Only numbers bought on the box's carrier account; imported numbers do not count.
|
|
58
|
+
numbers: int | None = None
|
|
59
|
+
# Invited and active members; disabled members hold no seat.
|
|
60
|
+
seats: int | None = None
|
|
61
|
+
llm_tokens: int | None = None
|
|
62
|
+
# Apps the box runs for the org from sources it uploaded.
|
|
63
|
+
hosted_apps: int | None = None
|
|
64
|
+
# Whole US dollars a calendar month, both worlds together: a new call is refused past it.
|
|
65
|
+
budget_usd: int | None = None
|
|
66
|
+
# Box vendor keys the org may use: None all, empty none, else `vendor` or `vendor/model`.
|
|
67
|
+
lends: frozenset[str] | None = None
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
for name, limit in self.limits.items():
|
|
71
|
+
if limit is None or limit >= 0:
|
|
72
|
+
continue
|
|
73
|
+
if name == "budget_usd":
|
|
74
|
+
raise DeclarationRefused(f"a budget is dollars, and cannot be {limit}")
|
|
75
|
+
raise DeclarationRefused(f"a quota is a count, and {name} cannot be {limit}")
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def limits(self) -> Mapping[QuotaName, int | None]:
|
|
79
|
+
"""Return every limit by its name."""
|
|
80
|
+
return {
|
|
81
|
+
"minutes": self.minutes,
|
|
82
|
+
"messages": self.messages,
|
|
83
|
+
"agents": self.agents,
|
|
84
|
+
"concurrent_calls": self.concurrent_calls,
|
|
85
|
+
"memory_facts": self.memory_facts,
|
|
86
|
+
"knowledge_chunks": self.knowledge_chunks,
|
|
87
|
+
"numbers": self.numbers,
|
|
88
|
+
"seats": self.seats,
|
|
89
|
+
"llm_tokens": self.llm_tokens,
|
|
90
|
+
"hosted_apps": self.hosted_apps,
|
|
91
|
+
"budget_usd": self.budget_usd,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def reached(self, quota: QuotaName, used: float) -> int | None:
|
|
95
|
+
"""Return the limit when `used` is at or past it, else None."""
|
|
96
|
+
limit = self.limits[quota]
|
|
97
|
+
return limit if limit is not None and used >= limit else None
|
|
98
|
+
|
|
99
|
+
def exceeded(self, quota: QuotaName, keeping: float) -> int | None:
|
|
100
|
+
"""Return the limit when `keeping` would pass it, else None; for pushes sized up front."""
|
|
101
|
+
limit = self.limits[quota]
|
|
102
|
+
return limit if limit is not None and keeping > limit else None
|
|
103
|
+
|
|
104
|
+
def switched_off(self, quota: QuotaName) -> bool:
|
|
105
|
+
"""Return whether the quota is zero: the feature is not in the plan."""
|
|
106
|
+
return self.limits[quota] == 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
QUOTAS: tuple[QuotaName, ...] = (
|
|
110
|
+
"minutes",
|
|
111
|
+
"messages",
|
|
112
|
+
"agents",
|
|
113
|
+
"concurrent_calls",
|
|
114
|
+
"memory_facts",
|
|
115
|
+
"knowledge_chunks",
|
|
116
|
+
"numbers",
|
|
117
|
+
"seats",
|
|
118
|
+
"llm_tokens",
|
|
119
|
+
"hosted_apps",
|
|
120
|
+
)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""People and their keys: members, roles, invitations, the scopes a key opens."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pinecall.domain.errors import DeclarationRefused
|
|
9
|
+
from pinecall.domain.names import AN_ADDRESS, PRODUCTION, Env
|
|
10
|
+
|
|
11
|
+
# Every gateway door belongs to exactly one scope; `fleet` is the box's worker, resolved per call
|
|
12
|
+
# instead of per key org, and only `keys issue --scope fleet` grants it. `runner` is the box's
|
|
13
|
+
# own too: the process that runs every org's hosted apps, handed their tokens and secrets.
|
|
14
|
+
type KeyScope = Literal[
|
|
15
|
+
"app",
|
|
16
|
+
"calls",
|
|
17
|
+
"talk",
|
|
18
|
+
"supervise",
|
|
19
|
+
"pipeline",
|
|
20
|
+
"knowledge",
|
|
21
|
+
"memory",
|
|
22
|
+
"evals",
|
|
23
|
+
"numbers",
|
|
24
|
+
"keys",
|
|
25
|
+
"providers",
|
|
26
|
+
"team",
|
|
27
|
+
"usage",
|
|
28
|
+
"words",
|
|
29
|
+
"fleet",
|
|
30
|
+
"runner",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# A role is only a preset of key scopes. Doors check scopes, never roles, so changing a role
|
|
35
|
+
# affects newly issued keys only.
|
|
36
|
+
type Role = Literal["qa", "supervisor", "manager", "admin", "developer"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# A room token's scope, which LiveKit carries on the seat as its `pinecall.scope` attribute, so
|
|
40
|
+
# the worker reads it without a lookup.
|
|
41
|
+
type RoomScope = Literal["talk", "chat", "observe", "supervise", "participate", "read"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# disabled: cannot log in and keys are revoked; the row is kept because the log references it.
|
|
45
|
+
type MemberStatus = Literal["invited", "active", "disabled"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class Member:
|
|
50
|
+
"""A person in one org, with their role, agents and status."""
|
|
51
|
+
|
|
52
|
+
id: str
|
|
53
|
+
org: str
|
|
54
|
+
email: str
|
|
55
|
+
name: str
|
|
56
|
+
role: Role
|
|
57
|
+
# Empty means all of the org's agents.
|
|
58
|
+
agents: frozenset[str] = frozenset()
|
|
59
|
+
status: MemberStatus = "invited"
|
|
60
|
+
# Box operator: their key also opens /v1/ops/*. Granted only with the ops key.
|
|
61
|
+
operator: bool = False
|
|
62
|
+
# Granted by an admin; checked per request, so revoking it takes effect immediately.
|
|
63
|
+
production: bool = False
|
|
64
|
+
# Email ownership proven by a mailed link, an IdP, or an operator invitation. Required
|
|
65
|
+
# before joining another org without a link.
|
|
66
|
+
verified: bool = False
|
|
67
|
+
|
|
68
|
+
def __post_init__(self) -> None:
|
|
69
|
+
if not self.id or not self.org:
|
|
70
|
+
raise DeclarationRefused("a member names their id and the org they belong to")
|
|
71
|
+
if not AN_ADDRESS.match(self.email):
|
|
72
|
+
raise DeclarationRefused(f"an email has one @ and a domain, not {self.email!r}")
|
|
73
|
+
if not self.name.strip():
|
|
74
|
+
raise DeclarationRefused("a member has a name: it is what a seat says when they sit")
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def scopes(self) -> frozenset[KeyScope]:
|
|
78
|
+
"""Return the scopes of keys minted for this member."""
|
|
79
|
+
return ROLE_SCOPES[self.role]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def opens_production(self) -> bool:
|
|
83
|
+
"""Return whether this member may act in production; admins always may."""
|
|
84
|
+
return self.role == "admin" or self.production
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
EVERY_SCOPE: tuple[KeyScope, ...] = (
|
|
88
|
+
"app",
|
|
89
|
+
"calls",
|
|
90
|
+
"talk",
|
|
91
|
+
"supervise",
|
|
92
|
+
"pipeline",
|
|
93
|
+
"knowledge",
|
|
94
|
+
"memory",
|
|
95
|
+
"evals",
|
|
96
|
+
"numbers",
|
|
97
|
+
"keys",
|
|
98
|
+
"providers",
|
|
99
|
+
"team",
|
|
100
|
+
"usage",
|
|
101
|
+
"words",
|
|
102
|
+
"fleet",
|
|
103
|
+
"runner",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
THE_FLEET: KeyScope = "fleet"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
THE_RUNNER: KeyScope = "runner"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
HOLDING: KeyScope = "app"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
THE_TEAM: KeyScope = "team"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
THE_KEYS: KeyScope = "keys"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Default scopes: every scope except the box's own two.
|
|
123
|
+
KEY_SCOPES: frozenset[KeyScope] = frozenset(EVERY_SCOPE) - {THE_FLEET, THE_RUNNER}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Doors take the org only from this record, never from the request.
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class Key:
|
|
129
|
+
"""A verified API key: its org, environment, scopes and holder."""
|
|
130
|
+
|
|
131
|
+
key_id: str
|
|
132
|
+
org: str
|
|
133
|
+
label: str | None = None
|
|
134
|
+
env: Env = PRODUCTION
|
|
135
|
+
scopes: frozenset[KeyScope] = KEY_SCOPES
|
|
136
|
+
# The person a key was minted for; None on a server's key. A person's one key opens both
|
|
137
|
+
# worlds, so its env is production's and the request names the world.
|
|
138
|
+
subject: str | None = None
|
|
139
|
+
name: str | None = None
|
|
140
|
+
# None never expires. Keys minted from a personal key never outlive it.
|
|
141
|
+
expires_at: datetime | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
ROLES: tuple[Role, ...] = ("qa", "supervisor", "manager", "admin", "developer")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
STATUSES: tuple[MemberStatus, ...] = ("invited", "active", "disabled")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Serving an agent, its calls, room tokens, knowledge pushes and evals; never the org's people,
|
|
151
|
+
# numbers or money.
|
|
152
|
+
SERVER_SCOPES: frozenset[KeyScope] = frozenset({"app", "calls", "talk", "knowledge", "evals"})
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
ROLE_SCOPES: Mapping[Role, frozenset[KeyScope]] = {
|
|
156
|
+
"qa": frozenset({"calls", "evals"}),
|
|
157
|
+
"supervisor": frozenset({"calls", "evals", "supervise", "talk", "memory", "words"}),
|
|
158
|
+
"manager": frozenset(
|
|
159
|
+
{
|
|
160
|
+
"calls",
|
|
161
|
+
"evals",
|
|
162
|
+
"supervise",
|
|
163
|
+
"talk",
|
|
164
|
+
"memory",
|
|
165
|
+
"numbers",
|
|
166
|
+
"keys",
|
|
167
|
+
"providers",
|
|
168
|
+
"usage",
|
|
169
|
+
"team",
|
|
170
|
+
"words",
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
"admin": KEY_SCOPES,
|
|
174
|
+
"developer": frozenset(
|
|
175
|
+
{"app", "calls", "talk", "supervise", "pipeline", "knowledge", "memory", "evals", "words"}
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# The browser's scopes, limited to their own call: the guest log and the widget's channel admit
|
|
181
|
+
# them.
|
|
182
|
+
READS_ITS_OWN_CALL: frozenset[RoomScope] = frozenset({"talk", "chat", "read", "participate"})
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def key_scopes(words: Iterable[str]) -> frozenset[KeyScope]:
|
|
186
|
+
"""Return the words as scopes, raising DeclarationRefused on the first that is none."""
|
|
187
|
+
scopes: set[KeyScope] = set()
|
|
188
|
+
for word in sorted(set(words)):
|
|
189
|
+
if word not in EVERY_SCOPE:
|
|
190
|
+
raise DeclarationRefused(
|
|
191
|
+
f"{word!r} is not a key scope; the scopes are {sorted(EVERY_SCOPE)}"
|
|
192
|
+
)
|
|
193
|
+
scopes.add(word)
|
|
194
|
+
return frozenset(scopes)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def parse_role(word: str) -> Role:
|
|
198
|
+
"""Return the word as a Role, raising DeclarationRefused when it is none."""
|
|
199
|
+
if word not in ROLES:
|
|
200
|
+
raise DeclarationRefused(f"a role is one of {sorted(ROLES)}, not {word!r}")
|
|
201
|
+
return word
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_status(word: str) -> MemberStatus:
|
|
205
|
+
"""Return the word as a member's status, raising DeclarationRefused when it is none."""
|
|
206
|
+
if word not in STATUSES:
|
|
207
|
+
raise DeclarationRefused(f"a member's status is one of {sorted(STATUSES)}, not {word!r}")
|
|
208
|
+
return word
|
pinecall/domain/scope.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""A scope: an org, one of its two worlds, and the holder a request acts as."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pinecall.domain.names import PRODUCTION, Env
|
|
6
|
+
|
|
7
|
+
# Holder value for org-owned rows: an empty string, not NULL, because it is part of a primary
|
|
8
|
+
# key and NULL never matches.
|
|
9
|
+
THE_ORGS_OWN = ""
|
|
10
|
+
|
|
11
|
+
SCOPE_ATTRIBUTE = "pinecall.scope"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# What a request may see: an org, one of its two worlds, and in the sandbox one developer's own.
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Scope:
|
|
17
|
+
"""An org's scope: the org, the environment and the holder ("" for the org itself)."""
|
|
18
|
+
|
|
19
|
+
org: str
|
|
20
|
+
env: Env = PRODUCTION
|
|
21
|
+
holder: str = THE_ORGS_OWN
|
|
File without changes
|