unitysvc-services 0.1.4__py3-none-any.whl → 0.1.6__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,34 +59,131 @@ 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
+ listing_filename: str | None = None,
71
+ ) -> dict[str, Any]:
72
+ """Recursively resolve file references and include content in data.
73
+
74
+ For Jinja2 template files (.j2), renders the template with provided context
75
+ and strips the .j2 extension from file_path.
76
+
77
+ Args:
78
+ data: Data dictionary potentially containing file_path references
79
+ base_path: Base path for resolving relative file paths
80
+ listing: Listing data for template rendering (optional)
81
+ offering: Offering data for template rendering (optional)
82
+ provider: Provider data for template rendering (optional)
83
+ seller: Seller data for template rendering (optional)
84
+ listing_filename: Listing filename for constructing output filenames (optional)
85
+
86
+ Returns:
87
+ Data with file references resolved and content loaded
88
+ """
64
89
  result: dict[str, Any] = {}
65
90
 
66
91
  for key, value in data.items():
67
92
  if isinstance(value, dict):
68
93
  # Recursively process nested dictionaries
69
- result[key] = self.resolve_file_references(value, base_path)
94
+ result[key] = self.resolve_file_references(
95
+ value,
96
+ base_path,
97
+ listing=listing,
98
+ offering=offering,
99
+ provider=provider,
100
+ seller=seller,
101
+ listing_filename=listing_filename,
102
+ )
70
103
  elif isinstance(value, list):
71
104
  # Process lists
72
105
  result[key] = [
73
- (self.resolve_file_references(item, base_path) if isinstance(item, dict) else item)
106
+ (
107
+ self.resolve_file_references(
108
+ item,
109
+ base_path,
110
+ listing=listing,
111
+ offering=offering,
112
+ provider=provider,
113
+ seller=seller,
114
+ listing_filename=listing_filename,
115
+ )
116
+ if isinstance(item, dict)
117
+ else item
118
+ )
74
119
  for item in value
75
120
  ]
76
121
  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}")
122
+ # This is a file reference - load the content and render if template
123
+ full_path = base_path / value if not Path(value).is_absolute() else Path(value)
124
+
125
+ if not full_path.exists():
126
+ raise FileNotFoundError(f"File not found: {full_path}")
127
+
128
+ # Render template if applicable
129
+ try:
130
+ content, actual_filename = render_template_file(
131
+ full_path,
132
+ listing=listing,
133
+ offering=offering,
134
+ provider=provider,
135
+ seller=seller,
136
+ )
137
+ result["file_content"] = content
138
+
139
+ # Update file_path to remove .j2 extension if it was a template
140
+ if full_path.name.endswith(".j2"):
141
+ # Strip .j2 from the path
142
+ new_path = str(value)[:-3] # Remove last 3 characters (.j2)
143
+ result[key] = new_path
144
+ else:
145
+ result[key] = value
146
+
147
+ except Exception as e:
148
+ raise ValueError(f"Failed to load/render file content from '{value}': {e}")
87
149
  else:
88
150
  result[key] = value
89
151
 
152
+ # After processing all fields, check if this is a code_examples document
153
+ # If so, try to load corresponding .out file and add to meta.output
154
+ if result.get("category") == "code_examples" and result.get("file_content") and listing_filename:
155
+ # Get the actual filename (after .j2 stripping if applicable)
156
+ # If file_path was updated (e.g., from "test.py.j2" to "test.py"), use that
157
+ # Otherwise, extract basename from original file_path
158
+ output_base_filename: str | None = None
159
+
160
+ # Check if file_path was modified (original might have had .j2)
161
+ file_path_value = result.get("file_path", "")
162
+ if file_path_value:
163
+ output_base_filename = Path(file_path_value).name
164
+
165
+ if output_base_filename:
166
+ # Construct output filename: {listing_stem}_{output_base_filename}.out
167
+ # e.g., "svclisting_test.py.out" for svclisting.json and test.py
168
+ listing_stem = Path(listing_filename).stem
169
+ output_filename = f"{listing_stem}_{output_base_filename}.out"
170
+
171
+ # Try to find the .out file in base_path (listing directory)
172
+ output_path = base_path / output_filename
173
+
174
+ if output_path.exists():
175
+ try:
176
+ with open(output_path, encoding="utf-8") as f:
177
+ output_content = f.read()
178
+
179
+ # Add output to meta field
180
+ if "meta" not in result or result["meta"] is None:
181
+ result["meta"] = {}
182
+ result["meta"]["output"] = output_content
183
+ except Exception:
184
+ # Don't fail if output file can't be read, just skip it
185
+ pass
186
+
90
187
  return result
