unitysvc-services 0.1.9__py3-none-any.whl → 0.1.11__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.

Potentially problematic release.


This version of unitysvc-services might be problematic. Click here for more details.

@@ -11,6 +11,9 @@ from typing import Any
11
11
  import httpx
12
12
  import typer
13
13
  from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ import unitysvc_services
14
17
 
15
18
  from .api import UnitySvcAPI
16
19
  from .models.base import ProviderStatusEnum, SellerStatusEnum
@@ -187,7 +190,7 @@ class ServiceDataPublisher(UnitySvcAPI):
187
190
  return result
188
191
 
189
192
  async def post( # type: ignore[override]
190
- self, endpoint: str, data: dict[str, Any], check_status: bool = True
193
+ self, endpoint: str, data: dict[str, Any], check_status: bool = True, dryrun: bool = False
191
194
  ) -> tuple[dict[str, Any], int]:
192
195
  """Make a POST request to the backend API with automatic curl fallback.
193
196
 
@@ -198,6 +201,7 @@ class ServiceDataPublisher(UnitySvcAPI):
198
201
  endpoint: API endpoint path (e.g., "/publish/seller")
199
202
  data: JSON data to post
200
203
  check_status: Whether to raise on non-2xx status codes (default: True)
204
+ dryrun: If True, adds dryrun=true as query parameter
201
205
 
202
206
  Returns:
203
207
  Tuple of (JSON response, HTTP status code)
@@ -205,16 +209,19 @@ class ServiceDataPublisher(UnitySvcAPI):
205
209
  Raises:
206
210
  RuntimeError: If both httpx and curl fail
207
211
  """
212
+ # Build query parameters
213
+ params = {"dryrun": "true"} if dryrun else None
214
+
208
215
  # Use base class client (self.client from UnitySvcQuery) with automatic curl fallback
209
216
  # If we already know curl is needed, use it directly
210
217
  if self.use_curl_fallback:
211
218
  # Use base class curl fallback method
212
- response_json = await super().post(endpoint, json_data=data)
219
+ response_json = await super().post(endpoint, json_data=data, params=params)
213
220
  # Curl POST doesn't return status code separately, assume 2xx if no exception
214
221
  status_code = 200
215
222
  else:
216
223
  try:
217
- response = await self.client.post(f"{self.base_url}{endpoint}", json=data)
224
+ response = await self.client.post(f"{self.base_url}{endpoint}", json=data, params=params)
218
225
  status_code = response.status_code
219
226
 
220
227
  if check_status:
@@ -224,7 +231,7 @@ class ServiceDataPublisher(UnitySvcAPI):
224
231
  except (httpx.ConnectError, OSError):
225
232
  # Connection failed - switch to curl fallback and retry
226
233
  self.use_curl_fallback = True
227
- response_json = await super().post(endpoint, json_data=data)
234
+ response_json = await super().post(endpoint, json_data=data, params=params)
228
235
  status_code = 200 # Assume success if curl didn't raise
229
236
 
230
237
  return (response_json, status_code)
@@ -237,6 +244,7 @@ class ServiceDataPublisher(UnitySvcAPI):
237
244
  entity_name: str,
238
245
  context_info: str = "",
239
246
  max_retries: int = 3,
247
+ dryrun: bool = False,
240
248
  ) -> dict[str, Any]:
241
249
  """
242
250
  Generic retry wrapper for posting data to backend API with task polling.
@@ -254,6 +262,7 @@ class ServiceDataPublisher(UnitySvcAPI):
254
262
  entity_name: Name of the entity being published (for error messages)
255
263
  context_info: Additional context for error messages (e.g., provider, service info)
256
264
  max_retries: Maximum number of retry attempts
265
+ dryrun: If True, runs in dry run mode (no actual changes)
257
266
 
258
267
  Returns:
259
268
  Response JSON from successful API call
@@ -265,7 +274,7 @@ class ServiceDataPublisher(UnitySvcAPI):
265
274
  for attempt in range(max_retries):
266
275
  try:
267
276
  # Use the public post() method with automatic curl fallback
268
- response_json, status_code = await self.post(endpoint, data, check_status=False)
277
+ response_json, status_code = await self.post(endpoint, data, check_status=False, dryrun=dryrun)
269
278
 
270
279
  # Handle task-based response (HTTP 202)
271
280
  if status_code == 202:
@@ -330,7 +339,9 @@ class ServiceDataPublisher(UnitySvcAPI):
330
339
  raise last_exception
331
340
  raise ValueError("Unexpected error in retry logic")
332
341
 
333
- async def post_service_listing_async(self, listing_file: Path, max_retries: int = 3) -> dict[str, Any]:
342
+ async def post_service_listing_async(
343
+ self, listing_file: Path, max_retries: int = 3, dryrun: bool = False
344
+ ) -> dict[str, Any]:
334
345
  """Async version of post_service_listing for concurrent publishing with retry logic."""
335
346
  # Load the listing data file
336
347
  data = self.load_data_file(listing_file)
@@ -466,6 +477,7 @@ class ServiceDataPublisher(UnitySvcAPI):
466
477
  entity_name=data.get("name", "unknown"),
467
478
  context_info=context_info,
468
479
  max_retries=max_retries,
480
+ dryrun=dryrun,
469
481
  )
470
482
 
471
483
  # Add local metadata to result for display purposes
@@ -475,7 +487,9 @@ class ServiceDataPublisher(UnitySvcAPI):
475
487
 
476
488
  return result
477
489
 
478
- async def post_service_offering_async(self, data_file: Path, max_retries: int = 3) -> dict[str, Any]:
490
+ async def post_service_offering_async(
491
+ self, data_file: Path, max_retries: int = 3, dryrun: bool = False
492
+ ) -> dict[str, Any]:
479
493
  """Async version of post_service_offering for concurrent publishing with retry logic."""
480
494
  # Load the data file
481
495
  data = self.load_data_file(data_file)
@@ -536,6 +550,7 @@ class ServiceDataPublisher(UnitySvcAPI):
536
550
  entity_name=data.get("name", "unknown"),
537
551
  context_info=context_info,
538
552
  max_retries=max_retries,
553
+ dryrun=dryrun,
539
554
  )
540
555
 
541
556
  # Add local metadata to result for display purposes
@@ -543,7 +558,7 @@ class ServiceDataPublisher(UnitySvcAPI):
543
558
 
544
559
  return result
545
560
 
546
- async def post_provider_async(self, data_file: Path, max_retries: int = 3) -> dict[str, Any]:
561
+ async def post_provider_async(self, data_file: Path, max_retries: int = 3, dryrun: bool = False) -> dict[str, Any]:
547
562
  """Async version of post_provider for concurrent publishing with retry logic."""
548
563
  # Load the data file
549
564
  data = self.load_data_file(data_file)
@@ -581,9 +596,10 @@ class ServiceDataPublisher(UnitySvcAPI):
581
596
  entity_type="provider",
582
597
  entity_name=data.get("name", "unknown"),
583
598
  max_retries=max_retries,
599
+ dryrun=dryrun,
584
600
  )
585
601
 
586
- async def post_seller_async(self, data_file: Path, max_retries: int = 3) -> dict[str, Any]:
602
+ async def post_seller_async(self, data_file: Path, max_retries: int = 3, dryrun: bool = False) -> dict[str, Any]:
587
603
  """Async version of post_seller for concurrent publishing with retry logic."""
588
604
  # Load the data file
589
605
  data = self.load_data_file(data_file)
@@ -619,6 +635,7 @@ class ServiceDataPublisher(UnitySvcAPI):
619
635
  entity_type="seller",
620
636
  entity_name=data.get("name", "unknown"),
621
637
  max_retries=max_retries,
638
+ dryrun=dryrun,
622
639
  )
623
640
 
624
641
  def find_offering_files(self, data_dir: Path) -> list[Path]:
@@ -641,8 +658,20 @@ class ServiceDataPublisher(UnitySvcAPI):
641
658
  files = find_files_by_schema(data_dir, "seller_v1")
642
659
  return sorted([f[0] for f in files])
643
660
 
661
+ @staticmethod
662
+ def _get_status_display(status: str) -> tuple[str, str]:
663
+ """Get color and symbol for status display."""
664
+ status_map = {
665
+ "created": ("[green]+[/green]", "green"),
666
+ "updated": ("[blue]~[/blue]", "blue"),
667
+ "unchanged": ("[dim]=[/dim]", "dim"),
668
+ "create": ("[yellow]?[/yellow]", "yellow"), # Dryrun: would be created
669
+ "update": ("[cyan]?[/cyan]", "cyan"), # Dryrun: would be updated
670
+ }
671
+ return status_map.get(status, ("[green]✓[/green]", "green"))
672
+
644
673
  async def _publish_offering_task(
645
- self, offering_file: Path, console: Console, semaphore: asyncio.Semaphore
674
+ self, offering_file: Path, console: Console, semaphore: asyncio.Semaphore, dryrun: bool = False
646
675
  ) -> tuple[Path, dict[str, Any] | Exception]:
647
676
  """
648
677
  Async task to publish a single offering with concurrency control.
@@ -656,7 +685,7 @@ class ServiceDataPublisher(UnitySvcAPI):
656
685
  offering_name = data.get("name", offering_file.stem)
657
686
 
658
687
  # Publish the offering
659
- result = await self.post_service_offering_async(offering_file)
688
+ result = await self.post_service_offering_async(offering_file, dryrun=dryrun)
660
689
 
661
690
  # Print complete statement after publication
662
691
  if result.get("skipped"):
@@ -664,8 +693,10 @@ class ServiceDataPublisher(UnitySvcAPI):
664
693
  console.print(f" [yellow]⊘[/yellow] Skipped offering: [cyan]{offering_name}[/cyan] - {reason}")
665
694
  else:
666
695
  provider_name = result.get("provider_name")
696
+ status = result.get("status", "created")
697
+ symbol, color = self._get_status_display(status)
667
698
  console.print(
668
- f" [green][/green] Published offering: [cyan]{offering_name}[/cyan] "
699
+ f" {symbol} [{color}]{status.capitalize()}[/{color}] offering: [cyan]{offering_name}[/cyan] "
669
700
  f"(provider: {provider_name})"
670
701
  )
671
702
 
@@ -676,15 +707,20 @@ class ServiceDataPublisher(UnitySvcAPI):
676
707
  console.print(f" [red]✗[/red] Failed to publish offering: [cyan]{offering_name}[/cyan] - {str(e)}")
677
708
  return (offering_file, e)
678
709
 
679
- async def publish_all_offerings(self, data_dir: Path) -> dict[str, Any]:
710
+ async def publish_all_offerings(self, data_dir: Path, dryrun: bool = False) -> dict[str, Any]:
680
711
  """
681
712
  Publish all service offerings found in a directory tree concurrently.
682
713
 
683
714
  Validates data consistency before publishing.
684
715
  Returns a summary of successes and failures.
716
+
717
+ Args:
718
+ data_dir: Directory to search for offering files
719
+ dryrun: If True, runs in dry run mode (no actual changes)
685
720
  """
686
721
  # Validate all service directories first
687
- validator = DataValidator(data_dir, data_dir.parent / "schema")
722
+ schema_dir = Path(unitysvc_services.__file__).parent / "schema"
723
+ validator = DataValidator(data_dir, schema_dir)
688
724
  validation_errors = validator.validate_all_service_directories(data_dir)
