socaity-cli 0.1.1__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.
- socaity_cli/__init__.py +30 -0
- socaity_cli/__main__.py +4 -0
- socaity_cli/apipod_commands.py +82 -0
- socaity_cli/auth.py +197 -0
- socaity_cli/backend_client.py +23 -0
- socaity_cli/cli.py +352 -0
- socaity_cli/clients/__init__.py +18 -0
- socaity_cli/clients/analytics.py +60 -0
- socaity_cli/clients/catalog.py +147 -0
- socaity_cli/clients/deployment.py +129 -0
- socaity_cli/clients/files.py +82 -0
- socaity_cli/clients/jobs.py +80 -0
- socaity_cli/clients/projects.py +67 -0
- socaity_cli/clients/sdk_install.py +16 -0
- socaity_cli/credentials.py +128 -0
- socaity_cli/deployment.py +349 -0
- socaity_cli/errors.py +35 -0
- socaity_cli/http.py +105 -0
- socaity_cli/prompts.py +17 -0
- socaity_cli/registry_push.py +122 -0
- socaity_cli/requires.py +47 -0
- socaity_cli/sdk_commands.py +28 -0
- socaity_cli-0.1.1.dist-info/METADATA +73 -0
- socaity_cli-0.1.1.dist-info/RECORD +27 -0
- socaity_cli-0.1.1.dist-info/WHEEL +5 -0
- socaity_cli-0.1.1.dist-info/entry_points.txt +2 -0
- socaity_cli-0.1.1.dist-info/top_level.txt +1 -0
socaity_cli/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Socaity platform CLI.
|
|
2
|
+
|
|
3
|
+
Lightweight bundle of all Socaity CLI commands and the backend HTTP client
|
|
4
|
+
(httpx + socaity-schemas only). Heavy features delegate to the optional
|
|
5
|
+
``socaity`` (SDK installs) and ``apipod`` (deployment tooling) packages via
|
|
6
|
+
the ``requires`` decorator.
|
|
7
|
+
"""
|
|
8
|
+
from socaity_cli.backend_client import SocaityBackendClient
|
|
9
|
+
from socaity_cli.credentials import (
|
|
10
|
+
Credentials,
|
|
11
|
+
clear_credentials,
|
|
12
|
+
get_api_key,
|
|
13
|
+
load_credentials,
|
|
14
|
+
require_credentials,
|
|
15
|
+
save_credentials,
|
|
16
|
+
)
|
|
17
|
+
from socaity_cli.requires import requires
|
|
18
|
+
from socaity_cli.prompts import input_yes_no
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SocaityBackendClient",
|
|
22
|
+
"Credentials",
|
|
23
|
+
"clear_credentials",
|
|
24
|
+
"get_api_key",
|
|
25
|
+
"load_credentials",
|
|
26
|
+
"require_credentials",
|
|
27
|
+
"save_credentials",
|
|
28
|
+
"requires",
|
|
29
|
+
"input_yes_no",
|
|
30
|
+
]
|
socaity_cli/__main__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Bridge to the APIPod CLI (requires ``pip install apipod``).
|
|
2
|
+
|
|
3
|
+
Imports stay inside the functions: ``apipod`` is optional for the CLI and the
|
|
4
|
+
``requires`` decorator guards each entry with a friendly install hint.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from socaity_cli.requires import requires
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _with_project_dir(argv: List[str], project_dir: Optional[str]) -> List[str]:
|
|
16
|
+
if project_dir:
|
|
17
|
+
return ["-C", project_dir, *argv]
|
|
18
|
+
return argv
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@requires("apipod")
|
|
22
|
+
def _run_apipod_argv(argv: List[str]) -> None:
|
|
23
|
+
import apipod.cli
|
|
24
|
+
|
|
25
|
+
previous = sys.argv
|
|
26
|
+
sys.argv = ["apipod", *argv]
|
|
27
|
+
try:
|
|
28
|
+
apipod.cli.main()
|
|
29
|
+
finally:
|
|
30
|
+
sys.argv = previous
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_scan(*, project_dir: Optional[str] = None) -> None:
|
|
34
|
+
_run_apipod_argv(_with_project_dir(["scan"], project_dir))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_build(target_file: Optional[str], *, project_dir: Optional[str] = None) -> None:
|
|
38
|
+
argv = ["build"]
|
|
39
|
+
if target_file:
|
|
40
|
+
argv.append(target_file)
|
|
41
|
+
_run_apipod_argv(_with_project_dir(argv, project_dir))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_start(
|
|
45
|
+
*,
|
|
46
|
+
entrypoint: Optional[str],
|
|
47
|
+
host: Optional[str],
|
|
48
|
+
port: Optional[int],
|
|
49
|
+
project_dir: Optional[str] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
argv = ["start"]
|
|
52
|
+
if entrypoint:
|
|
53
|
+
argv.append(entrypoint)
|
|
54
|
+
if host:
|
|
55
|
+
argv.extend(["--host", host])
|
|
56
|
+
if port is not None:
|
|
57
|
+
argv.extend(["--port", str(port)])
|
|
58
|
+
_run_apipod_argv(_with_project_dir(argv, project_dir))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run_deploy(
|
|
62
|
+
*,
|
|
63
|
+
target: Optional[str] = None,
|
|
64
|
+
project_dir: Optional[str] = None,
|
|
65
|
+
resume: Optional[str] = None,
|
|
66
|
+
skip_build: bool = False,
|
|
67
|
+
push_only: bool = False,
|
|
68
|
+
yes: bool = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Wrap ``apipod deploy`` (analyze, build, push, provision)."""
|
|
71
|
+
argv = ["deploy"]
|
|
72
|
+
if target:
|
|
73
|
+
argv.append(target)
|
|
74
|
+
if resume:
|
|
75
|
+
argv.extend(["--resume", resume])
|
|
76
|
+
if skip_build:
|
|
77
|
+
argv.append("--skip-build")
|
|
78
|
+
if push_only:
|
|
79
|
+
argv.append("--push-only")
|
|
80
|
+
if yes:
|
|
81
|
+
argv.append("--yes")
|
|
82
|
+
_run_apipod_argv(_with_project_dir(argv, project_dir))
|
socaity_cli/auth.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Browser-based CLI login (device authorization flow via web UI)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
import webbrowser
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
from urllib.parse import urljoin
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from socaity_cli.credentials import (
|
|
16
|
+
Credentials,
|
|
17
|
+
DEFAULT_BACKEND_URL,
|
|
18
|
+
DEFAULT_FRONTEND_URL,
|
|
19
|
+
require_credentials,
|
|
20
|
+
save_credentials,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
USER_AGENT = "socaity-cli/0.1.0"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _backend_base_url() -> str:
|
|
27
|
+
return os.environ.get("SOCAITY_BACKEND_URL", DEFAULT_BACKEND_URL).rstrip("/")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _frontend_base_url(frontend_url: Optional[str] = None) -> str:
|
|
31
|
+
if frontend_url is not None:
|
|
32
|
+
value = frontend_url.strip()
|
|
33
|
+
return value.rstrip("/") if value else DEFAULT_FRONTEND_URL
|
|
34
|
+
return os.environ.get("SOCAITY_FRONTEND_URL", DEFAULT_FRONTEND_URL).rstrip("/")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _web_api_url(base: str) -> str:
|
|
38
|
+
return urljoin(base + "/", "v1/")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def open_login_url(url: str) -> bool:
|
|
42
|
+
try:
|
|
43
|
+
if webbrowser.open(url):
|
|
44
|
+
return True
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
for cmd in ("wslview", "xdg-open"):
|
|
48
|
+
exe = shutil.which(cmd)
|
|
49
|
+
if not exe:
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
subprocess.run(
|
|
53
|
+
[exe, url],
|
|
54
|
+
check=False,
|
|
55
|
+
timeout=8,
|
|
56
|
+
stdout=subprocess.DEVNULL,
|
|
57
|
+
stderr=subprocess.DEVNULL,
|
|
58
|
+
)
|
|
59
|
+
return True
|
|
60
|
+
except Exception:
|
|
61
|
+
continue
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def start_cli_login(
|
|
66
|
+
*,
|
|
67
|
+
backend_url: Optional[str] = None,
|
|
68
|
+
frontend_url: Optional[str] = None,
|
|
69
|
+
expires_in_seconds: int = 600,
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
base = (backend_url or _backend_base_url()).rstrip("/")
|
|
72
|
+
payload: Dict[str, Any] = {"expires_in_seconds": expires_in_seconds}
|
|
73
|
+
payload["frontend_base_url"] = _frontend_base_url(frontend_url)
|
|
74
|
+
with httpx.Client(timeout=20.0) as client:
|
|
75
|
+
response = client.post(
|
|
76
|
+
_web_api_url(base) + "cli-auth/start",
|
|
77
|
+
json=payload,
|
|
78
|
+
headers={"User-Agent": USER_AGENT},
|
|
79
|
+
)
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
return response.json()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def poll_cli_login(
|
|
85
|
+
*,
|
|
86
|
+
request_id: str,
|
|
87
|
+
poll_code: str,
|
|
88
|
+
backend_url: Optional[str] = None,
|
|
89
|
+
) -> Dict[str, Any]:
|
|
90
|
+
base = (backend_url or _backend_base_url()).rstrip("/")
|
|
91
|
+
with httpx.Client(timeout=20.0) as client:
|
|
92
|
+
response = client.post(
|
|
93
|
+
_web_api_url(base) + "cli-auth/poll",
|
|
94
|
+
json={"request_id": request_id, "poll_code": poll_code},
|
|
95
|
+
headers={"User-Agent": USER_AGENT},
|
|
96
|
+
)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
return response.json()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def fetch_profile(*, api_key: str, backend_url: str) -> Dict[str, Any]:
|
|
102
|
+
base = backend_url.rstrip("/")
|
|
103
|
+
with httpx.Client(timeout=20.0) as client:
|
|
104
|
+
response = client.get(
|
|
105
|
+
_web_api_url(base) + "profile/profile",
|
|
106
|
+
headers={
|
|
107
|
+
"Authorization": f"Bearer {api_key}",
|
|
108
|
+
"User-Agent": USER_AGENT,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
return response.json()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def run_login(
|
|
116
|
+
*,
|
|
117
|
+
no_browser: bool = False,
|
|
118
|
+
timeout: int = 300,
|
|
119
|
+
backend_url: Optional[str] = None,
|
|
120
|
+
frontend_url: Optional[str] = None,
|
|
121
|
+
) -> Credentials:
|
|
122
|
+
base = (backend_url or _backend_base_url()).rstrip("/")
|
|
123
|
+
|
|
124
|
+
print("Login to Socaity")
|
|
125
|
+
session = start_cli_login(
|
|
126
|
+
backend_url=base,
|
|
127
|
+
frontend_url=frontend_url,
|
|
128
|
+
)
|
|
129
|
+
verification_uri = session["verification_uri"]
|
|
130
|
+
request_id = session["request_id"]
|
|
131
|
+
poll_code = session["poll_code"]
|
|
132
|
+
interval = max(int(session.get("interval_seconds", 2)), 1)
|
|
133
|
+
|
|
134
|
+
print(f"Open this URL to authenticate:\n{verification_uri}")
|
|
135
|
+
|
|
136
|
+
if not no_browser:
|
|
137
|
+
if open_login_url(verification_uri):
|
|
138
|
+
print("Browser opened for login.")
|
|
139
|
+
else:
|
|
140
|
+
print("Could not open a browser automatically. Open the URL manually.")
|
|
141
|
+
|
|
142
|
+
print("Waiting for login confirmation...")
|
|
143
|
+
deadline = time.time() + max(timeout, 10)
|
|
144
|
+
|
|
145
|
+
while time.time() < deadline:
|
|
146
|
+
poll_response = poll_cli_login(
|
|
147
|
+
request_id=request_id,
|
|
148
|
+
poll_code=poll_code,
|
|
149
|
+
backend_url=base,
|
|
150
|
+
)
|
|
151
|
+
status = poll_response.get("status")
|
|
152
|
+
|
|
153
|
+
if status == "pending":
|
|
154
|
+
time.sleep(interval)
|
|
155
|
+
continue
|
|
156
|
+
if status == "expired":
|
|
157
|
+
print("Login request expired. Run: socaity login")
|
|
158
|
+
raise SystemExit(1)
|
|
159
|
+
if status == "approved":
|
|
160
|
+
api_key = (poll_response.get("api_key") or "").strip()
|
|
161
|
+
if not api_key:
|
|
162
|
+
print("Login failed: missing API key in response.")
|
|
163
|
+
raise SystemExit(1)
|
|
164
|
+
creds = Credentials(
|
|
165
|
+
api_key=api_key,
|
|
166
|
+
user_id=poll_response.get("user_id"),
|
|
167
|
+
user_email=poll_response.get("user_email"),
|
|
168
|
+
backend_url=base,
|
|
169
|
+
)
|
|
170
|
+
path = save_credentials(creds)
|
|
171
|
+
email = creds.user_email or "user"
|
|
172
|
+
print(f"Logged in as {email}")
|
|
173
|
+
print(f"Credentials saved to {path}")
|
|
174
|
+
return creds
|
|
175
|
+
|
|
176
|
+
print(f"Login failed: unexpected status '{status}'")
|
|
177
|
+
raise SystemExit(1)
|
|
178
|
+
|
|
179
|
+
print("Login timed out. Run: socaity login")
|
|
180
|
+
raise SystemExit(1)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def run_whoami() -> None:
|
|
184
|
+
creds = require_credentials()
|
|
185
|
+
email = creds.user_email
|
|
186
|
+
user_id = creds.user_id
|
|
187
|
+
if not email or not user_id:
|
|
188
|
+
try:
|
|
189
|
+
profile = fetch_profile(api_key=creds.api_key, backend_url=creds.backend_url)
|
|
190
|
+
email = email or profile.get("email") or profile.get("user_email")
|
|
191
|
+
user_id = user_id or profile.get("id") or profile.get("user_id")
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
print(f"Could not fetch profile: {exc}")
|
|
194
|
+
raise SystemExit(1) from exc
|
|
195
|
+
print(f"user_id: {user_id}")
|
|
196
|
+
print(f"email: {email}")
|
|
197
|
+
print(f"backend: {creds.backend_url}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Composed platform backend client (catalog, jobs, projects, analytics, files, sdk, deployment)."""
|
|
2
|
+
from socaity_cli.clients.analytics import AnalyticsClient
|
|
3
|
+
from socaity_cli.clients.catalog import CatalogClient
|
|
4
|
+
from socaity_cli.clients.deployment import DeploymentClient
|
|
5
|
+
from socaity_cli.clients.files import FilesClient
|
|
6
|
+
from socaity_cli.clients.jobs import JobsClient
|
|
7
|
+
from socaity_cli.clients.projects import ProjectsClient
|
|
8
|
+
from socaity_cli.clients.sdk_install import SdkInstallClient
|
|
9
|
+
from socaity_cli.http import BackendHttp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SocaityBackendClient(
|
|
13
|
+
BackendHttp,
|
|
14
|
+
CatalogClient,
|
|
15
|
+
JobsClient,
|
|
16
|
+
ProjectsClient,
|
|
17
|
+
AnalyticsClient,
|
|
18
|
+
FilesClient,
|
|
19
|
+
SdkInstallClient,
|
|
20
|
+
DeploymentClient,
|
|
21
|
+
):
|
|
22
|
+
"""Facade over domain mixins. Prefer importing a domain client when you only need one surface."""
|
|
23
|
+
pass
|
socaity_cli/cli.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Unified Socaity CLI: auth, catalog, SDK installs, and APIPod deployment tooling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from socaity_cli.apipod_commands import run_build, run_deploy, run_scan, run_start
|
|
8
|
+
from socaity_cli.auth import run_login, run_whoami
|
|
9
|
+
from socaity_cli.backend_client import SocaityBackendClient
|
|
10
|
+
from socaity_cli.credentials import clear_credentials, require_credentials
|
|
11
|
+
from socaity_cli.sdk_commands import run_install, run_update
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run_list(entity: str, category: str | None, family: str | None, limit: int) -> None:
|
|
15
|
+
client = SocaityBackendClient()
|
|
16
|
+
if entity == "services":
|
|
17
|
+
services = client.list_services(category=category, limit=limit)
|
|
18
|
+
if not services:
|
|
19
|
+
print("No services found.")
|
|
20
|
+
return
|
|
21
|
+
for service in services:
|
|
22
|
+
name = service.display_name or service.name or service.id
|
|
23
|
+
official = " [official]" if service.is_official else ""
|
|
24
|
+
desc = f" - {service.short_desc}" if service.short_desc else ""
|
|
25
|
+
print(f"{name}{official}{desc}")
|
|
26
|
+
else:
|
|
27
|
+
models = client.list_models(family=family, limit=limit)
|
|
28
|
+
if not models:
|
|
29
|
+
print("No models found.")
|
|
30
|
+
return
|
|
31
|
+
for model in models:
|
|
32
|
+
name = model.display_name or model.name
|
|
33
|
+
family_info = f" ({model.family})" if model.family else ""
|
|
34
|
+
print(f"{name}{family_info}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _split_csv(value: str | None) -> list[str]:
|
|
38
|
+
return [part.strip() for part in (value or "").split(",") if part.strip()]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _run_jobs(
|
|
42
|
+
job_id: str | None,
|
|
43
|
+
expand: str | None,
|
|
44
|
+
status: str | None,
|
|
45
|
+
visibility: str,
|
|
46
|
+
limit: int,
|
|
47
|
+
q: str | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
client = SocaityBackendClient()
|
|
50
|
+
expand_list = _split_csv(expand)
|
|
51
|
+
|
|
52
|
+
def _print_job(job) -> None:
|
|
53
|
+
cost = ""
|
|
54
|
+
if job.billing and job.billing.cost_amount is not None:
|
|
55
|
+
cost = f" {job.billing.cost_amount:.4f} {job.billing.cost_currency or 'EUR'}"
|
|
56
|
+
name = f" '{job.data.display_name}'" if job.data and job.data.display_name else ""
|
|
57
|
+
print(f"{job.id} [{job.status}] {job.endpoint or '?'} {job.created_at or ''}{cost}{name}")
|
|
58
|
+
|
|
59
|
+
if job_id:
|
|
60
|
+
job = client.get_job(job_id, expand=expand_list)
|
|
61
|
+
if not job:
|
|
62
|
+
print(f"Job '{job_id}' not found.")
|
|
63
|
+
return
|
|
64
|
+
print(job.model_dump_json(indent=2, exclude_none=True))
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
filters = [f"status:eq:{status}"] if status else None
|
|
68
|
+
jobs = client.query_jobs(
|
|
69
|
+
q=q, filters=filters, expand=expand_list, visibility=visibility, limit=limit,
|
|
70
|
+
)
|
|
71
|
+
if not jobs:
|
|
72
|
+
print("No jobs found.")
|
|
73
|
+
return
|
|
74
|
+
for job in jobs:
|
|
75
|
+
_print_job(job)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run_projects(project_id: str | None, expand: str | None, limit: int) -> None:
|
|
79
|
+
client = SocaityBackendClient()
|
|
80
|
+
expand_list = _split_csv(expand)
|
|
81
|
+
|
|
82
|
+
if project_id:
|
|
83
|
+
project = client.get_project(project_id, expand=expand_list)
|
|
84
|
+
if not project:
|
|
85
|
+
print(f"Project '{project_id}' not found.")
|
|
86
|
+
return
|
|
87
|
+
print(project.model_dump_json(indent=2, exclude_none=True))
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
projects = client.query_projects(expand=expand_list, limit=limit)
|
|
91
|
+
if not projects:
|
|
92
|
+
print("No projects found.")
|
|
93
|
+
return
|
|
94
|
+
for project in projects:
|
|
95
|
+
name = project.display_name or project.id
|
|
96
|
+
print(f"{project.id} {name} ({project.n_jobs or 0} jobs, {project.n_files or 0} files)")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_search(query: str, collections: str | None, limit: int) -> None:
|
|
100
|
+
client = SocaityBackendClient()
|
|
101
|
+
targets = [part.strip() for part in (collections or "services,models").split(",") if part.strip()]
|
|
102
|
+
found = False
|
|
103
|
+
for collection in targets:
|
|
104
|
+
hits = client.search(query, collection=collection, limit=limit)
|
|
105
|
+
if not hits:
|
|
106
|
+
continue
|
|
107
|
+
found = True
|
|
108
|
+
for item in hits:
|
|
109
|
+
if collection == "jobs":
|
|
110
|
+
name = getattr(getattr(item, "data", None), "display_name", None) or item.id
|
|
111
|
+
status = f" [{item.status}]" if getattr(item, "status", None) else ""
|
|
112
|
+
print(f"[jobs] {name}{status}")
|
|
113
|
+
continue
|
|
114
|
+
name = item.display_name or item.name or item.id
|
|
115
|
+
desc = getattr(item, "short_desc", None) or getattr(item, "description", None) or ""
|
|
116
|
+
print(f"[{collection}] {name}" + (f" - {desc[:80]}" if desc else ""))
|
|
117
|
+
if not found:
|
|
118
|
+
print("No results.")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main(argv: list[str] | None = None) -> None:
|
|
122
|
+
parser = argparse.ArgumentParser(
|
|
123
|
+
prog="socaity",
|
|
124
|
+
description="Socaity platform CLI: authentication, catalog, SDK, and APIPod deployment",
|
|
125
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
126
|
+
epilog="""
|
|
127
|
+
Examples:
|
|
128
|
+
socaity login
|
|
129
|
+
socaity install black-forest-labs/flux-schnell
|
|
130
|
+
socaity update
|
|
131
|
+
socaity list services
|
|
132
|
+
socaity search "flux schnell"
|
|
133
|
+
socaity search "lighthouse" --collections jobs
|
|
134
|
+
socaity jobs --q lighthouse --expand data
|
|
135
|
+
socaity projects --expand jobs
|
|
136
|
+
socaity scan
|
|
137
|
+
socaity build main.py
|
|
138
|
+
socaity start --port 8123
|
|
139
|
+
socaity deploy serverless-runpod
|
|
140
|
+
socaity -C simple_test_service deploy serverless-runpod --yes
|
|
141
|
+
""",
|
|
142
|
+
)
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"-i", "--install",
|
|
145
|
+
metavar="SERVICE",
|
|
146
|
+
help="Install an AI service (requires login)",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"-C",
|
|
150
|
+
"--project-dir",
|
|
151
|
+
default=None,
|
|
152
|
+
metavar="DIR",
|
|
153
|
+
help="Service project directory for apipod commands (scan/build/start/deploy).",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
157
|
+
|
|
158
|
+
login_parser = subparsers.add_parser("login", help="Authenticate via browser and save credentials")
|
|
159
|
+
login_parser.add_argument("--no-browser", action="store_true", help="Do not open a browser automatically")
|
|
160
|
+
login_parser.add_argument("--timeout", type=int, default=300, help="Seconds to wait for browser login")
|
|
161
|
+
login_parser.add_argument(
|
|
162
|
+
"--backend-url",
|
|
163
|
+
default=None,
|
|
164
|
+
help="API base URL (default: SOCAITY_BACKEND_URL or https://webapi.socaity.ai)",
|
|
165
|
+
)
|
|
166
|
+
login_parser.add_argument(
|
|
167
|
+
"--frontend-url",
|
|
168
|
+
default=None,
|
|
169
|
+
help="Web UI base URL for sign-in (default: SOCAITY_FRONTEND_URL or https://socaity.ai)",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
subparsers.add_parser("logout", help="Remove stored credentials")
|
|
173
|
+
subparsers.add_parser("whoami", help="Show logged-in user")
|
|
174
|
+
|
|
175
|
+
install_parser = subparsers.add_parser("install", help="Install an AI service (requires socaity)")
|
|
176
|
+
install_parser.add_argument("service_name", help='Service name, ID, or "all"')
|
|
177
|
+
|
|
178
|
+
subparsers.add_parser("update", help="Sync installed services with the platform (requires socaity)")
|
|
179
|
+
|
|
180
|
+
list_parser = subparsers.add_parser("list", help="List catalog entries")
|
|
181
|
+
list_parser.add_argument("entity", choices=("services", "models"), help="What to list")
|
|
182
|
+
list_parser.add_argument("--category", default=None, help="Filter services by category id")
|
|
183
|
+
list_parser.add_argument("--family", default=None, help="Filter models by family")
|
|
184
|
+
list_parser.add_argument("--limit", type=int, default=50)
|
|
185
|
+
|
|
186
|
+
search_parser = subparsers.add_parser("search", help="Fuzzy-search services, models, or jobs")
|
|
187
|
+
search_parser.add_argument("query", help="Search text (typo-tolerant)")
|
|
188
|
+
search_parser.add_argument("--collections", default=None, help="Comma-separated: services,models,jobs")
|
|
189
|
+
search_parser.add_argument("--limit", type=int, default=20)
|
|
190
|
+
|
|
191
|
+
jobs_parser = subparsers.add_parser("jobs", help="List/search your jobs or inspect one (requires login)")
|
|
192
|
+
jobs_parser.add_argument("job_id", nargs="?", default=None, help="Job id for a detailed view")
|
|
193
|
+
jobs_parser.add_argument("--q", default=None, help="Fuzzy search over job prompts / display names")
|
|
194
|
+
jobs_parser.add_argument("--expand", default=None, help="Comma-separated: data,files,billing,social")
|
|
195
|
+
jobs_parser.add_argument("--status", default=None, help="Filter by status (e.g. FINISHED)")
|
|
196
|
+
jobs_parser.add_argument(
|
|
197
|
+
"--visibility",
|
|
198
|
+
choices=("user", "public", "user_visible"),
|
|
199
|
+
default="user_visible",
|
|
200
|
+
help="user: own jobs. public: published. user_visible: own + public (default).",
|
|
201
|
+
)
|
|
202
|
+
jobs_parser.add_argument("--limit", type=int, default=25)
|
|
203
|
+
|
|
204
|
+
projects_parser = subparsers.add_parser("projects", help="List your projects or inspect one (requires login)")
|
|
205
|
+
projects_parser.add_argument("project_id", nargs="?", default=None, help="Project id for a detailed view")
|
|
206
|
+
projects_parser.add_argument("--expand", default=None, help="Comma-separated: jobs (nested: jobs.data, ...)")
|
|
207
|
+
projects_parser.add_argument("--limit", type=int, default=25)
|
|
208
|
+
|
|
209
|
+
subparsers.add_parser("scan", help="Scan project and generate apipod.json (requires apipod)")
|
|
210
|
+
|
|
211
|
+
build_parser = subparsers.add_parser("build", help="Build deployment container (requires apipod)")
|
|
212
|
+
build_parser.add_argument("file", nargs="?", default=None, help="Entry Python file (e.g. main.py)")
|
|
213
|
+
|
|
214
|
+
start_parser = subparsers.add_parser("start", help="Start APIPod service locally (requires apipod)")
|
|
215
|
+
start_parser.add_argument("entrypoint", nargs="?", default=None, help="Service entrypoint file")
|
|
216
|
+
start_parser.add_argument("--host", default=None, help="Host to bind (default: 0.0.0.0)")
|
|
217
|
+
start_parser.add_argument("--port", type=int, default=None, help="Port to bind (default: 8000)")
|
|
218
|
+
|
|
219
|
+
deploy_parser = subparsers.add_parser(
|
|
220
|
+
"deploy",
|
|
221
|
+
help="Deploy to Socaity via APIPod (requires apipod)",
|
|
222
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
223
|
+
epilog=(
|
|
224
|
+
"Examples:\n"
|
|
225
|
+
" socaity deploy\n"
|
|
226
|
+
" socaity deploy serverless-runpod\n"
|
|
227
|
+
" socaity -C simple_test_service deploy serverless-runpod --yes\n"
|
|
228
|
+
" socaity deploy --skip-build\n"
|
|
229
|
+
" socaity deploy --resume DEPLOYMENT_ID"
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
deploy_parser.add_argument(
|
|
233
|
+
"target",
|
|
234
|
+
nargs="?",
|
|
235
|
+
default="serverless",
|
|
236
|
+
metavar="TARGET",
|
|
237
|
+
help="Deployment target '{compute}-{provider}' (default: serverless).",
|
|
238
|
+
)
|
|
239
|
+
deploy_parser.add_argument(
|
|
240
|
+
"--resume",
|
|
241
|
+
default=None,
|
|
242
|
+
metavar="DEPLOYMENT_ID",
|
|
243
|
+
help="Skip analyze/draft and push into an existing deployment attempt.",
|
|
244
|
+
)
|
|
245
|
+
deploy_parser.add_argument(
|
|
246
|
+
"--skip-build",
|
|
247
|
+
action="store_true",
|
|
248
|
+
help="Do not rebuild the container; push the existing local image.",
|
|
249
|
+
)
|
|
250
|
+
deploy_parser.add_argument(
|
|
251
|
+
"--push-only",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="With --resume: only push and poll, never build or create drafts.",
|
|
254
|
+
)
|
|
255
|
+
deploy_parser.add_argument(
|
|
256
|
+
"-y", "--yes",
|
|
257
|
+
action="store_true",
|
|
258
|
+
help="Non-interactive: skip confirmations (CI / automated runs).",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
args = parser.parse_args(argv)
|
|
262
|
+
|
|
263
|
+
if args.install:
|
|
264
|
+
require_credentials()
|
|
265
|
+
run_install(args.install)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
if args.command == "login":
|
|
269
|
+
run_login(
|
|
270
|
+
no_browser=args.no_browser,
|
|
271
|
+
timeout=args.timeout,
|
|
272
|
+
backend_url=args.backend_url,
|
|
273
|
+
frontend_url=args.frontend_url,
|
|
274
|
+
)
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
if args.command == "logout":
|
|
278
|
+
if clear_credentials():
|
|
279
|
+
print("Logged out. Credentials removed.")
|
|
280
|
+
else:
|
|
281
|
+
print("Not logged in.")
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
if args.command == "whoami":
|
|
285
|
+
run_whoami()
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
if args.command == "install":
|
|
289
|
+
require_credentials()
|
|
290
|
+
run_install(args.service_name)
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
if args.command == "update":
|
|
294
|
+
require_credentials()
|
|
295
|
+
run_update()
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
if args.command == "list":
|
|
299
|
+
_run_list(args.entity, args.category, args.family, args.limit)
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
if args.command == "search":
|
|
303
|
+
targets = [part.strip() for part in (args.collections or "services,models").split(",") if part.strip()]
|
|
304
|
+
if "jobs" in targets:
|
|
305
|
+
require_credentials()
|
|
306
|
+
_run_search(args.query, args.collections, args.limit)
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if args.command == "jobs":
|
|
310
|
+
if args.visibility != "public":
|
|
311
|
+
require_credentials()
|
|
312
|
+
_run_jobs(args.job_id, args.expand, args.status, args.visibility, args.limit, q=args.q)
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
if args.command == "projects":
|
|
316
|
+
require_credentials()
|
|
317
|
+
_run_projects(args.project_id, args.expand, args.limit)
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
if args.command == "scan":
|
|
321
|
+
run_scan(project_dir=args.project_dir)
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
if args.command == "build":
|
|
325
|
+
run_build(args.file, project_dir=args.project_dir)
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
if args.command == "start":
|
|
329
|
+
run_start(
|
|
330
|
+
entrypoint=args.entrypoint,
|
|
331
|
+
host=args.host,
|
|
332
|
+
port=args.port,
|
|
333
|
+
project_dir=args.project_dir,
|
|
334
|
+
)
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
if args.command == "deploy":
|
|
338
|
+
run_deploy(
|
|
339
|
+
target=args.target,
|
|
340
|
+
project_dir=args.project_dir,
|
|
341
|
+
resume=args.resume,
|
|
342
|
+
skip_build=args.skip_build,
|
|
343
|
+
push_only=args.push_only,
|
|
344
|
+
yes=args.yes,
|
|
345
|
+
)
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
parser.print_help()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
if __name__ == "__main__":
|
|
352
|
+
main()
|