91
188
 
92
189
  async def post( # type: ignore[override]
@@ -242,16 +339,15 @@ class ServiceDataPublisher(UnitySvcAPI):
242
339
  if "name" not in data or not data.get("name"):
243
340
  data["name"] = listing_file.stem
244
341
 
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
342
+ # Extract provider_name from directory structure (needed before loading provider data)
250
343
  parts = listing_file.parts
251
344
  try:
252
345
  services_idx = parts.index("services")
253
346
  provider_name = parts[services_idx - 1]
254
- data_with_content["provider_name"] = provider_name
347
+ data["provider_name"] = provider_name
348
+
349
+ # Find provider directory to load provider data
350
+ provider_dir = Path(*parts[:services_idx])
255
351
  except (ValueError, IndexError):
256
352
  raise ValueError(
257
353
  f"Cannot extract provider_name from path: {listing_file}. "
@@ -259,7 +355,7 @@ class ServiceDataPublisher(UnitySvcAPI):
259
355
  )
260
356
 
261
357
  # 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"]:
358
+ if "service_name" not in data or not data["service_name"]:
263
359
  # Find all service files in the same directory
264
360
  service_files = find_files_by_schema(listing_file.parent, "service_v1")
265
361
 
@@ -269,7 +365,7 @@ class ServiceDataPublisher(UnitySvcAPI):
269
365
  f"Listing files must be in the same directory as a service definition."
270
366
  )
271
367
  elif len(service_files) > 1:
272
- service_names = [data.get("name", "unknown") for _, _, data in service_files]
368
+ service_names = [svc_data.get("name", "unknown") for _, _, svc_data in service_files]
273
369
  raise ValueError(
274
370
  f"Multiple services found in {listing_file.parent}: {', '.join(service_names)}. "
275
371
  f"Please add 'service_name' field to {listing_file.name} to specify which "
@@ -278,11 +374,11 @@ class ServiceDataPublisher(UnitySvcAPI):
278
374
  else:
279
375
  # Exactly one service found - use it
280
376
  _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")
377
+ data["service_name"] = service_data.get("name")
378
+ data["service_version"] = service_data.get("version")
283
379
  else:
284
380
  # service_name is provided in listing data, find the matching service to get version
285
- service_name = data_with_content["service_name"]
381
+ service_name = data["service_name"]
286
382
  service_files = find_files_by_schema(
287
383
  listing_file.parent, "service_v1", field_filter=(("name", service_name),)
288
384
  )
@@ -294,7 +390,14 @@ class ServiceDataPublisher(UnitySvcAPI):
294
390
 
295
391
  # Get version from the found service
296
392
  _service_file, _format, service_data = service_files[0]
297
- data_with_content["service_version"] = service_data.get("version")
393
+ data["service_version"] = service_data.get("version")
394
+
395
+ # Load provider data for template rendering
396
+ provider_files = find_files_by_schema(provider_dir, "provider_v1")
397
+ if provider_files:
398
+ _provider_file, _format, provider_data = provider_files[0]
399
+ else:
400
+ provider_data = {}
298
401
 
299
402
  # Find seller_name from seller definition in the data directory
300
403
  # Navigate up to find the data directory and look for seller file
@@ -332,11 +435,23 @@ class ServiceDataPublisher(UnitySvcAPI):
332
435
  if not seller_name:
333
436
  raise ValueError("Seller data missing 'name' field")
334
437
 
335
- data_with_content["seller_name"] = seller_name
438
+ data["seller_name"] = seller_name
336
439
 
337
440
  # 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")
441
+ if "listing_status" in data:
442
+ data["status"] = data.pop("listing_status")
443
+
444
+ # NOW resolve file references with all context (listing, offering, provider, seller)
445
+ base_path = listing_file.parent
446
+ data_with_content = self.resolve_file_references(
447
+ data,
448
+ base_path,
449
+ listing=data,
450
+ offering=service_data,
451
+ provider=provider_data,
452
+ seller=seller_data,
453
+ listing_filename=listing_file.name,
454
+ )
340
455
 
341
456
  # Post to the endpoint using retry helper
342
457
  context_info = (
@@ -344,7 +459,7 @@ class ServiceDataPublisher(UnitySvcAPI):
344
459
  f"provider: {data_with_content.get('provider_name')}, "
345
460
  f"seller: {data_with_content.get('seller_name')}"
346
461
  )
347
- return await self._post_with_retry(
462
+ result = await self._post_with_retry(
348
463
  endpoint="/publish/listing",
349
464
  data=data_with_content,
350
465
  entity_type="listing",
@@ -353,29 +468,33 @@ class ServiceDataPublisher(UnitySvcAPI):
353
468
  max_retries=max_retries,
354
469
  )
355
470
 
471
+ # Add local metadata to result for display purposes
472
+ result["service_name"] = data_with_content.get("service_name")
473
+ result["provider_name"] = data_with_content.get("provider_name")
474
+ result["seller_name"] = data_with_content.get("seller_name")
475
+
476
+ return result
477
+
356
478
  async def post_service_offering_async(self, data_file: Path, max_retries: int = 3) -> dict[str, Any]:
357
479
  """Async version of post_service_offering for concurrent publishing with retry logic."""
358
480
  # Load the data file
359
481
  data = self.load_data_file(data_file)
360
482
 
361
- # Resolve file references and include content
483
+ # Convert convenience fields first
362
484
  base_path = data_file.parent
363
485
  data = convert_convenience_fields_to_documents(
364
486
  data, base_path, logo_field="logo", terms_field="terms_of_service"
365
487
  )
366
488
 
367
- # Resolve file references and include content
368
- data_with_content = self.resolve_file_references(data, base_path)
369
-
370
489
  # Extract provider_name from directory structure
371
490
  # Find the 'services' directory and use its parent as provider_name
372
491
  parts = data_file.parts
373
492
  try:
374
493
  services_idx = parts.index("services")
375
494
  provider_name = parts[services_idx - 1]
376
- data_with_content["provider_name"] = provider_name
495
+ data["provider_name"] = provider_name
377
496
 
378
- # Find provider directory to check status
497
+ # Find provider directory to check status and load data
379
498
  provider_dir = Path(*parts[:services_idx])
380
499
  except (ValueError, IndexError):
381
500
  raise ValueError(
@@ -383,7 +502,7 @@ class ServiceDataPublisher(UnitySvcAPI):
383
502
  f"Expected path to contain .../{{provider_name}}/services/..."
384
503
  )
385
504
 
386
- # Check provider status - skip if incomplete
505
+ # Load provider data for status check and template rendering
387
506
  provider_files = find_files_by_schema(provider_dir, "provider_v1")
388
507
  if provider_files:
389
508
  # Should only be one provider file in the directory
@@ -395,10 +514,22 @@ class ServiceDataPublisher(UnitySvcAPI):
395
514
  "reason": f"Provider status is '{provider_status}' - not publishing offering to backend",
396
515
  "name": data.get("name", "unknown"),
397
516
  }
517
+ else:
518
+ provider_data = {}
519
+
520
+ # NOW resolve file references with all context (offering, provider)
521
+ data_with_content = self.resolve_file_references(
522
+ data,
523
+ base_path,
524
+ listing=None,
525
+ offering=data,
526
+ provider=provider_data,
527
+ seller=None,
528
+ )
398
529
 
399
530
  # Post to the endpoint using retry helper
400
531
  context_info = f"provider: {data_with_content.get('provider_name')}"
401
- return await self._post_with_retry(
532
+ result = await self._post_with_retry(
402
533
  endpoint="/publish/offering",
403
534
  data=data_with_content,
404
535
  entity_type="offering",
@@ -407,6 +538,11 @@ class ServiceDataPublisher(UnitySvcAPI):
407
538
  max_retries=max_retries,
408
539
  )
409
540
 
541
+ # Add local metadata to result for display purposes
542
+ result["provider_name"] = data_with_content.get("provider_name")
543
+
544
+ return result
545
+
410
546
  async def post_provider_async(self, data_file: Path, max_retries: int = 3) -> dict[str, Any]:
411
547
  """Async version of post_provider for concurrent publishing with retry logic."""
412
548
  # Load the data file
@@ -428,8 +564,15 @@ class ServiceDataPublisher(UnitySvcAPI):
428
564
  data, base_path, logo_field="logo", terms_field="terms_of_service"
429
565
  )
430
566
 
431
- # Resolve file references and include content
432
- data_with_content = self.resolve_file_references(data, base_path)
567
+ # Resolve file references and include content with provider context
568
+ data_with_content = self.resolve_file_references(
569
+ data,
570
+ base_path,
571
+ listing=None,
572
+ offering=None,
573
+ provider=data,
574
+ seller=None,
575
+ )
433
576
 
434
577
  # Post to the endpoint using retry helper
435
578
  return await self._post_with_retry(
@@ -459,8 +602,15 @@ class ServiceDataPublisher(UnitySvcAPI):
459
602
  base_path = data_file.parent
460
603
  data = convert_convenience_fields_to_documents(data, base_path, logo_field="logo", terms_field=None)
461
604
 
462
- # Resolve file references and include content
463
- data_with_content = self.resolve_file_references(data, base_path)
605
+ # Resolve file references and include content with seller context
606
+ data_with_content = self.resolve_file_references(
607
+ data,
608
+ base_path,
609
+ listing=None,
610
+ offering=None,
611
+ provider=None,
612
+ seller=data,
613
+ )
464
614
 
465
615
  # Post to the endpoint using retry helper
466
616
  return await self._post_with_retry(
@@ -837,14 +987,6 @@ class ServiceDataPublisher(UnitySvcAPI):
837
987
 
838
988
  return all_results
839
989
 
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
990
 
849
991
  # CLI commands for publishing
850
992
  app = typer.Typer(help="Publish data to backend")
@@ -896,60 +1038,62 @@ def publish_callback(
896
1038
  console.print(f"[bold blue]Publishing all data from:[/bold blue] {data_path}")
897
1039
  console.print(f"[bold blue]Backend URL:[/bold blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
898
1040
 
1041
+ async def _publish_all_async():
1042
+ async with ServiceDataPublisher() as publisher:
1043
+ return await publisher.publish_all_models(data_path)
1044
+
899
1045
  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
- }
1046
+ all_results = asyncio.run(_publish_all_async())
1047
+
1048
+ # Display results for each data type
1049
+ data_type_display_names = {
1050
+ "sellers": "Sellers",
1051
+ "providers": "Providers",
1052
+ "offerings": "Service Offerings",
1053
+ "listings": "Service Listings",
1054
+ }
1055
+
1056
+ for data_type in ["sellers", "providers", "offerings", "listings"]:
1057
+ display_name = data_type_display_names[data_type]
1058
+ results = all_results[data_type]
911
1059
 
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
1060
  console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
936
- console.print("[bold]Final Publishing Summary[/bold]")
1061
+ console.print(f"[bold cyan]{display_name}[/bold cyan]")
937
1062
  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
- )
1063
+
1064
+ console.print(f" Total found: {results['total']}")
1065
+ console.print(f" [green] Success:[/green] {results['success']}")
1066
+ console.print(f" [red]✗ Failed:[/red] {results['failed']}")
1067
+
1068
+ # Display errors if any
1069
+ if results.get("errors"):
1070
+ console.print(f"\n[bold red]Errors in {display_name}:[/bold red]")
1071
+ for error in results["errors"]:
1072
+ # Check if this is a skipped item
1073
+ if isinstance(error, dict) and error.get("error", "").startswith("skipped"):
1074
+ continue
1075
+ console.print(f" [red][/red] {error.get('file', 'unknown')}")
1076
+ console.print(f" {error.get('error', 'unknown error')}")
1077
+
1078
+ # Final summary
1079
+ console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
1080
+ console.print("[bold]Final Publishing Summary[/bold]")
1081
+ console.print(f"[bold cyan]{'=' * 60}[/bold cyan]\n")
1082
+ console.print(f" Total found: {all_results['total_found']}")
1083
+ console.print(f" [green]✓ Success:[/green] {all_results['total_success']}")
1084
+ console.print(f" [red]✗ Failed:[/red] {all_results['total_failed']}")
1085
+
1086
+ if all_results["total_failed"] > 0:
1087
+ console.print(
1088
+ f"\n[yellow]⚠[/yellow] Completed with {all_results['total_failed']} failure(s)",
1089
+ style="bold yellow",
1090
+ )
1091
+ raise typer.Exit(code=1)
1092
+ else:
1093
+ console.print(
1094
+ "\n[green]✓[/green] All data published successfully!",
1095
+ style="bold green",
1096
+ )
953
1097
 
954
1098
  except typer.Exit:
955
1099
  raise
@@ -980,36 +1124,45 @@ def publish_providers(
980
1124
  console.print(f"[red]✗[/red] Path not found: {data_path}", style="bold red")
981
1125
  raise typer.Exit(code=1)
982
1126
 
983
- try:
984
- with ServiceDataPublisher() as publisher:
1127
+ # Handle single file
1128
+ if data_path.is_file():
1129
+ console.print(f"[blue]Publishing provider:[/blue] {data_path}")
1130
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1131
+ else:
1132
+ console.print(f"[blue]Scanning for providers in:[/blue] {data_path}")
1133
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1134
+
1135
+ async def _publish_providers_async():
1136
+ async with ServiceDataPublisher() as publisher:
985
1137
  # Handle single file
986
1138
  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)}")
1139
+ return await publisher.post_provider_async(data_path), True
992
1140
  # Handle directory
993
1141
  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)
1142
+ return await publisher.publish_all_providers(data_path), False
1143
+
1144
+ try:
1145
+ result, is_single = asyncio.run(_publish_providers_async())
1146
+
1147
+ if is_single:
1148
+ console.print("[green]✓[/green] Provider published successfully!")
1149
+ console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1150
+ else:
1151
+ # Display summary
1152
+ console.print("\n[bold]Publishing Summary:[/bold]")
1153
+ console.print(f" Total found: {result['total']}")
1154
+ console.print(f" [green]✓ Success:[/green] {result['success']}")
1155
+ console.print(f" [red]✗ Failed:[/red] {result['failed']}")
1156
+
1157
+ # Display errors if any
1158
+ if result["errors"]:
1159
+ console.print("\n[bold red]Errors:[/bold red]")
1160
+ for error in result["errors"]:
1161
+ console.print(f" [red]✗[/red] {error['file']}")
1162
+ console.print(f" {error['error']}")
1163
+
1164
+ if result["failed"] > 0:
1165
+ raise typer.Exit(code=1)
1013
1166
 
1014
1167
  except typer.Exit:
1015
1168
  raise
@@ -1039,34 +1192,43 @@ def publish_sellers(
1039
1192
  console.print(f"[red]✗[/red] Path not found: {data_path}", style="bold red")
1040
1193
  raise typer.Exit(code=1)
1041
1194
 
1042
- try:
1043
- with ServiceDataPublisher() as publisher:
1195
+ # Handle single file
1196
+ if data_path.is_file():
1197
+ console.print(f"[blue]Publishing seller:[/blue] {data_path}")
1198
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1199
+ else:
1200
+ console.print(f"[blue]Scanning for sellers in:[/blue] {data_path}")
1201
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1202
+
1203
+ async def _publish_sellers_async():
1204
+ async with ServiceDataPublisher() as publisher:
1044
1205
  # Handle single file
1045
1206
  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)}")
