unitysvc-services 0.1.0__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,172 +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("API key not provided. Use --api-key or set UNITYSVC_API_KEY env var.")
35
-
36
- self.base_url = base_url.rstrip("/")
37
- self.api_key = api_key
38
- self.client = httpx.Client(
39
- headers={
40
- "X-API-Key": api_key,
41
- "Content-Type": "application/json",
42
- },
43
- timeout=30.0,
44
- )
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).
45
28
 
46
- def list_service_offerings(self) -> list[dict[str, Any]]:
47
- """List all service offerings from the backend."""
48
- response = self.client.get(f"{self.base_url}/publish/service_offering")
49
- response.raise_for_status()
50
- 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}))
51
34
  return result.get("data", result) if isinstance(result, dict) else result
52
35
 
53
- def list_service_listings(self) -> list[dict[str, Any]]:
54
- """List all service listings from the backend."""
55
- response = self.client.get(f"{self.base_url}/services/")
56
- response.raise_for_status()
57
- 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}))
58
44
  return result.get("data", result) if isinstance(result, dict) else result
59
45
 
60
- def list_providers(self) -> list[dict[str, Any]]:
61
- """List all providers from the backend."""
62
- response = self.client.get(f"{self.base_url}/providers/")
63
- response.raise_for_status()
64
- 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}))
65
54
  return result.get("data", result) if isinstance(result, dict) else result
66
55
 
67
- def list_sellers(self) -> list[dict[str, Any]]:
68
- """List all sellers from the backend."""
69
- response = self.client.get(f"{self.base_url}/sellers/")
70
- response.raise_for_status()
71
- 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}))
72
64
  return result.get("data", result) if isinstance(result, dict) else result
73
65
 
74
- def list_access_interfaces(self) -> dict[str, Any]:
75
- """List all access interfaces from the backend (private endpoint)."""
76
- response = self.client.get(f"{self.base_url}/private/access_interfaces")
77
- response.raise_for_status()
78
- 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.
79
68
 
80
- def list_documents(self) -> dict[str, Any]:
81
- """List all documents from the backend (private endpoint)."""
82
- response = self.client.get(f"{self.base_url}/private/documents")
83
- response.raise_for_status()
84
- return response.json()
69
+ Args:
70
+ endpoint: API endpoint path
71
+ params: Query parameters
85
72
 
86
- def close(self):
87
- """Close the HTTP client."""
88
- self.client.close()
73
+ Returns:
74
+ JSON response as dictionary
75
+ """
76
+ return asyncio.run(super().get(endpoint, params))
89
77
 
90
- def __enter__(self):
91
- """Context manager entry."""
92
- 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.
93
82
 
94
- def __exit__(self, exc_type, exc_val, exc_tb):
95
- """Context manager exit."""
96
- self.close()
83
+ Args:
84
+ endpoint: API endpoint path
85
+ json_data: JSON body data
86
+ params: Query parameters
87
+
88
+ Returns:
89
+ JSON response as dictionary
90
+ """
91
+ return asyncio.run(super().post(endpoint, json_data, params))
97
92
 
98
- @staticmethod
99
- def from_env(
100
- backend_url: str | None = None, api_key: str | None = None
101
- ) -> "ServiceDataQuery":
102
- """Create ServiceDataQuery from environment variables or arguments.
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.
103
95
 
104
96
  Args:
105
- backend_url: Optional backend URL (falls back to UNITYSVC_BACKEND_URL env var)
106
- 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
107
100
 
108
101
  Returns:
109
- ServiceDataQuery instance
102
+ Task result dictionary
110
103
 
111
104
  Raises:
112
- ValueError: If required credentials are not provided
105
+ ValueError: If task fails or times out
113
106
  """
114
- resolved_backend_url = backend_url or os.getenv("UNITYSVC_BACKEND_URL") or ""
115
- resolved_api_key = api_key or os.getenv("UNITYSVC_API_KEY") or ""
116
- 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())
117
116
 
118
117
 
119
118
  @app.command("sellers")
120
119
  def query_sellers(
121
- backend_url: str | None = typer.Option(
122
- None,
123
- "--backend-url",
124
- "-u",
125
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
126
- ),
127
- api_key: str | None = typer.Option(
128
- None,
129
- "--api-key",
130
- "-k",
131
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
132
- ),
133
120
  format: str = typer.Option(
134
121
  "table",
135
122
  "--format",
136
123
  "-f",
137
124
  help="Output format: table, json",
138
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
+ ),
139
147
  ):
