unitysvc-services 0.1.1__py3-none-any.whl → 0.1.5__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.
@@ -1,174 +1,188 @@
1
1
  """Query command group - query backend API for data."""
2
2
 
3
+ import asyncio
3
4
  import json
4
- import os
5
5
  from typing import Any
6
6
 
7
- import httpx
8
7
  import typer
9
8
  from rich.console import Console
10
9
  from rich.table import Table
11
10
 
11
+ from .api import UnitySvcAPI
12
+
12
13
  app = typer.Typer(help="Query backend API for data")
13
14
  console = Console()
14
15
 
15
16
 
16
- class ServiceDataQuery:
17
- """Query service data from UnitySVC backend endpoints."""
18
-
19
- def __init__(self, base_url: str, api_key: str):
20
- """Initialize query client with backend URL and API key.
21
-
22
- Args:
23
- base_url: UnitySVC backend URL
24
- api_key: API key for authentication
25
-
26
- Raises:
27
- ValueError: If base_url or api_key is not provided
28
- """
29
- if not base_url:
30
- raise ValueError(
31
- "Backend URL not provided. Use --backend-url or set UNITYSVC_BACKEND_URL env var."
32
- )
33
- if not api_key:
34
- raise ValueError(
35
- "API key not provided. Use --api-key or set UNITYSVC_API_KEY env var."
36
- )
37
-
38
- self.base_url = base_url.rstrip("/")
39
- self.api_key = api_key
40
- self.client = httpx.Client(
41
- headers={
42
- "X-API-Key": api_key,
43
- "Content-Type": "application/json",
44
- },
45
- timeout=30.0,
46
- )
17
+ class ServiceDataQuery(UnitySvcAPI):
18
+ """Query service data from UnitySVC backend endpoints.
47
19
 
48
- def list_service_offerings(self) -> list[dict[str, Any]]:
49
- """List all service offerings from the backend."""
50
- response = self.client.get(f"{self.base_url}/publish/service_offerings")
51
- response.raise_for_status()
52
- result = response.json()
53
- return result.get("data", result) if isinstance(result, dict) else result
54
-
55
- def list_service_listings(self) -> list[dict[str, Any]]:
56
- """List all service listings from the backend."""
57
- response = self.client.get(f"{self.base_url}/publish/services")
58
- response.raise_for_status()
59
- result = response.json()
60
- return result.get("data", result) if isinstance(result, dict) else result
61
-
62
- def list_providers(self) -> list[dict[str, Any]]:
63
- """List all providers from the backend."""
64
- response = self.client.get(f"{self.base_url}/publish/providers")
65
- response.raise_for_status()
66
- result = response.json()
67
- return result.get("data", result) if isinstance(result, dict) else result
68
-
69
- def list_sellers(self) -> list[dict[str, Any]]:
70
- """List all sellers from the backend."""
71
- response = self.client.get(f"{self.base_url}/publish/sellers")
72
- response.raise_for_status()
73
- result = response.json()
74
- return result.get("data", result) if isinstance(result, dict) else result
75
-
76
- def list_access_interfaces(self) -> dict[str, Any]:
77
- """List all access interfaces from the backend (private endpoint)."""
78
- response = self.client.get(f"{self.base_url}/private/access_interfaces")
79
- response.raise_for_status()
80
- return response.json()
81
-
82
- def list_documents(self) -> dict[str, Any]:
83
- """List all documents from the backend (private endpoint)."""
84
- response = self.client.get(f"{self.base_url}/private/documents")
85
- response.raise_for_status()
86
- return response.json()
87
-
88
- def close(self):
89
- """Close the HTTP client."""
90
- self.client.close()
91
-
92
- def __enter__(self):
93
- """Context manager entry."""
94
- return self
95
-
96
- def __exit__(self, exc_type, exc_val, exc_tb):
97
- """Context manager exit."""
98
- self.close()
99
-
100
- @staticmethod
101
- def from_env(
102
- backend_url: str | None = None, api_key: str | None = None
103
- ) -> "ServiceDataQuery":
104
- """Create ServiceDataQuery from environment variables or arguments.
105
-
106
- Args:
107
- backend_url: Optional backend URL (falls back to UNITYSVC_BACKEND_URL env var)
108
- api_key: Optional API key (falls back to UNITYSVC_API_KEY env var)
109
-
110
- Returns:
111
- ServiceDataQuery instance
112
-
113
- Raises:
114
- ValueError: If required credentials are not provided
115
- """
116
- resolved_backend_url = backend_url or os.getenv("UNITYSVC_BACKEND_URL") or ""
117
- resolved_api_key = api_key or os.getenv("UNITYSVC_API_KEY") or ""
118
- return ServiceDataQuery(base_url=resolved_backend_url, api_key=resolved_api_key)
20
+ Inherits HTTP methods with automatic curl fallback from UnitySvcAPI.
21
+ Provides async methods for querying public service data.
22
+ Use with async context manager for proper resource cleanup.
23
+ """
24
+
25
+ pass
119
26
 
