socaity-cli 0.1.8__tar.gz → 0.1.9__tar.gz

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.
Files changed (41) hide show
  1. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/PKG-INFO +1 -1
  2. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/pyproject.toml +1 -1
  3. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/backend_client.py +2 -0
  4. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/cli.py +9 -4
  5. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/__init__.py +8 -0
  6. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/catalog.py +69 -109
  7. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/conversations.py +11 -5
  8. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/files.py +6 -6
  9. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/jobs.py +21 -1
  10. socaity_cli-0.1.9/socaity_cli/clients/profile.py +29 -0
  11. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/workflows.py +9 -9
  12. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/errors.py +4 -0
  13. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/http.py +17 -10
  14. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/sdk_commands.py +15 -12
  15. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/PKG-INFO +1 -1
  16. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/SOURCES.txt +1 -0
  17. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/README.md +0 -0
  18. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/setup.cfg +0 -0
  19. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/__init__.py +0 -0
  20. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/__main__.py +0 -0
  21. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/apipod_commands.py +0 -0
  22. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/auth.py +0 -0
  23. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/analytics.py +0 -0
  24. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/deployment.py +0 -0
  25. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/interrupts.py +0 -0
  26. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/projects.py +0 -0
  27. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/clients/sdk_install.py +0 -0
  28. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/credentials.py +0 -0
  29. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/deploy_messages.py +0 -0
  30. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/deployment.py +0 -0
  31. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/prompts.py +0 -0
  32. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/registry_push.py +0 -0
  33. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli/requires.py +0 -0
  34. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/dependency_links.txt +0 -0
  35. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/entry_points.txt +0 -0
  36. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/requires.txt +0 -0
  37. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/socaity_cli.egg-info/top_level.txt +0 -0
  38. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/test/test_cli.py +0 -0
  39. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/test/test_credentials.py +0 -0
  40. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/test/test_private_slot_error.py +0 -0
  41. {socaity_cli-0.1.8 → socaity_cli-0.1.9}/test/test_registry_push.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socaity-cli
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Socaity platform CLI: login, catalog browsing, SDK installs and APIPod deployment commands.
5
5
  Author: SocAIty
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "socaity-cli"
7
- version = "0.1.8"
7
+ version = "0.1.9"
8
8
  description = "Socaity platform CLI: login, catalog browsing, SDK installs and APIPod deployment commands."
9
9
  requires-python = ">=3.8"
