Skip to content

Commit 6202534

Browse files
committed
feat: updated cli
1 parent e8c9794 commit 6202534

19 files changed

Lines changed: 1582 additions & 3 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ logger/
1010
site/
1111
.*/
1212

13+
remote-server/
14+
1315
*.md
1416
!README.md
1517
!CODE_OF_CONDUCT.md
@@ -21,3 +23,4 @@ coverage.xml
2123
.coverage
2224
.pytest_cache/
2325
venv/
26+
test

src/fenn/cli/__init__.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import argparse
22

3+
import fenn.cli.auth as auth
34
import fenn.cli.dashboard as dashboard
45
import fenn.cli.list as list
56
import fenn.cli.pull as pull
7+
import fenn.cli.run as run
68

79

810
def build_parser() -> argparse.ArgumentParser:
@@ -61,6 +63,92 @@ def build_parser() -> argparse.ArgumentParser:
6163
p_dash.add_argument("--debug", action="store_true", help="Run Flask in debug mode")
6264
p_dash.set_defaults(func=dashboard.execute)
6365

66+
# ========= RUN =========
67+
p_run = subparsers.add_parser(
68+
"run",
69+
help="Run a Fenn project locally or on a remote host",
70+
)
71+
p_run.add_argument(
72+
"script",
73+
nargs="?",
74+
default=None,
75+
help="Path to the entrypoint script (default: main.py)",
76+
)
77+
p_run.add_argument(
78+
"--host",
79+
default=None,
80+
help="Remote host URL (e.g. https://api.fenn.dev). If omitted, runs locally.",
81+
)
82+
p_run.add_argument(
83+
"--api-key",
84+
default=None,
85+
help="API key (overrides env, credentials file, and .env)",
86+
)
87+
p_run.add_argument(
88+
"--profile",
89+
default=None,
90+
help="Credentials profile name (default: 'default' or $FENN_PROFILE)",
91+
)
92+
p_run.add_argument(
93+
"--max-runtime",
94+
type=int,
95+
default=3600,
96+
help="Maximum allowed wall-time in seconds (server enforces; default: 3600)",
97+
)
98+
p_run.add_argument(
99+
"--detach",
100+
action="store_true",
101+
help="Submit the job and exit without streaming logs",
102+
)
103+
p_run.add_argument(
104+
"--no-download",
105+
action="store_true",
106+
help="Do not download artifacts on completion",
107+
)
108+
p_run.add_argument(
109+
"--include",
110+
action="append",
111+
metavar="PATH",
112+
help="Extra path (relative to CWD) to include in the upload tarball",
113+
)
114+
p_run.add_argument(
115+
"--exclude",
116+
action="append",
117+
metavar="PATTERN",
118+
help="Extra shell-glob pattern to exclude from the upload tarball",
119+
)
120+
p_run.set_defaults(func=run.execute)
121+
122+
# ========= AUTH =========
123+
p_auth = subparsers.add_parser(
124+
"auth", help="Manage credentials for the Fenn remote service"
125+
)
126+
auth_subparsers = p_auth.add_subparsers(dest="auth_command", required=True)
127+
128+
p_login = auth_subparsers.add_parser(
129+
"login", help="Save an API key for a profile"
130+
)
131+
p_login.add_argument("--profile", default=None, help="Profile name (default: 'default')")
132+
p_login.add_argument("--host", default=None, help="Default host for this profile")
133+
p_login.add_argument(
134+
"--api-key",
135+
default=None,
136+
help="API key (if omitted, will prompt or read from stdin)",
137+
)
138+
p_login.set_defaults(func=auth.execute)
139+
140+
p_status = auth_subparsers.add_parser(
141+
"status", help="Show the currently configured profile and credit balance"
142+
)
143+
p_status.add_argument("--profile", default=None, help="Profile name (default: 'default')")
144+
p_status.set_defaults(func=auth.execute)
145+
146+
p_logout = auth_subparsers.add_parser(
147+
"logout", help="Remove a profile from the credentials file"
148+
)
149+
p_logout.add_argument("--profile", default=None, help="Profile name (default: 'default')")
150+
p_logout.set_defaults(func=auth.execute)
151+
64152
return parser
65153

66154

