inferenceport-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inferenceport_cli-1.0.0.dist-info/METADATA +132 -0
- inferenceport_cli-1.0.0.dist-info/RECORD +39 -0
- inferenceport_cli-1.0.0.dist-info/WHEEL +5 -0
- inferenceport_cli-1.0.0.dist-info/entry_points.txt +2 -0
- inferenceport_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
- inferenceport_cli-1.0.0.dist-info/top_level.txt +1 -0
- ipx/__init__.py +4 -0
- ipx/__main__.py +4 -0
- ipx/app.py +78 -0
- ipx/auth.py +260 -0
- ipx/chat.py +314 -0
- ipx/cli.py +142 -0
- ipx/clickx.py +160 -0
- ipx/cloud.py +340 -0
- ipx/commands/__init__.py +0 -0
- ipx/commands/agent.py +459 -0
- ipx/commands/auth.py +269 -0
- ipx/commands/cloudgen.py +559 -0
- ipx/commands/config_cmd.py +85 -0
- ipx/commands/featherless.py +252 -0
- ipx/commands/gen.py +3 -0
- ipx/commands/imports.py +198 -0
- ipx/commands/local.py +88 -0
- ipx/commands/mcp.py +182 -0
- ipx/commands/misc.py +168 -0
- ipx/commands/model.py +234 -0
- ipx/commands/org.py +915 -0
- ipx/commands/p2g.py +3 -0
- ipx/commands/server.py +328 -0
- ipx/commands/shield.py +1079 -0
- ipx/commands/usage.py +123 -0
- ipx/config.py +215 -0
- ipx/errors.py +41 -0
- ipx/http.py +373 -0
- ipx/local.py +90 -0
- ipx/mcp.py +134 -0
- ipx/policy.py +139 -0
- ipx/proxy.py +279 -0
- ipx/ui.py +360 -0
ipx/commands/auth.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""ipx auth - sign in, sign out, inspect the session and choose an organization context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from ipx import auth as A
|
|
14
|
+
from ipx import ui
|
|
15
|
+
from ipx.app import App, pass_app
|
|
16
|
+
from ipx.clickx import IpxGroup
|
|
17
|
+
from ipx.errors import ApiError, IpxError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group("auth", cls=IpxGroup, section="Account", short_help="Sign in/out, session status and organization context")
|
|
21
|
+
def group() -> None:
|
|
22
|
+
"""Manage your InferencePort AI identity.
|
|
23
|
+
|
|
24
|
+
Sign in with email and password, a browser OAuth flow (GitHub, Google, Microsoft, Hugging Face), your
|
|
25
|
+
organization's single sign-on, or a Lightning API key. `ipx auth context` selects the organization that
|
|
26
|
+
`ipx org ...` commands act on and that cloud usage is billed to.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_password(from_stdin: bool, given: Optional[str]) -> str:
|
|
31
|
+
if given:
|
|
32
|
+
return given
|
|
33
|
+
if from_stdin:
|
|
34
|
+
return sys.stdin.readline().rstrip("\r\n")
|
|
35
|
+
return ui.ask("Password", password=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@group.command("login", examples=[
|
|
39
|
+
"ipx auth login",
|
|
40
|
+
"ipx auth login --email me@example.com",
|
|
41
|
+
"ipx auth login --provider github",
|
|
42
|
+
"ipx auth login --sso me@company.com",
|
|
43
|
+
"ipx auth login --api-key ipa_live_xxxxxxxx",
|
|
44
|
+
"echo $PASSWORD | ipx auth login --email me@example.com --password-stdin",
|
|
45
|
+
])
|
|
46
|
+
@click.option("--email", help="Email address for password sign-in.")
|
|
47
|
+
@click.option("--password", help="Password (prefer --password-stdin; avoid shell history).")
|
|
48
|
+
@click.option("--password-stdin", is_flag=True, help="Read the password from standard input.")
|
|
49
|
+
@click.option("--api-key", help="Sign in with a Lightning API key (ipa_live_...).")
|
|
50
|
+
@click.option("--provider", type=click.Choice(sorted(A.OAUTH_PROVIDERS)), help="OAuth provider (opens your browser).")
|
|
51
|
+
@click.option("--sso", "sso_email", metavar="EMAIL", help="Single sign-on using your work email.")
|
|
52
|
+
@click.option("--paste", is_flag=True, help="Paste the browser redirect URL instead of using the local listener.")
|
|
53
|
+
@click.option("--no-browser", is_flag=True, help="Print the sign-in URL instead of opening a browser.")
|
|
54
|
+
@pass_app
|
|
55
|
+
def login(app: App, email, password, password_stdin, api_key, provider, sso_email, paste, no_browser) -> None:
|
|
56
|
+
"""Sign in to InferencePort AI."""
|
|
57
|
+
method = None
|
|
58
|
+
if api_key:
|
|
59
|
+
method = "api_key"
|
|
60
|
+
elif provider:
|
|
61
|
+
method = "oauth"
|
|
62
|
+
elif sso_email:
|
|
63
|
+
method = "sso"
|
|
64
|
+
elif email or password_stdin or password:
|
|
65
|
+
method = "password"
|
|
66
|
+
else:
|
|
67
|
+
ui.print_banner("sign in")
|
|
68
|
+
method = ui.choose("How would you like to sign in?", [
|
|
69
|
+
("password", "Email & password"),
|
|
70
|
+
("github", "GitHub"),
|
|
71
|
+
("google", "Google"),
|
|
72
|
+
("microsoft", "Microsoft"),
|
|
73
|
+
("huggingface", "Hugging Face"),
|
|
74
|
+
("sso", "Single sign-on (work email)"),
|
|
75
|
+
("api_key", "API key"),
|
|
76
|
+
])
|
|
77
|
+
if method in A.OAUTH_PROVIDERS:
|
|
78
|
+
provider, method = method, "oauth"
|
|
79
|
+
|
|
80
|
+
if method == "api_key":
|
|
81
|
+
key = api_key or ui.ask("API key (ipa_live_…)", password=True)
|
|
82
|
+
with ui.status("Verifying API key…"):
|
|
83
|
+
me = A.api_key_login(app.cfg, app.creds, key)
|
|
84
|
+
who = ((me.get("identity") or {}).get("email")) or "your account"
|
|
85
|
+
_signed_in(app, who, "API key")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if method == "password":
|
|
89
|
+
email = email or ui.ask("Email")
|
|
90
|
+
pw = _read_password(password_stdin, password)
|
|
91
|
+
with ui.status("Signing in…"):
|
|
92
|
+
session = A.password_login(app.cfg, app.creds, email, pw)
|
|
93
|
+
_signed_in(app, (session.get("user") or {}).get("email") or email, "Email & password")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# browser based: oauth / sso
|
|
97
|
+
if method == "sso":
|
|
98
|
+
sso_email = sso_email or ui.ask("Work email")
|
|
99
|
+
build = lambda redirect: A.sso_url(app.cfg, sso_email, redirect) # noqa: E731
|
|
100
|
+
label = "Single sign-on"
|
|
101
|
+
else:
|
|
102
|
+
build = lambda redirect: A.oauth_url(app.cfg, provider, redirect) # noqa: E731
|
|
103
|
+
label = A.OAUTH_PROVIDERS[provider]
|
|
104
|
+
|
|
105
|
+
if paste:
|
|
106
|
+
url = build(A.CALLBACK_PAGE)
|
|
107
|
+
ui.console.print(f"\n[accent]1.[/accent] Open this link and sign in:\n\n [underline]{url}[/underline]\n")
|
|
108
|
+
ui.console.print("[accent]2.[/accent] When the page finishes, copy the [bold]full address[/bold] from your browser and paste it here.\n")
|
|
109
|
+
session = A.parse_callback(ui.ask("Redirect URL"))
|
|
110
|
+
else:
|
|
111
|
+
def show(url: str) -> None:
|
|
112
|
+
ui.console.print(f"[muted]Opening your browser to sign in with {label}…[/muted]")
|
|
113
|
+
ui.console.print(f"[muted]If it does not open, visit:[/muted]\n [underline]{url}[/underline]\n")
|
|
114
|
+
|
|
115
|
+
def nudge() -> None:
|
|
116
|
+
ui.console.print("[muted]Still waiting. If the browser is on another machine or never returns here, press Ctrl+C and "
|
|
117
|
+
"run again with --paste.[/muted]")
|
|
118
|
+
|
|
119
|
+
timer = threading.Timer(30.0, nudge)
|
|
120
|
+
timer.daemon = True
|
|
121
|
+
timer.start()
|
|
122
|
+
try:
|
|
123
|
+
with ui.status(f"Waiting for {label} sign-in in your browser… (Ctrl+C to cancel)"):
|
|
124
|
+
session = A.browser_login(app.cfg, build, open_browser=not no_browser, on_url=show)
|
|
125
|
+
finally:
|
|
126
|
+
timer.cancel()
|
|
127
|
+
with ui.status("Finishing sign-in…"):
|
|
128
|
+
user = A.complete_session(app.cfg, app.creds, session)
|
|
129
|
+
_signed_in(app, user.get("email") or "your account", label)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _signed_in(app: App, who: str, via: str) -> None:
|
|
133
|
+
if app.json:
|
|
134
|
+
ui.emit_json({"signed_in": True, "email": who, "method": via})
|
|
135
|
+
return
|
|
136
|
+
ui.ok(f"Signed in as [bold]{who}[/bold] [muted]via {via}[/muted]")
|
|
137
|
+
if not app.cfg.org:
|
|
138
|
+
ui.dim("Tip: pick an organization with `ipx auth context <org>` · try `ipx gen chat` to start chatting.")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@group.command("logout", short_help="Sign out and forget stored credentials")
|
|
142
|
+
@pass_app
|
|
143
|
+
def logout(app: App) -> None:
|
|
144
|
+
"""Sign out and remove stored credentials (the org context is kept)."""
|
|
145
|
+
if not app.creds.data:
|
|
146
|
+
ui.info("You were not signed in.")
|
|
147
|
+
return
|
|
148
|
+
with ui.status("Signing out…"):
|
|
149
|
+
A.sign_out(app.cfg, app.creds)
|
|
150
|
+
app.out({"signed_in": False}, lambda _: ui.ok("Signed out. Local credentials removed."))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@group.command("status", short_help="Show who you are signed in as")
|
|
154
|
+
@pass_app
|
|
155
|
+
def status(app: App) -> None:
|
|
156
|
+
"""Show the active identity, session expiry, plan and organization context."""
|
|
157
|
+
c = app.creds
|
|
158
|
+
if not c.signed_in:
|
|
159
|
+
app.out({"signed_in": False}, lambda _: ui.fail("Not signed in.", "Run `ipx auth login`."))
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
plan: Dict[str, Any] = {}
|
|
162
|
+
try:
|
|
163
|
+
plan = app.api.get("/subscription", org=False) or {}
|
|
164
|
+
except IpxError:
|
|
165
|
+
pass
|
|
166
|
+
exp = None
|
|
167
|
+
if c.auth_type == "jwt" and c.expires_at:
|
|
168
|
+
exp = dt.datetime.fromtimestamp(c.expires_at).strftime("%Y-%m-%d %H:%M")
|
|
169
|
+
data = {
|
|
170
|
+
"signed_in": True,
|
|
171
|
+
"email": plan.get("email") or c.email,
|
|
172
|
+
"auth_type": c.auth_type,
|
|
173
|
+
"plan": plan.get("plan_name") or plan.get("plan_key"),
|
|
174
|
+
"session_expires": exp,
|
|
175
|
+
"org": app.org,
|
|
176
|
+
"api": app.cfg.base_url,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
def render(d: Dict[str, Any]) -> None:
|
|
180
|
+
rows = [
|
|
181
|
+
("Account", d["email"]),
|
|
182
|
+
("Sign-in", "API key" if d["auth_type"] == "api_key" else (c.data.get("provider") or "session").title()),
|
|
183
|
+
("Plan", d["plan"]),
|
|
184
|
+
("Session", f"refreshes automatically · token valid until {exp}" if exp else ("never expires (revoke in console)" if d["auth_type"] == "api_key" else None)),
|
|
185
|
+
("Organization", Text(d["org"], style="bold " + ui.VIOLET) if d["org"] else Text("personal (no org context)", style="muted")),
|
|
186
|
+
("API", d["api"]),
|
|
187
|
+
]
|
|
188
|
+
ui.console.print(ui.kv_panel("Session", rows))
|
|
189
|
+
|
|
190
|
+
app.out(data, render)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
group.alias("whoami", "status")
|
|
194
|
+
group.alias("signout", "logout")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@group.command("token", short_help="Print the current access token")
|
|
198
|
+
@pass_app
|
|
199
|
+
def token(app: App) -> None:
|
|
200
|
+
"""Print the bearer token for use with curl or other tools. Treat it like a password."""
|
|
201
|
+
app.require_login()
|
|
202
|
+
if app.creds.expiring():
|
|
203
|
+
A.refresh(app.cfg, app.creds)
|
|
204
|
+
sys.stdout.write((app.creds.access_token or "") + "\n")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _list_orgs(app: App) -> List[Dict[str, Any]]:
|
|
208
|
+
app.require_jwt("Listing organizations")
|
|
209
|
+
data = app.api.get("/v1/orgs", org=False) or {}
|
|
210
|
+
return list(data.get("orgs") or [])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def resolve_org(app: App, ref: str) -> Dict[str, Any]:
|
|
214
|
+
ref_l = ref.strip().lower()
|
|
215
|
+
orgs = _list_orgs(app)
|
|
216
|
+
for o in orgs:
|
|
217
|
+
if ref_l in (str(o.get("slug", "")).lower(), str(o.get("id", "")).lower()):
|
|
218
|
+
return o
|
|
219
|
+
for o in orgs:
|
|
220
|
+
if ref_l == str(o.get("name", "")).lower():
|
|
221
|
+
return o
|
|
222
|
+
names = ", ".join(o.get("slug", "?") for o in orgs) or "none"
|
|
223
|
+
raise IpxError(f"You are not a member of an organization matching “{ref}”.", f"Your organizations: {names}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@group.command("context", examples=["ipx auth context", "ipx auth context acme", "ipx auth context --clear"],
|
|
227
|
+
short_help="Show or set the active organization")
|
|
228
|
+
@click.argument("org", required=False)
|
|
229
|
+
@click.option("--clear", is_flag=True, help="Return to your personal account.")
|
|
230
|
+
@pass_app
|
|
231
|
+
def context(app: App, org: Optional[str], clear: bool) -> None:
|
|
232
|
+
"""Show or set the organization that org commands and cloud usage apply to.
|
|
233
|
+
|
|
234
|
+
ORG may be a slug, id or name. With no argument, shows the current context (and offers a picker in a terminal).
|
|
235
|
+
"""
|
|
236
|
+
if clear:
|
|
237
|
+
app.cfg.set_org(None)
|
|
238
|
+
app.out({"org": None}, lambda _: ui.ok("Context cleared — using your personal account."))
|
|
239
|
+
return
|
|
240
|
+
if not org:
|
|
241
|
+
current = app.cfg.org
|
|
242
|
+
if app.json or not ui.is_tty() or not app.creds.signed_in or app.creds.auth_type == "api_key":
|
|
243
|
+
app.out({"org": current}, lambda _: ui.console.print(
|
|
244
|
+
f"Organization context: [bold {ui.VIOLET}]{current}[/]" if current else "[muted]No organization context (personal account).[/muted]"))
|
|
245
|
+
return
|
|
246
|
+
orgs = _list_orgs(app)
|
|
247
|
+
if not orgs:
|
|
248
|
+
ui.info("You are not a member of any organization yet. Create one with `ipx org create`.")
|
|
249
|
+
return
|
|
250
|
+
opts = [("", "Personal account")] + [(o["slug"], f"{o['name']} [muted]({o['slug']} · {o.get('role')})[/muted]") for o in orgs]
|
|
251
|
+
default = next((i for i, (v, _) in enumerate(opts) if v == current), 0)
|
|
252
|
+
org = ui.choose("Which context should ipx use?", opts, default)
|
|
253
|
+
if not org:
|
|
254
|
+
app.cfg.set_org(None)
|
|
255
|
+
ui.ok("Using your personal account.")
|
|
256
|
+
return
|
|
257
|
+
if app.creds.auth_type == "api_key":
|
|
258
|
+
try:
|
|
259
|
+
app.api.get(f"/v1/orgs/{org.lower()}", org=False)
|
|
260
|
+
except ApiError as e:
|
|
261
|
+
raise IpxError(f"Could not access organization “{org}”: {e.message}", "Check the slug and that your API key belongs to it.")
|
|
262
|
+
slug = org.lower()
|
|
263
|
+
info: Dict[str, Any] = {"slug": slug, "name": slug}
|
|
264
|
+
else:
|
|
265
|
+
info = resolve_org(app, org)
|
|
266
|
+
slug = info["slug"]
|
|
267
|
+
app.cfg.set_org(slug)
|
|
268
|
+
app.out({"org": slug, "role": info.get("role")}, lambda _: ui.ok(
|
|
269
|
+
f"Context set to [bold {ui.VIOLET}]{info.get('name', slug)}[/] [muted]({slug}{' · ' + info['role'] if info.get('role') else ''})[/muted]"))
|