140
- """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
+
141
198
  try:
142
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
143
- sellers = query.list_sellers()
199
+ with ServiceDataQuery() as query:
200
+ sellers = query.list_sellers(skip=skip, limit=limit)
144
201
 
145
202
  if format == "json":
146
- 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))
147
209
  else:
148
210
  if not sellers:
149
211
  console.print("[yellow]No sellers found.[/yellow]")
150
212
  else:
151
213
  table = Table(title="Sellers")
152
- table.add_column("ID", style="cyan")
153
- table.add_column("Name", style="green")
154
- table.add_column("Display Name", style="blue")
155
- table.add_column("Type", style="magenta")
156
- table.add_column("Contact Email", style="yellow")
157
- table.add_column("Active", style="white")
158
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
159
258
  for seller in sellers:
160
- table.add_row(
161
- str(seller.get("id", "N/A")),
162
- seller.get("name", "N/A"),
163
- seller.get("display_name", "N/A"),
164
- seller.get("seller_type", "N/A"),
165
- seller.get("contact_email", "N/A"),
166
- "✓" if seller.get("is_active") else "✗",
167
- )
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)
168
269
 
169
270
  console.print(table)
271
+ console.print(f"\n[green]Total:[/green] {len(sellers)} seller(s)")
170
272
  except ValueError as e:
171
273
  console.print(f"[red]✗[/red] {e}", style="bold red")
172
274
  raise typer.Exit(code=1)
@@ -177,51 +279,146 @@ def query_sellers(
177
279
 
178
280
  @app.command("providers")
179
281
  def query_providers(
180
- backend_url: str | None = typer.Option(
181
- None,
182
- "--backend-url",
183
- "-u",
184
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
185
- ),
186
- api_key: str | None = typer.Option(
187
- None,
188
- "--api-key",
189
- "-k",
190
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
191
- ),
192
282
  format: str = typer.Option(
193
283
  "table",
194
284
  "--format",
195
285
  "-f",
196
286
  help="Output format: table, json",
197
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
+ ),
198
307
  ):
199
- """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
+
200
354
  try:
201
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
202
- providers = query.list_providers()
355
+ with ServiceDataQuery() as query:
356
+ providers = query.list_providers(skip=skip, limit=limit)
203
357
 
204
358
  if format == "json":
205
- 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))
206
367
  else:
207
368
  if not providers:
208
369
  console.print("[yellow]No providers found.[/yellow]")
209
370
  else:
210
371
  table = Table(title="Providers")
211
- table.add_column("ID", style="cyan")
212
- table.add_column("Name", style="green")
213
- table.add_column("Display Name", style="blue")
214
- table.add_column("Time Created", style="magenta")
215
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
216
408
  for provider in providers:
217
- table.add_row(
218
- str(provider.get("id", "N/A")),
219
- provider.get("name", "N/A"),
220
- provider.get("display_name", "N/A"),
221
- str(provider.get("time_created", "N/A")),
222
- )
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)
223
419
 
224
420
  console.print(table)
421
+ console.print(f"\n[green]Total:[/green] {len(providers)} provider(s)")
225
422
  except ValueError as e:
226
423
  console.print(f"[red]✗[/red] {e}", style="bold red")
227
424
  raise typer.Exit(code=1)