10
10
  authors = [
@@ -6,6 +6,7 @@ from socaity_cli.clients.deployment import DeploymentClient
6
6
  from socaity_cli.clients.files import FilesClient
7
7
  from socaity_cli.clients.interrupts import InterruptsClient
8
8
  from socaity_cli.clients.jobs import JobsClient
9
+ from socaity_cli.clients.profile import ProfileClient
9
10
  from socaity_cli.clients.projects import ProjectsClient
10
11
  from socaity_cli.clients.sdk_install import SdkInstallClient
11
12
  from socaity_cli.clients.workflows import WorkflowsClient
@@ -24,6 +25,7 @@ class SocaityBackendClient(
24
25
  ConversationsClient,
25
26
  InterruptsClient,
26
27
  WorkflowsClient,
28
+ ProfileClient,
27
29
  ):
28
30
  """Facade over domain mixins. Prefer importing a domain client when you only need one surface."""
29
31
  pass
@@ -16,7 +16,7 @@ from socaity_cli.sdk_commands import run_install, run_update, run_workflow_comma
16
16
  def _run_list(entity: str, category: str | None, family: str | None, limit: int) -> None:
17
17
  client = SocaityBackendClient()
18
18
  if entity == "services":
19
- services = client.list_services(category=category, limit=limit)
19
+ services = client.query_services(category=category, limit=limit)
20
20
  if not services:
21
21
  print("No services found.")
22
22
  return
@@ -26,7 +26,7 @@ def _run_list(entity: str, category: str | None, family: str | None, limit: int)
26
26
  desc = f" - {service.short_desc}" if service.short_desc else ""
27
27
  print(f"{name}{official}{desc}")
28
28
  else:
29
- models = client.list_models(family=family, limit=limit)
29
+ models = client.query_models(family=family, limit=limit)
30
30
  if not models:
31
31
  print("No models found.")
32
32
  return
@@ -137,7 +137,7 @@ def _run_workflows(args) -> None:
137
137
  command = args.workflows_command
138
138
 
139
139
  if command == "list":
140
- workflows = client.search_workflows(query=args.q, limit=args.limit)
140
+ workflows = client.query_workflows(q=args.q, limit=args.limit)
141
141
  if not workflows:
142
142
  print("No workflows found.")
143
143
  return
@@ -224,7 +224,12 @@ def _run_search(query: str, collections: str | None, limit: int) -> None:
224
224
  targets = [part.strip() for part in (collections or "services,models").split(",") if part.strip()]
225
225
  found = False
226
226
  for collection in targets:
227
- hits = client.search(query, collection=collection, limit=limit)
227
+ if collection == "models":
228
+ hits = client.query_models(q=query, limit=limit)
229
+ elif collection == "jobs":
230
+ hits = client.query_jobs(q=query, limit=limit)
231
+ else:
232
+ hits = client.query_services(q=query, limit=limit)
228
233
  if not hits:
229
234
  continue
230
235
  found = True
@@ -3,16 +3,24 @@ from socaity_cli.clients.analytics import AnalyticsClient
3
3
  from socaity_cli.clients.catalog import CatalogClient
4
4
  from socaity_cli.clients.deployment import DeploymentClient
5
5
  from socaity_cli.clients.files import FilesClient
6
+ from socaity_cli.clients.conversations import ConversationsClient
7
+ from socaity_cli.clients.interrupts import InterruptsClient
6
8
  from socaity_cli.clients.jobs import JobsClient
9
+ from socaity_cli.clients.profile import ProfileClient
7
10
  from socaity_cli.clients.projects import ProjectsClient
8
11
  from socaity_cli.clients.sdk_install import SdkInstallClient
12
+ from socaity_cli.clients.workflows import WorkflowsClient
9
13
 
10
14
  __all__ = [
11
15
  "AnalyticsClient",
12
16
  "CatalogClient",
17
+ "ConversationsClient",
13
18
  "DeploymentClient",
14
19
  "FilesClient",
20
+ "InterruptsClient",
15
21
  "JobsClient",
22
+ "ProfileClient",
16
23
  "ProjectsClient",
17
24
  "SdkInstallClient",
25
+ "WorkflowsClient",
18
26
  ]
@@ -1,8 +1,8 @@
1
1
  """AI catalog reads (`/v1/catalog`)."""
2
2
  import re
3
- from typing import Dict, List, Optional, Union
3
+ from typing import List, Optional
4
4
 
5
- from socaity_schemas.platform import AIModel, AIService, Job, ServiceCategory
5
+ from socaity_schemas.platform import AIModel, AIService, ServiceCategory
6
6
 
7
7
  SERVICE_LIST_FIELDS = ["id", "name", "display_name", "short_desc", "categories", "is_official", "n_usages"]
8
8
  SERVICE_DETAIL_EXPAND = ["deployments.contract", "endpoints", "models"]
@@ -10,38 +10,42 @@ MODEL_LIST_FIELDS = ["id", "name", "display_name", "family", "release_date", "hu
10
10
 
11
11
 
12
12
  class CatalogClient:
13
- """Mixin: catalog list/get/search. Requires ``BackendHttp`` methods on ``self``."""
13
+ """Mixin: catalog query/get. Requires ``BackendHttp`` methods on ``self``."""
14
14
 
15
15
  SERVICE_DETAIL_EXPAND = SERVICE_DETAIL_EXPAND
16
16
 
17
- def search_services(
17
+ def query_services(
18
18
  self,
19
- query: Optional[str] = None,
19
+ q: Optional[str] = None,
20
20
  category: Optional[str] = None,
21
21
  filters: Optional[List[str]] = None,
22
22
  expand: Optional[List[str]] = None,
23
23
  fields: Optional[List[str]] = None,
24
24
  sort: Optional[str] = None,
25
- limit: int = 20,
25
+ limit: int = 100,
26
26
  offset: int = 0,
27
+ known_ids: Optional[List[str]] = None,
28
+ known_fieldset: Optional[str] = None,
27
29
  mine: bool = False,
28
30
  ) -> List[AIService]:
29
- """Find AI services in the socaity catalog.
31
+ """Query AI services in the socaity catalog.
30
32
 
31
- Start here for any generation task. Search by capability ("text to image",
32
- "voice cloning"). Results are slim catalog rows; call ``get_service`` with
33
- a hit ``id`` for endpoints and the call signature under
34
- ``deployments.contract``.
33
+ Start here for any generation task. Query by capability ("text to image",
34
+ "voice cloning") or browse with filters. Results are slim catalog rows;
35
+ call ``get_service`` with a hit ``id`` for endpoints and the call
36
+ signature under ``deployments.contract``.
35
37
 
36
38
  Args:
37
- query: Free-text search. Omit to browse.
38
- category: Category id (not display name).
39
+ q: Free-text query. Omit to browse.
40
+ category: Category id (not display name). Appended as a filter.
39
41
  filters: Repeatable ``field:operator:value`` strings.
40
42
  expand: Relations to embed (``deployments``, ``models``, ``pricing``).
41
43
  fields: Sparse fieldset, e.g. ``["id", "name"]``.
42
44
  sort: ``field:asc`` or ``field:desc``.
43
45
  limit: Page size, 1 to 100.
44
46
  offset: Skip this many rows; increase by ``limit`` for the next page.
47
+ known_ids: Client cache ids for delta fetch.
48
+ known_fieldset: Client cache fieldset for delta fetch.
45
49
  mine: Only services owned by the caller.
46
50
 
47
51
  Returns:
@@ -50,51 +54,9 @@ class CatalogClient:
50
54
  service_filters = list(filters or [])
51
55
  if category:
52
56
  service_filters.append(f"categories:contains:{category}")
53
- rows = self.query_services(
54
- q=query,
55
- filters=service_filters or None,
56
- expand=expand,
57
- fields=fields,
58
- sort=sort,
59
- limit=limit,
60
- offset=offset,
61
- mine=mine,
62
- )
63
- # Stale Typesense (watermark after empty first sync, or a later
64
- # write-through of one service) can return [] or the wrong hit while
65
- # GET /services still lists the row. Browse + local token match fills
66
- # the gap without waiting for an index rebuild.
67
- if query and not mine:
68
- matched = [row for row in rows if _service_matches_query(row, query)]
69
- if matched:
70
- return matched[:limit]
71
- browsed = self.query_services(
72
- filters=service_filters or None,
73
- expand=expand,
74
- fields=fields,
75
- sort=sort,
76
- limit=max(limit, 100),
77
- offset=0,
78
- )
79
- return [row for row in browsed if _service_matches_query(row, query)][:limit]
80
- return rows
81
-
82
- def query_services(
83
- self,
84
- q: Optional[str] = None,
85
- filters: Optional[List[str]] = None,
86
- expand: Optional[List[str]] = None,
87
- fields: Optional[List[str]] = None,
88
- sort: Optional[str] = None,
89
- limit: int = 100,
90
- offset: int = 0,
91
- known_ids: Optional[List[str]] = None,
92
- known_fieldset: Optional[str] = None,
93
- mine: bool = False,
94
- ) -> List[AIService]:
95
57
  rows = self._unwrap_list(self._get("v1/catalog/services", params={
96
58
  "q": q,
97
- "filter": filters or [],
59
+ "filter": service_filters,
98
60
  "expand": expand or [],
99
61
  "fields": fields if fields is not None else SERVICE_LIST_FIELDS,
100
62
  "sort": sort,
@@ -104,24 +66,27 @@ class CatalogClient:
104
66
  "known_fieldset": known_fieldset,
105
67
  "mine": mine,
106
68
  }))
107
- return [AIService(**row) for row in rows]
108
-
109
- def list_services(
110
- self,
111
- category: Optional[str] = None,
112
- fields: Optional[List[str]] = None,
113
- expand: Optional[List[str]] = None,
114
- limit: int = 100,
115
- offset: int = 0,
116
- ) -> List[AIService]:
117
- filters = [f"categories:contains:{category}"] if category else None
118
- return self.query_services(
119
- filters=filters,
120
- expand=expand,
121
- fields=fields,
122
- limit=limit,
123
- offset=offset,
124
- )
69
+ services = [AIService(**row) for row in rows]
70
+ # Stale Typesense (watermark after empty first sync, or a later
71
+ # write-through of one service) can return [] or the wrong hit while
72
+ # GET /services still lists the row. Browse + local token match fills
73
+ # the gap without waiting for an index rebuild.
74
+ if q and not mine:
75
+ matched = [row for row in services if _service_matches_query(row, q)]
76
+ if matched:
77
+ return matched[:limit]
78
+ browsed = self._unwrap_list(self._get("v1/catalog/services", params={
79
+ "filter": service_filters,
80
+ "expand": expand or [],
81
+ "fields": fields if fields is not None else SERVICE_LIST_FIELDS,
82
+ "sort": sort,
83
+ "limit": max(limit, 100),
84
+ "offset": 0,
85
+ "mine": mine,
86
+ }))
87
+ browsed_services = [AIService(**row) for row in browsed]
88
+ return [row for row in browsed_services if _service_matches_query(row, q)][:limit]
89
+ return services
125
90
 
126
91
  def get_service(
127
92
  self,
@@ -137,7 +102,7 @@ class CatalogClient:
137
102
  ``deployments[].contract.endpoints[].parameters``.
138
103
 
139
104
  Args:
140
- identifier: Service id or slug from ``search_services``.
105
+ identifier: Service id or slug from ``query_services``.
141
106
  expand: Override the default expand list.
142
107
  fields: Sparse fieldset.
143
108
  filters: Post-fetch filters.
@@ -155,6 +120,8 @@ class CatalogClient:
155
120
  def query_models(
156
121
  self,
157
122
  q: Optional[str] = None,
123
+ family: Optional[str] = None,
124
+ task: Optional[str] = None,
158
125
  filters: Optional[List[str]] = None,
159
126
  expand: Optional[List[str]] = None,
160
127
  fields: Optional[List[str]] = None,
@@ -164,9 +131,32 @@ class CatalogClient:
164
131
  known_ids: Optional[List[str]] = None,
165
132
  known_fieldset: Optional[str] = None,
166
133
  ) -> List[AIModel]:
134
+ """Query AI models in the catalog.
135
+
136
+ Args:
137
+ q: Free-text query. Omit to browse.
138
+ family: Model family, e.g. ``flux``. Appended as a filter.
139
+ task: Task slug, e.g. ``text2image``. Appended as a filter.
140
+ filters: Repeatable ``field:operator:value`` strings.
141
+ expand: Relations to embed (``scores``, ``capabilities``).
142
+ fields: Sparse fieldset.
143
+ sort: ``field:asc`` or ``field:desc``.
144
+ limit: Page size.
145
+ offset: Skip this many rows.
146
+ known_ids: Client cache ids for delta fetch.
147
+ known_fieldset: Client cache fieldset for delta fetch.
148
+
149
+ Returns:
150
+ Model rows, or an empty list when nothing matches.
151
+ """
152
+ model_filters = list(filters or [])
153
+ if family:
154
+ model_filters.append(f"family:eq:{family}")
155
+ if task:
156
+ model_filters.append(f"tasks:contains:{task}")
167
157
  rows = self._unwrap_list(self._get("v1/catalog/models", params={
168
158
  "q": q,
169
- "filter": filters or [],
159
+ "filter": model_filters,
170
160
  "expand": expand or [],
171
161
  "fields": fields if fields is not None else MODEL_LIST_FIELDS,
172
162
  "sort": sort,
@@ -177,22 +167,6 @@ class CatalogClient:
177
167
  }))
178
168
  return [AIModel(**row) for row in rows]
179
169
 
180
- def list_models(
181
- self,
182
- family: Optional[str] = None,
183
- task: Optional[str] = None,
184
- fields: Optional[List[str]] = None,
185
- expand: Optional[List[str]] = None,
186
- limit: int = 100,
187
- offset: int = 0,
188
- ) -> List[AIModel]:
189
- filters: List[str] = []
190
- if family:
191
- filters.append(f"family:eq:{family}")
192
- if task:
193
- filters.append(f"tasks:contains:{task}")
194
- return self.query_models(filters=filters or None, expand=expand, fields=fields, limit=limit, offset=offset)
195
-
196
170
  def get_model(
197
171
  self,
198
172
  id_or_name: str,
@@ -205,33 +179,19 @@ class CatalogClient:
205
179
  })
206
180
  return AIModel(**row) if row else None
207
181
 
208
- def list_categories(self) -> List[ServiceCategory]:
182
+ def query_categories(self) -> List[ServiceCategory]:
183
+ """Query catalog task categories."""
209
184
  rows = self._get("v1/catalog/categories") or []
210
185
  if isinstance(rows, dict):
211
186
  rows = rows.get("entities") or rows.get("data") or []
212
187
  return [ServiceCategory(**row) for row in rows]
213
188
 
214
- def list_pricing_rules(self, active_only: bool = True) -> List[Dict]:
189
+ def list_pricing_rules(self, active_only: bool = True) -> List[dict]:
215
190
  rows = self._get("v1/catalog/pricing-rules", params={"active_only": active_only}) or []
216
191
  if isinstance(rows, dict):
217
192
  rows = rows.get("entities") or rows.get("data") or []
218
193
  return rows if isinstance(rows, list) else []
219
194
 
220
- def search(
221
- self,
222
- query: str,
223
- collection: str = "services",
224
- filters: Optional[List[str]] = None,
225
- limit: int = 20,
226
- visibility: str = "user_visible",
227
- ) -> List[Union[AIService, AIModel, Job]]:
228
- """Full-text search via catalog or jobs ``q`` param (Typesense)."""
229
- if collection == "models":
230
- return self.query_models(q=query, filters=filters, limit=limit)
231
- if collection == "jobs":
232
- return self.query_jobs(q=query, filters=filters, visibility=visibility, limit=limit)
233
- return self.search_services(query=query, filters=filters, limit=limit)
234
-
235
195
 
236
196
  def _service_matches_query(service: AIService, text: str) -> bool:
237
197
  tokens = _query_tokens(text)
@@ -6,9 +6,9 @@ from socaity_schemas.platform import Chat, ChatItem
6
6
 
7
7
 
8
8
  class ConversationsClient:
9
- """Mixin: conversation metadata and items. Requires ``BackendHttp`` methods on ``self``."""
9
+ """Mixin: conversation query/get/items. Requires ``BackendHttp`` methods on ``self``."""
10
10
 
11
- def list_conversations(
11
+ def query_conversations(
12
12
  self,
13
13
  q: Optional[str] = None,
14
14
  filters: Optional[List[str]] = None,
@@ -43,7 +43,7 @@ class ConversationsClient:
43
43
  })
44
44
  return Chat(**row) if row else None
45
45
 
46
- def list_conversation_items(
46
+ def query_conversation_items(
47
47
  self,
48
48
  conversation_id: str,
49
49
  branch: str = "active",
@@ -67,15 +67,21 @@ class ConversationsClient:
67
67
  conversation_id: str,
68
68
  title: Optional[str] = None,
69
69
  status: Optional[str] = None,
70
+ active_item_id: Optional[str] = None,
70
71
  ) -> Optional[Chat]:
71
- """Patch title and/or status on a conversation the caller owns."""
72
+ """Patch title, status, and/or active_item_id (1/N switch) on a conversation the caller owns."""
72
73
  row = self._request(
73
74
  "PATCH",
74
75
  f"v1/conversations/{conversation_id}",
75
- params={"title": title, "status": status},
76
+ params={"title": title, "status": status, "active_item_id": active_item_id},
76
77
  )
77
78
  return Chat(**row) if row else None
78
79
 
80
+ def fork_conversation(self, conversation_id: str) -> Optional[Chat]:
81
+ """Copy the conversation tree into a new chats.id."""
82
+ row = self._request("POST", f"v1/conversations/{conversation_id}/fork")
83
+ return Chat(**row) if row else None
84
+
79
85
  def delete_conversation(self, conversation_id: str) -> bool:
80
86
  """Soft-delete a conversation and clear its linked job data."""
81
87
  return bool(self._request("DELETE", f"v1/conversations/{conversation_id}"))
@@ -4,11 +4,11 @@ from typing import Dict, List, Optional, Union
4
4
 
5
5
  import httpx
6
6
 
7
- FILE_LIST_PURPOSES = ["USER_UPLOAD", "JOB_RESULT"]
7
+ FILE_LIST_PURPOSES = ["USER_UPLOAD", "JOB_OUTPUT"]
8
8
 
9
9
 
10
10
  class FilesClient:
11
- """Mixin: storage usage, list, get, upload, update, delete. Requires ``BackendHttp`` on ``self``."""
11
+ """Mixin: storage usage, query, get, upload, update, delete. Requires ``BackendHttp`` on ``self``."""
12
12
 
13
13
  def get_storage_usage(self) -> Optional[Dict]:
14
14
  """Return the caller's storage quota and current usage.
@@ -18,7 +18,7 @@ class FilesClient:
18
18
  """
19
19
  return self._get("v1/files/usage")
20
20
 
21
- def list_files(
21
+ def query_files(
22
22
  self,
23
23
  purpose: Union[str, List[str]] = FILE_LIST_PURPOSES,
24
24
  include_expired: bool = False,
@@ -28,7 +28,7 @@ class FilesClient:
28
28
  limit: int = 25,
29
29
  offset: int = 0,
30
30
  ) -> List[Dict]:
31
- """List stored files visible to the caller.
31
+ """Query stored files visible to the caller.
32
32
 
33
33
  Args:
34
34
  purpose: Filter by reference purpose. Default: uploads and job results.
@@ -83,7 +83,7 @@ class FilesClient:
83
83
  """Patch file metadata the caller owns. Requires approval in agent mode.
84
84
 
85
85
  Args:
86
- file_id: File id from ``list_files`` or ``get_file``.
86
+ file_id: File id from ``query_files`` or ``get_file``.
87
87
  is_public: Publish or unpublish the file.
88
88
  expires_at: ISO timestamp when the file should expire.
89
89
  clear_expires_at: Clear expiry and keep the file permanently.
@@ -101,7 +101,7 @@ class FilesClient:
101
101
  """Hard-delete a file the caller owns. Requires approval in agent mode.
102
102
 
103
103
  Args:
104
- file_id: File id from ``list_files`` or ``get_file``.
104
+ file_id: File id from ``query_files`` or ``get_file``.
105
105
 
106
106
  Returns:
107
107
  ``True`` when the delete succeeded, ``False`` otherwise.
@@ -24,6 +24,26 @@ class JobsClient:
24
24
  known_ids: Optional[List[str]] = None,
25
25
  known_fieldset: Optional[str] = None,
26
26
  ) -> List[Job]:
27
+ """Search and list jobs.
28
+
29
+ Last N jobs: ``limit=N`` (catalog default sort is newest first).
30
+ Pass ``q`` to search display names and prompts.
31
+
32
+ Args:
33
+ q: Optional search text.
34
+ filters: Optional catalog filter strings.
35
+ expand: Extra relations. Empty keeps the list slim.
36
+ fields: Optional field projection.
37
+ visibility: ``user_visible`` (default) or ``public``.
38
+ sort: Sort spec. Empty keeps the catalog default (newest first).
39
+ limit: Maximum rows (default 100).
40
+ offset: Skip this many rows.
41
+ known_ids: Client cache ids for delta fetch.
42
+ known_fieldset: Client cache fieldset for delta fetch.
43
+
44
+ Returns:
45
+ Job records the caller is allowed to see.
46
+ """
27
47
  rows = self._unwrap_list(self._get("v1/jobs", params={
28
48
  "q": q,
29
49
  "visibility": visibility,
@@ -54,7 +74,7 @@ class JobsClient:
54
74
  earlier result.
55
75
 
56
76
  Args:
57
- job_id: Platform job id from ``run_service`` or ``list_jobs``.
77
+ job_id: Platform job id from ``run_service`` or ``query_jobs``.
58
78
 
59
79
  Returns:
60
80
  The job record, or ``None`` when not found or not visible.
@@ -0,0 +1,29 @@
1
+ """Identity and token exchange (`/v1/profile`, `/v1/cli-auth`)."""
2
+ from typing import Any, Dict, Optional
3
+
4
+
5
+ class ProfileClient:
6
+ """Mixin: caller identity. Requires ``BackendHttp`` methods on ``self``."""
7
+
8
+ def whoami(self) -> Optional[Dict[str, Any]]:
9
+ """Return the authenticated caller's identity.
10
+
11
+ Used by hosts that need to verify an opaque ``sk_``/``tk_`` key.
12
+ The backend owns the answer; there is nothing to check locally.
13
+
14
+ Returns:
15
+ Identity dict with ``user_id`` when the Bearer key is valid,
16
+ or ``None`` when the key is unknown.
17
+ """
18
+ return self._get("v1/profile/whoami")
19
+
20
+ def exchange_cli_auth(self) -> Optional[Dict[str, Any]]:
21
+ """Exchange the current Bearer token (JWT) for a platform ``tk_`` key.
22
+
23
+ The Authorization header is the token to exchange, not a platform key.
24
+ MCP OAuth uses this after verifying a Supabase JWT.
25
+
26
+ Returns:
27
+ Payload containing ``api_key``, or ``None`` when exchange fails.
28
+ """
29
+ return self._post("v1/cli-auth/exchange")
@@ -11,7 +11,7 @@ from socaity_schemas.platform.workflow import (
11
11
  WorkflowUpsertResult,
12
12
  )
13
13
 
14
- WORKFLOW_DETAIL_EXPAND = ["document"]
14
+ WORKFLOW_DETAIL_EXPAND = ["document", "layout"]
15
15
 
16
16
 
17
17
  def _dump_model(value: Union[Dict[str, Any], Any, None]) -> Optional[Dict[str, Any]]:
@@ -26,9 +26,9 @@ class WorkflowsClient:
26
26
 
27
27
  WORKFLOW_DETAIL_EXPAND = WORKFLOW_DETAIL_EXPAND
28
28
 
29
- def search_workflows(
29
+ def query_workflows(
30
30
  self,
31
- query: Optional[str] = None,
31
+ q: Optional[str] = None,
32
32
  filters: Optional[List[str]] = None,
33
33
  expand: Optional[List[str]] = None,
34
34
  fields: Optional[List[str]] = None,
@@ -38,7 +38,7 @@ class WorkflowsClient:
38
38
  ) -> List[Workflow]:
39
39
  """Find workflows: your own plus public ones.
40
40
 
41
- ``query`` searches title, goal, slug and node text. Results are slim
41
+ ``q`` searches title, goal, slug and node text. Results are slim
42
42
  catalog rows; call ``get_workflow`` with a hit ``id`` (or slug) for the
43
43
  full document.
44
44
 
@@ -46,7 +46,7 @@ class WorkflowsClient:
46
46
  ``slug:eq:<slug>`` and ``visibility:eq:mine|public|blended``.
47
47
 
48
48
  Args:
49
- query: Free-text search. Omit to browse.
49
+ q: Free-text query. Omit to browse.
50
50
  filters: Repeatable ``field:operator:value`` strings.
51
51
  expand: Relations to embed (``document``, ``revisions``).
52
52
  fields: Sparse fieldset, e.g. ``["id", "slug", "title"]``.
@@ -58,7 +58,7 @@ class WorkflowsClient:
58
58
  Workflow rows, or an empty list when nothing matches.
59
59
  """
60
60
  rows = self._unwrap_list(self._get("v1/workflows", params={
61
- "q": query,
61
+ "q": q,
62
62
  "filter": filters or [],
63
63
  "expand": expand or [],
64
64
  "fields": fields or [],
@@ -82,8 +82,8 @@ class WorkflowsClient:
82
82
  expects. Pin an older state via ``revision_id`` or ``version``.
83
83
 
84
84
  Args:
85
- workflow_id: ``wf_`` id or slug from ``search_workflows``.
86
- expand: Override the default expand (``document``, ``revisions``).
85
+ workflow_id: ``wf_`` id or slug from ``query_workflows``.
86
+ expand: Override the default expand (``document``, ``layout``, ``revisions``).
87
87
  revision_id: ``rv_`` id to pin; wins over ``version``.
88
88
  version: Valid version int; default is the latest.
89
89
 
@@ -157,7 +157,7 @@ class WorkflowsClient:
157
157
  )
158
158
  return WorkflowUpsertResult(**row) if row else None
159
159
 
160
- def list_workflow_revisions(
160
+ def query_workflow_revisions(
161
161
  self,
162
162
  workflow_id: str,
163
163
  expand: Optional[List[str]] = None,
@@ -40,3 +40,7 @@ class RegistryQuotaExceededError(BackendApiError):
40
40
 
41
41
  def __init__(self, message: str, *, detail: Any = None):
42
42
  super().__init__(413, message, code="REGISTRY_QUOTA_EXCEEDED", detail=detail)
43
+
44
+
45
+ class BackendTransportError(Exception):
46
+ """Network failure talking to the Socaity backend."""
@@ -6,7 +6,12 @@ from typing import Any, Dict, Optional
6
6
  import httpx
7
7
 
8
8
  from socaity_cli.credentials import DEFAULT_BACKEND_URL, get_api_key, load_credentials
9
- from socaity_cli.errors import BackendApiError, PrivateSlotLimitError, RegistryQuotaExceededError
9
+ from socaity_cli.errors import (
10
+ BackendApiError,
11
+ BackendTransportError,
12
+ PrivateSlotLimitError,
13
+ RegistryQuotaExceededError,
14
+ )
10
15
 
11
16
 
12
17
  def resolve_backend_url(backend_url: Optional[str] = None) -> str:
@@ -82,16 +87,18 @@ class BackendHttp:
82
87
  headers=self._auth_headers,
83
88
  timeout=400,
84
89
  )
85
- if response.status_code in (200, 202):
86
- return response.json() if response.content else {}
87
- self._raise_for_structured_error(response.status_code, response.text)
88
- print(f"Request to {path} failed with status {response.status_code}: {response.text[:300]}")
89
- return None
90
- except (PrivateSlotLimitError, RegistryQuotaExceededError, BackendApiError):
91
- raise
92
- except Exception as e:
93
- print(f"Request to {path} failed: {e}")
90
+ except httpx.HTTPError as exc:
91
+ raise BackendTransportError(f"Request to {path} failed: {exc}") from exc
92
+
93
+ if response.status_code in (200, 201, 202):
94
+ return response.json() if response.content else {}
95
+ self._raise_for_structured_error(response.status_code, response.text)
96
+ if response.status_code == 404:
94
97
  return None
98
+ raise BackendApiError(
99
+ response.status_code,
100
+ (response.text or f"HTTP {response.status_code}")[:300],
101
+ )
95
102
 
96
103
  @staticmethod
97
104
  def _catalog_params(params: Dict) -> Dict:
@@ -33,22 +33,25 @@ def run_workflow_command(workflow: str, inputs: dict, timeout_s: float | None =
33
33
  """Run a workflow through the inference gateway and print the terminal result."""
34
34
  import json
35
35
 
36
- from socaity.tools.workflows import execute_workflow
36
+ from socaity.client import SocaityClient
37
+ from socaity.core.serialize import serialize_job
37
38
 
38
- def print_progress(progress: float, message: str) -> None:
39
- print(f"[{progress:>4.0%}] {message}")
39
+ client = SocaityClient()
40
40
 
41
- def print_job(job_id: str, _envelope) -> None:
42
- print(f"Job {job_id} submitted. Waiting...")
41
+ def on_progress(event) -> None:
42
+ if event.kind == "started":
43
+ print(f"Job {event.job_id} submitted. Waiting...")
44
+ elif event.kind == "progress" and event.message:
45
+ print(f"[{(event.progress or 0):>4.0%}] {event.message}")
43
46
 
44
47
  try:
45
- result = execute_workflow(
46
- workflow,
47
- inputs=inputs,
48
- timeout_s=timeout_s,
49
- on_progress=print_progress,
50
- on_job_start=print_job,
51
- )
48
+ job = client.run_workflow(workflow, inputs=inputs)
49
+ unsubscribe = job.subscribe(on_started=on_progress, on_progress=on_progress, replay=True)
50
+ try:
51
+ job.get_result(timeout_s=timeout_s)
52
+ finally:
53
+ unsubscribe()
54
+ result = serialize_job(job)
52
55
  except (ValueError, RuntimeError) as error:
53
56
  print(str(error))
54
57
  raise SystemExit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socaity-cli
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Socaity platform CLI: login, catalog browsing, SDK installs and APIPod deployment commands.
5
5
  Author: SocAIty
6
6
  License: MIT
@@ -29,6 +29,7 @@ socaity_cli/clients/deployment.py
29
29
  socaity_cli/clients/files.py
30
30
  socaity_cli/clients/interrupts.py
31
31
  socaity_cli/clients/jobs.py
32
+ socaity_cli/clients/profile.py
32
33
  socaity_cli/clients/projects.py
33
34
  socaity_cli/clients/sdk_install.py
34
35
  socaity_cli/clients/workflows.py
File without changes
File without changes