1207
+ return await publisher.post_seller_async(data_path), True
1051
1208
  # Handle directory
1052
1209
  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!")
1210
+ return await publisher.publish_all_sellers(data_path), False
1211
+
1212
+ try:
1213
+ result, is_single = asyncio.run(_publish_sellers_async())
1214
+
1215
+ if is_single:
1216
+ console.print("[green]✓[/green] Seller published successfully!")
1217
+ console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1218
+ else:
1219
+ console.print("\n[bold]Publishing Summary:[/bold]")
1220
+ console.print(f" Total found: {result['total']}")
1221
+ console.print(f" [green]✓ Success: {result['success']}[/green]")
1222
+ console.print(f" [red]✗ Failed: {result['failed']}[/red]")
1223
+
1224
+ if result["errors"]:
1225
+ console.print("\n[bold red]Errors:[/bold red]")
1226
+ for error in result["errors"]:
1227
+ console.print(f" [red]✗[/red] {error['file']}")
1228
+ console.print(f" {error['error']}")
1229
+ raise typer.Exit(code=1)
1230
+ else:
1231
+ console.print("\n[green]✓[/green] All sellers published successfully!")
1070
1232
 
1071
1233
  except typer.Exit:
1072
1234
  raise
@@ -1096,34 +1258,43 @@ def publish_offerings(
1096
1258
  console.print(f"[red]✗[/red] Path not found: {data_path}", style="bold red")
1097
1259
  raise typer.Exit(code=1)
1098
1260
 
1099
- try:
1100
- with ServiceDataPublisher() as publisher:
1261
+ # Handle single file
1262
+ if data_path.is_file():
1263
+ console.print(f"[blue]Publishing service offering:[/blue] {data_path}")
1264
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1265
+ else:
1266
+ console.print(f"[blue]Scanning for service offerings in:[/blue] {data_path}")
1267
+ console.print(f"[blue]Backend URL:[/bold blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1268
+
1269
+ async def _publish_offerings_async():
1270
+ async with ServiceDataPublisher() as publisher:
1101
1271
  # Handle single file
1102
1272
  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)}")
