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,18 @@
1
+ """Domain clients for the socaity platform backend."""
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
+
10
+ __all__ = [
11
+ "AnalyticsClient",
12
+ "CatalogClient",
13
+ "DeploymentClient",
14
+ "FilesClient",
15
+ "JobsClient",
16
+ "ProjectsClient",
17
+ "SdkInstallClient",
18
+ ]
@@ -0,0 +1,60 @@
1
+ """Analytics endpoints (`/v1/analytics`)."""
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from socaity_schemas.platform import EndpointPricing, EndpointStats, PriceEstimate, SimilarService
5
+
6
+
7
+ class AnalyticsClient:
8
+ """Mixin: estimate, stats, similar services, pricing. Requires ``BackendHttp`` on ``self``."""
9
+
10
+ def estimate(
11
+ self,
12
+ deployment_id: str,
13
+ endpoint_id: Optional[str] = None,
14
+ input_data: Optional[Dict[str, Any]] = None,
15
+ ) -> Optional[PriceEstimate]:
16
+ """Predict price and runtime for a job before it runs."""
17
+ row = self._request(
18
+ "POST",
19
+ "v1/analytics/estimate",
20
+ payload=input_data or {},
21
+ params={"deployment_id": deployment_id, "endpoint_id": endpoint_id},
22
+ )
23
+ return PriceEstimate(**row) if row else None
24
+
25
+ def get_stats(
26
+ self,
27
+ deployment_id: Optional[str] = None,
28
+ endpoint_path: Optional[str] = None,
29
+ ) -> List[EndpointStats]:
30
+ rows = self._post("v1/analytics/stats", params={
31
+ "deployment_id": deployment_id,
32
+ "endpoint_path": endpoint_path,
33
+ }) or []
34
+ if not isinstance(rows, list):
35
+ return []
36
+ return [EndpointStats(**row) for row in rows]
37
+
38
+ def get_similar_services(self, service_id: str, k: int = 8) -> List[SimilarService]:
39
+ rows = self._get("v1/analytics/similar-services", params={
40
+ "service_id": service_id,
41
+ "k": k,
42
+ }) or []
43
+ if not isinstance(rows, list):
44
+ return []
45
+ return [SimilarService(**row) for row in rows]
46
+
47
+ def get_service_pricing(
48
+ self,
49
+ service_id: Optional[str] = None,
50
+ deployment_id: Optional[str] = None,
51
+ endpoint_id: Optional[str] = None,
52
+ ) -> List[EndpointPricing]:
53
+ rows = self._get("v1/analytics/service-pricing", params={
54
+ "service_id": service_id,
55
+ "deployment_id": deployment_id,
56
+ "endpoint_id": endpoint_id,
57
+ }) or []
58
+ if not isinstance(rows, list):
59
+ return []
60
+ return [EndpointPricing(**row) for row in rows]
@@ -0,0 +1,147 @@
1
+ """AI catalog reads (`/v1/catalog`)."""
2
+ from typing import Dict, List, Optional, Union
3
+
4
+ from socaity_schemas.platform import AIModel, AIService, Job, ServiceCategory
5
+
6
+ SERVICE_LIST_FIELDS = ["id", "name", "display_name", "short_desc", "categories", "is_official", "n_usages"]
7
+ MODEL_LIST_FIELDS = ["id", "name", "display_name", "family", "release_date", "hugging_face_url"]
8
+
9
+
10
+ class CatalogClient:
11
+ """Mixin: catalog list/get/search. Requires ``BackendHttp`` methods on ``self``."""
12
+
13
+ def query_services(
14
+ self,
15
+ q: Optional[str] = None,
16
+ filters: Optional[List[str]] = None,
17
+ expand: Optional[List[str]] = None,
18
+ fields: Optional[List[str]] = None,
19
+ sort: Optional[str] = None,
20
+ limit: int = 100,
21
+ offset: int = 0,
22
+ known_ids: Optional[List[str]] = None,
23
+ known_fieldset: Optional[str] = None,
24
+ ) -> List[AIService]:
25
+ rows = self._unwrap_list(self._get("v1/catalog/services", params={
26
+ "q": q,
27
+ "filter": filters or [],
28
+ "expand": expand or [],
29
+ "fields": fields if fields is not None else SERVICE_LIST_FIELDS,
30
+ "sort": sort,
31
+ "limit": limit,
32
+ "offset": offset,
33
+ "known_ids": known_ids or [],
34
+ "known_fieldset": known_fieldset,
35
+ }))
36
+ return [AIService(**row) for row in rows]
37
+
38
+ def list_services(
39
+ self,
40
+ category: Optional[str] = None,
41
+ fields: Optional[List[str]] = None,
42
+ expand: Optional[List[str]] = None,
43
+ limit: int = 100,
44
+ offset: int = 0,
45
+ ) -> List[AIService]:
46
+ filters = [f"categories:contains:{category}"] if category else None
47
+ return self.query_services(
48
+ filters=filters,
49
+ expand=expand,
50
+ fields=fields,
51
+ limit=limit,
52
+ offset=offset,
53
+ )
54
+
55
+ def get_service(
56
+ self,
57
+ id_or_name: str,
58
+ expand: Optional[List[str]] = None,
59
+ fields: Optional[List[str]] = None,
60
+ filters: Optional[List[str]] = None,
61
+ ) -> Optional[AIService]:
62
+ row = self._get(f"v1/catalog/services/{id_or_name}", params={
63
+ "filter": filters or [],
64
+ "expand": expand if expand is not None else ["deployments", "endpoints", "models"],
65
+ "fields": fields or [],
66
+ })
67
+ return AIService(**row) if row else None
68
+
69
+ def query_models(
70
+ self,
71
+ q: Optional[str] = None,
72
+ filters: Optional[List[str]] = None,
73
+ expand: Optional[List[str]] = None,
74
+ fields: Optional[List[str]] = None,
75
+ sort: Optional[str] = None,
76
+ limit: int = 100,
77
+ offset: int = 0,
78
+ known_ids: Optional[List[str]] = None,
79
+ known_fieldset: Optional[str] = None,
80
+ ) -> List[AIModel]:
81
+ rows = self._unwrap_list(self._get("v1/catalog/models", params={
82
+ "q": q,
83
+ "filter": filters or [],
84
+ "expand": expand or [],
85
+ "fields": fields if fields is not None else MODEL_LIST_FIELDS,
86
+ "sort": sort,
87
+ "limit": limit,
88
+ "offset": offset,
89
+ "known_ids": known_ids or [],
90
+ "known_fieldset": known_fieldset,
91
+ }))
92
+ return [AIModel(**row) for row in rows]
93
+
94
+ def list_models(
95
+ self,
96
+ family: Optional[str] = None,
97
+ task: Optional[str] = None,
98
+ fields: Optional[List[str]] = None,
99
+ expand: Optional[List[str]] = None,
100
+ limit: int = 100,
101
+ offset: int = 0,
102
+ ) -> List[AIModel]:
103
+ filters: List[str] = []
104
+ if family:
105
+ filters.append(f"family:eq:{family}")
106
+ if task:
107
+ filters.append(f"tasks:contains:{task}")
108
+ return self.query_models(filters=filters or None, expand=expand, fields=fields, limit=limit, offset=offset)
109
+
110
+ def get_model(
111
+ self,
112
+ id_or_name: str,
113
+ expand: Optional[List[str]] = None,
114
+ fields: Optional[List[str]] = None,
115
+ ) -> Optional[AIModel]:
116
+ row = self._get(f"v1/catalog/models/{id_or_name}", params={
117
+ "expand": expand if expand is not None else ["scores"],
118
+ "fields": fields or [],
119
+ })
120
+ return AIModel(**row) if row else None
121
+
122
+ def list_categories(self) -> List[ServiceCategory]:
123
+ rows = self._get("v1/catalog/categories") or []
124
+ if isinstance(rows, dict):
125
+ rows = rows.get("entities") or rows.get("data") or []
126
+ return [ServiceCategory(**row) for row in rows]
127
+
128
+ def list_pricing_rules(self, active_only: bool = True) -> List[Dict]:
129
+ rows = self._get("v1/catalog/pricing-rules", params={"active_only": active_only}) or []
130
+ if isinstance(rows, dict):
131
+ rows = rows.get("entities") or rows.get("data") or []
132
+ return rows if isinstance(rows, list) else []
133
+
134
+ def search(
135
+ self,
136
+ query: str,
137
+ collection: str = "services",
138
+ filters: Optional[List[str]] = None,
139
+ limit: int = 20,
140
+ visibility: str = "user_visible",
141
+ ) -> List[Union[AIService, AIModel, Job]]:
142
+ """Full-text search via catalog or jobs ``q`` param (Typesense)."""
143
+ if collection == "models":
144
+ return self.query_models(q=query, filters=filters, limit=limit)
145
+ if collection == "jobs":
146
+ return self.query_jobs(q=query, filters=filters, visibility=visibility, limit=limit)
147
+ return self.query_services(q=query, filters=filters, limit=limit)
@@ -0,0 +1,129 @@
1
+ """Deployment / hosting write surface (`/v1/deployment`)."""
2
+ from typing import Any, Dict, List, Optional
3
+
4
+
5
+ class DeploymentClient:
6
+ """Mixin: deployment helpers. Requires ``BackendHttp`` methods on ``self``."""
7
+
8
+ def analyze_deployment(self, apipod_config: Dict) -> Optional[Dict]:
9
+ return self._post("v1/deployment/analyze", payload=apipod_config)
10
+
11
+ def create_deployment_draft(
12
+ self,
13
+ apipod_config: Dict,
14
+ model_resolutions: Optional[Dict[str, Optional[str]]] = None,
15
+ ) -> Optional[Dict]:
16
+ return self._post("v1/deployment/draft", payload={
17
+ "apipod_config": apipod_config,
18
+ "model_resolutions": model_resolutions or {},
19
+ })
20
+
21
+ def upsert_hf_token(self, token: str, alias: str = "default") -> Optional[Dict]:
22
+ return self._post("v1/deployment/hf_token", params={"token": token, "alias": alias})
23
+
24
+ def list_hf_tokens(self) -> List[Dict]:
25
+ return self._get("v1/deployment/hf_token") or []
26
+
27
+ # ---- registry deploy pipeline (apipod deploy) ----
28
+
29
+ def get_registry_usage(self) -> Optional[Dict]:
30
+ return self._get("v1/deployment/registry_usage")
31
+
32
+ def get_push_credentials(
33
+ self,
34
+ deployment_id: str,
35
+ *,
36
+ estimated_size_bytes: int = 0,
37
+ ) -> Optional[Dict]:
38
+ return self._post(
39
+ f"v1/deployment/{deployment_id}/push_credentials",
40
+ params={"estimated_size_bytes": estimated_size_bytes},
41
+ )
42
+
43
+ def confirm_image_pushed(
44
+ self,
45
+ deployment_id: str,
46
+ *,
47
+ digest: str,
48
+ size_bytes: int,
49
+ ) -> Optional[Dict]:
50
+ return self._post(
51
+ f"v1/deployment/{deployment_id}/pushed",
52
+ payload={"digest": digest, "size_bytes": size_bytes},
53
+ )
54
+
55
+ def get_deployment_pipeline_status(self, deployment_id: str) -> Optional[Dict]:
56
+ return self._get(f"v1/deployment/{deployment_id}")
57
+
58
+ def cancel_deployment_pipeline(self, deployment_id: str) -> Optional[Dict]:
59
+ return self._request("DELETE", f"v1/deployment/{deployment_id}")
60
+
61
+ def upsert_ai_service(self, service: Dict[str, Any]) -> Optional[Dict]:
62
+ return self._post("v1/deployment/upsert_ai_service", payload=service)
63
+
64
+ def upsert_service_endpoint(self, service_endpoint: Dict[str, Any]) -> Optional[Dict]:
65
+ return self._post("v1/deployment/upsert_service_endpoint", payload=service_endpoint)
66
+
67
+ def upsert_deployment(
68
+ self,
69
+ deployment: Dict[str, Any],
70
+ service: Optional[Dict[str, Any]] = None,
71
+ ) -> Optional[Dict]:
72
+ return self._post("v1/deployment/upsert_deployment", payload={
73
+ "deployment": deployment,
74
+ "service": service,
75
+ })
76
+
77
+ def upsert_service(self, service: Dict[str, Any], deployment: Dict[str, Any]) -> Optional[Dict]:
78
+ return self._post("v1/deployment/upsert_service", payload={
79
+ "service": service,
80
+ "deployment": deployment,
81
+ })
82
+
83
+ def order_async_deployment(
84
+ self,
85
+ service: Dict[str, Any],
86
+ deployment: Dict[str, Any],
87
+ ) -> Optional[Dict]:
88
+ return self._post("v1/deployment/order_async_deployment", payload={
89
+ "service": service,
90
+ "deployment": deployment,
91
+ })
92
+
93
+ def get_deployment_status(
94
+ self,
95
+ job_id: str,
96
+ service_id: str,
97
+ deployment_id: str,
98
+ **context,
99
+ ) -> Optional[Dict]:
100
+ params = {
101
+ "job_id": job_id,
102
+ "service_id": service_id,
103
+ "deployment_id": deployment_id,
104
+ **context,
105
+ }
106
+ return self._get("v1/deployment/deployment_status", params=params)
107
+
108
+ def is_dockerhub_repo_accessible(
109
+ self,
110
+ namespace: str,
111
+ repo: str,
112
+ tag: str,
113
+ pat_id: Optional[str] = None,
114
+ ) -> Any:
115
+ return self._post("v1/deployment/is_dockerhub_repo_accessible", params={
116
+ "namespace": namespace,
117
+ "repo": repo,
118
+ "tag": tag,
119
+ "pat_id": pat_id,
120
+ })
121
+
122
+ def is_service_name_available(self, service_name: str) -> bool:
123
+ return self._get(
124
+ "v1/deployment/is_service_name_available",
125
+ params={"service_name": service_name},
126
+ ) is True
127
+
128
+ def delete_service(self, service_id: str) -> Optional[Dict]:
129
+ return self._post("v1/deployment/delete_service", payload={"service_id": service_id})
@@ -0,0 +1,82 @@
1
+ """User files surface (`/v1/files`)."""
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List, Optional, Union
4
+
5
+ import httpx
6
+
7
+
8
+ class FilesClient:
9
+ """Mixin: storage usage, list, upload, delete. Requires ``BackendHttp`` on ``self``."""
10
+
11
+ def get_storage_usage(self) -> Optional[Dict]:
12
+ return self._get("v1/files/storage_usage")
13
+
14
+ def get_user_files(
15
+ self,
16
+ purpose: Union[str, List[str]] = "USER_UPLOAD",
17
+ include_temporary: bool = True,
18
+ include_deleted: bool = False,
19
+ is_public: Optional[bool] = None,
20
+ page: int = 1,
21
+ page_size: int = 25,
22
+ ) -> List[Dict]:
23
+ purposes = purpose if isinstance(purpose, list) else [purpose]
24
+ rows = self._post("v1/files/user_files", params={
25
+ "purpose": purposes,
26
+ "include_temporary": include_temporary,
27
+ "include_deleted": include_deleted,
28
+ "is_public": is_public,
29
+ "page": page,
30
+ "page_size": page_size,
31
+ })
32
+ return rows if isinstance(rows, list) else []
33
+
34
+ def delete_file(self, file_id: Union[str, int]) -> bool:
35
+ return self._post("v1/files/delete_file", params={"file_id": file_id}) is True
36
+
37
+ def upload_files(
38
+ self,
39
+ files: Union[str, Path, bytes, List[Union[str, Path, bytes]]],
40
+ file_purpose: str = "USER_UPLOAD",
41
+ is_public: bool = False,
42
+ is_temporary: bool = False,
43
+ job_id: Optional[str] = None,
44
+ ) -> List[Dict]:
45
+ """Multipart upload. ``files`` may be paths, bytes, or a list of either."""
46
+ file_list = files if isinstance(files, list) else [files]
47
+ multipart = []
48
+ open_handles = []
49
+ try:
50
+ for item in file_list:
51
+ if isinstance(item, (str, Path)):
52
+ handle = open(item, "rb")
53
+ open_handles.append(handle)
54
+ multipart.append(("files", (Path(item).name, handle)))
55
+ else:
56
+ multipart.append(("files", ("upload.bin", item)))
57
+
58
+ params = {
59
+ "file_purpose": file_purpose,
60
+ "is_public": str(is_public).lower(),
61
+ "is_temporary": str(is_temporary).lower(),
62
+ "job_id": job_id or "",
63
+ }
64
+ with httpx.Client() as client:
65
+ response = client.post(
66
+ self.backend_url + "v1/files/upload-files",
67
+ params=self._catalog_params(params),
68
+ files=multipart,
69
+ headers=self._auth_headers,
70
+ timeout=400,
71
+ )
72
+ if response.status_code == 200:
73
+ data = response.json()
74
+ return data if isinstance(data, list) else []
75
+ print(f"Upload failed with status {response.status_code}: {response.text[:300]}")
76
+ return []
77
+ except Exception as exc:
78
+ print(f"Upload failed: {exc}")
79
+ return []
80
+ finally:
81
+ for handle in open_handles:
82
+ handle.close()
@@ -0,0 +1,80 @@
1
+ """Jobs facade (`/v1/jobs`)."""
2
+ from typing import Dict, List, Optional
3
+
4
+ from socaity_schemas.platform import Job
5
+
6
+
7
+ class JobsClient:
8
+ """Mixin: jobs list/get/mutate. Requires ``BackendHttp`` methods on ``self``."""
9
+
10
+ def query_jobs(
11
+ self,
12
+ q: Optional[str] = None,
13
+ filters: Optional[List[str]] = None,
14
+ expand: Optional[List[str]] = None,
15
+ fields: Optional[List[str]] = None,
16
+ visibility: str = "user_visible",
17
+ sort: Optional[str] = None,
18
+ limit: int = 100,
19
+ offset: int = 0,
20
+ known_ids: Optional[List[str]] = None,
21
+ known_fieldset: Optional[str] = None,
22
+ ) -> List[Job]:
23
+ rows = self._unwrap_list(self._get("v1/jobs", params={
24
+ "q": q,
25
+ "visibility": visibility,
26
+ "filter": filters or [],
27
+ "expand": expand or [],
28
+ "fields": fields or [],
29
+ "sort": sort,
30
+ "limit": limit,
31
+ "offset": offset,
32
+ "known_ids": known_ids or [],
33
+ "known_fieldset": known_fieldset,
34
+ }))
35
+ return [Job(**row) for row in rows]
36
+
37
+ def refresh_job(self, job_id: str) -> Optional[Dict]:
38
+ """Trigger jobs-catalog webhook refresh (cache + Typesense) for one job."""
39
+ return self._post("v1/jobs/webhooks/finished", params={"job_id": job_id})
40
+
41
+ def get_job(
42
+ self,
43
+ job_id: str,
44
+ expand: Optional[List[str]] = None,
45
+ fields: Optional[List[str]] = None,
46
+ ) -> Optional[Job]:
47
+ row = self._get(f"v1/jobs/{job_id}", params={
48
+ "expand": expand or [],
49
+ "fields": fields or [],
50
+ })
51
+ return Job(**row) if row else None
52
+
53
+ def update_job(
54
+ self,
55
+ job_id: str,
56
+ display_name: Optional[str] = None,
57
+ is_public: Optional[bool] = None,
58
+ ) -> bool:
59
+ return self._request("PATCH", f"v1/jobs/{job_id}", params={
60
+ "display_name": display_name,
61
+ "is_public": is_public,
62
+ }) is True
63
+
64
+ def update_social_metrics(
65
+ self,
66
+ job_id: str,
67
+ increase_views: Optional[bool] = None,
68
+ increase_downloads: Optional[bool] = None,
69
+ increase_shares: Optional[bool] = None,
70
+ increase_likes: Optional[bool] = None,
71
+ ) -> bool:
72
+ return self._post(f"v1/jobs/{job_id}/social", params={
73
+ "increase_views": increase_views,
74
+ "increase_downloads": increase_downloads,
75
+ "increase_shares": increase_shares,
76
+ "increase_likes": increase_likes,
77
+ }) is True
78
+
79
+ def delete_job(self, job_id: str) -> bool:
80
+ return self._request("DELETE", f"v1/jobs/{job_id}") is True
@@ -0,0 +1,67 @@
1
+ """Projects facade (`/v1/projects`)."""
2
+ from typing import List, Optional, Union
3
+
4
+ from socaity_schemas.platform import Project
5
+
6
+
7
+ class ProjectsClient:
8
+ """Mixin: projects CRUD + members. Requires ``BackendHttp`` methods on ``self``."""
9
+
10
+ def query_projects(
11
+ self,
12
+ filters: Optional[List[str]] = None,
13
+ expand: Optional[List[str]] = None,
14
+ fields: Optional[List[str]] = None,
15
+ sort: Optional[str] = None,
16
+ limit: int = 100,
17
+ offset: int = 0,
18
+ ) -> List[Project]:
19
+ rows = self._unwrap_list(self._get("v1/projects", params={
20
+ "filter": filters or [],
21
+ "expand": expand or [],
22
+ "fields": fields or [],
23
+ "sort": sort or "created_at:desc",
24
+ "limit": limit,
25
+ "offset": offset,
26
+ }))
27
+ return [Project(**row) for row in rows]
28
+
29
+ def get_project(
30
+ self,
31
+ project_id: str,
32
+ expand: Optional[List[str]] = None,
33
+ fields: Optional[List[str]] = None,
34
+ ) -> Optional[Project]:
35
+ row = self._get(f"v1/projects/{project_id}", params={
36
+ "expand": expand or [],
37
+ "fields": fields or [],
38
+ })
39
+ return Project(**row) if row else None
40
+
41
+ def upsert_project(
42
+ self,
43
+ display_name: Optional[str] = None,
44
+ description: Optional[str] = None,
45
+ project_id: Optional[str] = None,
46
+ ) -> Optional[str]:
47
+ return self._request("PUT", "v1/projects", params={
48
+ "display_name": display_name,
49
+ "description": description,
50
+ "project_id": project_id,
51
+ })
52
+
53
+ def delete_project(self, project_id: str) -> bool:
54
+ return self._request("DELETE", f"v1/projects/{project_id}") is True
55
+
56
+ def modify_members(
57
+ self,
58
+ project_id: str,
59
+ job_ids: Optional[List[str]] = None,
60
+ file_ids: Optional[List[Union[str, int]]] = None,
61
+ add: bool = True,
62
+ ) -> bool:
63
+ return self._post(f"v1/projects/{project_id}/members", params={
64
+ "job_ids": job_ids or [],
65
+ "file_ids": file_ids or [],
66
+ "add": "true" if add else "false",
67
+ }) is True
@@ -0,0 +1,16 @@
1
+ """SDK install / update package endpoints."""
2
+ from typing import Dict, List, Optional
3
+
4
+
5
+ class SdkInstallClient:
6
+ """Mixin: SDK package install. Requires ``BackendHttp`` methods on ``self``."""
7
+
8
+ def get_service_updates(self, version_index: Dict[str, str]) -> List[Dict]:
9
+ payload = [
10
+ {"deployment_id": deployment_id, "specification_hash": spec_hash or ""}
11
+ for deployment_id, spec_hash in version_index.items()
12
+ ]
13
+ return self._post("v1/sdk/update_package", payload) or []
14
+
15
+ def install_service(self, service_identifier: str) -> Optional[Dict]:
16
+ return self._post("v1/sdk/install_service", params={"service": service_identifier})