unitysvc-services 0.1.24__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.
Files changed (37) hide show
  1. unitysvc_services/__init__.py +4 -0
  2. unitysvc_services/api.py +421 -0
  3. unitysvc_services/cli.py +23 -0
  4. unitysvc_services/format_data.py +140 -0
  5. unitysvc_services/interactive_prompt.py +1132 -0
  6. unitysvc_services/list.py +216 -0
  7. unitysvc_services/models/__init__.py +71 -0
  8. unitysvc_services/models/base.py +1375 -0
  9. unitysvc_services/models/listing_data.py +118 -0
  10. unitysvc_services/models/listing_v1.py +56 -0
  11. unitysvc_services/models/provider_data.py +79 -0
  12. unitysvc_services/models/provider_v1.py +54 -0
  13. unitysvc_services/models/seller_data.py +120 -0
  14. unitysvc_services/models/seller_v1.py +42 -0
  15. unitysvc_services/models/service_data.py +114 -0
  16. unitysvc_services/models/service_v1.py +81 -0
  17. unitysvc_services/populate.py +207 -0
  18. unitysvc_services/publisher.py +1628 -0
  19. unitysvc_services/py.typed +0 -0
  20. unitysvc_services/query.py +688 -0
  21. unitysvc_services/scaffold.py +1103 -0
  22. unitysvc_services/schema/base.json +777 -0
  23. unitysvc_services/schema/listing_v1.json +1286 -0
  24. unitysvc_services/schema/provider_v1.json +952 -0
  25. unitysvc_services/schema/seller_v1.json +379 -0
  26. unitysvc_services/schema/service_v1.json +1306 -0
  27. unitysvc_services/test.py +965 -0
  28. unitysvc_services/unpublisher.py +505 -0
  29. unitysvc_services/update.py +287 -0
  30. unitysvc_services/utils.py +533 -0
  31. unitysvc_services/validator.py +731 -0
  32. unitysvc_services-0.1.24.dist-info/METADATA +184 -0
  33. unitysvc_services-0.1.24.dist-info/RECORD +37 -0
  34. unitysvc_services-0.1.24.dist-info/WHEEL +5 -0
  35. unitysvc_services-0.1.24.dist-info/entry_points.txt +3 -0
  36. unitysvc_services-0.1.24.dist-info/licenses/LICENSE +21 -0
  37. unitysvc_services-0.1.24.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,688 @@