689
725
  if validation_errors:
690
726
  return {
@@ -699,6 +735,9 @@ class ServiceDataPublisher(UnitySvcAPI):
699
735
  "total": len(offering_files),
700
736
  "success": 0,
701
737
  "failed": 0,
738
+ "created": 0,
739
+ "updated": 0,
740
+ "unchanged": 0,
702
741
  "errors": [],
703
742
  }
704
743
 
@@ -710,7 +749,10 @@ class ServiceDataPublisher(UnitySvcAPI):
710
749
  # Run all offering publications concurrently with rate limiting
711
750
  # Create semaphore to limit concurrent requests
712
751
  semaphore = asyncio.Semaphore(self.max_concurrent_requests)
713
- tasks = [self._publish_offering_task(offering_file, console, semaphore) for offering_file in offering_files]
752
+ tasks = [
753
+ self._publish_offering_task(offering_file, console, semaphore, dryrun=dryrun)
754
+ for offering_file in offering_files
755
+ ]
714
756
  task_results = await asyncio.gather(*tasks)
715
757
 
716
758
  # Process results
@@ -720,11 +762,19 @@ class ServiceDataPublisher(UnitySvcAPI):
720
762
  results["errors"].append({"file": str(offering_file), "error": str(result)})
721
763
  else:
722
764
  results["success"] += 1
765
+ # Track status counts (handle both normal and dryrun statuses)
766
+ status = result.get("status", "created")
767
+ if status in ("created", "create"): # "create" is dryrun mode
768
+ results["created"] += 1
769
+ elif status in ("updated", "update"): # "update" is dryrun mode
770
+ results["updated"] += 1
771
+ elif status == "unchanged":
772
+ results["unchanged"] += 1
723
773
 
724
774
  return results
725
775
 
726
776
  async def _publish_listing_task(
727
- self, listing_file: Path, console: Console, semaphore: asyncio.Semaphore
777
+ self, listing_file: Path, console: Console, semaphore: asyncio.Semaphore, dryrun: bool = False
728
778
  ) -> tuple[Path, dict[str, Any] | Exception]:
729
779
  """
730
780
  Async task to publish a single listing with concurrency control.
@@ -738,7 +788,7 @@ class ServiceDataPublisher(UnitySvcAPI):
738
788
  listing_name = data.get("name", listing_file.stem)
739
789
 
740
790
  # Publish the listing
741
- result = await self.post_service_listing_async(listing_file)
791
+ result = await self.post_service_listing_async(listing_file, dryrun=dryrun)
742
792
 
743
793
  # Print complete statement after publication
744
794
  if result.get("skipped"):
@@ -747,8 +797,10 @@ class ServiceDataPublisher(UnitySvcAPI):
747
797
  else:
748
798
  service_name = result.get("service_name")
749
799
  provider_name = result.get("provider_name")
800
+ status = result.get("status", "created")
801
+ symbol, color = self._get_status_display(status)
750
802
  console.print(
751
- f" [green][/green] Published listing: [cyan]{listing_name}[/cyan] "
803
+ f" {symbol} [{color}]{status.capitalize()}[/{color}] listing: [cyan]{listing_name}[/cyan] "
752
804
  f"(service: {service_name}, provider: {provider_name})"
753
805
  )
754
806
 
@@ -759,7 +811,7 @@ class ServiceDataPublisher(UnitySvcAPI):
759
811
  console.print(f" [red]✗[/red] Failed to publish listing: [cyan]{listing_file}[/cyan] - {str(e)}")
760
812
  return (listing_file, e)
761
813
 
762
- async def publish_all_listings(self, data_dir: Path) -> dict[str, Any]:
814
+ async def publish_all_listings(self, data_dir: Path, dryrun: bool = False) -> dict[str, Any]:
763
815
  """
764
816
  Publish all service listings found in a directory tree concurrently.
765
817
 
@@ -767,7 +819,8 @@ class ServiceDataPublisher(UnitySvcAPI):
767
819
  Returns a summary of successes and failures.
768
820
  """
769
821
  # Validate all service directories first
770
- validator = DataValidator(data_dir, data_dir.parent / "schema")
822
+ schema_dir = Path(unitysvc_services.__file__).parent / "schema"
823
+ validator = DataValidator(data_dir, schema_dir)
771
824
  validation_errors = validator.validate_all_service_directories(data_dir)
772
825
  if validation_errors:
773
826
  return {
@@ -782,6 +835,9 @@ class ServiceDataPublisher(UnitySvcAPI):
782
835
  "total": len(listing_files),
783
836
  "success": 0,
784
837
  "failed": 0,
838
+ "created": 0,
839
+ "updated": 0,
840
+ "unchanged": 0,
785
841
  "errors": [],
786
842
  }
787
843
 
@@ -793,7 +849,10 @@ class ServiceDataPublisher(UnitySvcAPI):
793
849
  # Run all listing publications concurrently with rate limiting
794
850
  # Create semaphore to limit concurrent requests
795
851
  semaphore = asyncio.Semaphore(self.max_concurrent_requests)
796
- tasks = [self._publish_listing_task(listing_file, console, semaphore) for listing_file in listing_files]
852
+ tasks = [
853
+ self._publish_listing_task(listing_file, console, semaphore, dryrun=dryrun)
854
+ for listing_file in listing_files
855
+ ]
797
856
  task_results = await asyncio.gather(*tasks)
798
857
 
799
858
  # Process results
@@ -803,11 +862,19 @@ class ServiceDataPublisher(UnitySvcAPI):
803
862
  results["errors"].append({"file": str(listing_file), "error": str(result)})
804
863
  else:
805
864
  results["success"] += 1
865
+ # Track status counts (handle both normal and dryrun statuses)
866
+ status = result.get("status", "created")
867
+ if status in ("created", "create"): # "create" is dryrun mode
868
+ results["created"] += 1
869
+ elif status in ("updated", "update"): # "update" is dryrun mode
870
+ results["updated"] += 1
871
+ elif status == "unchanged":
872
+ results["unchanged"] += 1
806
873
 
807
874
  return results
808
875
 
809
876
  async def _publish_provider_task(
810
- self, provider_file: Path, console: Console, semaphore: asyncio.Semaphore
877
+ self, provider_file: Path, console: Console, semaphore: asyncio.Semaphore, dryrun: bool = False
811
878
  ) -> tuple[Path, dict[str, Any] | Exception]:
812
879
  """
