unitysvc-services 0.1.4__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.
@@ -18,101 +18,11 @@ class ServiceDataQuery(UnitySvcAPI):
18
18
  """Query service data from UnitySVC backend endpoints.
19
19
 
20
20
  Inherits HTTP methods with automatic curl fallback from UnitySvcAPI.
21
- Provides convenient methods for listing public service data.
22
-
23
- Provides sync wrapper methods for CLI usage that wrap async base class methods.
21
+ Provides async methods for querying public service data.
22
+ Use with async context manager for proper resource cleanup.
24
23
  """
25
24
 
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).
28
-
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}))
34
- return result.get("data", result) if isinstance(result, dict) else result
35
-
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}))
44
- return result.get("data", result) if isinstance(result, dict) else result
45
-
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}))
54
- return result.get("data", result) if isinstance(result, dict) else result
55
-
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}))
64
- return result.get("data", result) if isinstance(result, dict) else result
65
-
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.
68
-
69
- Args:
70
- endpoint: API endpoint path
71
- params: Query parameters
72
-
73
- Returns:
74
- JSON response as dictionary
75
- """
76
- return asyncio.run(super().get(endpoint, params))
77
-
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.
82
-
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))
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.
95
-
96
- Args:
97
- task_id: Celery task ID to poll
98
- poll_interval: Seconds between status checks
99
- timeout: Maximum seconds to wait
100
-
101
- Returns:
102
- Task result dictionary
103
-
104
- Raises:
105
- ValueError: If task fails or times out
106
- """
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())
25
+ pass
116
26
 
117
27
 
118
28
  @app.command("sellers")
@@ -195,80 +105,84 @@ def query_sellers(
195
105
  console.print(f"[yellow]Allowed fields:[/yellow] {', '.join(sorted(allowed_fields))}")
196
106
  raise typer.Exit(code=1)
197
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
+
198
113
  try:
199
- with ServiceDataQuery() as query:
200
- sellers = query.list_sellers(skip=skip, limit=limit)
201
-
202
- if format == "json":
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))
114
+ sellers = asyncio.run(_query_sellers_async())
115
+
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))
209
121
  else:
210
- if not sellers:
211
- console.print("[yellow]No sellers found.[/yellow]")
212
- else:
213
- table = Table(title="Sellers")
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
122
+ console.print(json.dumps(sellers, indent=2))
123
+ else:
124
+ if not sellers:
125
+ console.print("[yellow]No sellers found.[/yellow]")
126
+ else:
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 = []
252
174
  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
258
- for seller in sellers:
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)
269
-
270
- console.print(table)
271
- console.print(f"\n[green]Total:[/green] {len(sellers)} seller(s)")
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)")
272
186
  except ValueError as e:
273
187
  console.print(f"[red]✗[/red] {e}", style="bold red")
274
188
  raise typer.Exit(code=1)
@@ -351,74 +265,76 @@ def query_providers(
351
265
  console.print(f"[yellow]Allowed fields:[/yellow] {', '.join(sorted(allowed_fields))}")
352
266
  raise typer.Exit(code=1)
353
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
+
354
273
  try:
355
- with ServiceDataQuery() as query:
356
- providers = query.list_providers(skip=skip, limit=limit)
357
-
358
- if format == "json":
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))
274
+ providers = asyncio.run(_query_providers_async())
275
+
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:
282
+ console.print(json.dumps(providers, indent=2))
283
+ else:
284
+ if not providers:
285
+ console.print("[yellow]No providers found.[/yellow]")
367
286
  else:
368
- if not providers:
369
- console.print("[yellow]No providers found.[/yellow]")
370
- else:
371
- table = Table(title="Providers")
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
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 = []
402
326
  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
408
- for provider in providers:
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)
419
-
420
- console.print(table)
421
- console.print(f"\n[green]Total:[/green] {len(providers)} provider(s)")
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)")
422
338
  except ValueError as e:
423
339
  console.print(f"[red]✗[/red] {e}", style="bold red")
424
340
  raise typer.Exit(code=1)
@@ -440,7 +356,7 @@ def query_offerings(
440
356
  "--fields",
441
357
  help=(
442
358
  "Comma-separated list of fields to display. Available fields: "
443
- "id, definition_id, provider_id, status, price, service_name, "
359
+ "id, provider_id, status, price, service_name, "
444
360
  "service_type, provider_name"
445
361
  ),
446
362
  ),
@@ -454,6 +370,21 @@ def query_offerings(
454
370
  "--limit",
455
371
  help="Maximum number of records to return (default: 100)",
456
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
+ ),
457
388
  ):
