pi-ai-python 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pi_agent/__init__.py +127 -0
- pi_agent/agent.py +617 -0
- pi_agent/agent_loop.py +820 -0
- pi_agent/proxy.py +269 -0
- pi_agent/py.typed +0 -0
- pi_agent/stream_fn.py +27 -0
- pi_agent/tools.py +327 -0
- pi_agent/types.py +394 -0
- pi_ai/__init__.py +263 -0
- pi_ai/abort.py +315 -0
- pi_ai/api/__init__.py +1 -0
- pi_ai/api/anthropic_messages.py +1782 -0
- pi_ai/api/azure_openai_config.py +138 -0
- pi_ai/api/azure_openai_responses.py +245 -0
- pi_ai/api/bedrock_converse_stream.py +1598 -0
- pi_ai/api/classifier_shared.py +211 -0
- pi_ai/api/cloudflare.py +36 -0
- pi_ai/api/cloudflare_workers_ai_system_one.py +80 -0
- pi_ai/api/constrained_sampling.py +279 -0
- pi_ai/api/github_copilot_headers.py +30 -0
- pi_ai/api/google_generative_ai.py +138 -0
- pi_ai/api/google_shared.py +927 -0
- pi_ai/api/google_vertex.py +234 -0
- pi_ai/api/lazy.py +156 -0
- pi_ai/api/llama_cpp_classify.py +449 -0
- pi_ai/api/mistral_conversations.py +756 -0
- pi_ai/api/openai_codex_responses.py +1545 -0
- pi_ai/api/openai_completions.py +1594 -0
- pi_ai/api/openai_decisions.py +210 -0
- pi_ai/api/openai_prompt_cache.py +19 -0
- pi_ai/api/openai_responses.py +418 -0
- pi_ai/api/openai_responses_shared.py +840 -0
- pi_ai/api/openai_sdk.py +204 -0
- pi_ai/api/openrouter_images.py +153 -0
- pi_ai/api/pi_messages.py +449 -0
- pi_ai/api/registry.py +103 -0
- pi_ai/api/simple_options.py +119 -0
- pi_ai/api/system_one_shared.py +148 -0
- pi_ai/api/transform_messages.py +211 -0
- pi_ai/api/typesafe_system_one.py +43 -0
- pi_ai/auth/__init__.py +1 -0
- pi_ai/auth/context.py +47 -0
- pi_ai/auth/credential_store.py +208 -0
- pi_ai/auth/helpers.py +123 -0
- pi_ai/auth/oauth/__init__.py +5 -0
- pi_ai/auth/oauth/_common.py +454 -0
- pi_ai/auth/oauth/anthropic.py +283 -0
- pi_ai/auth/oauth/callback_server.py +339 -0
- pi_ai/auth/oauth/device_code.py +127 -0
- pi_ai/auth/oauth/github_copilot.py +508 -0
- pi_ai/auth/oauth/kimi_coding.py +283 -0
- pi_ai/auth/oauth/load.py +64 -0
- pi_ai/auth/oauth/meta.py +238 -0
- pi_ai/auth/oauth/openai_chatgpt.py +321 -0
- pi_ai/auth/oauth/openai_codex.py +391 -0
- pi_ai/auth/oauth/openrouter.py +173 -0
- pi_ai/auth/oauth/pkce.py +23 -0
- pi_ai/auth/oauth/radius.py +311 -0
- pi_ai/auth/oauth/xai.py +220 -0
- pi_ai/auth/resolve.py +190 -0
- pi_ai/auth/types.py +292 -0
- pi_ai/cli.py +265 -0
- pi_ai/compat.py +651 -0
- pi_ai/env_api_keys.py +137 -0
- pi_ai/event_stream.py +329 -0
- pi_ai/models.py +1191 -0
- pi_ai/models_store.py +121 -0
- pi_ai/oauth.py +90 -0
- pi_ai/providers/__init__.py +1 -0
- pi_ai/providers/all.py +154 -0
- pi_ai/providers/amazon_bedrock.py +116 -0
- pi_ai/providers/ant_ling.py +23 -0
- pi_ai/providers/anthropic.py +105 -0
- pi_ai/providers/azure.py +103 -0
- pi_ai/providers/baseten.py +23 -0
- pi_ai/providers/catalog.py +167 -0
- pi_ai/providers/cerebras.py +23 -0
- pi_ai/providers/cloudflare_ai_gateway.py +30 -0
- pi_ai/providers/cloudflare_auth.py +112 -0
- pi_ai/providers/cloudflare_stream.py +84 -0
- pi_ai/providers/cloudflare_workers_ai.py +26 -0
- pi_ai/providers/data/amazon-bedrock.json +1 -0
- pi_ai/providers/data/ant-ling.json +1 -0
- pi_ai/providers/data/anthropic.json +1 -0
- pi_ai/providers/data/azure.json +1 -0
- pi_ai/providers/data/baseten.json +1 -0
- pi_ai/providers/data/cerebras.json +1 -0
- pi_ai/providers/data/cloudflare-ai-gateway.json +1 -0
- pi_ai/providers/data/cloudflare-workers-ai.json +1 -0
- pi_ai/providers/data/deepseek.json +1 -0
- pi_ai/providers/data/fireworks.json +1 -0
- pi_ai/providers/data/github-copilot.json +1 -0
- pi_ai/providers/data/google-vertex.json +1 -0
- pi_ai/providers/data/google.json +1 -0
- pi_ai/providers/data/groq.json +1 -0
- pi_ai/providers/data/huggingface.json +1 -0
- pi_ai/providers/data/kimi-coding.json +1 -0
- pi_ai/providers/data/manifest.json +1 -0
- pi_ai/providers/data/meta.json +1 -0
- pi_ai/providers/data/minimax-cn.json +1 -0
- pi_ai/providers/data/minimax.json +1 -0
- pi_ai/providers/data/mistral.json +1 -0
- pi_ai/providers/data/moonshotai-cn.json +1 -0
- pi_ai/providers/data/moonshotai.json +1 -0
- pi_ai/providers/data/nvidia.json +1 -0
- pi_ai/providers/data/openai-codex.json +1 -0
- pi_ai/providers/data/openai.json +1 -0
- pi_ai/providers/data/opencode-go.json +1 -0
- pi_ai/providers/data/opencode.json +1 -0
- pi_ai/providers/data/openrouter.json +1 -0
- pi_ai/providers/data/qwen-token-plan-cn.json +1 -0
- pi_ai/providers/data/qwen-token-plan-individual.json +1 -0
- pi_ai/providers/data/qwen-token-plan.json +1 -0
- pi_ai/providers/data/radius.json +1 -0
- pi_ai/providers/data/together.json +1 -0
- pi_ai/providers/data/typesafe.json +1 -0
- pi_ai/providers/data/vercel-ai-gateway.json +1 -0
- pi_ai/providers/data/xai.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-ams.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-cn.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-sgp.json +1 -0
- pi_ai/providers/data/xiaomi.json +1 -0
- pi_ai/providers/data/zai-coding-cn.json +1 -0
- pi_ai/providers/data/zai.json +1 -0
- pi_ai/providers/deepseek.py +23 -0
- pi_ai/providers/faux.py +680 -0
- pi_ai/providers/fireworks.py +26 -0
- pi_ai/providers/github_copilot.py +46 -0
- pi_ai/providers/google.py +23 -0
- pi_ai/providers/google_vertex.py +131 -0
- pi_ai/providers/groq.py +23 -0
- pi_ai/providers/huggingface.py +23 -0
- pi_ai/providers/kimi_coding.py +32 -0
- pi_ai/providers/meta.py +32 -0
- pi_ai/providers/minimax.py +23 -0
- pi_ai/providers/minimax_cn.py +23 -0
- pi_ai/providers/mistral.py +23 -0
- pi_ai/providers/moonshotai.py +23 -0
- pi_ai/providers/moonshotai_cn.py +23 -0
- pi_ai/providers/nvidia.py +23 -0
- pi_ai/providers/openai.py +44 -0
- pi_ai/providers/openai_codex.py +26 -0
- pi_ai/providers/opencode.py +36 -0
- pi_ai/providers/opencode_go.py +33 -0
- pi_ai/providers/opencode_headers.py +60 -0
- pi_ai/providers/openrouter.py +145 -0
- pi_ai/providers/openrouter_api.py +122 -0
- pi_ai/providers/qwen_token_plan.py +23 -0
- pi_ai/providers/qwen_token_plan_cn.py +23 -0
- pi_ai/providers/qwen_token_plan_individual.py +23 -0
- pi_ai/providers/radius.py +146 -0
- pi_ai/providers/radius_config.py +136 -0
- pi_ai/providers/together.py +23 -0
- pi_ai/providers/typesafe.py +22 -0
- pi_ai/providers/vercel_ai_gateway.py +25 -0
- pi_ai/providers/xai.py +32 -0
- pi_ai/providers/xiaomi.py +23 -0
- pi_ai/providers/xiaomi_token_plan_ams.py +23 -0
- pi_ai/providers/xiaomi_token_plan_cn.py +23 -0
- pi_ai/providers/xiaomi_token_plan_sgp.py +23 -0
- pi_ai/providers/zai.py +23 -0
- pi_ai/providers/zai_coding_cn.py +23 -0
- pi_ai/py.typed +0 -0
- pi_ai/serialization.py +477 -0
- pi_ai/session_resources.py +33 -0
- pi_ai/types.py +1003 -0
- pi_ai/utils/__init__.py +1 -0
- pi_ai/utils/callbacks.py +15 -0
- pi_ai/utils/diagnostics.py +70 -0
- pi_ai/utils/error_body.py +89 -0
- pi_ai/utils/estimate.py +125 -0
- pi_ai/utils/fastjson.py +63 -0
- pi_ai/utils/http.py +409 -0
- pi_ai/utils/json_parse.py +424 -0
- pi_ai/utils/misc.py +193 -0
- pi_ai/utils/model_operations.py +73 -0
- pi_ai/utils/models_error.py +35 -0
- pi_ai/utils/oauth_page.py +121 -0
- pi_ai/utils/overflow.py +87 -0
- pi_ai/utils/retry.py +193 -0
- pi_ai/utils/schema.py +283 -0
- pi_ai/utils/text.py +65 -0
- pi_ai/utils/transcript.py +271 -0
- pi_ai/utils/validation.py +813 -0
- pi_ai_python-1.3.0.dist-info/METADATA +167 -0
- pi_ai_python-1.3.0.dist-info/RECORD +189 -0
- pi_ai_python-1.3.0.dist-info/WHEEL +4 -0
- pi_ai_python-1.3.0.dist-info/entry_points.txt +2 -0
- pi_ai_python-1.3.0.dist-info/licenses/LICENSE +22 -0
pi_ai/cli.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""``pi-ai`` command line: log in to the built-in OAuth providers.
|
|
2
|
+
|
|
3
|
+
``pi-ai login [provider]`` runs a provider's OAuth flow and saves the credential to
|
|
4
|
+
``auth.json`` in the current directory (the same format pi uses); ``pi-ai list``
|
|
5
|
+
lists the providers that support OAuth.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import contextlib
|
|
12
|
+
import getpass
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import uuid
|
|
18
|
+
import webbrowser
|
|
19
|
+
from collections import deque
|
|
20
|
+
from collections.abc import Callable, Sequence
|
|
21
|
+
from typing import TYPE_CHECKING, Any, TextIO
|
|
22
|
+
|
|
23
|
+
from .abort import AbortController, AbortError, AbortSignal, abort_scope
|
|
24
|
+
from .auth.credential_store import FileCredentialStore
|
|
25
|
+
from .auth.types import AuthEvent, AuthPrompt, LoginOptions
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .models import Models, Provider
|
|
29
|
+
|
|
30
|
+
__all__ = ["main", "run"]
|
|
31
|
+
|
|
32
|
+
AUTH_FILE = "auth.json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _LineReader:
|
|
36
|
+
"""Reads stdin lines on a daemon thread so a dismissed prompt never blocks exit.
|
|
37
|
+
|
|
38
|
+
A line goes to the oldest prompt still waiting; lines typed while no prompt waits
|
|
39
|
+
are dropped. (``asyncio.to_thread(input)`` would keep the interpreter alive until
|
|
40
|
+
Enter is pressed after the browser callback already finished the login.)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, stream: Callable[[], TextIO] = lambda: sys.stdin) -> None:
|
|
44
|
+
self._stream = stream
|
|
45
|
+
self._lock = threading.Lock()
|
|
46
|
+
self._waiters: deque[tuple[asyncio.AbstractEventLoop, asyncio.Future[str]]] = deque()
|
|
47
|
+
self._reading = False
|
|
48
|
+
|
|
49
|
+
async def readline(self) -> str:
|
|
50
|
+
loop = asyncio.get_running_loop()
|
|
51
|
+
future: asyncio.Future[str] = loop.create_future()
|
|
52
|
+
with self._lock:
|
|
53
|
+
self._waiters.append((loop, future))
|
|
54
|
+
start = not self._reading
|
|
55
|
+
self._reading = True
|
|
56
|
+
if start:
|
|
57
|
+
threading.Thread(target=self._read_loop, name="pi-ai-stdin", daemon=True).start()
|
|
58
|
+
return await future
|
|
59
|
+
|
|
60
|
+
def _read_loop(self) -> None:
|
|
61
|
+
while True:
|
|
62
|
+
try:
|
|
63
|
+
line: str | None = self._stream().readline()
|
|
64
|
+
except Exception:
|
|
65
|
+
line = ""
|
|
66
|
+
with self._lock:
|
|
67
|
+
while self._waiters and self._waiters[0][1].done():
|
|
68
|
+
self._waiters.popleft()
|
|
69
|
+
if not line:
|
|
70
|
+
waiters = list(self._waiters)
|
|
71
|
+
self._waiters.clear()
|
|
72
|
+
self._reading = False
|
|
73
|
+
else:
|
|
74
|
+
waiters = [self._waiters.popleft()] if self._waiters else []
|
|
75
|
+
if not self._waiters:
|
|
76
|
+
self._reading = False
|
|
77
|
+
reading = self._reading
|
|
78
|
+
for loop, future in waiters:
|
|
79
|
+
with contextlib.suppress(RuntimeError):
|
|
80
|
+
loop.call_soon_threadsafe(_settle_line, future, line)
|
|
81
|
+
if not reading:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _settle_line(future: asyncio.Future[str], line: str | None) -> None:
|
|
86
|
+
if future.done():
|
|
87
|
+
return
|
|
88
|
+
if not line:
|
|
89
|
+
future.set_exception(EOFError("No input (stdin closed)"))
|
|
90
|
+
else:
|
|
91
|
+
future.set_result(line.rstrip("\r\n"))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def _read_secret(question: str) -> str:
|
|
95
|
+
loop = asyncio.get_running_loop()
|
|
96
|
+
future: asyncio.Future[str] = loop.create_future()
|
|
97
|
+
|
|
98
|
+
def read() -> None:
|
|
99
|
+
try:
|
|
100
|
+
value = getpass.getpass(question)
|
|
101
|
+
except BaseException as error:
|
|
102
|
+
failure = error if isinstance(error, Exception) else EOFError("No input")
|
|
103
|
+
loop.call_soon_threadsafe(lambda: future.done() or future.set_exception(failure))
|
|
104
|
+
return
|
|
105
|
+
loop.call_soon_threadsafe(lambda: future.done() or future.set_result(value))
|
|
106
|
+
|
|
107
|
+
threading.Thread(target=read, name="pi-ai-getpass", daemon=True).start()
|
|
108
|
+
return await future
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _parse_int(value: str) -> int | None:
|
|
112
|
+
"""``Number.parseInt(value, 10)``."""
|
|
113
|
+
match = re.match(r"^\s*([+-]?\d+)", value)
|
|
114
|
+
return int(match.group(1)) if match else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _can_open_browser() -> bool:
|
|
118
|
+
# Without a display, `webbrowser` may start a text-mode browser inside this terminal.
|
|
119
|
+
if sys.platform.startswith("linux") or sys.platform.startswith("freebsd"):
|
|
120
|
+
return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _open_browser(url: str) -> None:
|
|
125
|
+
if not _can_open_browser():
|
|
126
|
+
return
|
|
127
|
+
with contextlib.suppress(Exception):
|
|
128
|
+
webbrowser.open(url)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _ConsoleInteraction:
|
|
132
|
+
"""Login interaction on stdin/stdout."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, lines: _LineReader, *, open_browser: Callable[[str], None] = _open_browser) -> None:
|
|
135
|
+
self.signal: AbortSignal | None = AbortController().signal
|
|
136
|
+
self._lines = lines
|
|
137
|
+
self._open_browser = open_browser
|
|
138
|
+
|
|
139
|
+
async def _ask(self, question: str, *, secret: bool = False, signal: Any = None) -> str:
|
|
140
|
+
if secret:
|
|
141
|
+
async with abort_scope(signal):
|
|
142
|
+
return await _read_secret(question)
|
|
143
|
+
print(question, end="", flush=True)
|
|
144
|
+
try:
|
|
145
|
+
async with abort_scope(signal):
|
|
146
|
+
return await self._lines.readline()
|
|
147
|
+
except (AbortError, asyncio.CancelledError):
|
|
148
|
+
# The prompt was dismissed (for example the browser callback finished the login).
|
|
149
|
+
print()
|
|
150
|
+
raise
|
|
151
|
+
|
|
152
|
+
async def prompt(self, prompt: AuthPrompt) -> str:
|
|
153
|
+
if prompt.type == "select":
|
|
154
|
+
options = prompt.options or []
|
|
155
|
+
print(f"\n{prompt.message}")
|
|
156
|
+
for index, option in enumerate(options):
|
|
157
|
+
print(f" {index + 1}. {option.label}")
|
|
158
|
+
answer = await self._ask(f"Enter number (1-{len(options)}): ", signal=prompt.signal)
|
|
159
|
+
choice = _parse_int(answer)
|
|
160
|
+
if choice is None or not 1 <= choice <= len(options):
|
|
161
|
+
raise ValueError("Invalid selection")
|
|
162
|
+
return options[choice - 1].id
|
|
163
|
+
suffix = f" ({prompt.placeholder})" if prompt.placeholder else ""
|
|
164
|
+
return await self._ask(f"{prompt.message}{suffix}: ", secret=prompt.type == "secret", signal=prompt.signal)
|
|
165
|
+
|
|
166
|
+
def notify(self, event: AuthEvent) -> None:
|
|
167
|
+
if event.type == "auth_url":
|
|
168
|
+
print(f"\nOpen this URL in your browser:\n{event.url}")
|
|
169
|
+
if event.instructions:
|
|
170
|
+
print(event.instructions)
|
|
171
|
+
if event.url:
|
|
172
|
+
self._open_browser(event.url)
|
|
173
|
+
elif event.type == "device_code":
|
|
174
|
+
print(f"\nOpen this URL in your browser:\n{event.verification_uri}")
|
|
175
|
+
print(f"Enter code: {event.user_code}")
|
|
176
|
+
if event.verification_uri:
|
|
177
|
+
self._open_browser(event.verification_uri)
|
|
178
|
+
elif event.message:
|
|
179
|
+
print(event.message)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _oauth_providers(models: Models) -> list[Provider]:
|
|
183
|
+
return [provider for provider in models.get_providers() if provider.auth.oauth is not None]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _usage(providers: Sequence[Provider]) -> str:
|
|
187
|
+
provider_list = "\n".join(f" {provider.id:<20} {provider.name}" for provider in providers)
|
|
188
|
+
return (
|
|
189
|
+
"Usage: pi-ai <command> [provider]\n\n"
|
|
190
|
+
"Commands:\n"
|
|
191
|
+
" login [provider] Login to an OAuth provider\n"
|
|
192
|
+
" list List available providers\n\n"
|
|
193
|
+
f"Providers:\n{provider_list}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def _login(models: Models, provider_id: str, interaction: _ConsoleInteraction) -> None:
|
|
198
|
+
# This CLI does not persist an installation ID; apps should reuse one across logins.
|
|
199
|
+
await models.login(provider_id, "oauth", interaction, LoginOptions(get_device_id=lambda: str(uuid.uuid4())))
|
|
200
|
+
print(f"\nCredentials saved to {AUTH_FILE}")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def run(
|
|
204
|
+
argv: Sequence[str],
|
|
205
|
+
*,
|
|
206
|
+
models: Models | None = None,
|
|
207
|
+
lines: _LineReader | None = None,
|
|
208
|
+
open_browser: Callable[[str], None] = _open_browser,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Run the CLI with ``argv`` (without the program name). Raises on errors."""
|
|
211
|
+
if models is None:
|
|
212
|
+
from .providers.all import builtin_models
|
|
213
|
+
|
|
214
|
+
models = builtin_models(credentials=FileCredentialStore(AUTH_FILE))
|
|
215
|
+
providers = _oauth_providers(models)
|
|
216
|
+
reader = lines if lines is not None else _LineReader()
|
|
217
|
+
|
|
218
|
+
command = argv[0] if argv else None
|
|
219
|
+
if not command or command in ("help", "--help", "-h"):
|
|
220
|
+
print(_usage(providers))
|
|
221
|
+
return
|
|
222
|
+
if command == "list":
|
|
223
|
+
for provider in providers:
|
|
224
|
+
print(f"{provider.id:<20} {provider.name}")
|
|
225
|
+
return
|
|
226
|
+
if command == "login":
|
|
227
|
+
interaction = _ConsoleInteraction(reader, open_browser=open_browser)
|
|
228
|
+
provider_id = argv[1] if len(argv) > 1 else None
|
|
229
|
+
if not provider_id:
|
|
230
|
+
for index, provider in enumerate(providers):
|
|
231
|
+
print(f" {index + 1}. {provider.name}")
|
|
232
|
+
print(f"Enter number (1-{len(providers)}): ", end="", flush=True)
|
|
233
|
+
choice = _parse_int(await reader.readline())
|
|
234
|
+
provider_id = providers[choice - 1].id if choice is not None and 1 <= choice <= len(providers) else None
|
|
235
|
+
if not provider_id or not any(provider.id == provider_id for provider in providers):
|
|
236
|
+
raise ValueError(f"Unknown provider: {provider_id or ''}")
|
|
237
|
+
await _login(models, provider_id, interaction)
|
|
238
|
+
return
|
|
239
|
+
raise ValueError(f"Unknown command: {command}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
async def _main(argv: Sequence[str]) -> None:
|
|
243
|
+
from .utils.http import aclose_http_clients
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
await run(argv)
|
|
247
|
+
finally:
|
|
248
|
+
await aclose_http_clients()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
252
|
+
"""Entry point of the ``pi-ai`` script."""
|
|
253
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
254
|
+
try:
|
|
255
|
+
asyncio.run(_main(args))
|
|
256
|
+
except KeyboardInterrupt:
|
|
257
|
+
print("\nLogin cancelled", file=sys.stderr)
|
|
258
|
+
sys.exit(130)
|
|
259
|
+
except Exception as error:
|
|
260
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
main()
|