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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Docker push with ephemeral registry credentials.
|
|
2
|
+
|
|
3
|
+
Credentials live only in a throwaway DOCKER_CONFIG directory that is deleted
|
|
4
|
+
after the push, so nothing persists in the user's ~/.docker/config.json.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
|
|
15
|
+
_DIGEST_RE = re.compile(r"digest:\s*(sha256:[0-9a-f]{64})")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DockerNotAvailable(RuntimeError):
|
|
19
|
+
"""Docker CLI missing or the daemon is not running."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CredentialsExpired(RuntimeError):
|
|
23
|
+
"""Registry rejected the credentials (expired mid-push); refresh and retry."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PushFailed(RuntimeError):
|
|
27
|
+
"""Any other push failure; message is safe to print."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _docker(args: list, *, env: Optional[dict] = None, input_bytes: Optional[bytes] = None) -> subprocess.CompletedProcess:
|
|
31
|
+
try:
|
|
32
|
+
return subprocess.run(["docker", *args], capture_output=True, env=env, input=input_bytes)
|
|
33
|
+
except FileNotFoundError as exc:
|
|
34
|
+
raise DockerNotAvailable("The docker CLI is not installed or not on PATH.") from exc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def image_size_bytes(local_tag: str) -> int:
|
|
38
|
+
"""Uncompressed size of a local image (upper bound of the pushed size)."""
|
|
39
|
+
result = _docker(["image", "inspect", local_tag, "--format", "{{.Size}}"])
|
|
40
|
+
if result.returncode != 0:
|
|
41
|
+
return 0
|
|
42
|
+
try:
|
|
43
|
+
return int(result.stdout.decode().strip())
|
|
44
|
+
except ValueError:
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def push_image(
|
|
49
|
+
registry: str,
|
|
50
|
+
repository: str,
|
|
51
|
+
tag: str,
|
|
52
|
+
username: str,
|
|
53
|
+
password: str,
|
|
54
|
+
local_tag: str,
|
|
55
|
+
) -> Tuple[str, int]:
|
|
56
|
+
"""Tag ``local_tag`` and push it as ``registry/repository:tag``.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
(manifest digest reported by the registry, local image size in bytes).
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
DockerNotAvailable, CredentialsExpired, PushFailed.
|
|
63
|
+
"""
|
|
64
|
+
ref = f"{registry}/{repository}:{tag}"
|
|
65
|
+
size = image_size_bytes(local_tag)
|
|
66
|
+
if size == 0:
|
|
67
|
+
raise PushFailed(f"Local image '{local_tag}' not found. Build it first (apipod build).")
|
|
68
|
+
|
|
69
|
+
result = _docker(["tag", local_tag, ref])
|
|
70
|
+
if result.returncode != 0:
|
|
71
|
+
raise PushFailed(f"docker tag failed: {result.stderr.decode()[:300]}")
|
|
72
|
+
|
|
73
|
+
with tempfile.TemporaryDirectory(prefix="socaity-push-") as docker_config:
|
|
74
|
+
env = {**os.environ, "DOCKER_CONFIG": docker_config}
|
|
75
|
+
try:
|
|
76
|
+
login = _docker(["login", registry, "-u", username, "--password-stdin"],
|
|
77
|
+
env=env, input_bytes=password.encode())
|
|
78
|
+
if login.returncode != 0:
|
|
79
|
+
stderr = login.stderr.decode()
|
|
80
|
+
if "unauthorized" in stderr.lower() or "401" in stderr:
|
|
81
|
+
raise CredentialsExpired("Registry login rejected; the push credentials expired.")
|
|
82
|
+
raise PushFailed(f"docker login failed: {stderr[:300]}")
|
|
83
|
+
|
|
84
|
+
push = subprocess.Popen(
|
|
85
|
+
["docker", "push", ref],
|
|
86
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, text=True,
|
|
87
|
+
)
|
|
88
|
+
digest, last_lines = None, []
|
|
89
|
+
for line in push.stdout:
|
|
90
|
+
line = line.rstrip()
|
|
91
|
+
last_lines = (last_lines + [line])[-5:]
|
|
92
|
+
match = _DIGEST_RE.search(line)
|
|
93
|
+
if match:
|
|
94
|
+
digest = match.group(1)
|
|
95
|
+
if line and not line.startswith(("The push refers", "Preparing", "Waiting")):
|
|
96
|
+
print(f" {line}")
|
|
97
|
+
push.wait()
|
|
98
|
+
if push.returncode != 0:
|
|
99
|
+
tail = " | ".join(last_lines)
|
|
100
|
+
if "unauthorized" in tail.lower() or "authentication required" in tail.lower():
|
|
101
|
+
raise CredentialsExpired("Push rejected mid-transfer; the credentials expired.")
|
|
102
|
+
raise PushFailed(f"docker push failed: {tail[:400]}")
|
|
103
|
+
if not digest:
|
|
104
|
+
digest = _digest_from_local_metadata(ref, env)
|
|
105
|
+
if not digest:
|
|
106
|
+
raise PushFailed("Push succeeded but no digest was reported by the registry.")
|
|
107
|
+
return digest, size
|
|
108
|
+
finally:
|
|
109
|
+
_docker(["rmi", ref], env=env) # remove the extra tag, keep the local image
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _digest_from_local_metadata(ref: str, env: dict) -> Optional[str]:
|
|
113
|
+
"""Fallback: read the RepoDigest docker recorded for the pushed reference."""
|
|
114
|
+
result = _docker(["image", "inspect", ref, "--format", "{{json .RepoDigests}}"], env=env)
|
|
115
|
+
if result.returncode != 0:
|
|
116
|
+
return None
|
|
117
|
+
repo = ref.rsplit(":", 1)[0]
|
|
118
|
+
for entry in json.loads(result.stdout.decode() or "[]"):
|
|
119
|
+
name, _, digest = entry.partition("@")
|
|
120
|
+
if name == repo and digest.startswith("sha256:"):
|
|
121
|
+
return digest
|
|
122
|
+
return None
|
socaity_cli/requires.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Optional-dependency guard for CLI commands and library addons.
|
|
2
|
+
|
|
3
|
+
Mirrors media_toolkit's ``dependency_requirements`` decorator. In CLI mode it
|
|
4
|
+
exits with a friendly install hint instead of raising: CLI users should see
|
|
5
|
+
one line, not a traceback. In library mode (``cli=False``) it raises
|
|
6
|
+
``ImportError`` so calling code can handle the missing dependency.
|
|
7
|
+
"""
|
|
8
|
+
import functools
|
|
9
|
+
from importlib.util import find_spec
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
# Cache of packages already confirmed importable (find_spec is not free).
|
|
13
|
+
_available: set = set()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def requires(package: str, pip_name: Optional[str] = None, cli: bool = True):
|
|
17
|
+
"""Run the decorated function only when ``package`` is installed.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
package: Importable module name to check (e.g. ``apipod``).
|
|
21
|
+
pip_name: PyPI name for the install hint when it differs from ``package``.
|
|
22
|
+
cli: When True (default), print an install hint and ``SystemExit(1)``.
|
|
23
|
+
When False, raise ``ImportError`` for programmatic use.
|
|
24
|
+
"""
|
|
25
|
+
install_name = pip_name or package
|
|
26
|
+
|
|
27
|
+
def decorator(func):
|
|
28
|
+
@functools.wraps(func)
|
|
29
|
+
def wrapper(*args, **kwargs):
|
|
30
|
+
if package not in _available:
|
|
31
|
+
try:
|
|
32
|
+
spec = find_spec(package)
|
|
33
|
+
except (ImportError, ValueError):
|
|
34
|
+
spec = None
|
|
35
|
+
if spec is None:
|
|
36
|
+
message = (
|
|
37
|
+
f"This {'command' if cli else 'feature'} needs the '{package}' package. "
|
|
38
|
+
f"Please install it with: pip install {install_name}"
|
|
39
|
+
)
|
|
40
|
+
if cli:
|
|
41
|
+
print(message)
|
|
42
|
+
raise SystemExit(1)
|
|
43
|
+
raise ImportError(message)
|
|
44
|
+
_available.add(package)
|
|
45
|
+
return func(*args, **kwargs)
|
|
46
|
+
return wrapper
|
|
47
|
+
return decorator
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Commands that need the socaity SDK package (stub generation, local model zoo).
|
|
2
|
+
|
|
3
|
+
Imports stay inside the functions: the whole point of this module is that
|
|
4
|
+
``socaity`` is optional for the CLI. The ``requires`` decorator guards each
|
|
5
|
+
entry with a friendly install hint.
|
|
6
|
+
"""
|
|
7
|
+
from socaity_cli.requires import requires
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@requires("socaity")
|
|
11
|
+
def run_install(service_name: str) -> None:
|
|
12
|
+
from socaity.core.socaity_service_registry import SocaityServiceRegistry
|
|
13
|
+
|
|
14
|
+
registry = SocaityServiceRegistry()
|
|
15
|
+
if service_name.lower() == "all":
|
|
16
|
+
print("Installing all available services...")
|
|
17
|
+
registry.install_all()
|
|
18
|
+
else:
|
|
19
|
+
print(f"Installing service: {service_name}...")
|
|
20
|
+
registry.install_service(service_name)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@requires("socaity")
|
|
24
|
+
def run_update() -> None:
|
|
25
|
+
from socaity.core.socaity_service_registry import SocaityServiceRegistry
|
|
26
|
+
|
|
27
|
+
print("Checking for service updates...")
|
|
28
|
+
SocaityServiceRegistry().update_package()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: socaity-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Socaity platform CLI: login, catalog browsing, SDK installs and APIPod deployment commands.
|
|
5
|
+
Author: SocAIty
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/SocAIty/socaity-cli
|
|
8
|
+
Project-URL: Homepage, https://www.socaity.ai
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx
|
|
12
|
+
Requires-Dist: socaity-schemas>=0.0.1
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy; extra == "dev"
|
|
17
|
+
|
|
18
|
+
# socaity-cli
|
|
19
|
+
|
|
20
|
+
The command line for the [socaity.ai](https://www.socaity.ai) platform: login, catalog browsing,
|
|
21
|
+
SDK installs, and APIPod deployment tooling. One `socaity` command, shared by the
|
|
22
|
+
[socaity SDK](https://github.com/SocAIty/socaity) and [APIPod](https://github.com/SocAIty/apipod).
|
|
23
|
+
|
|
24
|
+
The package is deliberately small: `httpx` and `socaity-schemas` are the only dependencies.
|
|
25
|
+
Commands that need more (stub generation, container builds) delegate to the `socaity` or
|
|
26
|
+
`apipod` package and tell you what to install when it is missing.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install socaity-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
You get `socaity-cli` as part of `pip install socaity` or `pip install apipod` as well.
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
| Command | Needs | What it does |
|
|
39
|
+
|---------|-------|--------------|
|
|
40
|
+
| `socaity login` / `logout` / `whoami` | - | Browser login, credentials in `~/.config/socaity/` |
|
|
41
|
+
| `socaity list services\|models` | - | Browse the AI catalog |
|
|
42
|
+
| `socaity search "flux schnel"` | - | Typo-tolerant search (`--collections services,models,jobs`) |
|
|
43
|
+
| `socaity jobs [job_id]` | login | List/search jobs; `--q`, `--expand data,files,billing,social`, `--visibility` |
|
|
44
|
+
| `socaity projects [project_id]` | login | Your projects; `--expand jobs` embeds member jobs |
|
|
45
|
+
| `socaity install <service>` | `socaity` | Generate an SDK stub for a service |
|
|
46
|
+
| `socaity update` | `socaity` | Sync installed services with the platform |
|
|
47
|
+
| `socaity scan` | `apipod` | Scan a project and generate `apipod.json` |
|
|
48
|
+
| `socaity build [file]` | `apipod` | Build the deployment container |
|
|
49
|
+
| `socaity start [entrypoint]` | `apipod` | Run an APIPod service locally |
|
|
50
|
+
| `socaity deploy [target]` | `apipod` | Deploy to Socaity (wraps `apipod deploy`) |
|
|
51
|
+
|
|
52
|
+
Use `-C DIR` with scan/build/start/deploy to point at a service project directory
|
|
53
|
+
(e.g. `socaity -C simple_test_service deploy serverless-runpod --yes`).
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
| Env var | Default | Purpose |
|
|
58
|
+
|---------|---------|---------|
|
|
59
|
+
| `SOCAITY_API_KEY` | from login | API key (wins over stored credentials) |
|
|
60
|
+
| `SOCAITY_BACKEND_URL` | `https://webapi.socaity.ai` | Platform API |
|
|
61
|
+
| `SOCAITY_FRONTEND_URL` | `https://socaity.ai` | Web UI for browser login |
|
|
62
|
+
|
|
63
|
+
## Using it as a library
|
|
64
|
+
|
|
65
|
+
`socaity` and `apipod` build on the same primitives this package exposes:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from socaity_cli import SocaityBackendClient, get_api_key, requires
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`SocaityBackendClient` covers catalog reads, search, and SDK install/update calls.
|
|
72
|
+
`requires("apipod")` guards a function behind an optional package with a friendly
|
|
73
|
+
install hint instead of an ImportError traceback.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
socaity_cli/__init__.py,sha256=_XBpsipDc33SmZMUhAXfdTb94cFNmQEmsHbZvzytaK4,811
|
|
2
|
+
socaity_cli/__main__.py,sha256=_LzdSFvyHkmOXVQip9vcACz_pkkahnjeKpq4RVQsM2A,72
|
|
3
|
+
socaity_cli/apipod_commands.py,sha256=eYDEJOi6WGxXXHKyvjS7bLEZcGt66U6zQGcOlJgwElo,2105
|
|
4
|
+
socaity_cli/auth.py,sha256=6bj0f6IesojmeCsrrYu_NHaiV8SZu3gEMeQFcJ9zk6U,5959
|
|
5
|
+
socaity_cli/backend_client.py,sha256=QVkQ_M2PW7eWdIqtJH43ODxhV4GUapl7SY-0PG2pFjs,827
|
|
6
|
+
socaity_cli/cli.py,sha256=eEUEHyO5aSQ6U1zdYZz9jfxe6xhkAH4Ux3NftgdfgZw,13223
|
|
7
|
+
socaity_cli/credentials.py,sha256=BciEbzqA0kerY4FN4pUmmd_-9NdDZimcNAhNuKH2Bp8,3999
|
|
8
|
+
socaity_cli/deployment.py,sha256=a2cKM_hLLx8MPnZVJx9aN6WnpO-5RSmvPeNYh9z7b2o,13171
|
|
9
|
+
socaity_cli/errors.py,sha256=QdsrNsngd8Jvca33Hw_w5mbTMJxUDwS8yrPHjGRtuJU,1094
|
|
10
|
+
socaity_cli/http.py,sha256=shQy_vBX3oUt0eh9BaBhA_aWcmxLDiP0DXdfys05jvE,4280
|
|
11
|
+
socaity_cli/prompts.py,sha256=kWMn1CbzyDGZegY8u5xMezXQL4ZCASCx4UOZkC6HUc4,639
|
|
12
|
+
socaity_cli/registry_push.py,sha256=Fwrdn07l1xrvEo8q6bKiKQ3aJkQwCn0FwqjsJXnJ1ss,4697
|
|
13
|
+
socaity_cli/requires.py,sha256=UYPTx4VIAu4kTgdQ0L4iI-jqCjvwLPxc7wQ4CvxGze4,1880
|
|
14
|
+
socaity_cli/sdk_commands.py,sha256=WLFbXP0aXDiTMOvXnLdyRNKU1DTY_NNBxM3LqIv_ycc,954
|
|
15
|
+
socaity_cli/clients/__init__.py,sha256=iSvjkoBsbZGMmoYvYdMcYHtGNw981VefEyQFhMgOPdY,608
|
|
16
|
+
socaity_cli/clients/analytics.py,sha256=ufIsrfpv6OuNK7AREASGPZdr0zIuyCFkVaJhyURZlwY,2146
|
|
17
|
+
socaity_cli/clients/catalog.py,sha256=Q-JCd8eOLXfUrSsya1BG-GjiaITMxoz3h7AcCcue4FY,5476
|
|
18
|
+
socaity_cli/clients/deployment.py,sha256=PEf9BCTRWReqZFdr9VryIkOE_vN7ijL1MzSSuxWtBQ4,4430
|
|
19
|
+
socaity_cli/clients/files.py,sha256=gieJtK886gQv4QgFBx8y7z3sMCZX-Updm4FHk0wEINY,3070
|
|
20
|
+
socaity_cli/clients/jobs.py,sha256=2vOBWTMi65Kh_hjhvMspRSxOKdC3CYSyuYTUv5A7UiQ,2679
|
|
21
|
+
socaity_cli/clients/projects.py,sha256=Tdd1N2gTNCKvRvnmv4l3hkUETDWtr9m6U6d1CKfd0ng,2171
|
|
22
|
+
socaity_cli/clients/sdk_install.py,sha256=2wepEZ9Xv1EF4nnRAISuuJHtvkKF4-OB--32wrbVri4,690
|
|
23
|
+
socaity_cli-0.1.1.dist-info/METADATA,sha256=j0zh-JTgFjzJKv8R4W365-cLsK5kwXtd400bPBm0FS4,3113
|
|
24
|
+
socaity_cli-0.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
25
|
+
socaity_cli-0.1.1.dist-info/entry_points.txt,sha256=mA_DYjFU2lbAQ6PVTq_onISa7yp85hP2BSynX_HgJUA,49
|
|
26
|
+
socaity_cli-0.1.1.dist-info/top_level.txt,sha256=G0kM_WkhvCrj-vW_f9xPP9Ae6ujdgiePN11_j0n7aN8,12
|
|
27
|
+
socaity_cli-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
socaity_cli
|