unitysvc-services 0.2.7__py3-none-any.whl → 0.3.0__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.
@@ -97,6 +97,67 @@ class ServiceDataPublisher:
97
97
 
98
98
  return result
99
99
 
100
+ async def _poll_task_status(
101
+ self,
102
+ task_id: str,
103
+ entity_type: str,
104
+ entity_name: str,
105
+ context_info: str = "",
106
+ poll_interval: float = 2.0,
107
+ timeout: float = 300.0,
108
+ ) -> dict[str, Any]:
109
+ """
110
+ Poll task status until completion or timeout.
111
+
112
+ Args:
113
+ task_id: Celery task ID
114
+ entity_type: Type of entity being published (for error messages)
115
+ entity_name: Name of the entity being published (for error messages)
116
+ context_info: Additional context for error messages
117
+ poll_interval: Seconds between status checks
118
+ timeout: Maximum seconds to wait
119
+
120
+ Returns:
121
+ Task result dictionary
122
+
123
+ Raises:
124
+ ValueError: If task fails or times out
125
+ """
126
+ import time
127
+
128
+ start_time = time.time()
129
+
130
+ while True:
131
+ elapsed = time.time() - start_time
132
+ if elapsed > timeout:
133
+ context_msg = f" ({context_info})" if context_info else ""
134
+ raise ValueError(f"Task timed out after {timeout}s for {entity_type} '{entity_name}'{context_msg}")
135
+
136
+ # Check task status
137
+ try:
138
+ response = await self.async_client.get(f"{self.base_url}/tasks/{task_id}")
139
+ response.raise_for_status()
140
+ status = response.json()
141
+ except (httpx.HTTPError, httpx.NetworkError, httpx.TimeoutException):
142
+ # Network error while checking status - retry
143
+ await asyncio.sleep(poll_interval)
144
+ continue
145
+
146
+ state = status.get("state", "PENDING")
147
+
148
+ # Check if task is complete
149
+ if status.get("status") == "completed" or state == "SUCCESS":
150
+ # Task succeeded
151
+ return status.get("result", {})
152
+ elif status.get("status") == "failed" or state == "FAILURE":
153
+ # Task failed
154
+ error = status.get("error", "Unknown error")
155
+ context_msg = f" ({context_info})" if context_info else ""
156
+ raise ValueError(f"Task failed for {entity_type} '{entity_name}'{context_msg}: {error}")
157
+
158
+ # Still processing - wait and retry
159
+ await asyncio.sleep(poll_interval)
160
+
100
161
  async def _post_with_retry(
101
162
  self,
102
163
  endpoint: str,
@@ -107,7 +168,13 @@ class ServiceDataPublisher:
107
168
  max_retries: int = 3,
108
169
  ) -> dict[str, Any]:
