unitysvc-services 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
unitysvc_services/api.py CHANGED
@@ -62,7 +62,8 @@ class UnitySvcAPI:
62
62
  JSON response as dictionary
63
63
 
64
64
  Raises:
65
- RuntimeError: If curl command fails or returns non-200 status
65
+ httpx.HTTPStatusError: If HTTP status code indicates error (with response details)
66
+ RuntimeError: If curl command fails or times out
66
67
  """
67
68
  url = f"{self.base_url}{endpoint}"
68
69
  if params:
@@ -71,7 +72,8 @@ class UnitySvcAPI:
71
72
  cmd = [
72
73
  "curl",
73
74
  "-s", # Silent mode
74
- "-f", # Fail on HTTP errors
75
+ "-w",
76
+ "\n%{http_code}", # Write status code on new line
75
77
  "-H",
76
78
  f"X-API-Key: {self.api_key}",
77
79
  "-H",
@@ -86,15 +88,34 @@ class UnitySvcAPI:
86
88
  stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30.0)
87
89
 
88
90
  if proc.returncode != 0:
89
- error_msg = stderr.decode().strip() if stderr else "Unknown error"
90
- raise RuntimeError(f"HTTP request failed: {error_msg}")
91
+ error_msg = stderr.decode().strip() if stderr else "Curl command failed"
92
+ raise RuntimeError(f"Curl error: {error_msg}")
91
93
 
94
+ # Parse response: last line is status code, rest is body
92
95
  output = stdout.decode().strip()
93
- return json.loads(output)
96
+ lines = output.split("\n")
97
+ status_code = int(lines[-1])
98
+ body = "\n".join(lines[:-1])
99
+
100
+ # Parse JSON response
101
+ try:
102
+ response_data = json.loads(body) if body else {}
103
+ except json.JSONDecodeError:
104
+ response_data = {"error": body}
105
+
106
+ # Raise exception for non-2xx status codes (mimics httpx behavior)
107
+ if status_code < 200 or status_code >= 300:
108
+ # Create a mock response object to raise HTTPStatusError
109
+ mock_request = httpx.Request("GET", url)
110
+ mock_response = httpx.Response(status_code=status_code, content=body.encode(), request=mock_request)
111
+ raise httpx.HTTPStatusError(f"HTTP {status_code}", request=mock_request, response=mock_response)
112
+
113
+ return response_data
94
114
  except TimeoutError:
95
115
  raise RuntimeError("Request timed out after 30 seconds")
96
- except json.JSONDecodeError as e:
97
- raise RuntimeError(f"Invalid JSON response: {e}")
116
+ except httpx.HTTPStatusError:
117
+ # Re-raise HTTP errors as-is
118
+ raise
98
119
 
99
120
  async def _make_post_request_curl(
100
121
  self, endpoint: str, json_data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
@@ -110,7 +131,8 @@ class UnitySvcAPI:
110
131
  JSON response as dictionary
111
132
 
112
133
  Raises:
113
- RuntimeError: If curl command fails or returns non-200 status
134
+ httpx.HTTPStatusError: If HTTP status code indicates error (with response details)
135
+ RuntimeError: If curl command fails or times out
114
136
  """
115
137
  url = f"{self.base_url}{endpoint}"
116
138
  if params:
@@ -119,7 +141,8 @@ class UnitySvcAPI:
119
141
  cmd = [
120
142
  "curl",
121
143
  "-s", # Silent mode
122
- "-f", # Fail on HTTP errors
144
+ "-w",
145
+ "\n%{http_code}", # Write status code on new line
123
146
  "-X",
124
147
  "POST",
125
148
  "-H",
@@ -142,15 +165,34 @@ class UnitySvcAPI:
142
165
  stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30.0)
143
166
 
144
167
  if proc.returncode != 0:
145
- error_msg = stderr.decode().strip() if stderr else "Unknown error"
146
- raise RuntimeError(f"HTTP request failed: {error_msg}")
168
+ error_msg = stderr.decode().strip() if stderr else "Curl command failed"
169
+ raise RuntimeError(f"Curl error: {error_msg}")
147
170
 
171
+ # Parse response: last line is status code, rest is body
148
172
  output = stdout.decode().strip()