@@ -232,53 +429,113 @@ def query_providers(
232
429
 
233
430
  @app.command("offerings")
234
431
  def query_offerings(
235
- backend_url: str | None = typer.Option(
236
- None,
237
- "--backend-url",
238
- "-u",
239
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
240
- ),
241
- api_key: str | None = typer.Option(
242
- None,
243
- "--api-key",
244
- "-k",
245
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
246
- ),
247
432
  format: str = typer.Option(
248
433
  "table",
249
434
  "--format",
250
435
  "-f",
251
436
  help="Output format: table, json",
252
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
+ ),
253
457
  ):
254
- """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
+
255
502
  try:
256
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
257
- offerings = query.list_service_offerings()
503
+ with ServiceDataQuery() as query:
504
+ offerings = query.list_service_offerings(skip=skip, limit=limit)
258
505
 
259
506
  if format == "json":
260
- 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))
261
515
  else:
262
516
  if not offerings:
263
517
  console.print("[yellow]No service offerings found.[/yellow]")
264
518
  else:
265
- table = Table(title="Service Offerings", show_lines=True)
266
- table.add_column("ID", style="cyan")
267
- table.add_column("Name", style="green")
268
- table.add_column("Display Name", style="blue")
269
- table.add_column("Type", style="magenta")
270
- table.add_column("Status", style="yellow")
271
- table.add_column("Version")
519
+ table = Table(title="Service Offerings")
272
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)
526
+
527
+ # Add rows
273
528
  for offering in offerings:
274
- table.add_row(
275
- str(offering.get("id", "N/A")),
276
- offering.get("name", "N/A"),
277
- offering.get("display_name", "N/A"),
278
- offering.get("service_type", "N/A"),
279
- offering.get("upstream_status", "N/A"),
280
- offering.get("version", "N/A"),
281
- )
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)
282
539
 
283
540
  console.print(table)
284
541
  console.print(f"\n[green]Total:[/green] {len(offerings)} service offering(s)")
@@ -292,52 +549,120 @@ def query_offerings(
292
549
 
293
550
  @app.command("listings")
294
551
  def query_listings(
295
- backend_url: str | None = typer.Option(
296
- None,
297
- "--backend-url",
298
- "-u",
299
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
300
- ),
301
- api_key: str | None = typer.Option(
302
- None,
303
- "--api-key",
304
- "-k",
305
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
306
- ),
307
552
  format: str = typer.Option(
308
553
  "table",
309
554
  "--format",
310
555
  "-f",
311
556
  help="Output format: table, json",
312
557
  ),
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
+ ),
567
+ ),
568
+ skip: int = typer.Option(
569
+ 0,
570
+ "--skip",
571
+ help="Number of records to skip (for pagination)",
572
+ ),
573
+ limit: int = typer.Option(
574
+ 100,
575
+ "--limit",
576
+ help="Maximum number of records to return (default: 100)",
577
+ ),
313
578
  ):
314
- """Query all service listings from UnitySVC backend."""
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:
624
+ console.print(
625
+ f"[red]Error:[/red] Invalid field(s): {', '.join(invalid_fields)}",
626
+ style="bold red",
627
+ )
628
+ console.print(f"[yellow]Available fields:[/yellow] {', '.join(sorted(allowed_fields))}")
629
+ raise typer.Exit(code=1)
630
+
315
631
  try:
316
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
317
- listings = query.list_service_listings()
632
+ with ServiceDataQuery() as query:
633
+ listings = query.list_service_listings(skip=skip, limit=limit)
318
634
 
319
635
  if format == "json":
320
- console.print(json.dumps(listings, 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))
321
642
  else:
322
643
  if not listings:
323
644
  console.print("[yellow]No service listings found.[/yellow]")
324
645
  else:
325
- table = Table(title="Service Listings", show_lines=True)
326
- table.add_column("ID", style="cyan")
327
- table.add_column("Service ID", style="blue")
328
- table.add_column("Seller", style="green")
329
- table.add_column("Status", style="yellow")
330
- table.add_column("Interfaces")
646
+ table = Table(title="Service Listings")
331
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
332
655
  for listing in listings:
333
- interfaces_count = len(listing.get("user_access_interfaces", []))
334
- table.add_row(
335
- str(listing.get("id", "N/A")),
336
- str(listing.get("service_id", "N/A")),
337
- listing.get("seller_name", "N/A"),
338
- listing.get("listing_status", "N/A"),
339
- str(interfaces_count),
340
- )
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)
341
666
 
342
667
  console.print(table)
343
668
  console.print(f"\n[green]Total:[/green] {len(listings)} service listing(s)")
@@ -347,125 +672,3 @@ def query_listings(
347
672
  except Exception as e:
348
673
  console.print(f"[red]✗[/red] Failed to query service listings: {e}", style="bold red")
349
674
  raise typer.Exit(code=1)
350
-
351
-
352
- @app.command("interfaces")
353
- def query_interfaces(
354
- backend_url: str | None = typer.Option(
355
- None,
356
- "--backend-url",
357
- "-u",
358
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
359
- ),
360
- api_key: str | None = typer.Option(
361
- None,
362
- "--api-key",
363
- "-k",
364
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
365
- ),
366
- format: str = typer.Option(
367
- "table",
368
- "--format",
369
- "-f",
370
- help="Output format: table, json",
371
- ),
372
- ):
373
- """Query all access interfaces from UnitySVC backend (private endpoint)."""
374
- try:
375
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
376
- data = query.list_access_interfaces()
377
-
378
- if format == "json":
379
- console.print(json.dumps(data, indent=2))
380
- else:
381
- interfaces = data.get("data", [])
382
- if not interfaces:
383
- console.print("[yellow]No access interfaces found.[/yellow]")
384
- else:
385
- table = Table(title="Access Interfaces", show_lines=True)
386
- table.add_column("ID", style="cyan")
387
- table.add_column("Name", style="green")
388
- table.add_column("Context", style="blue")
389
- table.add_column("Entity ID", style="yellow")
390
- table.add_column("Method", style="magenta")
391
- table.add_column("Active", style="green")
392
-
393
- for interface in interfaces:
394
- table.add_row(
395
- str(interface.get("id", "N/A"))[:8] + "...",
396
- interface.get("name", "N/A"),
397
- interface.get("context_type", "N/A"),
398
- str(interface.get("entity_id", "N/A"))[:8] + "...",
399
- interface.get("access_method", "N/A"),
400
- "✓" if interface.get("is_active") else "✗",
401
- )
402
-
403
- console.print(table)
404
- console.print(f"\n[green]Total:[/green] {data.get('count', 0)} access interface(s)")
405
- except ValueError as e:
406
- console.print(f"[red]✗[/red] {e}", style="bold red")
407
- raise typer.Exit(code=1)
408
- except Exception as e:
409
- console.print(f"[red]✗[/red] Failed to query access interfaces: {e}", style="bold red")
410
- raise typer.Exit(code=1)
411
-
412
-
413
- @app.command("documents")
414
- def query_documents(
415
- backend_url: str | None = typer.Option(
416
- None,
417
- "--backend-url",
418
- "-u",
419
- help="UnitySVC backend URL (default: from UNITYSVC_BACKEND_URL env var)",
420
- ),
421
- api_key: str | None = typer.Option(
422
- None,
423
- "--api-key",
424
- "-k",
425
- help="API key for authentication (default: from UNITYSVC_API_KEY env var)",
426
- ),
427
- format: str = typer.Option(
428
- "table",
429
- "--format",
430
- "-f",
431
- help="Output format: table, json",
432
- ),
433
- ):
434
- """Query all documents from UnitySVC backend (private endpoint)."""
435
- try:
436
- with ServiceDataQuery.from_env(backend_url, api_key) as query:
437
- data = query.list_documents()
438
-
439
- if format == "json":
440
- console.print(json.dumps(data, indent=2))
441
- else:
442
- documents = data.get("data", [])
443
- if not documents:
444
- console.print("[yellow]No documents found.[/yellow]")
445
- else:
446
- table = Table(title="Documents", show_lines=True)
447
- table.add_column("ID", style="cyan")
448
- table.add_column("Title", style="green")
449
- table.add_column("Category", style="blue")
450
- table.add_column("MIME Type", style="yellow")
451
- table.add_column("Context", style="magenta")
452
- table.add_column("Public", style="red")
453
-
454
- for doc in documents:
455
- table.add_row(
456
- str(doc.get("id", "N/A"))[:8] + "...",
457
- doc.get("title", "N/A")[:40],
458
- doc.get("category", "N/A"),
459
- doc.get("mime_type", "N/A"),
460
- doc.get("context_type", "N/A"),
461
- "✓" if doc.get("is_public") else "✗",
462
- )
463
-
464
- console.print(table)
465
- console.print(f"\n[green]Total:[/green] {data.get('count', 0)} document(s)")
466
- except ValueError as e:
467
- console.print(f"[red]✗[/red] {e}", style="bold red")
468
- raise typer.Exit(code=1)
469
- except Exception as e:
470
- console.print(f"[red]✗[/red] Failed to query documents: {e}", style="bold red")
471
- raise typer.Exit(code=1)