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.
@@ -14,7 +14,7 @@ from rich.console import Console
14
14
 
15
15
  from .api import UnitySvcAPI
16
16
  from .models.base import ProviderStatusEnum, SellerStatusEnum
17
- from .utils import convert_convenience_fields_to_documents, find_files_by_schema
17
+ from .utils import convert_convenience_fields_to_documents, find_files_by_schema, render_template_file
18
18
  from .validator import DataValidator
19
19
 
20
20
 
@@ -59,31 +59,79 @@ class ServiceDataPublisher(UnitySvcAPI):
59
59
  with open(full_path, "rb") as f:
60
60
  return base64.b64encode(f.read()).decode("ascii")
61
61
 
62
- def resolve_file_references(self, data: dict[str, Any], base_path: Path) -> dict[str, Any]:
63
- """Recursively resolve file references and include content in data."""
62
+ def resolve_file_references(
63
+ self,
64
+ data: dict[str, Any],
65
+ base_path: Path,
66
+ listing: dict[str, Any] | None = None,
67
+ offering: dict[str, Any] | None = None,
68
+ provider: dict[str, Any] | None = None,
69
+ seller: dict[str, Any] | None = None,
70
+ ) -> dict[str, Any]:
71
+ """Recursively resolve file references and include content in data.
72
+
73
+ For Jinja2 template files (.j2), renders the template with provided context
74
+ and strips the .j2 extension from file_path.
75
+
76
+ Args:
77
+ data: Data dictionary potentially containing file_path references
78
+ base_path: Base path for resolving relative file paths
79
+ listing: Listing data for template rendering (optional)
80
+ offering: Offering data for template rendering (optional)
81
+ provider: Provider data for template rendering (optional)
82
+ seller: Seller data for template rendering (optional)
83
+
84
+ Returns:
85
+ Data with file references resolved and content loaded
86
+ """
64
87
  result: dict[str, Any] = {}
65
88
 
66
89
  for key, value in data.items():
67
90
  if isinstance(value, dict):
68
91
  # Recursively process nested dictionaries
69
- result[key] = self.resolve_file_references(value, base_path)
92
+ result[key] = self.resolve_file_references(
93
+ value, base_path, listing=listing, offering=offering, provider=provider, seller=seller
94
+ )
70
95
  elif isinstance(value, list):
71
96
  # Process lists
72
97
  result[key] = [
73
- (self.resolve_file_references(item, base_path) if isinstance(item, dict) else item)
98
+ (
99
+ self.resolve_file_references(
100
+ item, base_path, listing=listing, offering=offering, provider=provider, seller=seller
101
+ )
102
+ if isinstance(item, dict)
103
+ else item
104
+ )
74
105
  for item in value
75
106
  ]
76
107
  elif key == "file_path" and isinstance(value, str):
77
- # This is a file reference - load the content
78
- # Store both the original path and the content
79
- result[key] = value
80
- # Add file_content field if not already present (for DocumentCreate compatibility)
81
- if "file_content" not in data:
82
- try:
83
- content = self.load_file_content(Path(value), base_path)
84
- result["file_content"] = content
85
- except Exception as e:
86
- raise ValueError(f"Failed to load file content from '{value}': {e}")
108
+ # This is a file reference - load the content and render if template
109
+ full_path = base_path / value if not Path(value).is_absolute() else Path(value)
110
+
111
+ if not full_path.exists():
112
+ raise FileNotFoundError(f"File not found: {full_path}")
113
+
114
+ # Render template if applicable
115
+ try:
116
+ content, actual_filename = render_template_file(
117
+ full_path,
118
+ listing=listing,
119
+ offering=offering,
120
+ provider=provider,
121
+ seller=seller,
122
+ )
123
+ result["file_content"] = content
124
+
125
+ # Update file_path to remove .j2 extension if it was a template
126
+ if full_path.name.endswith(".j2"):
127
+ # Strip .j2 from the path
128
+ new_path = str(value)[:-3] # Remove last 3 characters (.j2)
129
+ result[key] = new_path
130
+ else:
131
+ result[key] = value
132
+
133
+ except Exception as e:
134
+ raise ValueError(f"Failed to load/render file content from '{value}': {e}")
87
135
  else:
88
136
  result[key] = value
89
137
 
@@ -242,16 +290,15 @@ class ServiceDataPublisher(UnitySvcAPI):
242
290
  if "name" not in data or not data.get("name"):
243
291
  data["name"] = listing_file.stem
244
292
 
245
- # Resolve file references and include content
246
- base_path = listing_file.parent
247
- data_with_content = self.resolve_file_references(data, base_path)
248
-
249
- # Extract provider_name from directory structure
293
+ # Extract provider_name from directory structure (needed before loading provider data)
250
294
  parts = listing_file.parts
251
295
  try:
252
296
  services_idx = parts.index("services")
253
297
  provider_name = parts[services_idx - 1]
254
- data_with_content["provider_name"] = provider_name
298
+ data["provider_name"] = provider_name
299
+
300
+ # Find provider directory to load provider data
301
+ provider_dir = Path(*parts[:services_idx])
255
302
  except (ValueError, IndexError):
256
303
  raise ValueError(
257
304
  f"Cannot extract provider_name from path: {listing_file}. "
@@ -259,7 +306,7 @@ class ServiceDataPublisher(UnitySvcAPI):
259
306
  )
260
307
 
261
308
  # If service_name is not in listing data, find it from service files in the same directory
262
- if "service_name" not in data_with_content or not data_with_content["service_name"]:
309
+ if "service_name" not in data or not data["service_name"]:
263
310
  # Find all service files in the same directory
264
311
  service_files = find_files_by_schema(listing_file.parent, "service_v1")
265
312
 
@@ -269,7 +316,7 @@ class ServiceDataPublisher(UnitySvcAPI):
269
316
  f"Listing files must be in the same directory as a service definition."
270
317
  )
271
318
  elif len(service_files) > 1:
272
- service_names = [data.get("name", "unknown") for _, _, data in service_files]
319
+ service_names = [svc_data.get("name", "unknown") for _, _, svc_data in service_files]
273
320
  raise ValueError(
274
321
  f"Multiple services found in {listing_file.parent}: {', '.join(service_names)}. "
275
322
  f"Please add 'service_name' field to {listing_file.name} to specify which "
@@ -278,11 +325,11 @@ class ServiceDataPublisher(UnitySvcAPI):
278
325
  else:
279
326
  # Exactly one service found - use it
280
327
  _service_file, _format, service_data = service_files[0]
281
- data_with_content["service_name"] = service_data.get("name")
282
- data_with_content["service_version"] = service_data.get("version")
328
+ data["service_name"] = service_data.get("name")
329
+ data["service_version"] = service_data.get("version")
283
330
  else:
284
331
  # service_name is provided in listing data, find the matching service to get version
285
- service_name = data_with_content["service_name"]
332
+ service_name = data["service_name"]
286
333
  service_files = find_files_by_schema(
287
334
  listing_file.parent, "service_v1", field_filter=(("name", service_name),)
288
335
  )
@@ -294,7 +341,14 @@ class ServiceDataPublisher(UnitySvcAPI):
294
341
 
295
342
  # Get version from the found service
296
343
  _service_file, _format, service_data = service_files[0]
297
- data_with_content["service_version"] = service_data.get("version")
344
+ data["service_version"] = service_data.get("version")
345
+
346
+ # Load provider data for template rendering
347
+ provider_files = find_files_by_schema(provider_dir, "provider_v1")
348
+ if provider_files:
349
+ _provider_file, _format, provider_data = provider_files[0]
350
+ else:
351
+ provider_data = {}
298
352
 
299
353
  # Find seller_name from seller definition in the data directory
300
354
  # Navigate up to find the data directory and look for seller file
@@ -332,11 +386,22 @@ class ServiceDataPublisher(UnitySvcAPI):
332
386
  if not seller_name:
333
387
  raise ValueError("Seller data missing 'name' field")
334
388
 
335
- data_with_content["seller_name"] = seller_name
389
+ data["seller_name"] = seller_name
336
390
 
337
391
  # Map listing_status to status if present
338
- if "listing_status" in data_with_content:
339
- data_with_content["status"] = data_with_content.pop("listing_status")
392
+ if "listing_status" in data:
393
+ data["status"] = data.pop("listing_status")
394
+
395
+ # NOW resolve file references with all context (listing, offering, provider, seller)
396
+ base_path = listing_file.parent
397
+ data_with_content = self.resolve_file_references(
398
+ data,
399
+ base_path,
400
+ listing=data,
401
+ offering=service_data,
402
+ provider=provider_data,
403
+ seller=seller_data,
404
+ )
340
405
 
341
406
  # Post to the endpoint using retry helper
342
407
  context_info = (
@@ -344,7 +409,7 @@ class ServiceDataPublisher(UnitySvcAPI):
344
409
  f"provider: {data_with_content.get('provider_name')}, "
345
410
  f"seller: {data_with_content.get('seller_name')}"
346
411
  )
347
- return await self._post_with_retry(
412
+ result = await self._post_with_retry(
348
413
  endpoint="/publish/listing",
349
414
  data=data_with_content,
350
415
  entity_type="listing",
@@ -353,29 +418,33 @@ class ServiceDataPublisher(UnitySvcAPI):
353
418
  max_retries=max_retries,
354
419
  )
355
420
 
421
+ # Add local metadata to result for display purposes
422
+ result["service_name"] = data_with_content.get("service_name")
423
+ result["provider_name"] = data_with_content.get("provider_name")
424
+ result["seller_name"] = data_with_content.get("seller_name")
425
+
426
+ return result
427
+
356
428
  async def post_service_offering_async(self, data_file: Path, max_retries: int = 3) -> dict[str, Any]:
357
429
  """Async version of post_service_offering for concurrent publishing with retry logic."""
358
430
  # Load the data file
359
431
  data = self.load_data_file(data_file)
360
432
 
361
- # Resolve file references and include content
433
+ # Convert convenience fields first
362
434
  base_path = data_file.parent
363
435
  data = convert_convenience_fields_to_documents(
364
436
  data, base_path, logo_field="logo", terms_field="terms_of_service"
365
437
  )
366
438
 
367
- # Resolve file references and include content
368
- data_with_content = self.resolve_file_references(data, base_path)
369
-
370
439
  # Extract provider_name from directory structure
371
440
  # Find the 'services' directory and use its parent as provider_name
372
441
  parts = data_file.parts
373
442
  try:
374
443
  services_idx = parts.index("services")
375
444
  provider_name = parts[services_idx - 1]
376
- data_with_content["provider_name"] = provider_name
445
+ data["provider_name"] = provider_name
377
446
 
378
- # Find provider directory to check status
447
+ # Find provider directory to check status and load data
379
448
  provider_dir = Path(*parts[:services_idx])
380
449
  except (ValueError, IndexError):
381
450
  raise ValueError(
@@ -383,7 +452,7 @@ class ServiceDataPublisher(UnitySvcAPI):
383
452
  f"Expected path to contain .../{{provider_name}}/services/..."
384
453
  )
385
454
 
386
- # Check provider status - skip if incomplete
455
+ # Load provider data for status check and template rendering
387
456
  provider_files = find_files_by_schema(provider_dir, "provider_v1")
388
457
  if provider_files:
389
458
  # Should only be one provider file in the directory
@@ -395,10 +464,22 @@ class ServiceDataPublisher(UnitySvcAPI):
395
464
  "reason": f"Provider status is '{provider_status}' - not publishing offering to backend",
396
465
  "name": data.get("name", "unknown"),
397
466
  }
467
+ else:
468
+ provider_data = {}
469
+
470
+ # NOW resolve file references with all context (offering, provider)
471
+ data_with_content = self.resolve_file_references(
472
+ data,
473
+ base_path,
474
+ listing=None,
475
+ offering=data,
476
+ provider=provider_data,
477
+ seller=None,
478
+ )
398
479
 
399
480
  # Post to the endpoint using retry helper
400
481
  context_info = f"provider: {data_with_content.get('provider_name')}"
401
- return await self._post_with_retry(
482
+ result = await self._post_with_retry(
402
483
  endpoint="/publish/offering",
403
484
  data=data_with_content,
404
485
  entity_type="offering",
@@ -407,6 +488,11 @@ class ServiceDataPublisher(UnitySvcAPI):
407
488
  max_retries=max_retries,
408
489
  )
409
490
 
491
+ # Add local metadata to result for display purposes
492
+ result["provider_name"] = data_with_content.get("provider_name")
493
+
494
+ return result
495
+
410
496
  async def post_provider_async(self, data_file: Path, max_retries: int = 3) -> dict[str, Any]:
411
497
  """Async version of post_provider for concurrent publishing with retry logic."""
412
498
  # Load the data file
@@ -428,8 +514,15 @@ class ServiceDataPublisher(UnitySvcAPI):
428
514
  data, base_path, logo_field="logo", terms_field="terms_of_service"
429
515
  )
430
516
 
431
- # Resolve file references and include content
432
- data_with_content = self.resolve_file_references(data, base_path)
517
+ # Resolve file references and include content with provider context
518
+ data_with_content = self.resolve_file_references(
519
+ data,
520
+ base_path,
521
+ listing=None,
522
+ offering=None,
523
+ provider=data,
524
+ seller=None,
525
+ )
433
526
 
434
527
  # Post to the endpoint using retry helper
435
528
  return await self._post_with_retry(
@@ -459,8 +552,15 @@ class ServiceDataPublisher(UnitySvcAPI):
459
552
  base_path = data_file.parent
460
553
  data = convert_convenience_fields_to_documents(data, base_path, logo_field="logo", terms_field=None)
461
554
 
462
- # Resolve file references and include content
463
- data_with_content = self.resolve_file_references(data, base_path)
555
+ # Resolve file references and include content with seller context
556
+ data_with_content = self.resolve_file_references(
557
+ data,
558
+ base_path,
559
+ listing=None,
560
+ offering=None,
561
+ provider=None,
562
+ seller=data,
563
+ )
464
564
 
465
565
  # Post to the endpoint using retry helper
466
566
  return await self._post_with_retry(
@@ -837,14 +937,6 @@ class ServiceDataPublisher(UnitySvcAPI):
837
937
 
838
938
  return all_results
839
939
 
840
- def __enter__(self):
841
- """Sync context manager entry for CLI usage."""
842
- return self
843
-
844
- def __exit__(self, exc_type, exc_val, exc_tb):
845
- """Sync context manager exit for CLI usage."""
846
- asyncio.run(self.aclose())
847
-
848
940
 
849
941
  # CLI commands for publishing
850
942
  app = typer.Typer(help="Publish data to backend")
@@ -896,60 +988,62 @@ def publish_callback(
896
988
  console.print(f"[bold blue]Publishing all data from:[/bold blue] {data_path}")
897
989
  console.print(f"[bold blue]Backend URL:[/bold blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
898
990
 
991
+ async def _publish_all_async():
992
+ async with ServiceDataPublisher() as publisher:
993
+ return await publisher.publish_all_models(data_path)
994
+
899
995
  try:
900
- with ServiceDataPublisher() as publisher:
901
- # Call the publish_all_models method (now async)
902
- all_results = asyncio.run(publisher.publish_all_models(data_path))
903
-
904
- # Display results for each data type
905
- data_type_display_names = {
906
- "sellers": "Sellers",
907
- "providers": "Providers",
908
- "offerings": "Service Offerings",
909
- "listings": "Service Listings",
910
- }
996
+ all_results = asyncio.run(_publish_all_async())
997
+
998
+ # Display results for each data type
999
+ data_type_display_names = {
1000
+ "sellers": "Sellers",
1001
+ "providers": "Providers",
1002
+ "offerings": "Service Offerings",
1003
+ "listings": "Service Listings",
1004
+ }
1005
+
1006
+ for data_type in ["sellers", "providers", "offerings", "listings"]:
1007
+ display_name = data_type_display_names[data_type]
1008
+ results = all_results[data_type]
911
1009
 
912
- for data_type in ["sellers", "providers", "offerings", "listings"]:
913
- display_name = data_type_display_names[data_type]
914
- results = all_results[data_type]
915
-
916
- console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
917
- console.print(f"[bold cyan]{display_name}[/bold cyan]")
918
- console.print(f"[bold cyan]{'=' * 60}[/bold cyan]\n")
919
-
920
- console.print(f" Total found: {results['total']}")
921
- console.print(f" [green]✓ Success:[/green] {results['success']}")
922
- console.print(f" [red]✗ Failed:[/red] {results['failed']}")
923
-
924
- # Display errors if any
925
- if results.get("errors"):
926
- console.print(f"\n[bold red]Errors in {display_name}:[/bold red]")
927
- for error in results["errors"]:
928
- # Check if this is a skipped item
929
- if isinstance(error, dict) and error.get("error", "").startswith("skipped"):
930
- continue
931
- console.print(f" [red]✗[/red] {error.get('file', 'unknown')}")
932
- console.print(f" {error.get('error', 'unknown error')}")
933
-
934
- # Final summary
935
1010
  console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
936
- console.print("[bold]Final Publishing Summary[/bold]")
1011
+ console.print(f"[bold cyan]{display_name}[/bold cyan]")
937
1012
  console.print(f"[bold cyan]{'=' * 60}[/bold cyan]\n")
938
- console.print(f" Total found: {all_results['total_found']}")
939
- console.print(f" [green]✓ Success:[/green] {all_results['total_success']}")
940
- console.print(f" [red] Failed:[/red] {all_results['total_failed']}")
941
-
942
- if all_results["total_failed"] > 0:
943
- console.print(
944
- f"\n[yellow]⚠[/yellow] Completed with {all_results['total_failed']} failure(s)",
945
- style="bold yellow",
946
- )
947
- raise typer.Exit(code=1)
948
- else:
949
- console.print(
950
- "\n[green][/green] All data published successfully!",
951
- style="bold green",
952
- )
1013
+
1014
+ console.print(f" Total found: {results['total']}")
1015
+ console.print(f" [green] Success:[/green] {results['success']}")
1016
+ console.print(f" [red]✗ Failed:[/red] {results['failed']}")
1017
+
1018
+ # Display errors if any
1019
+ if results.get("errors"):
1020
+ console.print(f"\n[bold red]Errors in {display_name}:[/bold red]")
1021
+ for error in results["errors"]:
1022
+ # Check if this is a skipped item
1023
+ if isinstance(error, dict) and error.get("error", "").startswith("skipped"):
1024
+ continue
1025
+ console.print(f" [red][/red] {error.get('file', 'unknown')}")
1026
+ console.print(f" {error.get('error', 'unknown error')}")
1027
+
1028
+ # Final summary
1029
+ console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
1030
+ console.print("[bold]Final Publishing Summary[/bold]")
1031
+ console.print(f"[bold cyan]{'=' * 60}[/bold cyan]\n")
1032
+ console.print(f" Total found: {all_results['total_found']}")
1033
+ console.print(f" [green]✓ Success:[/green] {all_results['total_success']}")
1034
+ console.print(f" [red]✗ Failed:[/red] {all_results['total_failed']}")
1035
+
1036
+ if all_results["total_failed"] > 0:
1037
+ console.print(
1038
+ f"\n[yellow]⚠[/yellow] Completed with {all_results['total_failed']} failure(s)",
1039
+ style="bold yellow",
1040
+ )
1041
+ raise typer.Exit(code=1)
1042
+ else:
1043
+ console.print(
1044
+ "\n[green]✓[/green] All data published successfully!",
1045
+ style="bold green",
1046
+ )
953
1047
 
954
1048
  except typer.Exit:
955
1049
  raise
@@ -980,36 +1074,45 @@ def publish_providers(
980
1074
  console.print(f"[red]✗[/red] Path not found: {data_path}", style="bold red")
981
1075
  raise typer.Exit(code=1)
982
1076
 
983
- try:
984
- with ServiceDataPublisher() as publisher:
1077
+ # Handle single file
1078
+ if data_path.is_file():
1079
+ console.print(f"[blue]Publishing provider:[/blue] {data_path}")
1080
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1081
+ else:
1082
+ console.print(f"[blue]Scanning for providers in:[/blue] {data_path}")
1083
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1084
+
1085
+ async def _publish_providers_async():
1086
+ async with ServiceDataPublisher() as publisher:
985
1087
  # Handle single file
986
1088
  if data_path.is_file():
987
- console.print(f"[blue]Publishing provider:[/blue] {data_path}")
988
- console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
989
- result = asyncio.run(publisher.post_provider_async(data_path))
990
- console.print("[green]✓[/green] Provider published successfully!")
991
- console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1089
+ return await publisher.post_provider_async(data_path), True
992
1090
  # Handle directory
993
1091
  else:
994
- console.print(f"[blue]Scanning for providers in:[/blue] {data_path}")
995
- console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
996
- results = asyncio.run(publisher.publish_all_providers(data_path))
997
-
998
- # Display summary
999
- console.print("\n[bold]Publishing Summary:[/bold]")
1000
- console.print(f" Total found: {results['total']}")
1001
- console.print(f" [green]✓ Success:[/green] {results['success']}")
1002
- console.print(f" [red]✗ Failed:[/red] {results['failed']}")
1003
-
1004
- # Display errors if any
1005
- if results["errors"]:
1006
- console.print("\n[bold red]Errors:[/bold red]")
1007
- for error in results["errors"]:
1008
- console.print(f" [red]✗[/red] {error['file']}")
1009
- console.print(f" {error['error']}")
1010
-
1011
- if results["failed"] > 0:
1012
- raise typer.Exit(code=1)
1092
+ return await publisher.publish_all_providers(data_path), False
1093
+
1094
+ try:
1095
+ result, is_single = asyncio.run(_publish_providers_async())
1096
+
1097
+ if is_single:
1098
+ console.print("[green]✓[/green] Provider published successfully!")
1099
+ console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1100
+ else:
1101
+ # Display summary
1102
+ console.print("\n[bold]Publishing Summary:[/bold]")
1103
+ console.print(f" Total found: {result['total']}")
1104
+ console.print(f" [green]✓ Success:[/green] {result['success']}")
1105
+ console.print(f" [red]✗ Failed:[/red] {result['failed']}")
1106
+
1107
+ # Display errors if any
1108
+ if result["errors"]:
1109
+ console.print("\n[bold red]Errors:[/bold red]")
1110
+ for error in result["errors"]:
1111
+ console.print(f" [red]✗[/red] {error['file']}")
1112
+ console.print(f" {error['error']}")
1113
+
1114
+ if result["failed"] > 0:
1115
+ raise typer.Exit(code=1)
1013
1116
 
1014
1117
  except typer.Exit:
1015
1118
  raise
@@ -1039,34 +1142,43 @@ def publish_sellers(
1039
1142
  console.print(f"[red]✗[/red] Path not found: {data_path}", style="bold red")
1040
1143
  raise typer.Exit(code=1)
1041
1144
 
1042
- try:
1043
- with ServiceDataPublisher() as publisher:
1145
+ # Handle single file
1146
+ if data_path.is_file():
1147
+ console.print(f"[blue]Publishing seller:[/blue] {data_path}")
1148
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1149
+ else:
1150
+ console.print(f"[blue]Scanning for sellers in:[/blue] {data_path}")
1151
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1152
+
1153
+ async def _publish_sellers_async():
1154
+ async with ServiceDataPublisher() as publisher:
1044
1155
  # Handle single file
1045
1156
  if data_path.is_file():
1046
- console.print(f"[blue]Publishing seller:[/blue] {data_path}")
1047
- console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1048
- result = asyncio.run(publisher.post_seller_async(data_path))
1049
- console.print("[green]✓[/green] Seller published successfully!")
1050
- console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1157
+ return await publisher.post_seller_async(data_path), True
1051
1158
  # Handle directory
1052
1159
  else:
1053
- console.print(f"[blue]Scanning for sellers in:[/blue] {data_path}")
1054
- console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1055
- results = asyncio.run(publisher.publish_all_sellers(data_path))
1056
-
1057
- console.print("\n[bold]Publishing Summary:[/bold]")
1058
- console.print(f" Total found: {results['total']}")
1059
- console.print(f" [green]✓ Success: {results['success']}[/green]")
1060
- console.print(f" [red]✗ Failed: {results['failed']}[/red]")
1061
-
1062
- if results["errors"]:
1063
- console.print("\n[bold red]Errors:[/bold red]")
1064
- for error in results["errors"]:
1065
- console.print(f" [red]✗[/red] {error['file']}")
1066
- console.print(f" {error['error']}")
1067
- raise typer.Exit(code=1)
1068
- else:
1069
- console.print("\n[green]✓[/green] All sellers published successfully!")
1160
+ return await publisher.publish_all_sellers(data_path), False
1161
+
1162
+ try:
1163
+ result, is_single = asyncio.run(_publish_sellers_async())
1164
+
1165
+ if is_single:
1166
+ console.print("[green]✓[/green] Seller published successfully!")
1167
+ console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1168
+ else:
1169
+ console.print("\n[bold]Publishing Summary:[/bold]")
1170
+ console.print(f" Total found: {result['total']}")
1171
+ console.print(f" [green]✓ Success: {result['success']}[/green]")
1172
+ console.print(f" [red]✗ Failed: {result['failed']}[/red]")
1173
+
1174
+ if result["errors"]:
1175
+ console.print("\n[bold red]Errors:[/bold red]")
1176
+ for error in result["errors"]:
1177
+ console.print(f" [red]✗[/red] {error['file']}")
1178
+ console.print(f" {error['error']}")
1179
+ raise typer.Exit(code=1)
1180
+ else:
1181
+ console.print("\n[green]✓[/green] All sellers published successfully!")
1070
1182
 
1071
1183
  except typer.Exit:
1072
1184
  raise
@@ -1096,34 +1208,43 @@ def publish_offerings(
1096
1208
  console.print(f"[red]✗[/red] Path not found: {data_path}", style="bold red")
1097
1209
  raise typer.Exit(code=1)
1098
1210
 
1099
- try:
1100
- with ServiceDataPublisher() as publisher:
1211
+ # Handle single file
1212
+ if data_path.is_file():
1213
+ console.print(f"[blue]Publishing service offering:[/blue] {data_path}")
1214
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1215
+ else:
1216
+ console.print(f"[blue]Scanning for service offerings in:[/blue] {data_path}")
1217
+ console.print(f"[blue]Backend URL:[/bold blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1218
+
1219
+ async def _publish_offerings_async():
1220
+ async with ServiceDataPublisher() as publisher:
1101
1221
  # Handle single file
1102
1222
  if data_path.is_file():
1103
- console.print(f"[blue]Publishing service offering:[/blue] {data_path}")
1104
- console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1105
- result = asyncio.run(publisher.post_service_offering_async(data_path))
1106
- console.print("[green]✓[/green] Service offering published successfully!")
1107
- console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1223
+ return await publisher.post_service_offering_async(data_path), True
1108
1224
  # Handle directory
1109
1225
  else:
1110
- console.print(f"[blue]Scanning for service offerings in:[/blue] {data_path}")
1111
- console.print(f"[blue]Backend URL:[/bold blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1112
- results = asyncio.run(publisher.publish_all_offerings(data_path))
1113
-
1114
- console.print("\n[bold]Publishing Summary:[/bold]")
1115
- console.print(f" Total found: {results['total']}")
1116
- console.print(f" [green]✓ Success: {results['success']}[/green]")
1117
- console.print(f" [red]✗ Failed: {results['failed']}[/red]")
1118
-
1119
- if results["errors"]:
1120
- console.print("\n[bold red]Errors:[/bold red]")
1121
- for error in results["errors"]:
1122
- console.print(f" [red]✗[/red] {error['file']}")
1123
- console.print(f" {error['error']}")
1124
- raise typer.Exit(code=1)
1125
- else:
1126
- console.print("\n[green]✓[/green] All service offerings published successfully!")
1226
+ return await publisher.publish_all_offerings(data_path), False
1227
+
1228
+ try:
1229
+ result, is_single = asyncio.run(_publish_offerings_async())
1230
+
1231
+ if is_single:
1232
+ console.print("[green]✓[/green] Service offering published successfully!")
1233
+ console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1234
+ else:
1235
+ console.print("\n[bold]Publishing Summary:[/bold]")
1236
+ console.print(f" Total found: {result['total']}")
1237
+ console.print(f" [green]✓ Success: {result['success']}[/green]")
1238
+ console.print(f" [red]✗ Failed: {result['failed']}[/red]")
1239
+
1240
+ if result["errors"]:
1241
+ console.print("\n[bold red]Errors:[/bold red]")
1242
+ for error in result["errors"]:
1243
+ console.print(f" [red]✗[/red] {error['file']}")
1244
+ console.print(f" {error['error']}")
1245
+ raise typer.Exit(code=1)
1246
+ else:
1247
+ console.print("\n[green]✓[/green] All service offerings published successfully!")
1127
1248
 
1128
1249
  except typer.Exit:
1129
1250
  raise
@@ -1154,34 +1275,43 @@ def publish_listings(
1154
1275
  console.print(f"[red]✗[/red] Path not found: {data_path}", style="bold red")
1155
1276
  raise typer.Exit(code=1)
1156
1277
 
1157
- try:
1158
- with ServiceDataPublisher() as publisher:
1278
+ # Handle single file
1279
+ if data_path.is_file():
1280
+ console.print(f"[blue]Publishing service listing:[/blue] {data_path}")
1281
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1282
+ else:
1283
+ console.print(f"[blue]Scanning for service listings in:[/blue] {data_path}")
1284
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1285
+
1286
+ async def _publish_listings_async():
1287
+ async with ServiceDataPublisher() as publisher:
1159
1288
  # Handle single file
1160
1289
  if data_path.is_file():
1161
- console.print(f"[blue]Publishing service listing:[/blue] {data_path}")
1162
- console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1163
- result = asyncio.run(publisher.post_service_listing_async(data_path))
1164
- console.print("[green]✓[/green] Service listing published successfully!")
1165
- console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1290
+ return await publisher.post_service_listing_async(data_path), True
1166
1291
  # Handle directory
1167
1292
  else:
1168
- console.print(f"[blue]Scanning for service listings in:[/blue] {data_path}")
1169
- console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1170
- results = asyncio.run(publisher.publish_all_listings(data_path))
1171
-
1172
- console.print("\n[bold]Publishing Summary:[/bold]")
1173
- console.print(f" Total found: {results['total']}")
1174
- console.print(f" [green]✓ Success: {results['success']}[/green]")
1175
- console.print(f" [red]✗ Failed: {results['failed']}[/red]")
1176
-
1177
- if results["errors"]:
1178
- console.print("\n[bold red]Errors:[/bold red]")
1179
- for error in results["errors"]:
1180
- console.print(f" [red]✗[/red] {error['file']}")
1181
- console.print(f" {error['error']}")
1182
- raise typer.Exit(code=1)
1183
- else:
1184
- console.print("\n[green]✓[/green] All service listings published successfully!")
1293
+ return await publisher.publish_all_listings(data_path), False
1294
+
1295
+ try:
1296
+ result, is_single = asyncio.run(_publish_listings_async())
1297
+
1298
+ if is_single:
1299
+ console.print("[green]✓[/green] Service listing published successfully!")
1300
+ console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1301
+ else:
1302
+ console.print("\n[bold]Publishing Summary:[/bold]")
1303
+ console.print(f" Total found: {result['total']}")
1304
+ console.print(f" [green]✓ Success: {result['success']}[/green]")
1305
+ console.print(f" [red]✗ Failed: {result['failed']}[/red]")
1306
+
1307
+ if result["errors"]:
1308
+ console.print("\n[bold red]Errors:[/bold red]")
1309
+ for error in result["errors"]:
1310
+ console.print(f" [red]✗[/red] {error['file']}")
1311
+ console.print(f" {error['error']}")
1312
+ raise typer.Exit(code=1)
1313
+ else:
1314
+ console.print("\n[green]✓[/green] All service listings published successfully!")
1185
1315
 
1186
1316
  except typer.Exit:
1187
1317
  raise