120
27
 
121
28
  @app.command("sellers")
122
29
  def query_sellers(
123
- backend_url: str | None = typer.Option(
124
- None,
125
- "--backend-url",
126
- "-u",
127
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
128
- ),
129
- api_key: str | None = typer.Option(
130
- None,
131
- "--api-key",
132
- "-k",
133
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
134
- ),
135
30
  format: str = typer.Option(
136
31
  "table",
137
32
  "--format",
138
33
  "-f",
139
34
  help="Output format: table, json",
140
35
  ),
36
+ fields: str = typer.Option(
37
+ "id,name,display_name,seller_type",
38
+ "--fields",
39
+ help=(
40
+ "Comma-separated list of fields to display. Available fields: "
41
+ "id, name, display_name, seller_type, contact_email, "
42
+ "secondary_contact_email, homepage, description, "
43
+ "business_registration, tax_id, account_manager_id, "
44
+ "created_at, updated_at, status"
45
+ ),
46
+ ),
47
+ skip: int = typer.Option(
48
+ 0,
49
+ "--skip",
50
+ help="Number of records to skip (for pagination)",
51
+ ),
52
+ limit: int = typer.Option(
53
+ 100,
54
+ "--limit",
55
+ help="Maximum number of records to return (default: 100)",
56
+ ),
141
57
  ):
142
- """Query all sellers from the backend."""
58
+ """Query all sellers from the backend.
59
+
60
+ Examples:
61
+ # Use default fields
62
+ unitysvc_services query sellers
63
+
64
+ # Show only specific fields
65
+ unitysvc_services query sellers --fields id,name,contact_email
66
+
67
+ # Retrieve more than 100 records
68
+ unitysvc_services query sellers --limit 500
69
+
70
+ # Pagination: skip first 100, get next 100
71
+ unitysvc_services query sellers --skip 100 --limit 100
72
+
73
+ # Show all available fields
74
+ unitysvc_services query sellers --fields \\
75
+ id,name,display_name,seller_type,contact_email,homepage,created_at,updated_at
76
+ """
77
+ # Parse fields list
78
+ field_list = [f.strip() for f in fields.split(",")]
79
+
80
+ # Define allowed fields from SellerPublic model
81
+ allowed_fields = {
82
+ "id",
83
+ "name",
84
+ "display_name",
85
+ "seller_type",
86
+ "contact_email",
87
+ "secondary_contact_email",
88
+ "homepage",
89
+ "description",
90
+ "business_registration",
91
+ "tax_id",
92
+ "account_manager_id",
93
+ "created_at",
94
+ "updated_at",
95
+ "status",
96
+ }
97
+
98
+ # Validate fields
99
+ invalid_fields = [f for f in field_list if f not in allowed_fields]
100
+ if invalid_fields:
101
+ console.print(
102
+ f"[red]✗[/red] Invalid field(s): {', '.join(invalid_fields)}",
103
+ style="bold red",
104
+ )
105
+ console.print(f"[yellow]Allowed fields:[/yellow] {', '.join(sorted(allowed_fields))}")
106
+ raise typer.Exit(code=1)
107
+
108
+ async def _query_sellers_async():
109
+ async with ServiceDataQuery() as query:
110
+ sellers = await query.get("/publish/sellers", {"skip": skip, "limit": limit})
111
+ return sellers.get("data", sellers) if isinstance(sellers, dict) else sellers
112
+
143
113
  try:
144
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
145
- sellers = query.list_sellers()
114
+ sellers = asyncio.run(_query_sellers_async())
146
115
 
147
- if format == "json":
116
+ if format == "json":
117
+ # For JSON, filter fields if not all are requested
118
+ if set(field_list) != allowed_fields:
119
+ filtered_sellers = [{k: v for k, v in seller.items() if k in field_list} for seller in sellers]
120
+ console.print(json.dumps(filtered_sellers, indent=2))
121
+ else:
148
122
  console.print(json.dumps(sellers, indent=2))
123
+ else:
124
+ if not sellers:
125
+ console.print("[yellow]No sellers found.[/yellow]")
149
126
  else:
150
- if not sellers:
151
- console.print("[yellow]No sellers found.[/yellow]")
152
- else:
153
- table = Table(title="Sellers")
154
- table.add_column("ID", style="cyan")
155
- table.add_column("Name", style="green")
156
- table.add_column("Display Name", style="blue")
157
- table.add_column("Type", style="magenta")
158
- table.add_column("Contact Email", style="yellow")
159
- table.add_column("Active", style="white")
160
-
161
- for seller in sellers:
162
- table.add_row(
163
- str(seller.get("id", "N/A")),
164
- seller.get("name", "N/A"),
165
- seller.get("display_name", "N/A"),
166
- seller.get("seller_type", "N/A"),
167
- seller.get("contact_email", "N/A"),
168
- "✓" if seller.get("is_active") else "✗",
169
- )
170
-
171
- console.print(table)
127
+ table = Table(title="Sellers")
128
+
129
+ # Define column styles
130
+ field_styles = {
131
+ "id": "cyan",
132
+ "name": "green",
133
+ "display_name": "blue",
134
+ "seller_type": "magenta",
135
+ "contact_email": "yellow",
136
+ "secondary_contact_email": "yellow",
137
+ "homepage": "blue",
138
+ "description": "white",
139
+ "business_registration": "white",
140
+ "tax_id": "white",
141
+ "account_manager_id": "cyan",
142
+ "created_at": "white",
143
+ "updated_at": "white",
144
+ "status": "green",
145
+ }
146
+
147
+ # Define column headers
148
+ field_headers = {
149
+ "id": "ID",
150
+ "name": "Name",
151
+ "display_name": "Display Name",
152
+ "seller_type": "Type",
153
+ "contact_email": "Contact Email",
154
+ "secondary_contact_email": "Secondary Email",
155
+ "homepage": "Homepage",
156
+ "description": "Description",
157
+ "business_registration": "Business Reg",
158
+ "tax_id": "Tax ID",
159
+ "account_manager_id": "Account Manager ID",
160
+ "created_at": "Created At",
161
+ "updated_at": "Updated At",
162
+ "status": "Status",
163
+ }
164
+
165
+ # Add columns based on requested fields
166
+ for field in field_list:
167
+ header = field_headers.get(field, field.title())
168
+ style = field_styles.get(field, "white")
169
+ table.add_column(header, style=style)
170
+
171
+ # Add rows
172
+ for seller in sellers:
173
+ row = []
174
+ for field in field_list:
175
+ value = seller.get(field)
176
+ if value is None:
177
+ row.append("N/A")
178
+ elif isinstance(value, dict | list):
179
+ row.append(str(value)[:50]) # Truncate complex types
180
+ else:
181
+ row.append(str(value))
182
+ table.add_row(*row)
183
+
184
+ console.print(table)
185
+ console.print(f"\n[green]Total:[/green] {len(sellers)} seller(s)")
172
186
  except ValueError as e:
173
187
  console.print(f"[red]✗[/red] {e}", style="bold red")
174
188
  raise typer.Exit(code=1)