813
880
  Async task to publish a single provider with concurrency control.
@@ -821,14 +888,18 @@ class ServiceDataPublisher(UnitySvcAPI):
821
888
  provider_name = data.get("name", provider_file.stem)
822
889
 
823
890
  # Publish the provider
824
- result = await self.post_provider_async(provider_file)
891
+ result = await self.post_provider_async(provider_file, dryrun=dryrun)
825
892
 
826
893
  # Print complete statement after publication
827
894
  if result.get("skipped"):
828
895
  reason = result.get("reason", "unknown")
829
896
  console.print(f" [yellow]⊘[/yellow] Skipped provider: [cyan]{provider_name}[/cyan] - {reason}")
830
897
  else:
831
- console.print(f" [green]✓[/green] Published provider: [cyan]{provider_name}[/cyan]")
898
+ status = result.get("status", "created")
899
+ symbol, color = self._get_status_display(status)
900
+ console.print(
901
+ f" {symbol} [{color}]{status.capitalize()}[/{color}] provider: [cyan]{provider_name}[/cyan]"
902
+ )
832
903
 
833
904
  return (provider_file, result)
834
905
  except Exception as e:
@@ -837,7 +908,7 @@ class ServiceDataPublisher(UnitySvcAPI):
837
908
  console.print(f" [red]✗[/red] Failed to publish provider: [cyan]{provider_name}[/cyan] - {str(e)}")
838
909
  return (provider_file, e)
839
910
 
840
- async def publish_all_providers(self, data_dir: Path) -> dict[str, Any]:
911
+ async def publish_all_providers(self, data_dir: Path, dryrun: bool = False) -> dict[str, Any]:
841
912
  """
842
913
  Publish all providers found in a directory tree concurrently.
843
914
 
@@ -848,6 +919,9 @@ class ServiceDataPublisher(UnitySvcAPI):
848
919
  "total": len(provider_files),
849
920
  "success": 0,
850
921
  "failed": 0,
922
+ "created": 0,
923
+ "updated": 0,
924
+ "unchanged": 0,
851
925
  "errors": [],
852
926
  }
853
927
 
@@ -859,7 +933,10 @@ class ServiceDataPublisher(UnitySvcAPI):
859
933
  # Run all provider publications concurrently with rate limiting
860
934
  # Create semaphore to limit concurrent requests
861
935
  semaphore = asyncio.Semaphore(self.max_concurrent_requests)
862
- tasks = [self._publish_provider_task(provider_file, console, semaphore) for provider_file in provider_files]
936
+ tasks = [
937
+ self._publish_provider_task(provider_file, console, semaphore, dryrun=dryrun)
938
+ for provider_file in provider_files
939
+ ]
863
940
  task_results = await asyncio.gather(*tasks)
864
941
 
865
942
  # Process results
@@ -869,11 +946,19 @@ class ServiceDataPublisher(UnitySvcAPI):
869
946
  results["errors"].append({"file": str(provider_file), "error": str(result)})
870
947
  else:
871
948
  results["success"] += 1
949
+ # Track status counts (handle both normal and dryrun statuses)
950
+ status = result.get("status", "created")
951
+ if status in ("created", "create"): # "create" is dryrun mode
952
+ results["created"] += 1
953
+ elif status in ("updated", "update"): # "update" is dryrun mode
954
+ results["updated"] += 1
955
+ elif status == "unchanged":
956
+ results["unchanged"] += 1
872
957
 
873
958
  return results
874
959
 
875
960
  async def _publish_seller_task(
876
- self, seller_file: Path, console: Console, semaphore: asyncio.Semaphore
961
+ self, seller_file: Path, console: Console, semaphore: asyncio.Semaphore, dryrun: bool = False
877
962
  ) -> tuple[Path, dict[str, Any] | Exception]:
878
963
  """
879
964
  Async task to publish a single seller with concurrency control.
@@ -887,14 +972,18 @@ class ServiceDataPublisher(UnitySvcAPI):
887
972
  seller_name = data.get("name", seller_file.stem)
888
973
 
889
974
  # Publish the seller
890
- result = await self.post_seller_async(seller_file)
975
+ result = await self.post_seller_async(seller_file, dryrun=dryrun)
891
976
 
892
977
  # Print complete statement after publication
893
978
  if result.get("skipped"):
894
979
  reason = result.get("reason", "unknown")
895
980
  console.print(f" [yellow]⊘[/yellow] Skipped seller: [cyan]{seller_name}[/cyan] - {reason}")
896
981
  else:
897
- console.print(f" [green]✓[/green] Published seller: [cyan]{seller_name}[/cyan]")
982
+ status = result.get("status", "created")
983
+ symbol, color = self._get_status_display(status)
984
+ console.print(
985
+ f" {symbol} [{color}]{status.capitalize()}[/{color}] seller: [cyan]{seller_name}[/cyan]"
986
+ )
898
987
 
