simplismart-sdk 0.1.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.
@@ -0,0 +1,40 @@
1
+ """Simplismart Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .exceptions import SimplismartError
8
+ from .models import (
9
+ DeploymentCreate,
10
+ ModelRepoCompileAvatar,
11
+ ModelRepoCompileCreate,
12
+ ModelRepoCreate,
13
+ ModelRepoListParams,
14
+ ModelRepoProfilesRequest,
15
+ SecretCreate,
16
+ )
17
+
18
+ __all__ = [
19
+ "Simplismart",
20
+ "SimplismartClient",
21
+ "SimplismartError",
22
+ "DeploymentCreate",
23
+ "ModelRepoCreate",
24
+ "ModelRepoCompileCreate",
25
+ "ModelRepoCompileAvatar",
26
+ "ModelRepoListParams",
27
+ "ModelRepoProfilesRequest",
28
+ "SecretCreate",
29
+ ]
30
+
31
+ __version__ = "0.1.0"
32
+
33
+
34
+ def __getattr__(name: str) -> Any:
35
+ """Lazy-load client symbols so model imports stay lightweight."""
36
+ if name in {"Simplismart", "SimplismartClient"}:
37
+ from .client import Simplismart, SimplismartClient
38
+
39
+ return {"Simplismart": Simplismart, "SimplismartClient": SimplismartClient}[name]
40
+ raise AttributeError(f"module 'simplismart' has no attribute '{name}'")
@@ -0,0 +1,9 @@
1
+ from .deployments import DeploymentAPI
2
+ from .model_repos import ModelRepoAPI
3
+ from .secrets import SecretAPI
4
+
5
+ __all__ = [
6
+ "DeploymentAPI",
7
+ "ModelRepoAPI",
8
+ "SecretAPI",
9
+ ]
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from ..http import HTTPClient
6
+ from ..models.deployment import DeploymentCreate
7
+
8
+ _MAX_PAGE_SIZE = 20
9
+ _MAX_PAGE_OFFSET = 10_000
10
+
11
+
12
+ def _normalize_status_filter(status: str | None) -> str | None:
13
+ if status is None:
14
+ return None
15
+ normalized = status.strip().upper()
16
+ return normalized or None
17
+
18
+
19
+ def _status_matches(value: Any, *, status_filter: str) -> bool:
20
+ if not isinstance(value, str):
21
+ return False
22
+ normalized = value.upper()
23
+ return normalized == status_filter or normalized.startswith(f"{status_filter}_")
24
+
25
+
26
+ class DeploymentAPI:
27
+ """
28
+ Deployment operations exposed to PG-authenticated SDK clients.
29
+
30
+ - Private deployment create: `/deployments/private/deploy-model/`
31
+ - Model deployment create (BYOC-oriented): `/deployments/model-deployments/`
32
+ - Model deployment read/delete: `/deployments/model-deployments/{id}/`
33
+ - Generic deployment edit: `/deployments/<id>/edit`
34
+ - Shared lifecycle ops: start/stop/restart/health/autoscaling/delete
35
+ """
36
+
37
+ def __init__(self, http: HTTPClient) -> None:
38
+ self._http = http
39
+
40
+ def list(
41
+ self,
42
+ *,
43
+ model_repo_id: Optional[str] = None,
44
+ status: str | None = None,
45
+ offset: int = 0,
46
+ count: int = 20,
47
+ trace_id: str | None = None,
48
+ ) -> Any:
49
+ """
50
+ List deployments for the org associated with the token.
51
+ If model_repo_id is provided, filter to that repo.
52
+
53
+ Note: the current backend list endpoint does not expose offset/count query params.
54
+ SDK applies client-side pagination for consistency with model-repo listing.
55
+ """
56
+ normalized_status = _normalize_status_filter(status)
57
+ safe_offset = max(0, min(offset, _MAX_PAGE_OFFSET))
58
+ safe_count = max(0, min(count, _MAX_PAGE_SIZE))
59
+ params: dict[str, Any] = {}
60
+ if model_repo_id:
61
+ params["model_repo_id"] = model_repo_id
62
+ if normalized_status:
63
+ params["status"] = normalized_status
64
+ response = self._http.request(
65
+ "GET",
66
+ "/deployments/list/model/",
67
+ params=params or None,
68
+ trace_id=trace_id,
69
+ )
70
+ if isinstance(response, list):
71
+ if normalized_status:
72
+ response = [
73
+ item
74
+ for item in response
75
+ if _status_matches(item.get("status"), status_filter=normalized_status)
76
+ ]
77
+ end = safe_offset + safe_count
78
+ return response[safe_offset:end]
79
+ return response
80
+
81
+ def create_private_deployment(
82
+ self,
83
+ payload: DeploymentCreate,
84
+ *,
85
+ trace_id: str | None = None,
86
+ ) -> dict[str, Any]:
87
+ return self._http.request(
88
+ "POST",
89
+ "/deployments/private/deploy-model/",
90
+ json=payload.to_payload(),
91
+ trace_id=trace_id,
92
+ )
93
+
94
+ def list_model_deployments(
95
+ self,
96
+ *,
97
+ org_id: Optional[str] = None,
98
+ trace_id: str | None = None,
99
+ ) -> Any:
100
+ params = {"org": org_id} if org_id else None
101
+ return self._http.request(
102
+ "GET",
103
+ "/deployments/model-deployments/",
104
+ params=params,
105
+ trace_id=trace_id,
106
+ )
107
+
108
+ def create_byoc_deployment(self, payload: dict[str, Any], *, trace_id: str | None = None) -> Any:
109
+ return self._http.request(
110
+ "POST",
111
+ "/deployments/model-deployments/",
112
+ json=payload,
113
+ trace_id=trace_id,
114
+ )
115
+
116
+ def get_model_deployment(self, deployment_id: str, *, trace_id: str | None = None) -> Any:
117
+ return self._http.request(
118
+ "GET",
119
+ f"/deployments/model-deployments/{deployment_id}/",
120
+ trace_id=trace_id,
121
+ )
122
+
123
+ def delete_model_deployment(self, deployment_id: str, *, trace_id: str | None = None) -> bool:
124
+ self._http.request(
125
+ "DELETE",
126
+ f"/deployments/model-deployments/{deployment_id}/",
127
+ trace_id=trace_id,
128
+ )
129
+ return True
130
+
131
+ def update_deployment(
132
+ self,
133
+ deployment_id: str,
134
+ payload: dict[str, Any],
135
+ *,
136
+ trace_id: str | None = None,
137
+ ) -> Any:
138
+ return self._http.request(
139
+ "POST",
140
+ f"/deployments/{deployment_id}/edit",
141
+ json=payload,
142
+ trace_id=trace_id,
143
+ )
144
+
145
+ def stop(self, deployment_id: str, *, org_id: Optional[str] = None, trace_id: str | None = None) -> Any:
146
+ body: dict[str, Any] = {"action": "stop"}
147
+ if org_id:
148
+ body["org"] = org_id
149
+ return self._http.request(
150
+ "POST",
151
+ f"/deployments/{deployment_id}/startstop/",
152
+ json=body,
153
+ trace_id=trace_id,
154
+ )
155
+
156
+ def start(self, deployment_id: str, *, org_id: Optional[str] = None, trace_id: str | None = None) -> Any:
157
+ body: dict[str, Any] = {"action": "start"}
158
+ if org_id:
159
+ body["org"] = org_id
160
+ return self._http.request(
161
+ "POST",
162
+ f"/deployments/{deployment_id}/startstop/",
163
+ json=body,
164
+ trace_id=trace_id,
165
+ )
166
+
167
+ def restart(
168
+ self,
169
+ deployment_id: str,
170
+ *,
171
+ org_id: str,
172
+ namespace: str,
173
+ trace_id: str | None = None,
174
+ ) -> Any:
175
+ return self._http.request(
176
+ "POST",
177
+ f"/deployments/{deployment_id}/restart/",
178
+ json={"action": "restart", "org": org_id, "namespace": namespace},
179
+ trace_id=trace_id,
180
+ )
181
+
182
+ def fetch_health(self, deployment_id: str, *, trace_id: str | None = None) -> Any:
183
+ return self._http.request(
184
+ "GET",
185
+ f"/deployments/model-deployment/fetch-status/{deployment_id}/",
186
+ trace_id=trace_id,
187
+ )
188
+
189
+ def update_autoscaling(
190
+ self,
191
+ deployment_id: str,
192
+ *,
193
+ min_replicas: int,
194
+ max_replicas: int,
195
+ trace_id: str | None = None,
196
+ ) -> Any:
197
+ return self._http.request(
198
+ "POST",
199
+ f"/deployments/{deployment_id}/update-replicas/",
200
+ json={"min_replicas": min_replicas, "max_replicas": max_replicas},
201
+ trace_id=trace_id,
202
+ )
203
+
204
+ def delete(
205
+ self,
206
+ deployment_id: str,
207
+ *,
208
+ org_id: Optional[str] = None,
209
+ trace_id: str | None = None,
210
+ ) -> bool:
211
+ params = {"org_id": org_id} if org_id else None
212
+ self._http.request(
213
+ "DELETE",
214
+ f"/deployments/{deployment_id}/delete/",
215
+ params=params,
216
+ trace_id=trace_id,
217
+ )
218
+ return True
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from ..http import HTTPClient
6
+ from ..models.model_repo import (
7
+ ModelRepoCompileCreate,
8
+ ModelRepoCreate,
9
+ ModelRepoListParams,
10
+ ModelRepoProfilesRequest,
11
+ )
12
+
13
+
14
+ class ModelRepoAPI:
15
+ """
16
+ Model repository operations: list, create (container/BYOM),
17
+ create private compile, delete, and profile generation.
18
+ """
19
+
20
+ def __init__(self, http: HTTPClient) -> None:
21
+ self._http = http
22
+
23
+ def list(self, params: ModelRepoListParams, *, trace_id: str | None = None) -> Any:
24
+ return self._http.request(
25
+ "GET",
26
+ "/models/client/model-repos/",
27
+ params=params.to_params(),
28
+ trace_id=trace_id,
29
+ )
30
+
31
+ def create(self, model_repo: ModelRepoCreate, *, trace_id: str | None = None) -> Dict[str, Any]:
32
+ """Create a model repo via client endpoint (container/BYOM flow)."""
33
+ return self._http.request(
34
+ "POST",
35
+ "/models/client/model-repos/",
36
+ json=model_repo.to_payload(),
37
+ trace_id=trace_id,
38
+ )
39
+
40
+ def create_private_compile(
41
+ self,
42
+ payload: ModelRepoCompileCreate,
43
+ *,
44
+ trace_id: str | None = None,
45
+ ) -> Dict[str, Any]:
46
+ """
47
+ Create a private compile model repo.
48
+
49
+ This path always sends `use_simplismart_infrastructure=true`.
50
+ """
51
+ body = payload.to_payload()
52
+ body["use_simplismart_infrastructure"] = True
53
+ return self._http.request(
54
+ "POST",
55
+ "/models/client/model-repos/",
56
+ json=body,
57
+ trace_id=trace_id,
58
+ )
59
+
60
+ def profiles(self, payload: ModelRepoProfilesRequest, *, trace_id: str | None = None) -> Any:
61
+ return self._http.request(
62
+ "POST",
63
+ "/models/model-profiles/",
64
+ json=payload.to_payload(),
65
+ trace_id=trace_id,
66
+ )
67
+
68
+ def delete(self, model_id: str, *, trace_id: str | None = None) -> bool:
69
+ self._http.request(
70
+ "DELETE",
71
+ "/models/client/model-repos/",
72
+ params={"model_id": model_id},
73
+ trace_id=trace_id,
74
+ )
75
+ return True
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from ..http import HTTPClient
6
+ from ..models.secret import SecretCreate
7
+
8
+
9
+ class SecretAPI:
10
+ """
11
+ Secret operations. Uses PG token auth.
12
+
13
+ Notes:
14
+ - Secret config endpoint derives org context from pgToken.
15
+ - Secret list endpoint still requires explicit org_id query parameter on backend.
16
+ """
17
+
18
+ def __init__(self, http: HTTPClient) -> None:
19
+ self._http = http
20
+
21
+ def create(self, payload: SecretCreate, *, trace_id: str | None = None) -> Dict[str, Any]:
22
+ return self._http.request(
23
+ "POST",
24
+ "/accounts/secrets/",
25
+ json=payload.to_payload(),
26
+ trace_id=trace_id,
27
+ )
28
+
29
+ def list(self, *, org_id: str, trace_id: str | None = None) -> Any:
30
+ return self._http.request(
31
+ "GET",
32
+ "/accounts/secrets/",
33
+ params={"org_id": org_id},
34
+ trace_id=trace_id,
35
+ )
36
+
37
+ def get(self, secret_id: str, *, trace_id: str | None = None) -> Any:
38
+ return self._http.request("GET", f"/accounts/secrets/{secret_id}/", trace_id=trace_id)
39
+
40
+ def list_configs(self, *, trace_id: str | None = None) -> Any:
41
+ return self._http.request("GET", "/accounts/secret/config/", trace_id=trace_id)
@@ -0,0 +1 @@
1
+ """CLI package for Simplismart."""
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from .. import Simplismart, SimplismartError
9
+ from ..config import DEFAULT_TIMEOUT
10
+
11
+
12
+ DEFAULT_BASE_URL = "https://api.app.simplismart.ai"
13
+
14
+
15
+ def parse_env_list(items: Optional[List[str]]) -> Optional[Dict[str, str]]:
16
+ """Convert KEY=VALUE list into a dict."""
17
+ if not items:
18
+ return None
19
+ env: Dict[str, str] = {}
20
+ for item in items:
21
+ if "=" not in item:
22
+ raise argparse.ArgumentTypeError(f"Invalid env entry '{item}', expected KEY=VALUE.")
23
+ key, value = item.split("=", 1)
24
+ env[key] = value
25
+ return env
26
+
27
+
28
+ def parse_json_or_file(value: Optional[str]) -> Optional[Any]:
29
+ """Allow passing JSON string or @path to JSON file."""
30
+ if value is None:
31
+ return None
32
+ if value.startswith("@"):
33
+ path = value[1:]
34
+ with open(path, "r", encoding="utf-8") as f:
35
+ return json.load(f)
36
+ return json.loads(value)
37
+
38
+
39
+ def build_client(args: argparse.Namespace) -> Simplismart:
40
+ token = args.pg_token or os.getenv("SIMPLISMART_PG_TOKEN")
41
+ base_url = args.base_url or os.getenv("SIMPLISMART_BASE_URL", DEFAULT_BASE_URL)
42
+
43
+ timeout_value = args.timeout
44
+ if timeout_value is None:
45
+ env_timeout = os.getenv("SIMPLISMART_TIMEOUT")
46
+ timeout_value = float(env_timeout) if env_timeout else None
47
+
48
+ return Simplismart(
49
+ pg_token=token,
50
+ base_url=base_url,
51
+ timeout=timeout_value,
52
+ )
53
+
54
+
55
+ def handle_cli_errors(func):
56
+ """Decorator to wrap CLI handlers with consistent error output."""
57
+
58
+ def wrapper(args: argparse.Namespace) -> None:
59
+ try:
60
+ func(args)
61
+ except SimplismartError as exc:
62
+ payload = getattr(exc, "payload", None)
63
+ status = getattr(exc, "status_code", None)
64
+ message = str(exc.args[0]) if getattr(exc, "args", None) else str(exc)
65
+
66
+ response_body = {
67
+ "error": message,
68
+ "status": status,
69
+ }
70
+
71
+ payload_has_only_message = False
72
+ if isinstance(payload, dict):
73
+ candidate = (
74
+ payload.get("detail")
75
+ or payload.get("error")
76
+ or payload.get("message")
77
+ )
78
+ payload_has_only_message = (
79
+ candidate is not None
80
+ and str(candidate) == message
81
+ and len(payload) == 1
82
+ )
83
+
84
+ if payload and not payload_has_only_message:
85
+ response_body["payload"] = payload
86
+
87
+ print(json.dumps(response_body, indent=2, default=str))
88
+ raise SystemExit(1)
89
+ except Exception as exc: # pragma: no cover - CLI safeguard
90
+ print(json.dumps({"error": str(exc)}))
91
+ raise SystemExit(1)
92
+
93
+ return wrapper