109
170
  """
110
- Generic retry wrapper for posting data to backend API.
171
+ Generic retry wrapper for posting data to backend API with task polling.
172
+
173
+ The backend now returns HTTP 202 with a task_id. This method:
174
+ 1. Submits the publish request
175
+ 2. Gets the task_id from the response
176
+ 3. Polls /tasks/{task_id} until completion
177
+ 4. Returns the final result
111
178
 
112
179
  Args:
113
180
  endpoint: API endpoint path (e.g., "/publish/listing")
@@ -131,6 +198,25 @@ class ServiceDataPublisher:
131
198
  json=data,
132
199
  )
133
200
 
201
+ # Handle task-based response (HTTP 202)
202
+ if response.status_code == 202:
203
+ # Backend returns task_id - poll for completion
204
+ response_data = response.json()
205
+ task_id = response_data.get("task_id")
206
+
207
+ if not task_id:
208
+ context_msg = f" ({context_info})" if context_info else ""
209
+ raise ValueError(f"No task_id in response for {entity_type} '{entity_name}'{context_msg}")
210
+
211
+ # Poll task status until completion
212
+ result = await self._poll_task_status(
213
+ task_id=task_id,
214
+ entity_type=entity_type,
215
+ entity_name=entity_name,
216
+ context_info=context_info,
217
+ )
218
+ return result
219
+
134
220
  # Provide detailed error information if request fails
135
221
  if not response.is_success:
136
222
  # Don't retry on 4xx errors (client errors) - they won't succeed on retry
@@ -167,6 +253,7 @@ class ServiceDataPublisher:
167
253
  f"'{entity_name}'{context_msg}: {error_detail}"
168
254
  )
169
255
 
256
+ # For non-202 success responses, return the body
170
257
  return response.json()
171
258
 
172
259
  except (httpx.NetworkError, httpx.TimeoutException) as e:
@@ -478,7 +565,7 @@ class ServiceDataPublisher:
478
565
  console.print(f" [red]✗[/red] Failed to publish offering: [cyan]{offering_name}[/cyan] - {str(e)}")
479
566
  return (offering_file, e)
480
567
 
481
- def publish_all_offerings(self, data_dir: Path) -> dict[str, Any]:
568
+ async def publish_all_offerings(self, data_dir: Path) -> dict[str, Any]:
482
569
  """
483
570
  Publish all service offerings found in a directory tree concurrently.
484
571
 
@@ -510,14 +597,10 @@ class ServiceDataPublisher:
510
597
  console = Console()
511
598
 
512
599
  # Run all offering publications concurrently with rate limiting
513
- async def _publish_all() -> list[tuple[Path, dict[str, Any] | Exception]]:
514
- # Create semaphore to limit concurrent requests
515
- semaphore = asyncio.Semaphore(self.max_concurrent_requests)
516
- tasks = [self._publish_offering_task(offering_file, console, semaphore) for offering_file in offering_files]
517
- return await asyncio.gather(*tasks)
518
-
519
- # Execute async tasks
520
- task_results = asyncio.run(_publish_all())
600
+ # Create semaphore to limit concurrent requests
601
+ semaphore = asyncio.Semaphore(self.max_concurrent_requests)
602
+ tasks = [self._publish_offering_task(offering_file, console, semaphore) for offering_file in offering_files]
603
+ task_results = await asyncio.gather(*tasks)
521
604
 
522
605
  # Process results
523
606
  for offering_file, result in task_results:
@@ -565,7 +648,7 @@ class ServiceDataPublisher:
565
648
  console.print(f" [red]✗[/red] Failed to publish listing: [cyan]{listing_file}[/cyan] - {str(e)}")
566
649
  return (listing_file, e)
567
650
 
568
- def publish_all_listings(self, data_dir: Path) -> dict[str, Any]:
651
+ async def publish_all_listings(self, data_dir: Path) -> dict[str, Any]:
569
652
  """
570
653
  Publish all service listings found in a directory tree concurrently.
571
654
 
@@ -597,14 +680,10 @@ class ServiceDataPublisher:
597
680
  console = Console()
598
681
 
599
682
  # Run all listing publications concurrently with rate limiting
600
- async def _publish_all() -> list[tuple[Path, dict[str, Any] | Exception]]:
601
- # Create semaphore to limit concurrent requests
602
- semaphore = asyncio.Semaphore(self.max_concurrent_requests)
603
- tasks = [self._publish_listing_task(listing_file, console, semaphore) for listing_file in listing_files]
604
- return await asyncio.gather(*tasks)
605
-
606
- # Execute async tasks
607
- task_results = asyncio.run(_publish_all())
683
+ # Create semaphore to limit concurrent requests
684
+ semaphore = asyncio.Semaphore(self.max_concurrent_requests)
685
+ tasks = [self._publish_listing_task(listing_file, console, semaphore) for listing_file in listing_files]
686
+ task_results = await asyncio.gather(*tasks)
608
687
 
609
688
  # Process results
610
689
  for listing_file, result in task_results:
@@ -647,7 +726,7 @@ class ServiceDataPublisher:
647
726
  console.print(f" [red]✗[/red] Failed to publish provider: [cyan]{provider_name}[/cyan] - {str(e)}")
648
727
  return (provider_file, e)
649
728
 
650
- def publish_all_providers(self, data_dir: Path) -> dict[str, Any]:
729
+ async def publish_all_providers(self, data_dir: Path) -> dict[str, Any]:
651
730
  """