1
+ """Query command group - query backend API for data."""
2
+
3
+ import asyncio
4
+ import json
5
+ from typing import Any
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from .api import UnitySvcAPI
12
+
13
+ app = typer.Typer(help="Query backend API for data")
14
+ console = Console()
15
+
16
+
17
+ class ServiceDataQuery(UnitySvcAPI):
18
+ """Query service data from UnitySVC backend endpoints.
19
+
20
+ Inherits HTTP methods with automatic curl fallback from UnitySvcAPI.
21
+ Provides async methods for querying public service data.
22
+ Use with async context manager for proper resource cleanup.
23
+ """
24
+
25
+ pass
26
+
27
+
28
+ @app.command("sellers")
29
+ def query_sellers(
30
+ format: str = typer.Option(
31
+ "table",
32
+ "--format",
33
+ "-f",
34
+ help="Output format: table, json",
35
+ ),
36
+ fields: str = typer.Option(
37
+ "id,name,display_name,seller_type",
38
+ "--fields",
39
+ help=(
40
+ "Comma-separated list of fields to display. Available fields: "
41
+ "id, name, display_name, seller_type, contact_email, "
42
+ "secondary_contact_email, homepage, description, "
43
+ "business_registration, tax_id, account_manager_id, "
44
+ "created_at, updated_at, status"
45
+ ),
46
+ ),
47
+ skip: int = typer.Option(
48
+ 0,
49
+ "--skip",
50
+ help="Number of records to skip (for pagination)",
51
+ ),
52
+ limit: int = typer.Option(
53
+ 100,
54
+ "--limit",
55
+ help="Maximum number of records to return (default: 100)",
56
+ ),
57
+ ):
58
+ """Query all sellers from the backend.
59
+
60
+ Examples:
61
+ # Use default fields
62
+ unitysvc_services query sellers
63
+
64
+ # Show only specific fields
65
+ unitysvc_services query sellers --fields id,name,contact_email
66
+
67
+ # Retrieve more than 100 records
68
+ unitysvc_services query sellers --limit 500
69
+
70
+ # Pagination: skip first 100, get next 100
71
+ unitysvc_services query sellers --skip 100 --limit 100
72
+
73
+ # Show all available fields
74
+ unitysvc_services query sellers --fields \\
75
+ id,name,display_name,seller_type,contact_email,homepage,created_at,updated_at
76
+ """
77
+ # Parse fields list
78
+ field_list = [f.strip() for f in fields.split(",")]
79
+
80
+ # Define allowed fields from SellerPublic model
81
+ allowed_fields = {
82
+ "id",
83
+ "name",
84
+ "display_name",
85
+ "seller_type",
86
+ "contact_email",
87
+ "secondary_contact_email",
88
+ "homepage",
89
+ "description",
90
+ "business_registration",
91
+ "tax_id",
92
+ "account_manager_id",
93
+ "created_at",
94
+ "updated_at",
95
+ "status",
96
+ }
97
+
98
+ # Validate fields
99
+ invalid_fields = [f for f in field_list if f not in allowed_fields]
100
+ if invalid_fields:
101
+ console.print(
102
+ f"[red]✗[/red] Invalid field(s): {', '.join(invalid_fields)}",
103
+ style="bold red",
104
+ )
105
+ console.print(f"[yellow]Allowed fields:[/yellow] {', '.join(sorted(allowed_fields))}")
106
+ raise typer.Exit(code=1)
107
+
108
+ async def _query_sellers_async():
109
+ async with ServiceDataQuery() as query:
110
+ sellers = await query.get("/publish/sellers", {"skip": skip, "limit": limit})
111
+ return sellers.get("data", sellers) if isinstance(sellers, dict) else sellers
112
+
113
+ try:
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))
121
+ else:
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 = []
174
+ for field in field_list:
175
+ value = seller.get(field)
176
+ if value is None:
177
+ row.append("N/A")
178
+ elif isinstance(value, dict | list):
179
+ row.append(str(value)[:50]) # Truncate complex types
180
+ else:
181
+ row.append(str(value))
182
+ table.add_row(*row)
183
+
184
+ console.print(table)
185
+ console.print(f"\n[green]Total:[/green] {len(sellers)} seller(s)")
186
+ except ValueError as e:
187
+ console.print(f"[red]✗[/red] {e}", style="bold red")
188
+ raise typer.Exit(code=1)
189
+ except Exception as e:
190
+ console.print(f"[red]✗[/red] Failed to query sellers: {e}", style="bold red")
191
+ raise typer.Exit(code=1)
192
+
193
+
194
+ @app.command("providers")
195
+ def query_providers(
196
+ format: str = typer.Option(
197
+ "table",
198
+ "--format",
199
+ "-f",
200
+ help="Output format: table, json",
201
+ ),
202
+ fields: str = typer.Option(
203
+ "id,name,display_name,status",
204
+ "--fields",
205
+ help=(
206
+ "Comma-separated list of fields to display. Available fields: "
207
+ "id, name, display_name, contact_email, secondary_contact_email, "
208
+ "homepage, description, status, created_at, updated_at"
209
+ ),
210
+ ),
211
+ skip: int = typer.Option(
212
+ 0,
213
+ "--skip",
214
+ help="Number of records to skip (for pagination)",
215
+ ),
216
+ limit: int = typer.Option(
217
+ 100,
218
+ "--limit",
219
+ help="Maximum number of records to return (default: 100)",
220
+ ),
221
+ ):
222
+ """Query all providers from the backend.
223
+
224
+ Examples:
225
+ # Use default fields
226
+ unitysvc_services query providers
227
+
228
+ # Retrieve more than 100 records
229
+ unitysvc_services query providers --limit 500
230
+
231
+ # Pagination: skip first 100, get next 100
232
+ unitysvc_services query providers --skip 100 --limit 100
233
+
234
+ # Show only specific fields
235
+ unitysvc_services query providers --fields id,name,contact_email
236
+
237
+ # Show all available fields
238
+ unitysvc_services query providers --fields \\
239
+ id,name,display_name,contact_email,homepage,status,created_at,updated_at
240
+ """
241
+ # Parse fields list
242
+ field_list = [f.strip() for f in fields.split(",")]
243
+
244
+ # Define allowed fields from ProviderPublic model
245
+ allowed_fields = {
246
+ "id",
247
+ "name",
248
+ "display_name",
249
+ "contact_email",
250
+ "secondary_contact_email",
251
+ "homepage",
252
+ "description",
253
+ "status",
254
+ "created_at",
255
+ "updated_at",
256
+ }
257
+
258
+ # Validate fields
259
+ invalid_fields = [f for f in field_list if f not in allowed_fields]
260
+ if invalid_fields:
261
+ console.print(
262
+ f"[red]✗[/red] Invalid field(s): {', '.join(invalid_fields)}",
263
+ style="bold red",
264
+ )
265
+ console.print(f"[yellow]Allowed fields:[/yellow] {', '.join(sorted(allowed_fields))}")
266
+ raise typer.Exit(code=1)
267
+
268
+ async def _query_providers_async():
269
+ async with ServiceDataQuery() as query:
270
+ providers = await query.get("/publish/providers", {"skip": skip, "limit": limit})
271
+ return providers.get("data", providers) if isinstance(providers, dict) else providers
272
+
273
+ try:
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]")
286
+ else:
287
+ table = Table(title="Providers")
288
+
289
+ # Define column styles
290
+ field_styles = {
291
+ "id": "cyan",
292
+ "name": "green",
293
+ "display_name": "blue",
294
+ "contact_email": "yellow",
295
+ "secondary_contact_email": "yellow",
296
+ "homepage": "blue",
297
+ "description": "white",
298
+ "status": "green",
299
+ "created_at": "magenta",
300
+ "updated_at": "magenta",
301
+ }
302
+
303
+ # Define column headers
304
+ field_headers = {
305
+ "id": "ID",
306
+ "name": "Name",
307
+ "display_name": "Display Name",
308
+ "contact_email": "Contact Email",
309
+ "secondary_contact_email": "Secondary Email",
310
+ "homepage": "Homepage",
311
+ "description": "Description",
312
+ "status": "Status",
313
+ "created_at": "Created At",
314
+ "updated_at": "Updated At",
315
+ }
316
+
317
+ # Add columns based on requested fields
318
+ for field in field_list:
319
+ header = field_headers.get(field, field.title())
320
+ style = field_styles.get(field, "white")
321
+ table.add_column(header, style=style)
322
+
323
+ # Add rows
324
+ for provider in providers:
325
+ row = []
326
+ for field in field_list:
327
+ value = provider.get(field)
328
+ if value is None:
329
+ row.append("N/A")
330
+ elif isinstance(value, dict | list):
331
+ row.append(str(value)[:50]) # Truncate complex types
332
+ else:
333
+ row.append(str(value))
334
+ table.add_row(*row)
335
+
336
+ console.print(table)
337
+ console.print(f"\n[green]Total:[/green] {len(providers)} provider(s)")
338
+ except ValueError as e:
339
+ console.print(f"[red]✗[/red] {e}", style="bold red")
340
+ raise typer.Exit(code=1)
341
+ except Exception as e:
342
+ console.print(f"[red]✗[/red] Failed to query providers: {e}", style="bold red")
343
+ raise typer.Exit(code=1)
344
+
345
+
346
+ @app.command("offerings")
347
+ def query_offerings(
348
+ format: str = typer.Option(
349
+ "table",
350
+ "--format",
351
+ "-f",
352
+ help="Output format: table, json",
353
+ ),
354
+ fields: str = typer.Option(
355
+ "id,name,service_type,provider_name,status",
356
+ "--fields",
357
+ help=(
358
+ "Comma-separated list of fields to display. Available fields: "
359
+ "id, provider_id, status, price, service_name, "
360
+ "service_type, provider_name"
361
+ ),
362
+ ),
363
+ skip: int = typer.Option(
364
+ 0,
365
+ "--skip",
366
+ help="Number of records to skip (for pagination)",
367
+ ),
368
+ limit: int = typer.Option(
369
+ 100,
370
+ "--limit",
371
+ help="Maximum number of records to return (default: 100)",
372
+ ),
373
+ provider: str | None = typer.Option(
374
+ None,
375
+ "--provider",
376
+ "-p",
377
+ help="Filter by provider name (case-insensitive partial match)",
378
+ ),
379
+ service_type: str | None = typer.Option(
380
+ None,
381
+ "--service-type",
382
+ help="Filter by service type (exact match, e.g., llm, vectordb, embedding)",
383
+ ),
384
+ name: str | None = typer.Option(
385
+ None,
386
+ "--name",
387
+ help="Filter by service name (case-insensitive partial match)",
388
+ ),
389
+ ):
390
+ """Query all service offerings from UnitySVC backend.
391
+
392
+ Examples:
393
+ # Use default fields
394
+ unitysvc_services query offerings
395
+
396
+ # Show only specific fields
397
+ unitysvc_services query offerings --fields id,name,status
398
+
399
+ # Filter by provider name
400
+ unitysvc_services query offerings --provider openai
401
+
402
+ # Filter by service type
403
+ unitysvc_services query offerings --service-type llm
404
+
405
+ # Filter by service name
406
+ unitysvc_services query offerings --name llama
407
+
408
+ # Combine multiple filters
409
+ unitysvc_services query offerings --service-type llm --provider openai
410
+
411
+ # Retrieve more than 100 records
412
+ unitysvc_services query offerings --limit 500
413
+
414
+ # Pagination: skip first 100, get next 100
415
+ unitysvc_services query offerings --skip 100 --limit 100
416
+
417
+ # Show all available fields
418
+ unitysvc_services query offerings --fields \\
419
+ id,service_name,service_type,provider_name,status,price,provider_id
420
+ """
421
+ # Parse fields list
422
+ field_list = [f.strip() for f in fields.split(",")]
423
+
424
+ # Define allowed fields from ServiceOfferingPublic model
425
+ allowed_fields = {
426
+ "id",
427
+ "provider_id",
428
+ "status",
429
+ "price",
430
+ "name",
431
+ "service_type",
432
+ "provider_name",
433
+ }
434
+
435
+ # Validate fields
436
+ invalid_fields = [f for f in field_list if f not in allowed_fields]
437
+ if invalid_fields:
438
+ console.print(
439
+ f"[red]Error:[/red] Invalid field(s): {', '.join(invalid_fields)}",
440
+ style="bold red",
441
+ )
442
+ console.print(f"[yellow]Available fields:[/yellow] {', '.join(sorted(allowed_fields))}")
443
+ raise typer.Exit(code=1)
444
+
445
+ async def _query_offerings_async():
446
+ async with ServiceDataQuery() as query:
447
+ params: dict[str, Any] = {"skip": skip, "limit": limit}
448
+ if provider:
449
+ params["provider_name"] = provider
450
+ if service_type:
451
+ params["service_type"] = service_type
452
+ if name:
453
+ params["name"] = name
454
+
455
+ offerings = await query.get("/publish/offerings", params)
456
+ return offerings.get("data", offerings) if isinstance(offerings, dict) else offerings
457
+
458
+ try:
459
+ offerings = asyncio.run(_query_offerings_async())
460
+
461
+ if format == "json":
462
+ # For JSON, filter fields if not all are requested
463
+ if set(field_list) != allowed_fields:
464
+ filtered_offerings = [{k: v for k, v in offering.items() if k in field_list} for offering in offerings]
465
+ console.print(json.dumps(filtered_offerings, indent=2))
466
+ else:
467
+ console.print(json.dumps(offerings, indent=2))
468
+ else:
469
+ if not offerings:
470
+ console.print("[yellow]No service offerings found.[/yellow]")
471
+ else:
472
+ table = Table(title="Service Offerings")
473
+
474
+ # Add columns dynamically based on selected fields
475
+ for field in field_list:
476
+ # Capitalize and format field names for display
477
+ column_name = field.replace("_", " ").title()
478
+ table.add_column(column_name)
479
+
480
+ # Add rows
481
+ for offering in offerings:
482
+ row = []
483
+ for field in field_list:
484
+ value = offering.get(field)
485
+ if value is None:
486
+ row.append("N/A")
487
+ elif isinstance(value, dict | list):
488
+ row.append(str(value)[:50]) # Truncate complex types
489
+ else:
490
+ row.append(str(value))
491
+ table.add_row(*row)
492
+
493
+ console.print(table)
494
+ console.print(f"\n[green]Total:[/green] {len(offerings)} service offering(s)")
495
+ except ValueError as e:
496
+ console.print(f"[red]✗[/red] {e}", style="bold red")
497
+ raise typer.Exit(code=1)
498
+ except Exception as e:
499
+ console.print(f"[red]✗[/red] Failed to query service offerings: {e}", style="bold red")
500
+ raise typer.Exit(code=1)
501
+
502
+
503
+ @app.command("listings")
504
+ def query_listings(
505
+ format: str = typer.Option(
506
+ "table",
507
+ "--format",
508
+ "-f",
509
+ help="Output format: table, json",
510
+ ),
511
+ fields: str = typer.Option(
512
+ "id,service_name,service_type,seller_name,listing_type,status",
513
+ "--fields",
514
+ help=(
515
+ "Comma-separated list of fields to display. Available fields: "
516
+ "id, offering_id, offering_status, seller_id, status, created_at, updated_at, "
517
+ "parameters_schema, parameters_ui_schema, tags, service_name, "
518
+ "service_type, provider_name, seller_name, listing_type"
519
+ ),
520
+ ),
521
+ skip: int = typer.Option(
522
+ 0,
523
+ "--skip",
524
+ help="Number of records to skip (for pagination)",
525
+ ),
526
+ limit: int = typer.Option(
527
+ 100,
528
+ "--limit",
529
+ help="Maximum number of records to return (default: 100)",
530
+ ),
531
+ seller: str | None = typer.Option(
532
+ None,
533
+ "--seller",
534
+ help="Filter by seller name (case-insensitive partial match)",
535
+ ),
536
+ provider: str | None = typer.Option(
537
+ None,
538
+ "--provider",
539
+ "-p",
540
+ help="Filter by provider name (case-insensitive partial match)",
541
+ ),
542
+ services: str | None = typer.Option(
543
+ None,
544
+ "--services",
545
+ "-s",
546
+ help="Comma-separated list of service names (case-insensitive partial match)",
547
+ ),
548
+ service_type: str | None = typer.Option(
549
+ None,
550
+ "--service-type",
551
+ help="Filter by service type (exact match, e.g., llm, vectordb, embedding)",
552
+ ),
553
+ listing_type: str | None = typer.Option(
554
+ None,
555
+ "--listing-type",
556
+ help="Filter by listing type (exact match: regular, byop, self_hosted)",
557
+ ),
558
+ ):
559
+ """Query all service listings from UnitySVC backend.
560
+
561
+ Examples:
562
+ # Use default fields
563
+ unitysvc_services query listings
564
+
565
+ # Show only specific fields
566
+ unitysvc_services query listings --fields id,service_name,status
567
+
568
+ # Filter by seller name
569
+ unitysvc_services query listings --seller chutes
570
+
571
+ # Filter by provider name
572
+ unitysvc_services query listings --provider openai
573
+
574
+ # Filter by service names (comma-separated)
575
+ unitysvc_services query listings --services "gpt-4,claude-3"
576
+
577
+ # Filter by service type
578
+ unitysvc_services query listings --service-type llm
579
+
580
+ # Filter by listing type
581
+ unitysvc_services query listings --listing-type byop
582
+
583
+ # Combine multiple filters
584
+ unitysvc_services query listings --service-type llm --listing-type regular
585
+
586
+ # Retrieve more than 100 records
587
+ unitysvc_services query listings --limit 500
588
+
589
+ # Pagination: skip first 100, get next 100
590
+ unitysvc_services query listings --skip 100 --limit 100
591
+
592
+ # Show all available fields
593
+ unitysvc_services query listings --fields \\
594
+ id,name,service_name,service_type,seller_name,listing_type,status,provider_name
595
+ """
596
+ # Parse fields list
597
+ field_list = [f.strip() for f in fields.split(",")]
598
+
599
+ # Define allowed fields from ServiceListingPublic model
600
+ allowed_fields = {
601
+ "id",
602
+ "name",
603
+ "offering_id",
604
+ "offering_status",
605
+ "seller_id",
606
+ "status",
607
+ "created_at",
608
+ "updated_at",
609
+ "parameters_schema",
610
+ "parameters_ui_schema",
611
+ "tags",
612
+ "service_name",
613
+ "service_type",
614
+ "provider_name",
615
+ "seller_name",
616
+ "listing_type",
617
+ }
618
+
619
+ # Validate fields
620
+ invalid_fields = [f for f in field_list if f not in allowed_fields]
621
+ if invalid_fields:
622
+ console.print(
623
+ f"[red]Error:[/red] Invalid field(s): {', '.join(invalid_fields)}",
624
+ style="bold red",
625
+ )
626
+ console.print(f"[yellow]Available fields:[/yellow] {', '.join(sorted(allowed_fields))}")
627
+ raise typer.Exit(code=1)
628
+
629
+ async def _query_listings_async():
630
+ async with ServiceDataQuery() as query:
631
+ params: dict[str, Any] = {"skip": skip, "limit": limit}
632
+ if seller:
633
+ params["seller_name"] = seller
634
+ if provider:
635
+ params["provider_name"] = provider
636
+ if services:
637
+ params["service_name"] = services
638
+ if service_type:
639
+ params["service_type"] = service_type
640
+ if listing_type:
641
+ params["listing_type"] = listing_type
642
+
643
+ listings = await query.get("/publish/listings", params)
644
+ return listings.get("data", listings) if isinstance(listings, dict) else listings
645
+
646
+ try:
647
+ listings = asyncio.run(_query_listings_async())
648
+
649
+ if format == "json":
650
+ # For JSON, filter fields if not all are requested
651
+ if set(field_list) != allowed_fields:
652
+ filtered_listings = [{k: v for k, v in listing.items() if k in field_list} for listing in listings]
653
+ console.print(json.dumps(filtered_listings, indent=2))
654
+ else:
655
+ console.print(json.dumps(listings, indent=2))
656
+ else:
657
+ if not listings:
658
+ console.print("[yellow]No service listings found.[/yellow]")
659
+ else:
660
+ table = Table(title="Service Listings")
661
+
662
+ # Add columns dynamically based on selected fields
663
+ for field in field_list:
664
+ # Capitalize and format field names for display
665
+ column_name = field.replace("_", " ").title()
666
+ table.add_column(column_name)
667
+
668
+ # Add rows
669
+ for listing in listings:
670
+ row = []
671
+ for field in field_list:
672
+ value = listing.get(field)
673
+ if value is None:
674
+ row.append("N/A")
675
+ elif isinstance(value, dict | list):
676
+ row.append(str(value)[:50]) # Truncate complex types
677
+ else:
678
+ row.append(str(value))
679
+ table.add_row(*row)
680
+
681
+ console.print(table)
682
+ console.print(f"\n[green]Total:[/green] {len(listings)} service listing(s)")
683
+ except ValueError as e:
684
+ console.print(f"[red]✗[/red] {e}", style="bold red")
685
+ raise typer.Exit(code=1)
686
+ except Exception as e:
687
+ console.print(f"[red]✗[/red] Failed to query service listings: {e}", style="bold red")
688
+ raise typer.Exit(code=1)