@@ -179,51 +193,148 @@ def query_sellers(
179
193
 
180
194
  @app.command("providers")
181
195
  def query_providers(
182
- backend_url: str | None = typer.Option(
183
- None,
184
- "--backend-url",
185
- "-u",
186
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
187
- ),
188
- api_key: str | None = typer.Option(
189
- None,
190
- "--api-key",
191
- "-k",
192
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
193
- ),
194
196
  format: str = typer.Option(
195
197
  "table",
196
198
  "--format",
197
199
  "-f",
198
200
  help="Output format: table, json",
199
201
  ),
202
+ fields: str = typer.Option(
203
+ "id,name,display_name,status",
204
+ "--fields",
205
+ help=(
206
+ "Comma-separated list of fields to display. Available fields: "
207
+ "id, name, display_name, contact_email, secondary_contact_email, "
208
+ "homepage, description, status, created_at, updated_at"
209
+ ),
210
+ ),
211
+ skip: int = typer.Option(
212
+ 0,
213
+ "--skip",
214
+ help="Number of records to skip (for pagination)",
215
+ ),
216
+ limit: int = typer.Option(
217
+ 100,
218
+ "--limit",
219
+ help="Maximum number of records to return (default: 100)",
220
+ ),
200
221
  ):
201
- """Query all providers from the backend."""
222
+ """Query all providers from the backend.
223
+
224
+ Examples:
225
+ # Use default fields
226
+ unitysvc_services query providers
227
+
228
+ # Retrieve more than 100 records
229
+ unitysvc_services query providers --limit 500
230
+
231
+ # Pagination: skip first 100, get next 100
232
+ unitysvc_services query providers --skip 100 --limit 100
233
+
234
+ # Show only specific fields
235
+ unitysvc_services query providers --fields id,name,contact_email
236
+
237
+ # Show all available fields
238
+ unitysvc_services query providers --fields \\
239
+ id,name,display_name,contact_email,homepage,status,created_at,updated_at
240
+ """
241
+ # Parse fields list
242
+ field_list = [f.strip() for f in fields.split(",")]
243
+
244
+ # Define allowed fields from ProviderPublic model
245
+ allowed_fields = {
246
+ "id",
247
+ "name",
248
+ "display_name",
249
+ "contact_email",
250
+ "secondary_contact_email",
251
+ "homepage",
252
+ "description",
253
+ "status",
254
+ "created_at",
255
+ "updated_at",
256
+ }
257
+
258
+ # Validate fields
259
+ invalid_fields = [f for f in field_list if f not in allowed_fields]
260
+ if invalid_fields:
261
+ console.print(
262
+ f"[red]✗[/red] Invalid field(s): {', '.join(invalid_fields)}",
263
+ style="bold red",
264
+ )
265
+ console.print(f"[yellow]Allowed fields:[/yellow] {', '.join(sorted(allowed_fields))}")
266
+ raise typer.Exit(code=1)
267
+
268
+ async def _query_providers_async():
269
+ async with ServiceDataQuery() as query:
270
+ providers = await query.get("/publish/providers", {"skip": skip, "limit": limit})
271
+ return providers.get("data", providers) if isinstance(providers, dict) else providers
272
+
202
273
  try:
203
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
204
- providers = query.list_providers()
274
+ providers = asyncio.run(_query_providers_async())
205
275
 
206
- if format == "json":
276
+ if format == "json":
277
+ # For JSON, filter fields if not all are requested
278
+ if set(field_list) != allowed_fields:
279
+ filtered_providers = [{k: v for k, v in provider.items() if k in field_list} for provider in providers]
280
+ console.print(json.dumps(filtered_providers, indent=2))
281
+ else:
207
282
  console.print(json.dumps(providers, indent=2))
283
+ else:
284
+ if not providers:
285
+ console.print("[yellow]No providers found.[/yellow]")
208
286
  else:
209
- if not providers:
210
- console.print("[yellow]No providers found.[/yellow]")
211
- else:
212
- table = Table(title="Providers")
213
- table.add_column("ID", style="cyan")
214
- table.add_column("Name", style="green")
215
- table.add_column("Display Name", style="blue")
216
- table.add_column("Time Created", style="magenta")
217
-
218
- for provider in providers:
219
- table.add_row(
220
- str(provider.get("id", "N/A")),
221
- provider.get("name", "N/A"),
222
- provider.get("display_name", "N/A"),
223
- str(provider.get("time_created", "N/A")),
224
- )
225
-
226
- console.print(table)
287
+ table = Table(title="Providers")
288
+
289
+ # Define column styles
290
+ field_styles = {
291
+ "id": "cyan",
292
+ "name": "green",
293
+ "display_name": "blue",
294
+ "contact_email": "yellow",
295
+ "secondary_contact_email": "yellow",
296
+ "homepage": "blue",
297
+ "description": "white",
298
+ "status": "green",
299
+ "created_at": "magenta",
300
+ "updated_at": "magenta",
301
+ }
302
+
303
+ # Define column headers
304
+ field_headers = {
305
+ "id": "ID",
306
+ "name": "Name",
307
+ "display_name": "Display Name",
308
+ "contact_email": "Contact Email",
309
+ "secondary_contact_email": "Secondary Email",
310
+ "homepage": "Homepage",
311
+ "description": "Description",
312
+ "status": "Status",
313
+ "created_at": "Created At",
314
+ "updated_at": "Updated At",
315
+ }
316
+
317
+ # Add columns based on requested fields
318
+ for field in field_list:
319
+ header = field_headers.get(field, field.title())
320
+ style = field_styles.get(field, "white")
321
+ table.add_column(header, style=style)
322
+
323
+ # Add rows
324
+ for provider in providers:
325
+ row = []
326
+ for field in field_list:
327
+ value = provider.get(field)
328
+ if value is None:
329
+ row.append("N/A")
330
+ elif isinstance(value, dict | list):
331
+ row.append(str(value)[:50]) # Truncate complex types
332
+ else:
333
+ row.append(str(value))
334
+ table.add_row(*row)
335
+
336
+ console.print(table)
337
+ console.print(f"\n[green]Total:[/green] {len(providers)} provider(s)")
227
338
  except ValueError as e:
228
339
  console.print(f"[red]✗[/red] {e}", style="bold red")
229
340
  raise typer.Exit(code=1)
@@ -234,256 +345,338 @@ def query_providers(
234
345
 
235
346
  @app.command("offerings")
236
347
  def query_offerings(
237
- backend_url: str | None = typer.Option(
238
- None,
239
- "--backend-url",
240
- "-u",
241
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
242
- ),
243
- api_key: str | None = typer.Option(
244
- None,
245
- "--api-key",
246
- "-k",
247
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
248
- ),
249
348
  format: str = typer.Option(
250
349
  "table",
251
350
  "--format",
252
351
  "-f",
253
352
  help="Output format: table, json",
254
353
  ),
354
+ fields: str = typer.Option(
355
+ "id,name,service_type,provider_name,status",
356
+ "--fields",
357
+ help=(
358
+ "Comma-separated list of fields to display. Available fields: "
359
+ "id, provider_id, status, price, service_name, "
360
+ "service_type, provider_name"
361
+ ),
362
+ ),
363
+ skip: int = typer.Option(
364
+ 0,
365
+ "--skip",
366
+ help="Number of records to skip (for pagination)",
367
+ ),
368
+ limit: int = typer.Option(
369
+ 100,
370
+ "--limit",
371
+ help="Maximum number of records to return (default: 100)",
372
+ ),
373
+ provider_name: str | None = typer.Option(
374
+ None,
375
+ "--provider-name",
376
+ help="Filter by provider name (case-insensitive partial match)",
377
+ ),
378
+ service_type: str | None = typer.Option(
379
+ None,
380
+ "--service-type",
381
+ help="Filter by service type (exact match, e.g., llm, vectordb, embedding)",
382
+ ),
383
+ name: str | None = typer.Option(
384
+ None,
385
+ "--name",
386
+ help="Filter by service name (case-insensitive partial match)",
387
+ ),
255
388
  ):
256
- """Query all service offerings from UnitySVC backend."""
389
+ """Query all service offerings from UnitySVC backend.
390
+
391
+ Examples:
392
+ # Use default fields
393
+ unitysvc_services query offerings
394
+
395
+ # Show only specific fields
396
+ unitysvc_services query offerings --fields id,name,status
397
+
398
+ # Filter by provider name
399
+ unitysvc_services query offerings --provider-name openai
400
+
401
+ # Filter by service type
402
+ unitysvc_services query offerings --service-type llm
403
+
404
+ # Filter by service name
405
+ unitysvc_services query offerings --name llama
406
+
407
+ # Combine multiple filters
408
+ unitysvc_services query offerings --service-type llm --provider-name openai
409
+
410
+ # Retrieve more than 100 records
411
+ unitysvc_services query offerings --limit 500
412
+
413
+ # Pagination: skip first 100, get next 100
414
+ unitysvc_services query offerings --skip 100 --limit 100
415
+
416
+ # Show all available fields
417
+ unitysvc_services query offerings --fields \\
418
+ id,service_name,service_type,provider_name,status,price,provider_id
419
+ """
420
+ # Parse fields list
421
+ field_list = [f.strip() for f in fields.split(",")]
422
+
423
+ # Define allowed fields from ServiceOfferingPublic model
424
+ allowed_fields = {
425
+ "id",
426
+ "provider_id",
427
+ "status",
428
+ "price",
429
+ "name",
430
+ "service_type",
431
+ "provider_name",
432
+ }
433
+
434
+ # Validate fields
435
+ invalid_fields = [f for f in field_list if f not in allowed_fields]
436
+ if invalid_fields:
437
+ console.print(
438
+ f"[red]Error:[/red] Invalid field(s): {', '.join(invalid_fields)}",
439
+ style="bold red",
440
+ )
441
+ console.print(f"[yellow]Available fields:[/yellow] {', '.join(sorted(allowed_fields))}")
442
+ raise typer.Exit(code=1)
443
+
444
+ async def _query_offerings_async():
445
+ async with ServiceDataQuery() as query:
446
+ params: dict[str, Any] = {"skip": skip, "limit": limit}
447
+ if provider_name:
448
+ params["provider_name"] = provider_name
449
+ if service_type:
450
+ params["service_type"] = service_type
451
+ if name:
452
+ params["name"] = name
453
+
454
+ offerings = await query.get("/publish/offerings", params)
455
+ return offerings.get("data", offerings) if isinstance(offerings, dict) else offerings
456
+
257
457
  try:
258
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
259
- offerings = query.list_service_offerings()
458
+ offerings = asyncio.run(_query_offerings_async())
260
459
 
261
- if format == "json":
460
+ if format == "json":
461
+ # For JSON, filter fields if not all are requested
462
+ if set(field_list) != allowed_fields:
463
+ filtered_offerings = [{k: v for k, v in offering.items() if k in field_list} for offering in offerings]
464
+ console.print(json.dumps(filtered_offerings, indent=2))
465
+ else:
262
466
  console.print(json.dumps(offerings, indent=2))
467
+ else:
468
+ if not offerings:
469
+ console.print("[yellow]No service offerings found.[/yellow]")
263
470
  else:
264
- if not offerings:
265
- console.print("[yellow]No service offerings found.[/yellow]")
266
- else:
267
- table = Table(title="Service Offerings", show_lines=True)
268
- table.add_column("ID", style="cyan")
269
- table.add_column("Name", style="green")
270
- table.add_column("Display Name", style="blue")
271
- table.add_column("Type", style="magenta")
272
- table.add_column("Status", style="yellow")
273
- table.add_column("Version")
274
-
275
- for offering in offerings:
276
- table.add_row(
277
- str(offering.get("id", "N/A")),
278
- offering.get("name", "N/A"),
279
- offering.get("display_name", "N/A"),
280
- offering.get("service_type", "N/A"),
281
- offering.get("upstream_status", "N/A"),
282
- offering.get("version", "N/A"),
283
- )
284
-
285
- console.print(table)
286
- console.print(
287
- f"\n[green]Total:[/green] {len(offerings)} service offering(s)"
288
- )
471
+ table = Table(title="Service Offerings")
472
+
473
+ # Add columns dynamically based on selected fields
474
+ for field in field_list:
475
+ # Capitalize and format field names for display
476
+ column_name = field.replace("_", " ").title()
477
+ table.add_column(column_name)
478
+
479
+ # Add rows
480
+ for offering in offerings:
481
+ row = []
482
+ for field in field_list:
483
+ value = offering.get(field)
484
+ if value is None:
485
+ row.append("N/A")
486
+ elif isinstance(value, dict | list):
487
+ row.append(str(value)[:50]) # Truncate complex types
488
+ else:
489
+ row.append(str(value))
490
+ table.add_row(*row)
491
+
492
+ console.print(table)
493
+ console.print(f"\n[green]Total:[/green] {len(offerings)} service offering(s)")
289
494
  except ValueError as e:
290
495
  console.print(f"[red]✗[/red] {e}", style="bold red")
291
496
  raise typer.Exit(code=1)
292
497
  except Exception as e:
293
- console.print(
294
- f"[red]✗[/red] Failed to query service offerings: {e}", style="bold red"
295
- )
498
+ console.print(f"[red]✗[/red] Failed to query service offerings: {e}", style="bold red")
296
499
  raise typer.Exit(code=1)
297
500
 
298
501
 
299
502
  @app.command("listings")
300
503
  def query_listings(
301
- backend_url: str | None = typer.Option(
302
- None,
303
- "--backend-url",
304
- "-u",
305
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
306
- ),
307
- api_key: str | None = typer.Option(
308
- None,
309
- "--api-key",
310
- "-k",
311
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
312
- ),
313
504
  format: str = typer.Option(
314
505
  "table",
315
506
  "--format",
316
507
  "-f",
317
508
  help="Output format: table, json",
318
509
  ),
319
- ):
320
- """Query all service listings from UnitySVC backend."""
321
- try:
322
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
323
- listings = query.list_service_listings()
324
-
325
- if format == "json":
326
- console.print(json.dumps(listings, indent=2))
327
- else:
328
- if not listings:
329
- console.print("[yellow]No service listings found.[/yellow]")
330
- else:
331
- table = Table(title="Service Listings", show_lines=True)
332
- table.add_column("ID", style="cyan")
333
- table.add_column("Service ID", style="blue")
334
- table.add_column("Seller", style="green")
335
- table.add_column("Status", style="yellow")
336
- table.add_column("Interfaces")
337
-
338
- for listing in listings:
339
- interfaces_count = len(
340
- listing.get("user_access_interfaces", [])
341
- )
342
- table.add_row(
343
- str(listing.get("id", "N/A")),
344
- str(listing.get("service_id", "N/A")),
345
- listing.get("seller_name", "N/A"),
346
- listing.get("listing_status", "N/A"),
347
- str(interfaces_count),
348
- )
349
-
350
- console.print(table)
351
- console.print(
352
- f"\n[green]Total:[/green] {len(listings)} service listing(s)"
353
- )
354
- except ValueError as e:
355
- console.print(f"[red]✗[/red] {e}", style="bold red")
356
- raise typer.Exit(code=1)
357
- except Exception as e:
358
- console.print(
359
- f"[red]✗[/red] Failed to query service listings: {e}", style="bold red"
360
- )
361
- raise typer.Exit(code=1)
362
-
363
-
364
- @app.command("interfaces")
365
- def query_interfaces(
366
- backend_url: str | None = typer.Option(
510
+ fields: str = typer.Option(
511
+ "id,service_name,service_type,seller_name,listing_type,status",
512
+ "--fields",
513
+ help=(
514
+ "Comma-separated list of fields to display. Available fields: "
515
+ "id, offering_id, offering_status, seller_id, status, created_at, updated_at, "
516
+ "parameters_schema, parameters_ui_schema, tags, service_name, "
517
+ "service_type, provider_name, seller_name, listing_type"
518
+ ),
519
+ ),
520
+ skip: int = typer.Option(
521
+ 0,
522
+ "--skip",
523
+ help="Number of records to skip (for pagination)",
524
+ ),
525
+ limit: int = typer.Option(
526
+ 100,
527
+ "--limit",
528
+ help="Maximum number of records to return (default: 100)",
529
+ ),
530
+ seller_name: str | None = typer.Option(
367
531
  None,
368
- "--backend-url",
369
- "-u",
370
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
532
+ "--seller-name",
533
+ help="Filter by seller name (case-insensitive partial match)",
371
534
  ),
372
- api_key: str | None = typer.Option(
535
+ provider_name: str | None = typer.Option(
373
536
  None,
374
- "--api-key",
375
- "-k",
376
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
537
+ "--provider-name",
538
+ help="Filter by provider name (case-insensitive partial match)",
377
539
  ),
378
- format: str = typer.Option(
379
- "table",
380
- "--format",
381
- "-f",
382
- help="Output format: table, json",
540
+ service_name: str | None = typer.Option(
541
+ None,
542
+ "--service-name",
543
+ help="Filter by service name (case-insensitive partial match)",
544
+ ),
545
+ service_type: str | None = typer.Option(
546
+ None,
547
+ "--service-type",
548
+ help="Filter by service type (exact match, e.g., llm, vectordb, embedding)",
549
+ ),
550
+ listing_type: str | None = typer.Option(
551
+ None,
552
+ "--listing-type",
553
+ help="Filter by listing type (exact match: regular, byop, self_hosted)",
383
554
  ),
384
555
  ):
385
- """Query all access interfaces from UnitySVC backend (private endpoint)."""
386
- try:
387
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
388
- data = query.list_access_interfaces()
389
-
390
- if format == "json":
391
- console.print(json.dumps(data, indent=2))
392
- else:
393
- interfaces = data.get("data", [])
394
- if not interfaces:
395
- console.print("[yellow]No access interfaces found.[/yellow]")
396
- else:
397
- table = Table(title="Access Interfaces", show_lines=True)
398
- table.add_column("ID", style="cyan")
399
- table.add_column("Name", style="green")
400
- table.add_column("Context", style="blue")
401
- table.add_column("Entity ID", style="yellow")
402
- table.add_column("Method", style="magenta")
403
- table.add_column("Active", style="green")
404
-
405
- for interface in interfaces:
406
- table.add_row(
407
- str(interface.get("id", "N/A"))[:8] + "...",
408
- interface.get("name", "N/A"),
409
- interface.get("context_type", "N/A"),
410
- str(interface.get("entity_id", "N/A"))[:8] + "...",
411
- interface.get("access_method", "N/A"),
412
- "✓" if interface.get("is_active") else "✗",
413
- )
414
-
415
- console.print(table)
416
- console.print(
417
- f"\n[green]Total:[/green] {data.get('count', 0)} access interface(s)"
418
- )
419
- except ValueError as e:
420
- console.print(f"[red]✗[/red] {e}", style="bold red")
421
- raise typer.Exit(code=1)
422
- except Exception as e:
556
+ """Query all service listings from UnitySVC backend.
557
+
558
+ Examples:
559
+ # Use default fields
560
+ unitysvc_services query listings
561
+
562
+ # Show only specific fields
563
+ unitysvc_services query listings --fields id,service_name,status
564
+
565
+ # Filter by seller name
566
+ unitysvc_services query listings --seller-name chutes
567
+
568
+ # Filter by provider name
569
+ unitysvc_services query listings --provider-name openai
570
+
571
+ # Filter by service type
572
+ unitysvc_services query listings --service-type llm
573
+
574
+ # Filter by listing type
575
+ unitysvc_services query listings --listing-type byop
576
+
577
+ # Combine multiple filters
578
+ unitysvc_services query listings --service-type llm --listing-type regular
579
+
580
+ # Retrieve more than 100 records
581
+ unitysvc_services query listings --limit 500
582
+
583
+ # Pagination: skip first 100, get next 100
584
+ unitysvc_services query listings --skip 100 --limit 100
585
+
586
+ # Show all available fields
587
+ unitysvc_services query listings --fields \\
588
+ id,name,service_name,service_type,seller_name,listing_type,status,provider_name
589
+ """
590
+ # Parse fields list
591
+ field_list = [f.strip() for f in fields.split(",")]
592
+
593
+ # Define allowed fields from ServiceListingPublic model
594
+ allowed_fields = {
595
+ "id",
596
+ "name",
597
+ "offering_id",
598
+ "offering_status",
599
+ "seller_id",
600
+ "status",
601
+ "created_at",
602
+ "updated_at",
603
+ "parameters_schema",
604
+ "parameters_ui_schema",
605
+ "tags",
606
+ "service_name",
607
+ "service_type",
608
+ "provider_name",
609
+ "seller_name",
610
+ "listing_type",
611
+ }
612
+
613
+ # Validate fields
614
+ invalid_fields = [f for f in field_list if f not in allowed_fields]
615
+ if invalid_fields:
423
616
  console.print(
424
- f"[red][/red] Failed to query access interfaces: {e}", style="bold red"
617
+ f"[red]Error:[/red] Invalid field(s): {', '.join(invalid_fields)}",
618
+ style="bold red",
425
619
  )
620
+ console.print(f"[yellow]Available fields:[/yellow] {', '.join(sorted(allowed_fields))}")
426
621
  raise typer.Exit(code=1)
427
622
 
623
+ async def _query_listings_async():
624
+ async with ServiceDataQuery() as query:
625
+ params: dict[str, Any] = {"skip": skip, "limit": limit}
626
+ if seller_name:
627
+ params["seller_name"] = seller_name
628
+ if provider_name:
629
+ params["provider_name"] = provider_name
630
+ if service_name:
631
+ params["service_name"] = service_name
632
+ if service_type:
633
+ params["service_type"] = service_type
634
+ if listing_type:
635
+ params["listing_type"] = listing_type
636
+
637
+ listings = await query.get("/publish/listings", params)
638
+ return listings.get("data", listings) if isinstance(listings, dict) else listings
428
639
 
429
- @app.command("documents")
430
- def query_documents(
431
- backend_url: str | None = typer.Option(
432
- None,
433
- "--backend-url",
434
- "-u",
435
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
436
- ),
437
- api_key: str | None = typer.Option(
438
- None,
439
- "--api-key",
440
- "-k",
441
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
442
- ),
443
- format: str = typer.Option(
444
- "table",
445
- "--format",
446
- "-f",
447
- help="Output format: table, json",
448
- ),
449
- ):
450
- """Query all documents from UnitySVC backend (private endpoint)."""
451
640
  try:
452
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
453
- data = query.list_documents()
641
+ listings = asyncio.run(_query_listings_async())
454
642
 
455
- if format == "json":
456
- console.print(json.dumps(data, indent=2))
643
+ if format == "json":
644
+ # For JSON, filter fields if not all are requested
645
+ if set(field_list) != allowed_fields:
646
+ filtered_listings = [{k: v for k, v in listing.items() if k in field_list} for listing in listings]
647
+ console.print(json.dumps(filtered_listings, indent=2))
648
+ else:
649
+ console.print(json.dumps(listings, indent=2))
650
+ else:
651
+ if not listings:
652
+ console.print("[yellow]No service listings found.[/yellow]")
457
653
  else:
458
- documents = data.get("data", [])
459
- if not documents:
460
- console.print("[yellow]No documents found.[/yellow]")
461
- else:
462
- table = Table(title="Documents", show_lines=True)
463
- table.add_column("ID", style="cyan")
464
- table.add_column("Title", style="green")
465
- table.add_column("Category", style="blue")
466
- table.add_column("MIME Type", style="yellow")
467
- table.add_column("Context", style="magenta")
468
- table.add_column("Public", style="red")
469
-
470
- for doc in documents:
471
- table.add_row(
472
- str(doc.get("id", "N/A"))[:8] + "...",
473
- doc.get("title", "N/A")[:40],
474
- doc.get("category", "N/A"),
475
- doc.get("mime_type", "N/A"),
476
- doc.get("context_type", "N/A"),
477
- "✓" if doc.get("is_public") else "✗",
478
- )
479
-
480
- console.print(table)
481
- console.print(
482
- f"\n[green]Total:[/green] {data.get('count', 0)} document(s)"
483
- )
654
+ table = Table(title="Service Listings")
655
+
656
+ # Add columns dynamically based on selected fields
657
+ for field in field_list:
658
+ # Capitalize and format field names for display
659
+ column_name = field.replace("_", " ").title()
660
+ table.add_column(column_name)
661
+
662
+ # Add rows
663
+ for listing in listings:
664
+ row = []
665
+ for field in field_list:
666
+ value = listing.get(field)
667
+ if value is None:
668
+ row.append("N/A")
669
+ elif isinstance(value, dict | list):
670
+ row.append(str(value)[:50]) # Truncate complex types
671
+ else:
672
+ row.append(str(value))
673
+ table.add_row(*row)
674
+
675
+ console.print(table)
676
+ console.print(f"\n[green]Total:[/green] {len(listings)} service listing(s)")
484
677
  except ValueError as e:
485
678
  console.print(f"[red]✗[/red] {e}", style="bold red")
486
679
  raise typer.Exit(code=1)
487
680
  except Exception as e:
488
- console.print(f"[red]✗[/red] Failed to query documents: {e}", style="bold red")
681
+ console.print(f"[red]✗[/red] Failed to query service listings: {e}", style="bold red")
489
682
  raise typer.Exit(code=1)