652
731
  Publish all providers found in a directory tree concurrently.
653
732
 
@@ -667,14 +746,10 @@ class ServiceDataPublisher:
667
746
  console = Console()
668
747
 
669
748
  # Run all provider publications concurrently with rate limiting
670
- async def _publish_all() -> list[tuple[Path, dict[str, Any] | Exception]]:
671
- # Create semaphore to limit concurrent requests
672
- semaphore = asyncio.Semaphore(self.max_concurrent_requests)
673
- tasks = [self._publish_provider_task(provider_file, console, semaphore) for provider_file in provider_files]
674
- return await asyncio.gather(*tasks)
675
-
676
- # Execute async tasks
677
- task_results = asyncio.run(_publish_all())
749
+ # Create semaphore to limit concurrent requests
750
+ semaphore = asyncio.Semaphore(self.max_concurrent_requests)
751
+ tasks = [self._publish_provider_task(provider_file, console, semaphore) for provider_file in provider_files]
752
+ task_results = await asyncio.gather(*tasks)
678
753
 
679
754
  # Process results
680
755
  for provider_file, result in task_results:
@@ -717,7 +792,7 @@ class ServiceDataPublisher:
717
792
  console.print(f" [red]✗[/red] Failed to publish seller: [cyan]{seller_name}[/cyan] - {str(e)}")
718
793
  return (seller_file, e)
719
794
 
720
- def publish_all_sellers(self, data_dir: Path) -> dict[str, Any]:
795
+ async def publish_all_sellers(self, data_dir: Path) -> dict[str, Any]:
721
796
  """
722
797
  Publish all sellers found in a directory tree concurrently.
723
798
 
@@ -737,14 +812,10 @@ class ServiceDataPublisher:
737
812
  console = Console()
738
813
 
739
814
  # Run all seller publications concurrently with rate limiting
740
- async def _publish_all() -> list[tuple[Path, dict[str, Any] | Exception]]:
741
- # Create semaphore to limit concurrent requests
742
- semaphore = asyncio.Semaphore(self.max_concurrent_requests)
743
- tasks = [self._publish_seller_task(seller_file, console, semaphore) for seller_file in seller_files]
744
- return await asyncio.gather(*tasks)
745
-
746
- # Execute async tasks
747
- task_results = asyncio.run(_publish_all())
815
+ # Create semaphore to limit concurrent requests
816
+ semaphore = asyncio.Semaphore(self.max_concurrent_requests)
817
+ tasks = [self._publish_seller_task(seller_file, console, semaphore) for seller_file in seller_files]
818
+ task_results = await asyncio.gather(*tasks)
748
819
 
749
820
  # Process results
750
821
  for seller_file, result in task_results:
@@ -756,7 +827,7 @@ class ServiceDataPublisher:
756
827
 
757
828
  return results
758
829
 
759
- def publish_all_models(self, data_dir: Path) -> dict[str, Any]:
830
+ async def publish_all_models(self, data_dir: Path) -> dict[str, Any]:
760
831
  """
761
832
  Publish all data types in the correct order.
762
833
 
@@ -788,7 +859,7 @@ class ServiceDataPublisher:
788
859
 
789
860
  for data_type, publish_method in publish_order:
790
861
  try:
791
- results = publish_method(data_dir)
862
+ results = await publish_method(data_dir)
792
863
  all_results[data_type] = results
793
864
  all_results["total_success"] += results["success"]