899
988
  return (seller_file, result)
900
989
  except Exception as e:
@@ -903,7 +992,7 @@ class ServiceDataPublisher(UnitySvcAPI):
903
992
  console.print(f" [red]✗[/red] Failed to publish seller: [cyan]{seller_name}[/cyan] - {str(e)}")
904
993
  return (seller_file, e)
905
994
 
906
- async def publish_all_sellers(self, data_dir: Path) -> dict[str, Any]:
995
+ async def publish_all_sellers(self, data_dir: Path, dryrun: bool = False) -> dict[str, Any]:
907
996
  """
908
997
  Publish all sellers found in a directory tree concurrently.
909
998
 
@@ -914,6 +1003,9 @@ class ServiceDataPublisher(UnitySvcAPI):
914
1003
  "total": len(seller_files),
915
1004
  "success": 0,
916
1005
  "failed": 0,
1006
+ "created": 0,
1007
+ "updated": 0,
1008
+ "unchanged": 0,
917
1009
  "errors": [],
918
1010
  }
919
1011
 
@@ -925,7 +1017,9 @@ class ServiceDataPublisher(UnitySvcAPI):
925
1017
  # Run all seller publications concurrently with rate limiting
926
1018
  # Create semaphore to limit concurrent requests
927
1019
  semaphore = asyncio.Semaphore(self.max_concurrent_requests)
928
- tasks = [self._publish_seller_task(seller_file, console, semaphore) for seller_file in seller_files]
1020
+ tasks = [
1021
+ self._publish_seller_task(seller_file, console, semaphore, dryrun=dryrun) for seller_file in seller_files
1022
+ ]
929
1023
  task_results = await asyncio.gather(*tasks)
930
1024
 
931
1025
  # Process results
@@ -935,10 +1029,18 @@ class ServiceDataPublisher(UnitySvcAPI):
935
1029
  results["errors"].append({"file": str(seller_file), "error": str(result)})
936
1030
  else:
937
1031
  results["success"] += 1
1032
+ # Track status counts (handle both normal and dryrun statuses)
1033
+ status = result.get("status", "created")
1034
+ if status in ("created", "create"): # "create" is dryrun mode
1035
+ results["created"] += 1
1036
+ elif status in ("updated", "update"): # "update" is dryrun mode
1037
+ results["updated"] += 1
1038
+ elif status == "unchanged":
1039
+ results["unchanged"] += 1
938
1040
 
939
1041
  return results
940
1042
 
941
- async def publish_all_models(self, data_dir: Path) -> dict[str, Any]:
1043
+ async def publish_all_models(self, data_dir: Path, dryrun: bool = False) -> dict[str, Any]:
942
1044
  """
943
1045
  Publish all data types in the correct order.
944
1046
 
@@ -958,6 +1060,9 @@ class ServiceDataPublisher(UnitySvcAPI):
958
1060
  "total_success": 0,
959
1061
  "total_failed": 0,
960
1062
  "total_found": 0,
1063
+ "total_created": 0,
1064
+ "total_updated": 0,
1065
+ "total_unchanged": 0,
961
1066
  }
962
1067
 
963
1068
  # Publish in order: sellers -> providers -> offerings -> listings
@@ -970,11 +1075,14 @@ class ServiceDataPublisher(UnitySvcAPI):
970
1075
 
971
1076
  for data_type, publish_method in publish_order:
972
1077
  try:
973
- results = await publish_method(data_dir)
1078
+ results = await publish_method(data_dir, dryrun=dryrun)
974
1079
  all_results[data_type] = results
975
1080
  all_results["total_success"] += results["success"]
976
1081
  all_results["total_failed"] += results["failed"]
977
1082
  all_results["total_found"] += results["total"]
1083
+ all_results["total_created"] += results.get("created", 0)
1084
+ all_results["total_updated"] += results.get("updated", 0)
1085
+ all_results["total_unchanged"] += results.get("unchanged", 0)
978
1086
  except Exception as e:
979
1087
  # If a publish method fails catastrophically, record the error
980
1088
  all_results[data_type] = {
@@ -1002,6 +1110,11 @@ def publish_callback(
1002
1110
  "-d",
1003
1111
  help="Path to data directory (default: current directory)",
1004
1112
  ),
1113
+ dryrun: bool = typer.Option(
1114
+ False,
1115
+ "--dryrun",
1116
+ help="Run in dry run mode (no actual changes)",
1117
+ ),
1005
1118
  ):