149
- return json.loads(output)
173
+ lines = output.split("\n")
174
+ status_code = int(lines[-1])
175
+ body = "\n".join(lines[:-1])
176
+
177
+ # Parse JSON response
178
+ try:
179
+ response_data = json.loads(body) if body else {}
180
+ except json.JSONDecodeError:
181
+ response_data = {"error": body}
182
+
183
+ # Raise exception for non-2xx status codes (mimics httpx behavior)
184
+ if status_code < 200 or status_code >= 300:
185
+ # Create a mock response object to raise HTTPStatusError
186
+ mock_request = httpx.Request("POST", url)
187
+ mock_response = httpx.Response(status_code=status_code, content=body.encode(), request=mock_request)
188
+ raise httpx.HTTPStatusError(f"HTTP {status_code}", request=mock_request, response=mock_response)
189
+
190
+ return response_data
150
191
  except TimeoutError:
151
192
  raise RuntimeError("Request timed out after 30 seconds")
152
- except json.JSONDecodeError as e:
153
- raise RuntimeError(f"Invalid JSON response: {e}")
193
+ except httpx.HTTPStatusError:
194
+ # Re-raise HTTP errors as-is
195
+ raise
154
196
 
155
197
  async def get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
156
198
  """Make a GET request to the backend API with automatic curl fallback.
@@ -246,8 +288,9 @@ class UnitySvcAPI:
246
288
  raise ValueError(f"Task {task_id} timed out after {timeout}s")
247
289
 
248
290
  # Check task status using get() with automatic curl fallback
291
+ # Use UnitySvcAPI.get to ensure we call the async version, not sync wrapper
249
292
  try:
250
- status = await self.get(f"/tasks/{task_id}")
293
+ status = await UnitySvcAPI.get(self, f"/tasks/{task_id}")
251
294
  except Exception:
252
295
  # Network error while checking status - retry
253
296
  await asyncio.sleep(poll_interval)
unitysvc_services/cli.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import typer
4
4
 
5
- from . import format_data, populate, publisher, query, scaffold, update, validator
5
+ from . import format_data, populate, publisher, query, scaffold, test, update, validator
6
6
  from . import list as list_cmd
7
7
 
8
8
  app = typer.Typer()
@@ -14,6 +14,7 @@ app.add_typer(list_cmd.app, name="list")
14
14
  app.add_typer(query.app, name="query")
15
15
  app.add_typer(publisher.app, name="publish")
16
16
  app.add_typer(update.app, name="update")
17
+ app.add_typer(test.app, name="test")
17
18
 
18
19
  # Register standalone commands at root level
19
20
  app.command("format")(format_data.format_data)
@@ -255,6 +255,18 @@ class Document(BaseModel):
255
255
  default=False,
256
256
  description="Whether document is publicly accessible without authentication",
257
257
  )
258
+ requirements: list[str] | None = Field(
259
+ default=None,
260
+ description="Required packages/modules for running this code example (e.g., ['openai', 'httpx'])",
261
+ )
262
+ expect: str | None = Field(
263
+ default=None,
264
+ max_length=500,
265
+ description=(
266
+ "Expected output substring for code example validation. "
267
+ "If specified, test passes only if stdout contains this string."
268
+ ),
269
+ )
258
270
 
259
271
 
260
272
  class RateLimit(BaseModel):
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
  import typer
10
10
  from rich.console import Console
11
11
 
12
+ from .format_data import format_data
12
13
  from .utils import find_files_by_schema
13
14
 
14
15
  app = typer.Typer(help="Populate services")
@@ -38,6 +39,9 @@ def populate(
38
39
 
39
40
  This command scans provider files for 'services_populator' configuration and executes
40
41
  the specified commands with environment variables from 'provider_access_info'.
42
+
43
+ After successful execution, automatically runs formatting on all generated files to
44
+ ensure they conform to the format specification (equivalent to running 'usvc format').
41
45
  """
42
46
  # Set data directory
43
47
  if data_dir is None:
@@ -178,5 +182,19 @@ def populate(
178
182
  console.print(f" [yellow]⏭️ Skipped: {total_skipped}[/yellow]")
179
183
  console.print(f" [red]✗ Failed: {total_failed}[/red]")
180
184
 
185
+ # Format generated files if any populate scripts executed successfully
186
+ if total_executed > 0 and not dry_run:
187
+ console.print("\n" + "=" * 50)
188
+ console.print("[bold cyan]Formatting generated files...[/bold cyan]")
189
+ console.print("[dim]Running automatic formatting to ensure data conforms to format specification[/dim]\n")
190
+
191
+ try:
192
+ # Run format command on the data directory
193
+ format_data(data_dir)
194
+ console.print("\n[green]✓ Formatting completed successfully[/green]")
195
+ except Exception as e:
196
+ console.print(f"\n[yellow]⚠ Warning: Formatting failed: {e}[/yellow]")
197
+ console.print("[dim]You may want to run 'usvc format' manually to fix formatting issues[/dim]")
198
+
181
199
  if total_failed > 0:
182
200
  raise typer.Exit(code=1)