794
865
  all_results["total_failed"] += results["failed"]
@@ -805,9 +876,25 @@ class ServiceDataPublisher:
805
876
 
806
877
  return all_results
807
878
 
879
+ async def aclose(self):
880
+ """Close HTTP client asynchronously."""
881
+ await self.async_client.aclose()
882
+
808
883
  def close(self):
809
- """Close HTTP client."""
810
- asyncio.run(self.async_client.aclose())
884
+ """Close HTTP client synchronously (best effort)."""
885
+ try:
886
+ # Try to close if there's an event loop running
887
+ loop = asyncio.get_event_loop()
888
+ if loop.is_running():
889
+ # Can't close synchronously if loop is running
890
+ # The client will be garbage collected
891
+ return
892
+ else:
893
+ # Loop exists but not running, we can use it
894
+ loop.run_until_complete(self.async_client.aclose())
895
+ except RuntimeError:
896
+ # No event loop or loop is closed - just let it be garbage collected
897
+ pass
811
898
 
812
899
  def __enter__(self):
813
900
  """Context manager entry."""
@@ -870,8 +957,8 @@ def publish_callback(
870
957
 
871
958
  try:
872
959
  with ServiceDataPublisher() as publisher:
873
- # Call the publish_all_models method
874
- all_results = publisher.publish_all_models(data_path)
960
+ # Call the publish_all_models method (now async)
961
+ all_results = asyncio.run(publisher.publish_all_models(data_path))
875
962
 
876
963
  # Display results for each data type
877
964
  data_type_display_names = {
@@ -965,7 +1052,7 @@ def publish_providers(
965
1052
  else:
966
1053
  console.print(f"[blue]Scanning for providers in:[/blue] {data_path}")
967
1054
  console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
968
- results = publisher.publish_all_providers(data_path)
1055
+ results = asyncio.run(publisher.publish_all_providers(data_path))
969
1056
 
970
1057
  # Display summary
971
1058
  console.print("\n[bold]Publishing Summary:[/bold]")
@@ -1024,7 +1111,7 @@ def publish_sellers(
1024
1111
  else:
1025
1112
  console.print(f"[blue]Scanning for sellers in:[/blue] {data_path}")
1026
1113
  console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1027
- results = publisher.publish_all_sellers(data_path)
1114
+ results = asyncio.run(publisher.publish_all_sellers(data_path))
1028
1115
 
1029
1116
  console.print("\n[bold]Publishing Summary:[/bold]")
1030
1117
  console.print(f" Total found: {results['total']}")
@@ -1080,8 +1167,8 @@ def publish_offerings(
1080
1167
  # Handle directory
1081
1168
  else:
1082
1169
  console.print(f"[blue]Scanning for service offerings in:[/blue] {data_path}")
1083
- console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1084
- results = publisher.publish_all_offerings(data_path)
1170
+ console.print(f"[blue]Backend URL:[/bold blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1171
+ results = asyncio.run(publisher.publish_all_offerings(data_path))
1085
1172
 
1086
1173
  console.print("\n[bold]Publishing Summary:[/bold]")
1087
1174
  console.print(f" Total found: {results['total']}")
@@ -1139,7 +1226,7 @@ def publish_listings(
1139
1226
  else:
1140
1227
  console.print(f"[blue]Scanning for service listings in:[/blue] {data_path}")
1141
1228
  console.print(f"[blue]Backend URL:[/blue] {os.getenv('UNITYSVC_BASE_URL', 'N/A')}\n")
1142
- results = publisher.publish_all_listings(data_path)
1229
+ results = asyncio.run(publisher.publish_all_listings(data_path))
1143
1230
 
1144
1231
  console.print("\n[bold]Publishing Summary:[/bold]")
1145
1232
  console.print(f" Total found: {results['total']}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-services
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: SDK for digital service providers on UnitySVC
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -3,7 +3,7 @@ unitysvc_services/cli.py,sha256=OK0IZyAckxP15jRWU_W49hl3t7XcNRtd8BoDMyRKqNM,682
3
3
  unitysvc_services/format_data.py,sha256=Jl9Vj3fRX852fHSUa5DzO-oiFQwuQHC3WMCDNIlo1Lc,5460
4
4
  unitysvc_services/list.py,sha256=QDp9BByaoeFeJxXJN9RQ-jU99mH9Guq9ampfXCbpZmI,7033
5
5
  unitysvc_services/populate.py,sha256=zkcjIy8BWuQSO7JwiRNHKgGoxQvc3ujluUQdYixdBvY,6626
6
- unitysvc_services/publisher.py,sha256=xQqIajb3JRDX9Qg6N94hqtT_mc0NBYbUYKKMm4zsKyE,48686
6
+ unitysvc_services/publisher.py,sha256=sNqGbLQ3QulNCGzyRjBg6ks-I2nTVgip4vGFV4XCUto,52285
7
7
  unitysvc_services/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  unitysvc_services/query.py,sha256=x2VUnfva21-mVd-JgtChajNBgXG1AQJ6c3umCw2FNWU,24089
9
9
  unitysvc_services/scaffold.py,sha256=Y73IX8vskImxSvxDgR0mvEFuAMYnBKfttn3bjcz3jmQ,40331
@@ -16,9 +16,9 @@ unitysvc_services/models/listing_v1.py,sha256=PPb9hIdWQp80AWKLxFXYBDcWXzNcDrO4v6
16
16
  unitysvc_services/models/provider_v1.py,sha256=76EK1i0hVtdx_awb00-ZMtSj4Oc9Zp4xZ-DeXmG3iTY,2701
17
17
  unitysvc_services/models/seller_v1.py,sha256=oll2ZZBPBDX8wslHrbsCKf_jIqHNte2VEj5RJ9bawR4,3520
18
18
  unitysvc_services/models/service_v1.py,sha256=Xpk-K-95M1LRqYM8nNJcll8t-lsW9Xdi2_bVbYNs8-M,3019
19
- unitysvc_services-0.2.7.dist-info/licenses/LICENSE,sha256=_p8V6A8OMPu2HIztn3O01v0-urZFwk0Dd3Yk_PTIlL8,1065
20
- unitysvc_services-0.2.7.dist-info/METADATA,sha256=8KqcRPrJwkYb9zCgT3rytQTOrmIBGm3hHctp-7VYM3A,6628
21
- unitysvc_services-0.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- unitysvc_services-0.2.7.dist-info/entry_points.txt,sha256=-vodnbPmo7QQmFu8jdG6sCyGRVM727w9Nhwp4Vwau_k,64
23
- unitysvc_services-0.2.7.dist-info/top_level.txt,sha256=GIotQj-Ro2ruR7eupM1r58PWqIHTAq647ORL7E2kneo,18
24
- unitysvc_services-0.2.7.dist-info/RECORD,,
19
+ unitysvc_services-0.3.0.dist-info/licenses/LICENSE,sha256=_p8V6A8OMPu2HIztn3O01v0-urZFwk0Dd3Yk_PTIlL8,1065
20
+ unitysvc_services-0.3.0.dist-info/METADATA,sha256=QPZzgXqlKCPdsJCuWnbxxcXe7sfk-SBTxCJbq2npEc8,6628
21
+ unitysvc_services-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ unitysvc_services-0.3.0.dist-info/entry_points.txt,sha256=-vodnbPmo7QQmFu8jdG6sCyGRVM727w9Nhwp4Vwau_k,64
23
+ unitysvc_services-0.3.0.dist-info/top_level.txt,sha256=GIotQj-Ro2ruR7eupM1r58PWqIHTAq647ORL7E2kneo,18
24
+ unitysvc_services-0.3.0.dist-info/RECORD,,