1006
1119
  """
1007
1120
  Publish data to backend.
@@ -1040,48 +1153,77 @@ def publish_callback(
1040
1153
 
1041
1154
  async def _publish_all_async():
1042
1155
  async with ServiceDataPublisher() as publisher:
1043
- return await publisher.publish_all_models(data_path)
1156
+ return await publisher.publish_all_models(data_path, dryrun=dryrun)
1044
1157
 
1045
1158
  try:
1046
1159
  all_results = asyncio.run(_publish_all_async())
1047
1160
 
1048
- # Display results for each data type
1161
+ # Create summary table
1162
+ console.print("\n[bold cyan]Publishing Summary[/bold cyan]")
1163
+
1164
+ table = Table(show_header=True, header_style="bold cyan", border_style="cyan")
1165
+ table.add_column("Type", style="cyan", no_wrap=True)
1166
+ table.add_column("Found", justify="right")
1167
+ table.add_column("Success", justify="right", style="green")
1168
+ table.add_column("Failed", justify="right", style="red")
1169
+ table.add_column("Created", justify="right", style="green")
1170
+ table.add_column("Updated", justify="right", style="blue")
1171
+ table.add_column("Unchanged", justify="right", style="dim")
1172
+
1049
1173
  data_type_display_names = {
1050
1174
  "sellers": "Sellers",
1051
1175
  "providers": "Providers",
1052
- "offerings": "Service Offerings",
1053
- "listings": "Service Listings",
1176
+ "offerings": "Offerings",
1177
+ "listings": "Listings",
1054
1178
  }
1055
1179
 
1180
+ # Add rows for each data type
1056
1181
  for data_type in ["sellers", "providers", "offerings", "listings"]:
1057
1182
  display_name = data_type_display_names[data_type]
1058
1183
  results = all_results[data_type]
1059
1184
 
1060
- console.print(f"\n[bold cyan]{'=' * 60}[/bold cyan]")
1061
- console.print(f"[bold cyan]{display_name}[/bold cyan]")
1062
- console.print(f"[bold cyan]{'=' * 60}[/bold cyan]\n")
1185
+ table.add_row(
1186
+ display_name,
1187
+ str(results["total"]),
1188
+ str(results["success"]),
1189
+ str(results["failed"]) if results["failed"] > 0 else "",
1190
+ str(results.get("created", 0)) if results.get("created", 0) > 0 else "",
1191
+ str(results.get("updated", 0)) if results.get("updated", 0) > 0 else "",
1192
+ str(results.get("unchanged", 0)) if results.get("unchanged", 0) > 0 else "",
1193
+ )
1063
1194
 
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']}")
1195
+ # Add separator and total row
1196
+ table.add_section()
1197
+ table.add_row(
1198
+ "[bold]Total[/bold]",
1199
+ f"[bold]{all_results['total_found']}[/bold]",
1200
+ f"[bold green]{all_results['total_success']}[/bold green]",
1201
+ f"[bold red]{all_results['total_failed']}[/bold red]" if all_results["total_failed"] > 0 else "",
1202
+ f"[bold green]{all_results['total_created']}[/bold green]" if all_results["total_created"] > 0 else "",
1203
+ f"[bold blue]{all_results['total_updated']}[/bold blue]" if all_results["total_updated"] > 0 else "",
1204
+ f"[bold]{all_results['total_unchanged']}[/bold]" if all_results["total_unchanged"] > 0 else "",
1205
+ )
1206
+
1207
+ console.print(table)
1208
+
1209
+ # Display errors if any
1210
+ has_errors = False
1211
+ for data_type in ["sellers", "providers", "offerings", "listings"]:
1212
+ display_name = data_type_display_names[data_type]
1213
+ results = all_results[data_type]
1067
1214
 
1068
- # Display errors if any
1069
1215
  if results.get("errors"):
1070
- console.print(f"\n[bold red]Errors in {display_name}:[/bold red]")
1216
+ if not has_errors:
1217
+ console.print("\n[bold red]Errors:[/bold red]")
1218
+ has_errors = True
1219
+
1220
+ console.print(f"\n [bold red]{display_name}:[/bold red]")
1071
1221
  for error in results["errors"]:
1072
1222
  # Check if this is a skipped item
1073
1223
  if isinstance(error, dict) and error.get("error", "").startswith("skipped"):
1074
1224
  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']}")
1225
+ console.print(f" [red]✗[/red] {error.get('file', 'unknown')}")
1226
+ console.print(f" {error.get('error', 'unknown error')}")
1085
1227
 
1086
1228
  if all_results["total_failed"] > 0:
1087
1229
  console.print(
@@ -1090,10 +1232,16 @@ def publish_callback(
1090
1232
  )
1091
1233
  raise typer.Exit(code=1)
1092
1234
  else:
1093
- console.print(
1094
- "\n[green]✓[/green] All data published successfully!",
1095
- style="bold green",
1096
- )
1235
+ if dryrun:
1236
+ console.print(
1237
+ "\n[green]✓[/green] Dry run completed successfully - no changes made!",
1238
+ style="bold green",
1239
+ )
1240
+ else:
1241
+ console.print(
1242
+ "\n[green]✓[/green] All data published successfully!",
1243
+ style="bold green",
1244
+ )
1097
1245
 
1098
1246
  except typer.Exit:
1099
1247
  raise
@@ -1110,6 +1258,11 @@ def publish_providers(
1110
1258
  "-d",
1111
1259
  help="Path to provider file or directory (default: current directory)",
1112
1260
  ),
1261
+ dryrun: bool = typer.Option(
1262
+ False,
1263
+ "--dryrun",
1264
+ help="Run in dry run mode (no actual changes)",
1265
+ ),
1113
1266
  ):
1114
1267
  """Publish provider(s) from a file or directory."""
1115
1268
 
@@ -1136,10 +1289,10 @@ def publish_providers(
1136
1289
  async with ServiceDataPublisher() as publisher:
1137
1290
  # Handle single file
1138
1291
  if data_path.is_file():
1139
- return await publisher.post_provider_async(data_path), True
1292
+ return await publisher.post_provider_async(data_path, dryrun=dryrun), True
1140
1293
  # Handle directory
1141
1294
  else:
1142
- return await publisher.publish_all_providers(data_path), False
1295
+ return await publisher.publish_all_providers(data_path, dryrun=dryrun), False
1143
1296
 
1144
1297
  try:
1145
1298
  result, is_single = asyncio.run(_publish_providers_async())
@@ -1149,10 +1302,27 @@ def publish_providers(
1149
1302
  console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1150
1303
  else:
1151
1304
  # 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']}")
1305
+ console.print("\n[bold cyan]Publishing Summary[/bold cyan]")
1306
+ table = Table(show_header=True, header_style="bold cyan", border_style="cyan")
1307
+ table.add_column("Type", style="cyan")
1308
+ table.add_column("Found", justify="right")
1309
+ table.add_column("Success", justify="right")
1310
+ table.add_column("Failed", justify="right")
1311
+ table.add_column("Created", justify="right")
1312
+ table.add_column("Updated", justify="right")
1313
+ table.add_column("Unchanged", justify="right")
1314
+
1315
+ table.add_row(
1316
+ "Providers",
1317
+ str(result["total"]),
1318
+ f"[green]{result['success']}[/green]",
1319
+ f"[red]{result['failed']}[/red]" if result["failed"] > 0 else "",
1320
+ f"[green]{result['created']}[/green]" if result["created"] > 0 else "",
1321
+ f"[blue]{result['updated']}[/blue]" if result["updated"] > 0 else "",
1322
+ f"[dim]{result['unchanged']}[/dim]" if result["unchanged"] > 0 else "",
1323
+ )
1324
+
1325
+ console.print(table)
1156
1326
 
1157
1327
  # Display errors if any
1158
1328
  if result["errors"]:
@@ -1163,6 +1333,11 @@ def publish_providers(
1163
1333
 
1164
1334
  if result["failed"] > 0:
1165
1335
  raise typer.Exit(code=1)
1336
+ else:
1337
+ if dryrun:
1338
+ console.print("\n[green]✓[/green] Dry run completed successfully - no changes made!")
1339
+ else:
1340
+ console.print("\n[green]✓[/green] All providers published successfully!")
1166
1341
 
1167
1342
  except typer.Exit:
1168
1343
  raise
@@ -1179,6 +1354,11 @@ def publish_sellers(
1179
1354
  "-d",
1180
1355
  help="Path to seller file or directory (default: current directory)",
1181
1356
  ),
1357
+ dryrun: bool = typer.Option(
1358
+ False,
1359
+ "--dryrun",
1360
+ help="Run in dry run mode (no actual changes)",
1361
+ ),
1182
1362
  ):
1183
1363
  """Publish seller(s) from a file or directory."""
1184
1364
  # Set data path
@@ -1204,10 +1384,10 @@ def publish_sellers(
1204
1384
  async with ServiceDataPublisher() as publisher:
1205
1385
  # Handle single file
1206
1386
  if data_path.is_file():
1207
- return await publisher.post_seller_async(data_path), True
1387
+ return await publisher.post_seller_async(data_path, dryrun=dryrun), True
1208
1388
  # Handle directory
1209
1389
  else:
1210
- return await publisher.publish_all_sellers(data_path), False
1390
+ return await publisher.publish_all_sellers(data_path, dryrun=dryrun), False
1211
1391
 
1212
1392
  try:
1213
1393
  result, is_single = asyncio.run(_publish_sellers_async())
@@ -1216,10 +1396,27 @@ def publish_sellers(
1216
1396
  console.print("[green]✓[/green] Seller published successfully!")
1217
1397
  console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1218
1398
  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]")
1399
+ console.print("\n[bold cyan]Publishing Summary[/bold cyan]")
1400
+ table = Table(show_header=True, header_style="bold cyan", border_style="cyan")
1401
+ table.add_column("Type", style="cyan")
1402
+ table.add_column("Found", justify="right")
1403
+ table.add_column("Success", justify="right")
1404
+ table.add_column("Failed", justify="right")
1405
+ table.add_column("Created", justify="right")
1406
+ table.add_column("Updated", justify="right")
1407
+ table.add_column("Unchanged", justify="right")
1408
+
1409
+ table.add_row(
1410
+ "Sellers",
1411
+ str(result["total"]),
1412
+ f"[green]{result['success']}[/green]",
1413
+ f"[red]{result['failed']}[/red]" if result["failed"] > 0 else "",
1414
+ f"[green]{result['created']}[/green]" if result["created"] > 0 else "",
1415
+ f"[blue]{result['updated']}[/blue]" if result["updated"] > 0 else "",
1416
+ f"[dim]{result['unchanged']}[/dim]" if result["unchanged"] > 0 else "",
1417
+ )
1418
+
1419
+ console.print(table)
1223
1420
 
1224
1421
  if result["errors"]:
1225
1422
  console.print("\n[bold red]Errors:[/bold red]")
@@ -1228,7 +1425,10 @@ def publish_sellers(
1228
1425
  console.print(f" {error['error']}")
1229
1426
  raise typer.Exit(code=1)
1230
1427
  else:
1231
- console.print("\n[green]✓[/green] All sellers published successfully!")
1428
+ if dryrun:
1429
+ console.print("\n[green]✓[/green] Dry run completed successfully - no changes made!")
1430
+ else:
1431
+ console.print("\n[green]✓[/green] All sellers published successfully!")
1232
1432
 
1233
1433
  except typer.Exit:
1234
1434
  raise
@@ -1245,6 +1445,11 @@ def publish_offerings(
1245
1445
  "-d",
1246
1446
  help="Path to service offering file or directory (default: current directory)",
1247
1447
  ),
1448
+ dryrun: bool = typer.Option(
1449
+ False,
1450
+ "--dryrun",
1451
+ help="Run in dry run mode (no actual changes)",
1452
+ ),
1248
1453
  ):
1249
1454
  """Publish service offering(s) from a file or directory."""
1250
1455
  # Set data path
@@ -1264,16 +1469,16 @@ def publish_offerings(
1264
1469
  console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1265
1470
  else:
1266
1471
  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")
1472
+ console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1268
1473
 
1269
1474
  async def _publish_offerings_async():
1270
1475
  async with ServiceDataPublisher() as publisher:
1271
1476
  # Handle single file
1272
1477
  if data_path.is_file():
1273
- return await publisher.post_service_offering_async(data_path), True
1478
+ return await publisher.post_service_offering_async(data_path, dryrun=dryrun), True
1274
1479
  # Handle directory
1275
1480
  else:
1276
- return await publisher.publish_all_offerings(data_path), False
1481
+ return await publisher.publish_all_offerings(data_path, dryrun=dryrun), False
1277
1482
 
1278
1483
  try:
1279
1484
  result, is_single = asyncio.run(_publish_offerings_async())
@@ -1282,10 +1487,27 @@ def publish_offerings(
1282
1487
  console.print("[green]✓[/green] Service offering published successfully!")
1283
1488
  console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1284
1489
  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]")
1490
+ console.print("\n[bold cyan]Publishing Summary[/bold cyan]")
1491
+ table = Table(show_header=True, header_style="bold cyan", border_style="cyan")
1492
+ table.add_column("Type", style="cyan")
1493
+ table.add_column("Found", justify="right")
1494
+ table.add_column("Success", justify="right")
1495
+ table.add_column("Failed", justify="right")
1496
+ table.add_column("Created", justify="right")
1497
+ table.add_column("Updated", justify="right")
1498
+ table.add_column("Unchanged", justify="right")
1499
+
1500
+ table.add_row(
1501
+ "Offerings",
1502
+ str(result["total"]),
1503
+ f"[green]{result['success']}[/green]",
1504
+ f"[red]{result['failed']}[/red]" if result["failed"] > 0 else "",
1505
+ f"[green]{result['created']}[/green]" if result["created"] > 0 else "",
1506
+ f"[blue]{result['updated']}[/blue]" if result["updated"] > 0 else "",
1507
+ f"[dim]{result['unchanged']}[/dim]" if result["unchanged"] > 0 else "",
1508
+ )
1509
+
1510
+ console.print(table)
1289
1511
 
1290
1512
  if result["errors"]:
1291
1513
  console.print("\n[bold red]Errors:[/bold red]")
@@ -1294,7 +1516,10 @@ def publish_offerings(
1294
1516
  console.print(f" {error['error']}")
1295
1517
  raise typer.Exit(code=1)
1296
1518
  else:
1297
- console.print("\n[green]✓[/green] All service offerings published successfully!")
1519
+ if dryrun:
1520
+ console.print("\n[green]✓[/green] Dry run completed successfully - no changes made!")
1521
+ else:
1522
+ console.print("\n[green]✓[/green] All service offerings published successfully!")
1298
1523
 
1299
1524
  except typer.Exit:
1300
1525
  raise
@@ -1311,6 +1536,11 @@ def publish_listings(
1311
1536
  "-d",
1312
1537
  help="Path to service listing file or directory (default: current directory)",
1313
1538
  ),
1539
+ dryrun: bool = typer.Option(
1540
+ False,
1541
+ "--dryrun",
1542
+ help="Run in dry run mode (no actual changes)",
1543
+ ),
1314
1544
  ):
1315
1545
  """Publish service listing(s) from a file or directory."""
1316
1546
 
@@ -1337,10 +1567,10 @@ def publish_listings(
1337
1567
  async with ServiceDataPublisher() as publisher:
1338
1568
  # Handle single file
1339
1569
  if data_path.is_file():
1340
- return await publisher.post_service_listing_async(data_path), True
1570
+ return await publisher.post_service_listing_async(data_path, dryrun=dryrun), True
1341
1571
  # Handle directory
1342
1572
  else:
1343
- return await publisher.publish_all_listings(data_path), False
1573
+ return await publisher.publish_all_listings(data_path, dryrun=dryrun), False
1344
1574
 
1345
1575
  try:
1346
1576
  result, is_single = asyncio.run(_publish_listings_async())
@@ -1349,10 +1579,27 @@ def publish_listings(
1349
1579
  console.print("[green]✓[/green] Service listing published successfully!")
1350
1580
  console.print(f"[cyan]Response:[/cyan] {json.dumps(result, indent=2)}")
1351
1581
  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]")
1582
+ console.print("\n[bold cyan]Publishing Summary[/bold cyan]")
1583
+ table = Table(show_header=True, header_style="bold cyan", border_style="cyan")
1584
+ table.add_column("Type", style="cyan")
1585
+ table.add_column("Found", justify="right")
1586
+ table.add_column("Success", justify="right")
1587
+ table.add_column("Failed", justify="right")
1588
+ table.add_column("Created", justify="right")
1589
+ table.add_column("Updated", justify="right")
1590
+ table.add_column("Unchanged", justify="right")
1591
+
1592
+ table.add_row(
1593
+ "Listings",
1594
+ str(result["total"]),
1595
+ f"[green]{result['success']}[/green]",
1596
+ f"[red]{result['failed']}[/red]" if result["failed"] > 0 else "",
1597
+ f"[green]{result['created']}[/green]" if result["created"] > 0 else "",
1598
+ f"[blue]{result['updated']}[/blue]" if result["updated"] > 0 else "",
1599
+ f"[dim]{result['unchanged']}[/dim]" if result["unchanged"] > 0 else "",
1600
+ )
1601
+
1602
+ console.print(table)
1356
1603
 
1357
1604
  if result["errors"]:
1358
1605
  console.print("\n[bold red]Errors:[/bold red]")
@@ -1361,7 +1608,10 @@ def publish_listings(
1361
1608
  console.print(f" {error['error']}")
1362
1609
  raise typer.Exit(code=1)
1363
1610
  else:
1364
- console.print("\n[green]✓[/green] All service listings published successfully!")
1611
+ if dryrun:
1612
+ console.print("\n[green]✓[/green] Dry run completed successfully - no changes made!")
1613
+ else:
1614
+ console.print("\n[green]✓[/green] All service listings published successfully!")
1365
1615
 
1366
1616
  except typer.Exit:
1367
1617
  raise