1273
+ return await publisher.post_service_offering_async(data_path), True
1108
1274
  # Handle directory
1109
1275
  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!")
1276
+ return await publisher.publish_all_offerings(data_path), False
1277
+
1278
+ try:
1279
+ result, is_single = asyncio.run(_publish_offerings_async())
1280
+
1281
+ if is_single:
1282
+ console.print("[green]✓[/green] Service offering published successfully!")
1283
+ console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1284
+ else:
1285
+ console.print("\n[bold]Publishing Summary:[/bold]")
1286
+ console.print(f" Total found: {result['total']}")
1287
+ console.print(f" [green]✓ Success: {result['success']}[/green]")
1288
+ console.print(f" [red]✗ Failed: {result['failed']}[/red]")
1289
+
1290
+ if result["errors"]:
1291
+ console.print("\n[bold red]Errors:[/bold red]")
1292
+ for error in result["errors"]:
1293
+ console.print(f" [red]✗[/red] {error['file']}")
1294
+ console.print(f" {error['error']}")
1295
+ raise typer.Exit(code=1)
1296
+ else:
1297
+ console.print("\n[green]✓[/green] All service offerings published successfully!")
1127
1298
 
1128
1299
  except typer.Exit:
1129
1300
  raise
@@ -1154,34 +1325,43 @@ def publish_listings(
1154
1325
  console.print(f"[red]✗[/red] Path not found: {data_path}", style="bold red")
1155
1326
  raise typer.Exit(code=1)
1156
1327
 
1157
- try:
1158
- with ServiceDataPublisher() as publisher:
1328
+ # Handle single file
1329
+ if data_path.is_file():
1330
+ console.print(f"[blue]Publishing service listing:[/blue] {data_path}")
1331
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1332
+ else:
1333
+ console.print(f"[blue]Scanning for service listings in:[/blue] {data_path}")
1334
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1335
+
1336
+ async def _publish_listings_async():
1337
+ async with ServiceDataPublisher() as publisher:
1159
1338
  # Handle single file
1160
1339
  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)}")
1340
+ return await publisher.post_service_listing_async(data_path), True
1166
1341
  # Handle directory
1167
1342
  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!")
1343
+ return await publisher.publish_all_listings(data_path), False
1344
+
1345
+ try:
1346
+ result, is_single = asyncio.run(_publish_listings_async())
1347
+
1348
+ if is_single:
1349
+ console.print("[green]✓[/green] Service listing published successfully!")
1350
+ console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1351
+ else:
1352
+ console.print("\n[bold]Publishing Summary:[/bold]")
1353
+ console.print(f" Total found: {result['total']}")
1354
+ console.print(f" [green]✓ Success: {result['success']}[/green]")
1355
+ console.print(f" [red]✗ Failed: {result['failed']}[/red]")
1356
+
1357
+ if result["errors"]:
1358
+ console.print("\n[bold red]Errors:[/bold red]")
1359
+ for error in result["errors"]:
1360
+ console.print(f" [red]✗[/red] {error['file']}")
1361
+ console.print(f" {error['error']}")
1362
+ raise typer.Exit(code=1)
1363
+ else:
1364
+ console.print("\n[green]✓[/green] All service listings published successfully!")
1185
1365
 
1186
1366
  except typer.Exit:
1187
1367
  raise