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.
@@ -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
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ socaity = socaity_cli.cli:main
@@ -0,0 +1 @@
1
+ socaity_cli