458
389
  """Query all service offerings from UnitySVC backend.
459
390
 
@@ -464,6 +395,18 @@ def query_offerings(
464
395
  # Show only specific fields
465
396
  unitysvc_services query offerings --fields id,name,status
466
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
+
467
410
  # Retrieve more than 100 records
468
411
  unitysvc_services query offerings --limit 500
469
412
 
@@ -472,7 +415,7 @@ def query_offerings(
472
415
 
473
416
  # Show all available fields
474
417
  unitysvc_services query offerings --fields \\
475
- id,service_name,service_type,provider_name,status,price,definition_id,provider_id
418
+ id,service_name,service_type,provider_name,status,price,provider_id
476
419
  """
477
420
  # Parse fields list
478
421
  field_list = [f.strip() for f in fields.split(",")]
@@ -480,7 +423,6 @@ def query_offerings(
480
423
  # Define allowed fields from ServiceOfferingPublic model
481
424
  allowed_fields = {
482
425
  "id",
483
- "definition_id",
484
426
  "provider_id",
485
427
  "status",
486
428
  "price",
@@ -499,46 +441,56 @@ def query_offerings(
499
441
  console.print(f"[yellow]Available fields:[/yellow] {', '.join(sorted(allowed_fields))}")
500
442
  raise typer.Exit(code=1)
501
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
+
502
457
  try:
503
- with ServiceDataQuery() as query:
504
- offerings = query.list_service_offerings(skip=skip, limit=limit)
505
-
506
- if format == "json":
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))
458
+ offerings = asyncio.run(_query_offerings_async())
459
+
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))
515
465
  else:
516
- if not offerings:
517
- console.print("[yellow]No service offerings found.[/yellow]")
518
- else:
519
- table = Table(title="Service Offerings")
466
+ console.print(json.dumps(offerings, indent=2))
467
+ else:
468
+ if not offerings:
469
+ console.print("[yellow]No service offerings found.[/yellow]")
470
+ else:
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)
520
478
 
521
- # Add columns dynamically based on selected fields
479
+ # Add rows
480
+ for offering in offerings:
481
+ row = []
522
482
  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
528
- for offering in offerings:
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)
539
-
540
- console.print(table)
541
- console.print(f"\n[green]Total:[/green] {len(offerings)} service offering(s)")
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)")
542
494
  except ValueError as e:
543
495
  console.print(f"[red]✗[/red] {e}", style="bold red")
544
496
  raise typer.Exit(code=1)
@@ -575,6 +527,31 @@ def query_listings(
575
527
  "--limit",
576
528
  help="Maximum number of records to return (default: 100)",
577
529
  ),
530
+ seller_name: str | None = typer.Option(
531
+ None,
532
+ "--seller-name",
533
+ help="Filter by seller name (case-insensitive partial match)",
534
+ ),
535
+ provider_name: str | None = typer.Option(
536
+ None,
537
+ "--provider-name",
538
+ help="Filter by provider name (case-insensitive partial match)",
539
+ ),
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)",
554
+ ),
578
555
  ):
579
556
  """Query all service listings from UnitySVC backend.
580
557
 
@@ -585,6 +562,21 @@ def query_listings(
585
562
  # Show only specific fields
586
563
  unitysvc_services query listings --fields id,service_name,status
587
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
+
588
580
  # Retrieve more than 100 records
589
581
  unitysvc_services query listings --limit 500
590
582
 
@@ -628,44 +620,60 @@ def query_listings(
628
620
  console.print(f"[yellow]Available fields:[/yellow] {', '.join(sorted(allowed_fields))}")
629
621
  raise typer.Exit(code=1)
630
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
639
+
631
640
  try:
632
- with ServiceDataQuery() as query:
633
- listings = query.list_service_listings(skip=skip, limit=limit)
634
-
635
- if format == "json":
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))
641
+ listings = asyncio.run(_query_listings_async())
642
+
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))
642
648
  else:
643
- if not listings:
644
- console.print("[yellow]No service listings found.[/yellow]")
645
- else:
646
- table = Table(title="Service Listings")
649
+ console.print(json.dumps(listings, indent=2))
650
+ else:
651
+ if not listings:
652
+ console.print("[yellow]No service listings found.[/yellow]")
653
+ else:
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)
647
661
 
648
- # Add columns dynamically based on selected fields
662
+ # Add rows
663
+ for listing in listings:
664
+ row = []
649
665
  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)
666
-
667
- console.print(table)
668
- console.print(f"\n[green]Total:[/green] {len(listings)} service listing(s)")
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)")
669
677
  except ValueError as e:
670
678
  console.print(f"[red]✗[/red] {e}", style="bold red")
671
679
  raise typer.Exit(code=1)