unitysvc-services 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {unitysvc_services-0.3.0/src/unitysvc_services.egg-info → unitysvc_services-0.3.1}/PKG-INFO +1 -1
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/pyproject.toml +1 -1
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/publisher.py +163 -64
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/query.py +88 -15
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1/src/unitysvc_services.egg-info}/PKG-INFO +1 -1
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/CONTRIBUTING.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/HISTORY.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/LICENSE +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/MANIFEST.in +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/README.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/api-reference.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/cli-reference.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/contributing.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/data-structure.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/development.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/file-schemas.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/getting-started.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/index.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/installation.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/usage.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/docs/workflows.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/setup.cfg +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/__init__.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/cli.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/format_data.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/list.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/__init__.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/base.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/listing_v1.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/provider_v1.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/seller_v1.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/service_v1.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/populate.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/py.typed +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/scaffold.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/update.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/utils.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/validator.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/SOURCES.txt +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/dependency_links.txt +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/entry_points.txt +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/requires.txt +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/top_level.txt +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/__init__.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/README.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider1/README.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider1/provider.toml +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider1/services/service1/code-example.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider1/services/service1/service.toml +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider1/services/service1/svcreseller.toml +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider1/terms-of-service.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider2/README.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider2/provider.json +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider2/services/service2/code-example.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider2/services/service2/service.json +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider2/services/service2/svcreseller.json +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider2/terms-of-service.md +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/seller.json +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/test_utils.py +0 -0
- {unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/test_validator.py +0 -0
@@ -18,7 +18,11 @@ from .validator import DataValidator
|
|
18
18
|
|
19
19
|
|
20
20
|
class ServiceDataPublisher:
|
21
|
-
"""Publishes service data to UnitySVC backend endpoints.
|
21
|
+
"""Publishes service data to UnitySVC backend endpoints.
|
22
|
+
|
23
|
+
Uses httpx by default, with automatic fallback to curl for systems
|
24
|
+
with network restrictions (e.g., macOS with conda Python).
|
25
|
+
"""
|
22
26
|
|
23
27
|
def __init__(self) -> None:
|
24
28
|
self.base_url = os.environ.get("UNITYSVC_BASE_URL")
|
@@ -30,6 +34,7 @@ class ServiceDataPublisher:
|
|
30
34
|
raise ValueError("UNITYSVC_API_KEY environment variable not set")
|
31
35
|
|
32
36
|
self.base_url = self.base_url.rstrip("/")
|
37
|
+
self.use_curl_fallback = False
|
33
38
|
self.async_client = httpx.AsyncClient(
|
34
39
|
headers={
|
35
40
|
"X-API-Key": self.api_key,
|
@@ -97,25 +102,128 @@ class ServiceDataPublisher:
|
|
97
102
|
|
98
103
|
return result
|
99
104
|
|
100
|
-
async def
|
101
|
-
self,
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
105
|
+
async def _make_request_curl_async(
|
106
|
+
self, endpoint: str, method: str = "GET", data: dict[str, Any] | None = None
|
107
|
+
) -> tuple[dict[str, Any], int]:
|
108
|
+
"""Make HTTP request using curl (async version).
|
109
|
+
|
110
|
+
Args:
|
111
|
+
endpoint: API endpoint path
|
112
|
+
method: HTTP method (GET or POST)
|
113
|
+
data: JSON data for POST requests
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Tuple of (JSON response, HTTP status code)
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
RuntimeError: If curl command fails
|
109
120
|
"""
|
110
|
-
|
121
|
+
url = f"{self.base_url}{endpoint}"
|
122
|
+
|
123
|
+
cmd = [
|
124
|
+
"curl",
|
125
|
+
"-s", # Silent mode
|
126
|
+
"-w",
|
127
|
+
"\n%{http_code}", # Write HTTP status code on new line
|
128
|
+
"-X",
|
129
|
+
method,
|
130
|
+
"-H",
|
131
|
+
f"X-API-Key: {self.api_key}",
|
132
|
+
"-H",
|
133
|
+
"Content-Type: application/json",
|
134
|
+
]
|
135
|
+
|
136
|
+
if data:
|
137
|
+
cmd.extend(["-d", json.dumps(data)])
|
138
|
+
|
139
|
+
cmd.append(url)
|
140
|
+
|
141
|
+
try:
|
142
|
+
proc = await asyncio.create_subprocess_exec(
|
143
|
+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
144
|
+
)
|
145
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30.0)
|
146
|
+
|
147
|
+
if proc.returncode != 0:
|
148
|
+
error_msg = stderr.decode().strip() if stderr else "Unknown error"
|
149
|
+
raise RuntimeError(f"curl command failed: {error_msg}")
|
150
|
+
|
151
|
+
output = stdout.decode().strip()
|
152
|
+
lines = output.split("\n")
|
153
|
+
status_code = int(lines[-1])
|
154
|
+
body = "\n".join(lines[:-1])
|
155
|
+
|
156
|
+
response_data = json.loads(body) if body else {}
|
157
|
+
return (response_data, status_code)
|
158
|
+
|
159
|
+
except TimeoutError:
|
160
|
+
raise RuntimeError("Request timed out after 30 seconds")
|
161
|
+
except json.JSONDecodeError as e:
|
162
|
+
raise RuntimeError(f"Invalid JSON response: {e}")
|
163
|
+
|
164
|
+
async def post(self, endpoint: str, data: dict[str, Any], check_status: bool = True) -> tuple[dict[str, Any], int]:
|
165
|
+
"""Make a POST request to the backend API with automatic curl fallback.
|
166
|
+
|
167
|
+
Public utility method for making POST requests. Tries httpx first for performance,
|
168
|
+
automatically falls back to curl if network restrictions are detected (e.g., macOS
|
169
|
+
with conda Python).
|
170
|
+
|
171
|
+
Can be used by other packages that need the same fallback behavior.
|
111
172
|
|
112
173
|
Args:
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
174
|
+
endpoint: API endpoint path (e.g., "/publish/seller")
|
175
|
+
data: JSON data to post
|
176
|
+
check_status: Whether to raise on non-2xx status codes (default: True)
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
Tuple of (JSON response, HTTP status code)
|
180
|
+
|
181
|
+
Raises:
|
182
|
+
RuntimeError: If both httpx and curl fail
|
183
|
+
"""
|
184
|
+
|
185
|
+
# Helper class to simulate httpx response for curl
|
186
|
+
class CurlResponse:
|
187
|
+
def __init__(self, json_data: dict[str, Any], status: int):
|
188
|
+
self._json_data = json_data
|
189
|
+
self.status_code = status
|
190
|
+
self.is_success = 200 <= status < 300
|
191
|
+
self.text = json.dumps(json_data)
|
192
|
+
|
193
|
+
def json(self) -> dict[str, Any]:
|
194
|
+
return self._json_data
|
195
|
+
|
196
|
+
def raise_for_status(self) -> None:
|
197
|
+
if not self.is_success:
|
198
|
+
raise RuntimeError(f"HTTP {self.status_code}: {self.text}")
|
199
|
+
|
200
|
+
# If we already know curl is needed, use it directly
|
201
|
+
if self.use_curl_fallback:
|
202
|
+
response_data, status_code = await self._make_request_curl_async(endpoint, method="POST", data=data)
|
203
|
+
response = CurlResponse(response_data, status_code)
|
204
|
+
else:
|
205
|
+
try:
|
206
|
+
response = await self.async_client.post(f"{self.base_url}{endpoint}", json=data)
|
207
|
+
except (httpx.ConnectError, OSError):
|
208
|
+
# Connection failed - switch to curl fallback and retry
|
209
|
+
self.use_curl_fallback = True
|
210
|
+
response_data, status_code = await self._make_request_curl_async(endpoint, method="POST", data=data)
|
211
|
+
response = CurlResponse(response_data, status_code)
|
212
|
+
|
213
|
+
if check_status:
|
214
|
+
response.raise_for_status()
|
215
|
+
|
216
|
+
return (response.json(), response.status_code)
|
217
|
+
|
218
|
+
async def check_task(self, task_id: str, poll_interval: float = 2.0, timeout: float = 300.0) -> dict[str, Any]:
|
219
|
+
"""Check and wait for task completion.
|
220
|
+
|
221
|
+
Utility function to poll a Celery task until it completes or times out.
|
222
|
+
|
223
|
+
Args:
|
224
|
+
task_id: Celery task ID to poll
|
225
|
+
poll_interval: Seconds between status checks (default: 2.0)
|
226
|
+
timeout: Maximum seconds to wait (default: 300.0)
|
119
227
|
|
120
228
|
Returns:
|
121
229
|
Task result dictionary
|
@@ -130,15 +238,22 @@ class ServiceDataPublisher:
|
|
130
238
|
while True:
|
131
239
|
elapsed = time.time() - start_time
|
132
240
|
if elapsed > timeout:
|
133
|
-
|
134
|
-
raise ValueError(f"Task timed out after {timeout}s for {entity_type} '{entity_name}'{context_msg}")
|
241
|
+
raise ValueError(f"Task {task_id} timed out after {timeout}s")
|
135
242
|
|
136
|
-
# Check task status
|
243
|
+
# Check task status using the get() equivalent for async
|
137
244
|
try:
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
245
|
+
if self.use_curl_fallback:
|
246
|
+
status, _ = await self._make_request_curl_async(f"/tasks/{task_id}", method="GET")
|
247
|
+
else:
|
248
|
+
try:
|
249
|
+
response = await self.async_client.get(f"{self.base_url}/tasks/{task_id}")
|
250
|
+
response.raise_for_status()
|
251
|
+
status = response.json()
|
252
|
+
except (httpx.ConnectError, OSError):
|
253
|
+
# Connection failed - switch to curl fallback
|
254
|
+
self.use_curl_fallback = True
|
255
|
+
status, _ = await self._make_request_curl_async(f"/tasks/{task_id}", method="GET")
|
256
|
+
except (httpx.HTTPError, httpx.NetworkError, httpx.TimeoutException, RuntimeError):
|
142
257
|
# Network error while checking status - retry
|
143
258
|
await asyncio.sleep(poll_interval)
|
144
259
|
continue
|
@@ -147,13 +262,10 @@ class ServiceDataPublisher:
|
|
147
262
|
|
148
263
|
# Check if task is complete
|
149
264
|
if status.get("status") == "completed" or state == "SUCCESS":
|
150
|
-
# Task succeeded
|
151
265
|
return status.get("result", {})
|
152
266
|
elif status.get("status") == "failed" or state == "FAILURE":
|
153
|
-
# Task failed
|
154
267
|
error = status.get("error", "Unknown error")
|
155
|
-
|
156
|
-
raise ValueError(f"Task failed for {entity_type} '{entity_name}'{context_msg}: {error}")
|
268
|
+
raise ValueError(f"Task {task_id} failed: {error}")
|
157
269
|
|
158
270
|
# Still processing - wait and retry
|
159
271
|
await asyncio.sleep(poll_interval)
|
@@ -193,70 +305,57 @@ class ServiceDataPublisher:
|
|
193
305
|
last_exception = None
|
194
306
|
for attempt in range(max_retries):
|
195
307
|
try:
|
196
|
-
|
197
|
-
|
198
|
-
json=data,
|
199
|
-
)
|
308
|
+
# Use the public post() method with automatic curl fallback
|
309
|
+
response_json, status_code = await self.post(endpoint, data, check_status=False)
|
200
310
|
|
201
311
|
# Handle task-based response (HTTP 202)
|
202
|
-
if
|
312
|
+
if status_code == 202:
|
203
313
|
# Backend returns task_id - poll for completion
|
204
|
-
|
205
|
-
task_id = response_data.get("task_id")
|
314
|
+
task_id = response_json.get("task_id")
|
206
315
|
|
207
316
|
if not task_id:
|
208
317
|
context_msg = f" ({context_info})" if context_info else ""
|
209
318
|
raise ValueError(f"No task_id in response for {entity_type} '{entity_name}'{context_msg}")
|
210
319
|
|
211
|
-
# Poll task status until completion
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
320
|
+
# Poll task status until completion using check_task utility
|
321
|
+
try:
|
322
|
+
result = await self.check_task(task_id)
|
323
|
+
return result
|
324
|
+
except ValueError as e:
|
325
|
+
# Add context to task errors
|
326
|
+
context_msg = f" ({context_info})" if context_info else ""
|
327
|
+
raise ValueError(f"Task failed for {entity_type} '{entity_name}'{context_msg}: {e}")
|
219
328
|
|
220
|
-
#
|
221
|
-
if
|
329
|
+
# Check for errors
|
330
|
+
if status_code >= 400:
|
222
331
|
# Don't retry on 4xx errors (client errors) - they won't succeed on retry
|
223
|
-
if 400 <=
|
224
|
-
error_detail = "
|
225
|
-
try:
|
226
|
-
error_json = response.json()
|
227
|
-
error_detail = error_json.get("detail", str(error_json))
|
228
|
-
except Exception:
|
229
|
-
error_detail = response.text or f"HTTP {response.status_code}"
|
230
|
-
|
332
|
+
if 400 <= status_code < 500:
|
333
|
+
error_detail = response_json.get("detail", str(response_json))
|
231
334
|
context_msg = f" ({context_info})" if context_info else ""
|
232
335
|
raise ValueError(
|
233
336
|
f"Failed to publish {entity_type} '{entity_name}'{context_msg}: {error_detail}"
|
234
337
|
)
|
235
338
|
|
236
|
-
# 5xx errors
|
339
|
+
# 5xx errors - retry with exponential backoff
|
237
340
|
if attempt < max_retries - 1:
|
238
341
|
wait_time = 2**attempt # Exponential backoff: 1s, 2s, 4s
|
239
342
|
await asyncio.sleep(wait_time)
|
240
343
|
continue
|
241
344
|
else:
|
242
345
|
# Last attempt failed
|
243
|
-
error_detail = "
|
244
|
-
try:
|
245
|
-
error_json = response.json()
|
246
|
-
error_detail = error_json.get("detail", str(error_json))
|
247
|
-
except Exception:
|
248
|
-
error_detail = response.text or f"HTTP {response.status_code}"
|
249
|
-
|
346
|
+
error_detail = response_json.get("detail", str(response_json))
|
250
347
|
context_msg = f" ({context_info})" if context_info else ""
|
251
348
|
raise ValueError(
|
252
349
|
f"Failed to publish {entity_type} after {max_retries} attempts: "
|
253
350
|
f"'{entity_name}'{context_msg}: {error_detail}"
|
254
351
|
)
|
255
352
|
|
256
|
-
#
|
257
|
-
return
|
353
|
+
# Success response (2xx)
|
354
|
+
return response_json
|
258
355
|
|
259
|
-
except (httpx.NetworkError, httpx.TimeoutException) as e:
|
356
|
+
except (httpx.NetworkError, httpx.TimeoutException, RuntimeError) as e:
|
357
|
+
# Network/connection errors - the post() method should have tried curl fallback
|
358
|
+
# If we're here, both httpx and curl failed
|
260
359
|
last_exception = e
|
261
360
|
if attempt < max_retries - 1:
|
262
361
|
wait_time = 2**attempt # Exponential backoff: 1s, 2s, 4s
|
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
import json
|
4
4
|
import os
|
5
|
+
import subprocess
|
5
6
|
from typing import Any
|
7
|
+
from urllib.parse import urlencode
|
6
8
|
|
7
9
|
import httpx
|
8
10
|
import typer
|
@@ -14,7 +16,11 @@ console = Console()
|
|
14
16
|
|
15
17
|
|
16
18
|
class ServiceDataQuery:
|
17
|
-
"""Query service data from UnitySVC backend endpoints.
|
19
|
+
"""Query service data from UnitySVC backend endpoints.
|
20
|
+
|
21
|
+
Uses httpx by default, with automatic fallback to curl for systems
|
22
|
+
with network restrictions (e.g., macOS with conda Python).
|
23
|
+
"""
|
18
24
|
|
19
25
|
def __init__(self) -> None:
|
20
26
|
"""Initialize query client from environment variables.
|
@@ -31,6 +37,7 @@ class ServiceDataQuery:
|
|
31
37
|
raise ValueError("UNITYSVC_API_KEY environment variable not set")
|
32
38
|
|
33
39
|
self.base_url = self.base_url.rstrip("/")
|
40
|
+
self.use_curl_fallback = False
|
34
41
|
self.client = httpx.Client(
|
35
42
|
headers={
|
36
43
|
"X-API-Key": self.api_key,
|
@@ -39,6 +46,80 @@ class ServiceDataQuery:
|
|
39
46
|
timeout=30.0,
|
40
47
|
)
|
41
48
|
|
49
|
+
def _make_request_curl(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
50
|
+
"""Make HTTP GET request using curl fallback.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
endpoint: API endpoint path (e.g., "/publish/sellers")
|
54
|
+
params: Query parameters
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
JSON response as dictionary
|
58
|
+
|
59
|
+
Raises:
|
60
|
+
RuntimeError: If curl command fails or returns non-200 status
|
61
|
+
"""
|
62
|
+
url = f"{self.base_url}{endpoint}"
|
63
|
+
if params:
|
64
|
+
url = f"{url}?{urlencode(params)}"
|
65
|
+
|
66
|
+
cmd = [
|
67
|
+
"curl",
|
68
|
+
"-s", # Silent mode
|
69
|
+
"-f", # Fail on HTTP errors
|
70
|
+
"-H",
|
71
|
+
f"X-API-Key: {self.api_key}",
|
72
|
+
"-H",
|
73
|
+
"Accept: application/json",
|
74
|
+
url,
|
75
|
+
]
|
76
|
+
|
77
|
+
try:
|
78
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=30)
|
79
|
+
return json.loads(result.stdout)
|
80
|
+
except subprocess.CalledProcessError as e:
|
81
|
+
error_msg = e.stderr.strip() if e.stderr else "Unknown error"
|
82
|
+
raise RuntimeError(f"HTTP request failed: {error_msg}")
|
83
|
+
except subprocess.TimeoutExpired:
|
84
|
+
raise RuntimeError("Request timed out after 30 seconds")
|
85
|
+
except json.JSONDecodeError as e:
|
86
|
+
raise RuntimeError(f"Invalid JSON response: {e}")
|
87
|
+
|
88
|
+
def get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
89
|
+
"""Make a GET request to the backend API with automatic curl fallback.
|
90
|
+
|
91
|
+
Public utility method for making GET requests. Tries httpx first for performance,
|
92
|
+
automatically falls back to curl if network restrictions are detected (e.g., macOS
|
93
|
+
with conda Python).
|
94
|
+
|
95
|
+
Can be used by subclasses and other packages (e.g., unitysvc_admin) that need
|
96
|
+
the same fallback behavior.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
endpoint: API endpoint path (e.g., "/publish/sellers", "/admin/documents")
|
100
|
+
params: Query parameters
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
JSON response as dictionary
|
104
|
+
|
105
|
+
Raises:
|
106
|
+
RuntimeError: If both httpx and curl fail
|
107
|
+
"""
|
108
|
+
# If we already know curl is needed, use it directly
|
109
|
+
if self.use_curl_fallback:
|
110
|
+
return self._make_request_curl(endpoint, params)
|
111
|
+
|
112
|
+
# Try httpx first
|
113
|
+
try:
|
114
|
+
response = self.client.get(f"{self.base_url}{endpoint}", params=params)
|
115
|
+
response.raise_for_status()
|
116
|
+
return response.json()
|
117
|
+
except (httpx.ConnectError, OSError) as e:
|
118
|
+
# Connection failed - likely network restrictions
|
119
|
+
# Fall back to curl and remember this for future requests
|
120
|
+
self.use_curl_fallback = True
|
121
|
+
return self._make_request_curl(endpoint, params)
|
122
|
+
|
42
123
|
def list_service_offerings(self, skip: int = 0, limit: int = 100) -> list[dict[str, Any]]:
|
43
124
|
"""List all service offerings from the backend.
|
44
125
|
|
@@ -46,9 +127,7 @@ class ServiceDataQuery:
|
|
46
127
|
skip: Number of records to skip (for pagination)
|
47
128
|
limit: Maximum number of records to return
|
48
129
|
"""
|
49
|
-
|
50
|
-
response.raise_for_status()
|
51
|
-
result = response.json()
|
130
|
+
result = self.get("/publish/offerings", {"skip": skip, "limit": limit})
|
52
131
|
return result.get("data", result) if isinstance(result, dict) else result
|
53
132
|
|
54
133
|
def list_service_listings(self, skip: int = 0, limit: int = 100) -> list[dict[str, Any]]:
|
@@ -58,9 +137,7 @@ class ServiceDataQuery:
|
|
58
137
|
skip: Number of records to skip (for pagination)
|
59
138
|
limit: Maximum number of records to return
|
60
139
|
"""
|
61
|
-
|
62
|
-
response.raise_for_status()
|
63
|
-
result = response.json()
|
140
|
+
result = self.get("/publish/listings", {"skip": skip, "limit": limit})
|
64
141
|
return result.get("data", result) if isinstance(result, dict) else result
|
65
142
|
|
66
143
|
def list_providers(self, skip: int = 0, limit: int = 100) -> list[dict[str, Any]]:
|
@@ -70,9 +147,7 @@ class ServiceDataQuery:
|
|
70
147
|
skip: Number of records to skip (for pagination)
|
71
148
|
limit: Maximum number of records to return
|
72
149
|
"""
|
73
|
-
|
74
|
-
response.raise_for_status()
|
75
|
-
result = response.json()
|
150
|
+
result = self.get("/publish/providers", {"skip": skip, "limit": limit})
|
76
151
|
return result.get("data", result) if isinstance(result, dict) else result
|
77
152
|
|
78
153
|
def list_sellers(self, skip: int = 0, limit: int = 100) -> list[dict[str, Any]]:
|
@@ -82,14 +157,12 @@ class ServiceDataQuery:
|
|
82
157
|
skip: Number of records to skip (for pagination)
|
83
158
|
limit: Maximum number of records to return
|
84
159
|
"""
|
85
|
-
|
86
|
-
response.raise_for_status()
|
87
|
-
result = response.json()
|
160
|
+
result = self.get("/publish/sellers", {"skip": skip, "limit": limit})
|
88
161
|
return result.get("data", result) if isinstance(result, dict) else result
|
89
162
|
|
90
163
|
def close(self):
|
91
|
-
"""Close the HTTP client."""
|
92
|
-
|
164
|
+
"""Close the HTTP client (no-op for curl-based implementation)."""
|
165
|
+
pass
|
93
166
|
|
94
167
|
def __enter__(self):
|
95
168
|
"""Context manager entry."""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/__init__.py
RENAMED
File without changes
|
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/listing_v1.py
RENAMED
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/provider_v1.py
RENAMED
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/seller_v1.py
RENAMED
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services/models/service_v1.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/SOURCES.txt
RENAMED
File without changes
|
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/entry_points.txt
RENAMED
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/requires.txt
RENAMED
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/src/unitysvc_services.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider1/provider.toml
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider1/terms-of-service.md
RENAMED
File without changes
|
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider2/provider.json
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{unitysvc_services-0.3.0 → unitysvc_services-0.3.1}/tests/example_data/provider2/terms-of-service.md
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|