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
@@ -0,0 +1,505 @@
1
+ """Data unpublisher module for deleting service data from UnitySVC backend."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
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
+ from .utils import find_files_by_schema, load_data_file
13
+
14
+ app = typer.Typer(help="Unpublish (delete) data from backend")
15
+ console = Console()
16
+
17
+
18
+ class ServiceDataUnpublisher(UnitySvcAPI):
19
+ """Unpublishes (deletes) service data from UnitySVC backend endpoints.
20
+
21
+ Inherits base HTTP client with curl fallback from UnitySvcAPI.
22
+ Provides methods for deleting offerings, listings, providers, and sellers.
23
+ """
24
+
25
+ async def delete_service_offering(
26
+ self,
27
+ offering_id: str,
28
+ dryrun: bool = False,
29
+ force: bool = False,
30
+ ) -> dict[str, Any]:
31
+ """Delete a service offering from backend.
32
+
33
+ Args:
34
+ offering_id: UUID of the offering to delete
35
+ dryrun: If True, show what would be deleted without actually deleting
36
+ force: If True, force deletion even with active subscriptions
37
+
38
+ Returns:
39
+ Response from backend with deletion details
40
+
41
+ Raises:
42
+ httpx.HTTPStatusError: If deletion fails (404, 403, etc.)
43
+ """
44
+ params = {}
45
+ if dryrun:
46
+ params["dryrun"] = "true"
47
+ if force:
48
+ params["force"] = "true"
49
+
50
+ return await self.delete(f"/publish/offering/{offering_id}", params=params)
51
+
52
+ async def delete_service_listing(
53
+ self,
54
+ listing_id: str,
55
+ dryrun: bool = False,
56
+ force: bool = False,
57
+ ) -> dict[str, Any]:
58
+ """Delete a service listing from backend.
59
+
60
+ Args:
61
+ listing_id: UUID of the listing to delete
62
+ dryrun: If True, show what would be deleted without actually deleting
63
+ force: If True, force deletion even with active subscriptions
64
+
65
+ Returns:
66
+ Response from backend with deletion details
67
+
68
+ Raises:
69
+ httpx.HTTPStatusError: If deletion fails (404, 403, etc.)
70
+ """
71
+ params = {}
72
+ if dryrun:
73
+ params["dryrun"] = "true"
74
+ if force:
75
+ params["force"] = "true"
76
+
77
+ return await self.delete(f"/publish/listing/{listing_id}", params=params)
78
+
79
+ async def delete_provider(
80
+ self,
81
+ provider_name: str,
82
+ dryrun: bool = False,
83
+ force: bool = False,
84
+ ) -> dict[str, Any]:
85
+ """Delete a provider from backend.
86
+
87
+ Args:
88
+ provider_name: Name of the provider to delete
89
+ dryrun: If True, show what would be deleted without actually deleting
90
+ force: If True, force deletion even with active subscriptions
91
+
92
+ Returns:
93
+ Response from backend with deletion details
94
+
95
+ Raises:
96
+ httpx.HTTPStatusError: If deletion fails (404, 403, etc.)
97
+ """
98
+ params = {}
99
+ if dryrun:
100
+ params["dryrun"] = "true"
101
+ if force:
102
+ params["force"] = "true"
103
+
104
+ return await self.delete(f"/publish/provider/{provider_name}", params=params)
105
+
106
+ async def delete_seller(
107
+ self,
108
+ seller_name: str,
109
+ dryrun: bool = False,
110
+ force: bool = False,
111
+ ) -> dict[str, Any]:
112
+ """Delete a seller from backend.
113
+
114
+ Args:
115
+ seller_name: Name of the seller to delete
116
+ dryrun: If True, show what would be deleted without actually deleting
117
+ force: If True, force deletion even with active subscriptions
118
+
119
+ Returns:
120
+ Response from backend with deletion details
121
+
122
+ Raises:
123
+ httpx.HTTPStatusError: If deletion fails (404, 403, etc.)
124
+ """
125
+ params = {}
126
+ if dryrun:
127
+ params["dryrun"] = "true"
128
+ if force:
129
+ params["force"] = "true"
130
+
131
+ return await self.delete(f"/publish/seller/{seller_name}", params=params)
132
+
133
+
134
+ @app.command("offerings")
135
+ def unpublish_offerings(
136
+ data_dir: Path | None = typer.Argument(
137
+ None,
138
+ help="Directory containing service offering files (default: current directory)",
139
+ ),
140
+ services: str | None = typer.Option(
141
+ None,
142
+ "--services",
143
+ "-s",
144
+ help="Comma-separated list of service names to unpublish",
145
+ ),
146
+ provider_name: str | None = typer.Option(
147
+ None,
148
+ "--provider",
149
+ "-p",
150
+ help="Unpublish offerings from specific provider",
151
+ ),
152
+ dryrun: bool = typer.Option(
153
+ False,
154
+ "--dryrun",
155
+ help="Show what would be deleted without actually deleting",
156
+ ),
157
+ force: bool = typer.Option(
158
+ False,
159
+ "--force",
160
+ help="Force deletion even with active subscriptions",
161
+ ),
162
+ yes: bool = typer.Option(
163
+ False,
164
+ "--yes",
165
+ "-y",
166
+ help="Skip confirmation prompt",
167
+ ),
168
+ ):
169
+ """Unpublish (delete) service offerings from backend.
170
+
171
+ This command reads offering files to get offering IDs, then deletes them from the backend.
172
+
173
+ Examples:
174
+ # Dry-run to see what would be deleted
175
+ usvc unpublish offerings --services "gpt-4" --dryrun
176
+
177
+ # Delete specific offering
178
+ usvc unpublish offerings --services "gpt-4"
179
+
180
+ # Delete all offerings from a provider
181
+ usvc unpublish offerings --provider openai
182
+
183
+ # Force delete (ignore active subscriptions)
184
+ usvc unpublish offerings --services "gpt-4" --force --yes
185
+ """
186
+ if data_dir is None:
187
+ data_dir = Path.cwd()
188
+
189
+ console.print(f"[cyan]Searching for offering files in {data_dir}...[/cyan]\n")
190
+
191
+ # Find all offering files
192
+ offering_files = []
193
+ for result in find_files_by_schema(data_dir, "service_v1"):
194
+ file_path, _format, _data = result
195
+ offering_files.append((file_path, _format))
196
+
197
+ if not offering_files:
198
+ console.print("[yellow]No offering files found[/yellow]")
199
+ raise typer.Exit(code=0)
200
+
201
+ # Load offerings and filter
202
+ offerings_to_delete = []
203
+ for file_path, _format in offering_files:
204
+ data, _ = load_data_file(file_path)
205
+ service_name = data.get("name", "Unknown")
206
+ offering_id = data.get("id")
207
+ provider = data.get("provider_name", "Unknown")
208
+
209
+ # Apply filters
210
+ if services:
211
+ service_list = [s.strip() for s in services.split(",")]
212
+ if service_name not in service_list:
213
+ continue
214
+
215
+ if provider_name and provider_name.lower() not in provider.lower():
216
+ continue
217
+
218
+ if not offering_id:
219
+ console.print(f"[yellow]⚠ No offering ID found in {file_path}, skipping[/yellow]")
220
+ continue
221
+
222
+ offerings_to_delete.append(
223
+ {
224
+ "id": offering_id,
225
+ "name": service_name,
226
+ "provider": provider,
227
+ "file_path": str(file_path),
228
+ }
229
+ )
230
+
231
+ if not offerings_to_delete:
232
+ console.print("[yellow]No offerings found matching filters[/yellow]")
233
+ raise typer.Exit(code=0)
234
+
235
+ # Display what will be deleted
236
+ table = Table(title="Offerings to Unpublish")
237
+ table.add_column("Service Name", style="cyan")
238
+ table.add_column("Provider", style="blue")
239
+ table.add_column("Offering ID", style="white")
240
+
241
+ for offering in offerings_to_delete:
242
+ table.add_row(offering["name"], offering["provider"], offering["id"])
243
+
244
+ console.print(table)
245
+ console.print()
246
+
247
+ if dryrun:
248
+ console.print("[yellow]Dry-run mode: No actual deletion performed[/yellow]")
249
+ raise typer.Exit(code=0)
250
+
251
+ # Confirmation prompt
252
+ if not yes:
253
+ confirm = typer.confirm(
254
+ f"⚠️ Delete {len(offerings_to_delete)} offering(s) and all associated listings/subscriptions?"
255
+ )
256
+ if not confirm:
257
+ console.print("[yellow]Cancelled[/yellow]")
258
+ raise typer.Exit(code=0)
259
+
260
+ # Delete offerings
261
+ async def _delete_all():
262
+ unpublisher = ServiceDataUnpublisher()
263
+ results = []
264
+ for offering in offerings_to_delete:
265
+ try:
266
+ result = await unpublisher.delete_service_offering(
267
+ offering["id"],
268
+ dryrun=dryrun,
269
+ force=force,
270
+ )
271
+ results.append((offering, result, None))
272
+ except Exception as e:
273
+ results.append((offering, None, str(e)))
274
+ return results
275
+
276
+ results = asyncio.run(_delete_all())
277
+
278
+ # Display results
279
+ console.print("\n[cyan]Results:[/cyan]\n")
280
+ success_count = 0
281
+ error_count = 0
282
+
283
+ for offering, result, error in results:
284
+ if error:
285
+ console.print(f"[red]✗ {offering['name']}:[/red] {error}")
286
+ error_count += 1
287
+ else:
288
+ console.print(f"[green]✓ {offering['name']}:[/green] Deleted")
289
+ if result and result.get("cascade_deleted"):
290
+ cascade = result["cascade_deleted"]
291
+ if cascade.get("listings"):
292
+ console.print(f" [dim]→ Deleted {cascade['listings']} listing(s)[/dim]")
293
+ if cascade.get("subscriptions"):
294
+ console.print(f" [dim]→ Deleted {cascade['subscriptions']} subscription(s)[/dim]")
295
+ success_count += 1
296
+
297
+ console.print()
298
+ console.print(f"[green]✓ Success:[/green] {success_count}/{len(results)}")
299
+ if error_count > 0:
300
+ console.print(f"[red]✗ Failed:[/red] {error_count}/{len(results)}")
301
+ raise typer.Exit(code=1)
302
+
303
+
304
+ @app.command("listings")
305
+ def unpublish_listings(
306
+ listing_id: str = typer.Argument(..., help="Listing ID to unpublish"),
307
+ dryrun: bool = typer.Option(
308
+ False,
309
+ "--dryrun",
310
+ help="Show what would be deleted without actually deleting",
311
+ ),
312
+ force: bool = typer.Option(
313
+ False,
314
+ "--force",
315
+ help="Force deletion even with active subscriptions",
316
+ ),
317
+ yes: bool = typer.Option(
318
+ False,
319
+ "--yes",
320
+ "-y",
321
+ help="Skip confirmation prompt",
322
+ ),
323
+ ):
324
+ """Unpublish (delete) a service listing from backend.
325
+
326
+ Examples:
327
+ # Dry-run
328
+ usvc unpublish listings abc-123 --dryrun
329
+
330
+ # Delete listing
331
+ usvc unpublish listings abc-123
332
+
333
+ # Force delete
334
+ usvc unpublish listings abc-123 --force --yes
335
+ """
336
+ console.print(f"[cyan]Unpublishing listing {listing_id}...[/cyan]\n")
337
+
338
+ if not yes and not dryrun:
339
+ confirm = typer.confirm("⚠️ Delete this listing and all associated subscriptions?")
340
+ if not confirm:
341
+ console.print("[yellow]Cancelled[/yellow]")
342
+ raise typer.Exit(code=0)
343
+
344
+ async def _delete():
345
+ unpublisher = ServiceDataUnpublisher()
346
+ return await unpublisher.delete_service_listing(listing_id, dryrun=dryrun, force=force)
347
+
348
+ try:
349
+ result = asyncio.run(_delete())
350
+
351
+ if dryrun:
352
+ console.print("[yellow]Dry-run mode: No actual deletion performed[/yellow]")
353
+ console.print(f"[dim]Would delete: {result}[/dim]")
354
+ else:
355
+ console.print(f"[green]✓ Successfully deleted listing {listing_id}[/green]")
356
+ if result.get("cascade_deleted"):
357
+ cascade = result["cascade_deleted"]
358
+ if cascade.get("subscriptions"):
359
+ console.print(f" [dim]→ Deleted {cascade['subscriptions']} subscription(s)[/dim]")
360
+
361
+ except Exception as e:
362
+ console.print(f"[red]✗ Failed to delete listing:[/red] {e}")
363
+ raise typer.Exit(code=1)
364
+
365
+
366
+ @app.command("providers")
367
+ def unpublish_providers(
368
+ provider_name: str = typer.Argument(..., help="Provider name to unpublish"),
369
+ dryrun: bool = typer.Option(
370
+ False,
371
+ "--dryrun",
372
+ help="Show what would be deleted without actually deleting",
373
+ ),
374
+ force: bool = typer.Option(
375
+ False,
376
+ "--force",
377
+ help="Force deletion even with active subscriptions",
378
+ ),
379
+ yes: bool = typer.Option(
380
+ False,
381
+ "--yes",
382
+ "-y",
383
+ help="Skip confirmation prompt",
384
+ ),
385
+ ):
386
+ """Unpublish (delete) a provider from backend.
387
+
388
+ This will delete the provider and ALL associated offerings, listings, and subscriptions.
389
+
390
+ Examples:
391
+ # Dry-run
392
+ usvc unpublish providers openai --dryrun
393
+
394
+ # Delete provider
395
+ usvc unpublish providers openai
396
+
397
+ # Force delete
398
+ usvc unpublish providers openai --force --yes
399
+ """
400
+ console.print(f"[cyan]Unpublishing provider {provider_name}...[/cyan]\n")
401
+
402
+ if not yes and not dryrun:
403
+ confirm = typer.confirm(
404
+ f"⚠️ Delete provider '{provider_name}' and ALL associated offerings/listings/subscriptions?"
405
+ )
406
+ if not confirm:
407
+ console.print("[yellow]Cancelled[/yellow]")
408
+ raise typer.Exit(code=0)
409
+
410
+ async def _delete():
411
+ unpublisher = ServiceDataUnpublisher()
412
+ return await unpublisher.delete_provider(provider_name, dryrun=dryrun, force=force)
413
+
414
+ try:
415
+ result = asyncio.run(_delete())
416
+
417
+ if dryrun:
418
+ console.print("[yellow]Dry-run mode: No actual deletion performed[/yellow]")
419
+ console.print(f"[dim]Would delete: {result}[/dim]")
420
+ else:
421
+ console.print(f"[green]✓ Successfully deleted provider {provider_name}[/green]")
422
+ if result.get("cascade_deleted"):
423
+ cascade = result["cascade_deleted"]
424
+ if cascade.get("offerings"):
425
+ console.print(f" [dim]→ Deleted {cascade['offerings']} offering(s)[/dim]")
426
+ if cascade.get("listings"):
427
+ console.print(f" [dim]→ Deleted {cascade['listings']} listing(s)[/dim]")
428
+ if cascade.get("subscriptions"):
429
+ console.print(f" [dim]→ Deleted {cascade['subscriptions']} subscription(s)[/dim]")
430
+
431
+ except Exception as e:
432
+ console.print(f"[red]✗ Failed to delete provider:[/red] {e}")
433
+ raise typer.Exit(code=1)
434
+
435
+
436
+ @app.command("sellers")
437
+ def unpublish_sellers(
438
+ seller_name: str = typer.Argument(..., help="Seller name to unpublish"),
439
+ dryrun: bool = typer.Option(
440
+ False,
441
+ "--dryrun",
442
+ help="Show what would be deleted without actually deleting",
443
+ ),
444
+ force: bool = typer.Option(
445
+ False,
446
+ "--force",
447
+ help="Force deletion even with active subscriptions",
448
+ ),
449
+ yes: bool = typer.Option(
450
+ False,
451
+ "--yes",
452
+ "-y",
453
+ help="Skip confirmation prompt",
454
+ ),
455
+ ):
456
+ """Unpublish (delete) a seller from backend.
457
+
458
+ This will delete the seller and ALL associated providers, offerings, listings, and subscriptions.
459
+
460
+ Examples:
461
+ # Dry-run
462
+ usvc unpublish sellers my-company --dryrun
463
+
464
+ # Delete seller
465
+ usvc unpublish sellers my-company
466
+
467
+ # Force delete
468
+ usvc unpublish sellers my-company --force --yes
469
+ """
470
+ console.print(f"[cyan]Unpublishing seller {seller_name}...[/cyan]\n")
471
+
472
+ if not yes and not dryrun:
473
+ confirm = typer.confirm(
474
+ f"⚠️ Delete seller '{seller_name}' and ALL associated providers/offerings/listings/subscriptions?"
475
+ )
476
+ if not confirm:
477
+ console.print("[yellow]Cancelled[/yellow]")
478
+ raise typer.Exit(code=0)
479
+
480
+ async def _delete():
481
+ unpublisher = ServiceDataUnpublisher()
482
+ return await unpublisher.delete_seller(seller_name, dryrun=dryrun, force=force)
483
+
484
+ try:
485
+ result = asyncio.run(_delete())
486
+
487
+ if dryrun:
488
+ console.print("[yellow]Dry-run mode: No actual deletion performed[/yellow]")
489
+ console.print(f"[dim]Would delete: {result}[/dim]")
490
+ else:
491
+ console.print(f"[green]✓ Successfully deleted seller {seller_name}[/green]")
492
+ if result.get("cascade_deleted"):
493
+ cascade = result["cascade_deleted"]
494
+ if cascade.get("providers"):
495
+ console.print(f" [dim]→ Deleted {cascade['providers']} provider(s)[/dim]")
496
+ if cascade.get("offerings"):
497
+ console.print(f" [dim]→ Deleted {cascade['offerings']} offering(s)[/dim]")
498
+ if cascade.get("listings"):
499
+ console.print(f" [dim]→ Deleted {cascade['listings']} listing(s)[/dim]")
500
+ if cascade.get("subscriptions"):
501
+ console.print(f" [dim]→ Deleted {cascade['subscriptions']} subscription(s)[/dim]")
502
+
503
+ except Exception as e:
504
+ console.print(f"[red]✗ Failed to delete seller:[/red] {e}")
505
+ raise typer.Exit(code=1)