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