src/fenn/cli/auth.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""``fenn auth`` — manage credentials for the Fenn remote service."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import getpass
7+
import sys
8+
9+
from colorama import Fore, Style
10+
11+
from fenn.remote.credentials import (
12+
DEFAULT_PROFILE,
13+
delete_profile,
14+
load_credentials,
15+
mask_key,
16+
write_credentials,
17+
)
18+
from fenn.remote.exceptions import RemoteError
19+
20+
21+
def execute(args: argparse.Namespace) -> None:
22+
sub = getattr(args, "auth_command", None)
23+
if sub is None:
24+
print(
25+
f"{Fore.RED}Missing auth subcommand. Try: "
26+
f"{Fore.LIGHTYELLOW_EX}fenn auth login{Style.RESET_ALL}",
27+
file=sys.stderr,
28+
)
29+
sys.exit(1)
30+
31+
if sub == "login":
32+
_login(args)
33+
elif sub == "status":
34+
_status(args)
35+
elif sub == "logout":
36+
_logout(args)
37+
else:
38+
print(f"{Fore.RED}Unknown auth subcommand: {sub}{Style.RESET_ALL}", file=sys.stderr)
39+
sys.exit(1)
40+
41+
42+
def _login(args: argparse.Namespace) -> None:
43+
profile = args.profile or DEFAULT_PROFILE
44+
host = args.host
45+
46+
api_key = args.api_key
47+
if not api_key:
48+
if sys.stdin.isatty():
49+
api_key = getpass.getpass(
50+
f"Paste Fenn API key for profile [{profile}]: "
51+
).strip()
52+
else:
53+
api_key = sys.stdin.readline().strip()
54+
55+
if not api_key:
56+
print(f"{Fore.RED}No API key provided.{Style.RESET_ALL}", file=sys.stderr)
57+
sys.exit(1)
58+
59+
path = write_credentials(api_key, profile=profile, host=host)
60+
print(
61+
f"{Fore.GREEN}Saved credentials to "
62+
f"{Fore.LIGHTYELLOW_EX}{path}{Fore.GREEN} (profile: {profile}).{Style.RESET_ALL}"
63+
)
64+
65+
66+
def _status(args: argparse.Namespace) -> None:
67+
profile = args.profile or DEFAULT_PROFILE
68+
creds = load_credentials(profile)
69+
if creds is None:
70+
print(
71+
f"{Fore.YELLOW}No saved credentials for profile {profile!r}. "
72+
f"Run {Fore.LIGHTYELLOW_EX}fenn auth login{Fore.YELLOW} to add one.{Style.RESET_ALL}"
73+
)
74+
sys.exit(1)
75+
76+
print(f"{Fore.CYAN}profile : {Fore.LIGHTYELLOW_EX}{creds.profile}{Style.RESET_ALL}")
77+
print(f"{Fore.CYAN}api_key : {Fore.LIGHTYELLOW_EX}{mask_key(creds.api_key)}{Style.RESET_ALL}")
78+
if creds.host:
79+
print(f"{Fore.CYAN}host : {Fore.LIGHTYELLOW_EX}{creds.host}{Style.RESET_ALL}")
80+
81+
if creds.host:
82+
try:
83+
from fenn.remote.client import RemoteClient
84+
85+
with RemoteClient(creds.host, creds.api_key) as client:
86+
me = client.me()
87+
credits_remaining = me.get("credits")
88+
plan = me.get("plan")
89+
print(
90+
f"{Fore.GREEN}credits : {Fore.LIGHTYELLOW_EX}{credits_remaining}"
91+
f"{Fore.GREEN} plan: {plan}{Style.RESET_ALL}"
92+
)
93+
except RemoteError as exc:
94+
print(
95+
f"{Fore.RED}Could not reach host: {exc}{Style.RESET_ALL}",
96+
file=sys.stderr,
97+
)
98+
99+
100+
def _logout(args: argparse.Namespace) -> None:
101+
profile = args.profile or DEFAULT_PROFILE
102+
if delete_profile(profile):
103+
print(
104+
f"{Fore.GREEN}Removed credentials for profile "
105+
f"{Fore.LIGHTYELLOW_EX}{profile}{Style.RESET_ALL}"
106+
)
107+
else:
108+
print(
109+
f"{Fore.YELLOW}No credentials found for profile {profile!r}.{Style.RESET_ALL}"
110+
)

0 commit comments

Comments
 (0)