unitysvc-services 0.1.1__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 +321 -0
- unitysvc_services/cli.py +2 -1
- unitysvc_services/format_data.py +2 -7
- unitysvc_services/list.py +14 -43
- unitysvc_services/models/base.py +169 -102
- unitysvc_services/models/listing_v1.py +25 -9
- unitysvc_services/models/provider_v1.py +19 -8
- unitysvc_services/models/seller_v1.py +10 -8
- unitysvc_services/models/service_v1.py +8 -1
- unitysvc_services/populate.py +20 -6
- unitysvc_services/publisher.py +897 -462
- unitysvc_services/py.typed +0 -0
- unitysvc_services/query.py +577 -384
- unitysvc_services/test.py +769 -0
- unitysvc_services/update.py +4 -13
- unitysvc_services/utils.py +55 -6
- unitysvc_services/validator.py +117 -86
- unitysvc_services-0.1.5.dist-info/METADATA +182 -0
- unitysvc_services-0.1.5.dist-info/RECORD +26 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/entry_points.txt +1 -0
- unitysvc_services-0.1.1.dist-info/METADATA +0 -173
- unitysvc_services-0.1.1.dist-info/RECORD +0 -23
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/WHEEL +0 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,769 @@
|
|
1
|
+
"""Test command group - test code examples with upstream credentials."""
|
2
|
+
|
3
|
+
import fnmatch
|
4
|
+
import json
|
5
|
+
import os
|
6
|
+
import shutil
|
7
|
+
import subprocess
|
8
|
+
import tempfile
|
9
|
+
import tomllib
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Any
|
12
|
+
|
13
|
+
import typer
|
14
|
+
from rich.console import Console
|
15
|
+
from rich.table import Table
|
16
|
+
|
17
|
+
from .models.base import DocumentCategoryEnum
|
18
|
+
from .utils import find_files_by_schema, render_template_file
|
19
|
+
|
20
|
+
app = typer.Typer(help="Test code examples with upstream credentials")
|
21
|
+
console = Console()
|
22
|
+
|
23
|
+
|
24
|
+
def extract_service_directory_name(listing_file: Path) -> str | None:
|
25
|
+
"""Extract service directory name from listing file path.
|
26
|
+
|
27
|
+
The service directory is the directory immediately after "services" directory.
|
28
|
+
For example: .../services/llama-3-1-405b-instruct/listing-svcreseller.json
|
29
|
+
Returns: "llama-3-1-405b-instruct"
|
30
|
+
|
31
|
+
Args:
|
32
|
+
listing_file: Path to the listing file
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Service directory name or None if not found
|
36
|
+
"""
|
37
|
+
parts = listing_file.parts
|
38
|
+
try:
|
39
|
+
services_idx = parts.index("services")
|
40
|
+
# Service directory is immediately after "services"
|
41
|
+
if services_idx + 1 < len(parts):
|
42
|
+
return parts[services_idx + 1]
|
43
|
+
except (ValueError, IndexError):
|
44
|
+
pass
|
45
|
+
return None
|
46
|
+
|
47
|
+
|
48
|
+
def extract_code_examples_from_listing(listing_data: dict[str, Any], listing_file: Path) -> list[dict[str, Any]]:
|
49
|
+
"""Extract code example documents from a listing file.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
listing_data: Parsed listing data
|
53
|
+
listing_file: Path to the listing file for resolving relative paths
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
List of code example documents with resolved file paths
|
57
|
+
"""
|
58
|
+
code_examples = []
|
59
|
+
|
60
|
+
# Get service name for display - use directory name as fallback
|
61
|
+
service_name = listing_data.get("service_name")
|
62
|
+
if not service_name:
|
63
|
+
# Use service directory name as fallback
|
64
|
+
service_name = extract_service_directory_name(listing_file) or "unknown"
|
65
|
+
|
66
|
+
# Check user_access_interfaces
|
67
|
+
interfaces = listing_data.get("user_access_interfaces", [])
|
68
|
+
|
69
|
+
for interface in interfaces:
|
70
|
+
documents = interface.get("documents", [])
|
71
|
+
|
72
|
+
for doc in documents:
|
73
|
+
# Match both "code_example" and "code_examples"
|
74
|
+
category = doc.get("category", "")
|
75
|
+
if category == DocumentCategoryEnum.code_examples:
|
76
|
+
# Resolve file path relative to listing file
|
77
|
+
file_path = doc.get("file_path")
|
78
|
+
if file_path:
|
79
|
+
# Resolve relative path
|
80
|
+
absolute_path = (listing_file.parent / file_path).resolve()
|
81
|
+
|
82
|
+
code_example = {
|
83
|
+
"service_name": service_name,
|
84
|
+
"title": doc.get("title", "Untitled"),
|
85
|
+
"mime_type": doc.get("mime_type", "python"),
|
86
|
+
"file_path": str(absolute_path),
|
87
|
+
"listing_data": listing_data, # Full listing data for templates
|
88
|
+
"listing_file": listing_file, # Path to listing file for loading related data
|
89
|
+
"expect": doc.get("expect"), # Expected output substring for validation
|
90
|
+
}
|
91
|
+
code_examples.append(code_example)
|
92
|
+
|
93
|
+
return code_examples
|
94
|
+
|
95
|
+
|
96
|
+
def load_related_data(listing_file: Path) -> dict[str, Any]:
|
97
|
+
"""Load offering, provider, and seller data related to a listing file.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
listing_file: Path to the listing file
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
Dictionary with offering, provider, and seller data (may be empty dicts if not found)
|
104
|
+
"""
|
105
|
+
result: dict[str, Any] = {
|
106
|
+
"offering": {},
|
107
|
+
"provider": {},
|
108
|
+
"seller": {},
|
109
|
+
}
|
110
|
+
|
111
|
+
try:
|
112
|
+
# Find offering file (service.json in same directory as listing) using find_files_by_schema
|
113
|
+
offering_results = find_files_by_schema(listing_file.parent, "service_v1")
|
114
|
+
if offering_results:
|
115
|
+
# Unpack tuple: (file_path, format, data)
|
116
|
+
# Data is already loaded by find_files_by_schema
|
117
|
+
_file_path, _format, offering_data = offering_results[0]
|
118
|
+
result["offering"] = offering_data
|
119
|
+
else:
|
120
|
+
console.print(f"[yellow]Warning: No service_v1 file found in {listing_file.parent}[/yellow]")
|
121
|
+
|
122
|
+
# Find provider file using find_files_by_schema
|
123
|
+
# Structure: data/{provider}/services/{service}/listing.json
|
124
|
+
# Go up to provider directory (2 levels up from listing)
|
125
|
+
provider_dir = listing_file.parent.parent.parent
|
126
|
+
provider_results = find_files_by_schema(provider_dir, "provider_v1")
|
127
|
+
if provider_results:
|
128
|
+
# Unpack tuple: (file_path, format, data)
|
129
|
+
# Data is already loaded by find_files_by_schema
|
130
|
+
_file_path, _format, provider_data = provider_results[0]
|
131
|
+
result["provider"] = provider_data
|
132
|
+
else:
|
133
|
+
console.print(f"[yellow]Warning: No provider_v1 file found in {provider_dir}[/yellow]")
|
134
|
+
|
135
|
+
# Find seller file using find_files_by_schema
|
136
|
+
# Go up to data directory (3 levels up from listing)
|
137
|
+
data_dir = listing_file.parent.parent.parent.parent
|
138
|
+
seller_results = find_files_by_schema(data_dir, "seller_v1")
|
139
|
+
if seller_results:
|
140
|
+
# Unpack tuple: (file_path, format, data)
|
141
|
+
# Data is already loaded by find_files_by_schema
|
142
|
+
_file_path, _format, seller_data = seller_results[0]
|
143
|
+
result["seller"] = seller_data
|
144
|
+
else:
|
145
|
+
console.print(f"[yellow]Warning: No seller_v1 file found in {data_dir}[/yellow]")
|
146
|
+
|
147
|
+
except Exception as e:
|
148
|
+
console.print(f"[yellow]Warning: Failed to load related data: {e}[/yellow]")
|
149
|
+
|
150
|
+
return result
|
151
|
+
|
152
|
+
|
153
|
+
def load_provider_credentials(provider_file: Path) -> dict[str, str] | None:
|
154
|
+
"""Load API key and endpoint from provider file.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
provider_file: Path to provider.toml or provider.json
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
Dictionary with api_key and api_endpoint, or None if not found
|
161
|
+
"""
|
162
|
+
try:
|
163
|
+
if provider_file.suffix == ".toml":
|
164
|
+
with open(provider_file, "rb") as f:
|
165
|
+
provider_data = tomllib.load(f)
|
166
|
+
else:
|
167
|
+
with open(provider_file) as f:
|
168
|
+
provider_data = json.load(f)
|
169
|
+
|
170
|
+
access_info = provider_data.get("provider_access_info", {})
|
171
|
+
api_key = access_info.get("api_key") or access_info.get("FIREWORKS_API_KEY")
|
172
|
+
api_endpoint = access_info.get("api_endpoint") or access_info.get("FIREWORKS_API_BASE_URL")
|
173
|
+
|
174
|
+
if api_key and api_endpoint:
|
175
|
+
return {
|
176
|
+
"api_key": str(api_key),
|
177
|
+
"api_endpoint": str(api_endpoint),
|
178
|
+
}
|
179
|
+
except Exception as e:
|
180
|
+
console.print(f"[yellow]Warning: Failed to load provider credentials: {e}[/yellow]")
|
181
|
+
|
182
|
+
return None
|
183
|
+
|
184
|
+
|
185
|
+
def execute_code_example(code_example: dict[str, Any], credentials: dict[str, str]) -> dict[str, Any]:
|
186
|
+
"""Execute a code example script with upstream credentials.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
code_example: Code example metadata with file_path and listing_data
|
190
|
+
credentials: Dictionary with api_key and api_endpoint
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
Result dictionary with success, exit_code, stdout, stderr, rendered_content, file_suffix
|
194
|
+
"""
|
195
|
+
result: dict[str, Any] = {
|
196
|
+
"success": False,
|
197
|
+
"exit_code": None,
|
198
|
+
"error": None,
|
199
|
+
"stdout": None,
|
200
|
+
"stderr": None,
|
201
|
+
"rendered_content": None,
|
202
|
+
"file_suffix": None,
|
203
|
+
}
|
204
|
+
|
205
|
+
file_path = code_example.get("file_path")
|
206
|
+
if not file_path or not Path(file_path).exists():
|
207
|
+
result["error"] = f"File not found: {file_path}"
|
208
|
+
return result
|
209
|
+
|
210
|
+
try:
|
211
|
+
# Get original file extension
|
212
|
+
original_path = Path(file_path)
|
213
|
+
|
214
|
+
# Load related data for template rendering (if needed)
|
215
|
+
listing_data = code_example.get("listing_data", {})
|
216
|
+
listing_file = code_example.get("listing_file")
|
217
|
+
related_data = {}
|
218
|
+
if listing_file:
|
219
|
+
related_data = load_related_data(Path(listing_file))
|
220
|
+
|
221
|
+
# Render template if applicable (handles both .j2 and non-.j2 files)
|
222
|
+
try:
|
223
|
+
file_content, actual_filename = render_template_file(
|
224
|
+
original_path,
|
225
|
+
listing=listing_data,
|
226
|
+
offering=related_data.get("offering", {}),
|
227
|
+
provider=related_data.get("provider", {}),
|
228
|
+
seller=related_data.get("seller", {}),
|
229
|
+
)
|
230
|
+
except Exception as e:
|
231
|
+
result["error"] = f"Template rendering failed: {str(e)}"
|
232
|
+
return result
|
233
|
+
|
234
|
+
# Get file suffix from the actual filename (after .j2 stripping if applicable)
|
235
|
+
file_suffix = Path(actual_filename).suffix or ".txt"
|
236
|
+
|
237
|
+
# Store rendered content and file suffix for later use (e.g., writing failed tests)
|
238
|
+
result["rendered_content"] = file_content
|
239
|
+
result["file_suffix"] = file_suffix
|
240
|
+
|
241
|
+
# Determine interpreter to use
|
242
|
+
lines = file_content.split("\n")
|
243
|
+
interpreter_cmd = None
|
244
|
+
|
245
|
+
# First, try to parse shebang
|
246
|
+
if lines and lines[0].startswith("#!"):
|
247
|
+
shebang = lines[0][2:].strip()
|
248
|
+
if "/env " in shebang:
|
249
|
+
# e.g., #!/usr/bin/env python3
|
250
|
+
interpreter_cmd = shebang.split("/env ", 1)[1].strip().split()[0]
|
251
|
+
else:
|
252
|
+
# e.g., #!/usr/bin/python3
|
253
|
+
interpreter_cmd = shebang.split("/")[-1].split()[0]
|
254
|
+
|
255
|
+
# If no shebang found, determine interpreter based on file extension
|
256
|
+
if not interpreter_cmd:
|
257
|
+
if file_suffix == ".py":
|
258
|
+
# Try python3 first, fallback to python
|
259
|
+
if shutil.which("python3"):
|
260
|
+
interpreter_cmd = "python3"
|
261
|
+
elif shutil.which("python"):
|
262
|
+
interpreter_cmd = "python"
|
263
|
+
else:
|
264
|
+
result["error"] = "Neither 'python3' nor 'python' found. Please install Python to run this test."
|
265
|
+
return result
|
266
|
+
elif file_suffix == ".js":
|
267
|
+
# JavaScript files need Node.js
|
268
|
+
if shutil.which("node"):
|
269
|
+
interpreter_cmd = "node"
|
270
|
+
else:
|
271
|
+
result["error"] = "'node' not found. Please install Node.js to run JavaScript tests."
|
272
|
+
return result
|
273
|
+
elif file_suffix == ".sh":
|
274
|
+
# Shell scripts use bash
|
275
|
+
if shutil.which("bash"):
|
276
|
+
interpreter_cmd = "bash"
|
277
|
+
else:
|
278
|
+
result["error"] = "'bash' not found. Please install bash to run shell script tests."
|
279
|
+
return result
|
280
|
+
else:
|
281
|
+
# Unknown file type - try python3/python as fallback
|
282
|
+
if shutil.which("python3"):
|
283
|
+
interpreter_cmd = "python3"
|
284
|
+
elif shutil.which("python"):
|
285
|
+
interpreter_cmd = "python"
|
286
|
+
else:
|
287
|
+
result["error"] = f"Unknown file type '{file_suffix}' and no Python interpreter found."
|
288
|
+
return result
|
289
|
+
else:
|
290
|
+
# Shebang was found - verify the interpreter exists
|
291
|
+
if not shutil.which(interpreter_cmd):
|
292
|
+
result["error"] = (
|
293
|
+
f"Interpreter '{interpreter_cmd}' from shebang not found. Please install it to run this test."
|
294
|
+
)
|
295
|
+
return result
|
296
|
+
|
297
|
+
# Prepare environment variables
|
298
|
+
env = os.environ.copy()
|
299
|
+
env["API_KEY"] = credentials["api_key"]
|
300
|
+
env["API_ENDPOINT"] = credentials["api_endpoint"]
|
301
|
+
|
302
|
+
# Write script to temporary file with original extension
|
303
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=file_suffix, delete=False) as temp_file:
|
304
|
+
temp_file.write(file_content)
|
305
|
+
temp_file_path = temp_file.name
|
306
|
+
|
307
|
+
try:
|
308
|
+
# Execute the script (interpreter availability already verified)
|
309
|
+
process = subprocess.run(
|
310
|
+
[interpreter_cmd, temp_file_path],
|
311
|
+
env=env,
|
312
|
+
capture_output=True,
|
313
|
+
text=True,
|
314
|
+
timeout=30,
|
315
|
+
)
|
316
|
+
|
317
|
+
result["exit_code"] = process.returncode
|
318
|
+
result["stdout"] = process.stdout
|
319
|
+
result["stderr"] = process.stderr
|
320
|
+
|
321
|
+
# Determine if test passed
|
322
|
+
# Test passes if: exit_code == 0 AND (expect is None OR expect in stdout)
|
323
|
+
expected_output = code_example.get("expect")
|
324
|
+
|
325
|
+
if process.returncode != 0:
|
326
|
+
# Failed: non-zero exit code
|
327
|
+
result["success"] = False
|
328
|
+
result["error"] = f"Script exited with code {process.returncode}. stderr: {process.stderr[:200]}"
|
329
|
+
elif expected_output and expected_output not in process.stdout:
|
330
|
+
# Failed: exit code is 0 but expected string not found in output
|
331
|
+
result["success"] = False
|
332
|
+
result["error"] = (
|
333
|
+
f"Output validation failed: expected substring '{expected_output}' "
|
334
|
+
f"not found in stdout. stdout: {process.stdout[:200]}"
|
335
|
+
)
|
336
|
+
else:
|
337
|
+
# Passed: exit code is 0 AND (no expect field OR expected string found)
|
338
|
+
result["success"] = True
|
339
|
+
|
340
|
+
finally:
|
341
|
+
try:
|
342
|
+
os.unlink(temp_file_path)
|
343
|
+
except Exception:
|
344
|
+
pass
|
345
|
+
|
346
|
+
except subprocess.TimeoutExpired:
|
347
|
+
result["error"] = "Script execution timeout (30 seconds)"
|
348
|
+
except Exception as e:
|
349
|
+
result["error"] = f"Error executing script: {str(e)}"
|
350
|
+
|
351
|
+
return result
|
352
|
+
|
353
|
+
|
354
|
+
@app.command("list")
|
355
|
+
def list_code_examples(
|
356
|
+
data_dir: Path | None = typer.Argument(
|
357
|
+
None,
|
358
|
+
help="Directory containing provider data files (default: current directory)",
|
359
|
+
),
|
360
|
+
provider_name: str | None = typer.Option(
|
361
|
+
None,
|
362
|
+
"--provider",
|
363
|
+
"-p",
|
364
|
+
help="Only list code examples for a specific provider",
|
365
|
+
),
|
366
|
+
services: str | None = typer.Option(
|
367
|
+
None,
|
368
|
+
"--services",
|
369
|
+
"-s",
|
370
|
+
help="Comma-separated list of service patterns (supports wildcards, e.g., 'llama*,gpt-4*')",
|
371
|
+
),
|
372
|
+
):
|
373
|
+
"""List available code examples without running them.
|
374
|
+
|
375
|
+
This command scans for code examples in listing files and displays them in a table
|
376
|
+
with file paths shown relative to the data directory.
|
377
|
+
|
378
|
+
Useful for exploring available examples before running tests.
|
379
|
+
|
380
|
+
Examples:
|
381
|
+
# List all code examples
|
382
|
+
usvc test list
|
383
|
+
|
384
|
+
# List for specific provider
|
385
|
+
usvc test list --provider fireworks
|
386
|
+
|
387
|
+
# List for specific services
|
388
|
+
usvc test list --services "llama*,gpt-4*"
|
389
|
+
"""
|
390
|
+
# Set data directory
|
391
|
+
if data_dir is None:
|
392
|
+
data_dir = Path.cwd()
|
393
|
+
|
394
|
+
if not data_dir.is_absolute():
|
395
|
+
data_dir = Path.cwd() / data_dir
|
396
|
+
|
397
|
+
if not data_dir.exists():
|
398
|
+
console.print(
|
399
|
+
f"[red]✗[/red] Data directory not found: {data_dir}",
|
400
|
+
style="bold red",
|
401
|
+
)
|
402
|
+
raise typer.Exit(code=1)
|
403
|
+
|
404
|
+
# Parse service patterns if provided
|
405
|
+
service_patterns: list[str] = []
|
406
|
+
if services:
|
407
|
+
service_patterns = [s.strip() for s in services.split(",") if s.strip()]
|
408
|
+
|
409
|
+
console.print(f"[blue]Scanning for code examples in:[/blue] {data_dir}\n")
|
410
|
+
|
411
|
+
# Find all provider files
|
412
|
+
provider_results = find_files_by_schema(data_dir, "provider_v1")
|
413
|
+
provider_names: set[str] = set()
|
414
|
+
|
415
|
+
for _provider_file, _format, provider_data in provider_results:
|
416
|
+
prov_name = provider_data.get("name", "unknown")
|
417
|
+
if not provider_name or prov_name == provider_name:
|
418
|
+
provider_names.add(prov_name)
|
419
|
+
|
420
|
+
if not provider_names:
|
421
|
+
console.print("[yellow]No providers found.[/yellow]")
|
422
|
+
raise typer.Exit(code=0)
|
423
|
+
|
424
|
+
# Find all listing files
|
425
|
+
listing_results = find_files_by_schema(data_dir, "listing_v1")
|
426
|
+
|
427
|
+
if not listing_results:
|
428
|
+
console.print("[yellow]No listing files found.[/yellow]")
|
429
|
+
raise typer.Exit(code=0)
|
430
|
+
|
431
|
+
# Extract code examples from all listings
|
432
|
+
all_code_examples: list[tuple[dict[str, Any], str, str]] = []
|
433
|
+
|
434
|
+
for listing_file, _format, listing_data in listing_results:
|
435
|
+
# Determine provider for this listing
|
436
|
+
parts = listing_file.parts
|
437
|
+
prov_name = "unknown"
|
438
|
+
|
439
|
+
try:
|
440
|
+
services_idx = parts.index("services")
|
441
|
+
if services_idx > 0:
|
442
|
+
prov_name = parts[services_idx - 1]
|
443
|
+
except (ValueError, IndexError):
|
444
|
+
pass
|
445
|
+
|
446
|
+
# Skip if provider filter is set and doesn't match
|
447
|
+
if provider_name and prov_name != provider_name:
|
448
|
+
continue
|
449
|
+
|
450
|
+
# Skip if provider not in our list
|
451
|
+
if prov_name not in provider_names:
|
452
|
+
continue
|
453
|
+
|
454
|
+
# Filter by service directory name if patterns are provided
|
455
|
+
if service_patterns:
|
456
|
+
service_dir = extract_service_directory_name(listing_file)
|
457
|
+
if not service_dir:
|
458
|
+
continue
|
459
|
+
|
460
|
+
# Check if service matches any of the patterns
|
461
|
+
matches = any(fnmatch.fnmatch(service_dir, pattern) for pattern in service_patterns)
|
462
|
+
if not matches:
|
463
|
+
continue
|
464
|
+
|
465
|
+
code_examples = extract_code_examples_from_listing(listing_data, listing_file)
|
466
|
+
|
467
|
+
for example in code_examples:
|
468
|
+
# Get file extension
|
469
|
+
file_path = example.get("file_path", "")
|
470
|
+
file_ext = Path(file_path).suffix or "unknown"
|
471
|
+
all_code_examples.append((example, prov_name, file_ext))
|
472
|
+
|
473
|
+
if not all_code_examples:
|
474
|
+
console.print("[yellow]No code examples found.[/yellow]")
|
475
|
+
raise typer.Exit(code=0)
|
476
|
+
|
477
|
+
# Display results in table
|
478
|
+
table = Table(title="Available Code Examples")
|
479
|
+
table.add_column("Service", style="cyan")
|
480
|
+
table.add_column("Provider", style="blue")
|
481
|
+
table.add_column("Title", style="white")
|
482
|
+
table.add_column("Type", style="magenta")
|
483
|
+
table.add_column("File Path", style="dim")
|
484
|
+
|
485
|
+
for example, prov_name, file_ext in all_code_examples:
|
486
|
+
file_path = example.get("file_path", "N/A")
|
487
|
+
|
488
|
+
# Show path relative to data directory
|
489
|
+
if file_path != "N/A":
|
490
|
+
try:
|
491
|
+
abs_path = Path(file_path).resolve()
|
492
|
+
rel_path = abs_path.relative_to(data_dir.resolve())
|
493
|
+
file_path = str(rel_path)
|
494
|
+
except ValueError:
|
495
|
+
# If relative_to fails, just show the path as-is
|
496
|
+
file_path = str(file_path)
|
497
|
+
|
498
|
+
row = [
|
499
|
+
example["service_name"],
|
500
|
+
prov_name,
|
501
|
+
example["title"],
|
502
|
+
file_ext,
|
503
|
+
file_path,
|
504
|
+
]
|
505
|
+
|
506
|
+
table.add_row(*row)
|
507
|
+
|
508
|
+
console.print(table)
|
509
|
+
console.print(f"\n[green]Total:[/green] {len(all_code_examples)} code example(s)")
|
510
|
+
|
511
|
+
|
512
|
+
@app.command()
|
513
|
+
def run(
|
514
|
+
data_dir: Path | None = typer.Argument(
|
515
|
+
None,
|
516
|
+
help="Directory containing provider data files (default: current directory)",
|
517
|
+
),
|
518
|
+
provider_name: str | None = typer.Option(
|
519
|
+
None,
|
520
|
+
"--provider",
|
521
|
+
"-p",
|
522
|
+
help="Only test code examples for a specific provider",
|
523
|
+
),
|
524
|
+
services: str | None = typer.Option(
|
525
|
+
None,
|
526
|
+
"--services",
|
527
|
+
"-s",
|
528
|
+
help="Comma-separated list of service patterns (supports wildcards, e.g., 'llama*,gpt-4*')",
|
529
|
+
),
|
530
|
+
verbose: bool = typer.Option(
|
531
|
+
False,
|
532
|
+
"--verbose",
|
533
|
+
"-v",
|
534
|
+
help="Show detailed output including stdout/stderr from scripts",
|
535
|
+
),
|
536
|
+
):
|
537
|
+
"""Test code examples with upstream API credentials.
|
538
|
+
|
539
|
+
This command:
|
540
|
+
1. Scans for all listing files (schema: listing_v1)
|
541
|
+
2. Extracts code example documents
|
542
|
+
3. Loads provider credentials from provider.toml
|
543
|
+
4. Executes each code example with API_KEY and API_ENDPOINT set to upstream values
|
544
|
+
5. Displays test results
|
545
|
+
|
546
|
+
Examples:
|
547
|
+
# Test all code examples
|
548
|
+
unitysvc_services test run
|
549
|
+
|
550
|
+
# Test specific provider
|
551
|
+
unitysvc_services test run --provider fireworks
|
552
|
+
|
553
|
+
# Test specific services (with wildcards)
|
554
|
+
unitysvc_services test run --services "llama*,gpt-4*"
|
555
|
+
|
556
|
+
# Test single service
|
557
|
+
unitysvc_services test run --services "llama-3-1-405b-instruct"
|
558
|
+
|
559
|
+
# Combine filters
|
560
|
+
unitysvc_services test run --provider fireworks --services "llama*"
|
561
|
+
|
562
|
+
# Show detailed output
|
563
|
+
unitysvc_services test run --verbose
|
564
|
+
"""
|
565
|
+
# Set data directory
|
566
|
+
if data_dir is None:
|
567
|
+
data_dir = Path.cwd()
|
568
|
+
|
569
|
+
if not data_dir.is_absolute():
|
570
|
+
data_dir = Path.cwd() / data_dir
|
571
|
+
|
572
|
+
if not data_dir.exists():
|
573
|
+
console.print(
|
574
|
+
f"[red]✗[/red] Data directory not found: {data_dir}",
|
575
|
+
style="bold red",
|
576
|
+
)
|
577
|
+
raise typer.Exit(code=1)
|
578
|
+
|
579
|
+
# Parse service patterns if provided
|
580
|
+
service_patterns: list[str] = []
|
581
|
+
if services:
|
582
|
+
service_patterns = [s.strip() for s in services.split(",") if s.strip()]
|
583
|
+
console.print(f"[blue]Service filter patterns:[/blue] {', '.join(service_patterns)}\n")
|
584
|
+
|
585
|
+
console.print(f"[blue]Scanning for listing files in:[/blue] {data_dir}\n")
|
586
|
+
|
587
|
+
# Find all provider files first to get credentials
|
588
|
+
provider_results = find_files_by_schema(data_dir, "provider_v1")
|
589
|
+
provider_credentials: dict[str, dict[str, str]] = {}
|
590
|
+
|
591
|
+
for provider_file, _format, provider_data in provider_results:
|
592
|
+
prov_name = provider_data.get("name", "unknown")
|
593
|
+
|
594
|
+
# Skip if provider filter is set and doesn't match
|
595
|
+
if provider_name and prov_name != provider_name:
|
596
|
+
continue
|
597
|
+
|
598
|
+
credentials = load_provider_credentials(provider_file)
|
599
|
+
if credentials:
|
600
|
+
provider_credentials[prov_name] = credentials
|
601
|
+
console.print(f"[green]✓[/green] Loaded credentials for provider: {prov_name}")
|
602
|
+
|
603
|
+
if not provider_credentials:
|
604
|
+
console.print("[yellow]No provider credentials found.[/yellow]")
|
605
|
+
raise typer.Exit(code=0)
|
606
|
+
|
607
|
+
console.print()
|
608
|
+
|
609
|
+
# Find all listing files
|
610
|
+
listing_results = find_files_by_schema(data_dir, "listing_v1")
|
611
|
+
|
612
|
+
if not listing_results:
|
613
|
+
console.print("[yellow]No listing files found.[/yellow]")
|
614
|
+
raise typer.Exit(code=0)
|
615
|
+
|
616
|
+
console.print(f"[cyan]Found {len(listing_results)} listing file(s)[/cyan]\n")
|
617
|
+
|
618
|
+
# Extract code examples from all listings
|
619
|
+
all_code_examples: list[tuple[dict[str, Any], str]] = []
|
620
|
+
|
621
|
+
for listing_file, _format, listing_data in listing_results:
|
622
|
+
# Determine provider for this listing
|
623
|
+
# Provider is the directory name before "services"
|
624
|
+
parts = listing_file.parts
|
625
|
+
prov_name = "unknown"
|
626
|
+
|
627
|
+
try:
|
628
|
+
services_idx = parts.index("services")
|
629
|
+
if services_idx > 0:
|
630
|
+
prov_name = parts[services_idx - 1]
|
631
|
+
except (ValueError, IndexError):
|
632
|
+
pass
|
633
|
+
|
634
|
+
# Skip if provider filter is set and doesn't match
|
635
|
+
if provider_name and prov_name != provider_name:
|
636
|
+
continue
|
637
|
+
|
638
|
+
# Skip if we don't have credentials for this provider
|
639
|
+
if prov_name not in provider_credentials:
|
640
|
+
continue
|
641
|
+
|
642
|
+
# Filter by service directory name if patterns are provided
|
643
|
+
if service_patterns:
|
644
|
+
service_dir = extract_service_directory_name(listing_file)
|
645
|
+
if not service_dir:
|
646
|
+
continue
|
647
|
+
|
648
|
+
# Check if service matches any of the patterns
|
649
|
+
matches = any(fnmatch.fnmatch(service_dir, pattern) for pattern in service_patterns)
|
650
|
+
if not matches:
|
651
|
+
continue
|
652
|
+
|
653
|
+
code_examples = extract_code_examples_from_listing(listing_data, listing_file)
|
654
|
+
|
655
|
+
for example in code_examples:
|
656
|
+
all_code_examples.append((example, prov_name))
|
657
|
+
|
658
|
+
if not all_code_examples:
|
659
|
+
console.print("[yellow]No code examples found in listings.[/yellow]")
|
660
|
+
raise typer.Exit(code=0)
|
661
|
+
|
662
|
+
console.print(f"[cyan]Found {len(all_code_examples)} code example(s)[/cyan]\n")
|
663
|
+
|
664
|
+
# Execute each code example
|
665
|
+
results = []
|
666
|
+
|
667
|
+
for example, prov_name in all_code_examples:
|
668
|
+
service_name = example["service_name"]
|
669
|
+
title = example["title"]
|
670
|
+
|
671
|
+
console.print(f"[bold]Testing:[/bold] {service_name} - {title}")
|
672
|
+
|
673
|
+
credentials = provider_credentials[prov_name]
|
674
|
+
result = execute_code_example(example, credentials)
|
675
|
+
|
676
|
+
results.append(
|
677
|
+
{
|
678
|
+
"service_name": service_name,
|
679
|
+
"provider": prov_name,
|
680
|
+
"title": title,
|
681
|
+
"result": result,
|
682
|
+
}
|
683
|
+
)
|
684
|
+
|
685
|
+
if result["success"]:
|
686
|
+
console.print(f" [green]✓ Success[/green] (exit code: {result['exit_code']})")
|
687
|
+
if verbose and result["stdout"]:
|
688
|
+
console.print(f" [dim]stdout:[/dim] {result['stdout'][:200]}")
|
689
|
+
else:
|
690
|
+
console.print(f" [red]✗ Failed[/red] - {result['error']}")
|
691
|
+
if verbose:
|
692
|
+
if result["stdout"]:
|
693
|
+
console.print(f" [dim]stdout:[/dim] {result['stdout'][:200]}")
|
694
|
+
if result["stderr"]:
|
695
|
+
console.print(f" [dim]stderr:[/dim] {result['stderr'][:200]}")
|
696
|
+
|
697
|
+
# Write failed test content to current directory
|
698
|
+
if result.get("rendered_content"):
|
699
|
+
# Create safe filename: failed_<service_name>_<title><ext>
|
700
|
+
# Sanitize service name and title for filename
|
701
|
+
safe_service = service_name.replace("/", "_").replace(" ", "_")
|
702
|
+
safe_title = title.replace("/", "_").replace(" ", "_")
|
703
|
+
file_suffix = result.get("file_suffix", ".txt")
|
704
|
+
|
705
|
+
# Create filename
|
706
|
+
failed_filename = f"failed_{safe_service}_{safe_title}{file_suffix}"
|
707
|
+
|
708
|
+
# Prepare content with environment variables as header comments
|
709
|
+
content_with_env = result["rendered_content"]
|
710
|
+
|
711
|
+
# Add environment variables as comments at the top
|
712
|
+
env_header = (
|
713
|
+
"# Environment variables used for this test:\n"
|
714
|
+
f"# API_KEY={credentials['api_key']}\n"
|
715
|
+
f"# API_ENDPOINT={credentials['api_endpoint']}\n"
|
716
|
+
"#\n"
|
717
|
+
"# To reproduce this test, export these variables:\n"
|
718
|
+
f"# export API_KEY='{credentials['api_key']}'\n"
|
719
|
+
f"# export API_ENDPOINT='{credentials['api_endpoint']}'\n"
|
720
|
+
"#\n\n"
|
721
|
+
)
|
722
|
+
|
723
|
+
content_with_env = env_header + content_with_env
|
724
|
+
|
725
|
+
# Write to current directory
|
726
|
+
try:
|
727
|
+
with open(failed_filename, "w", encoding="utf-8") as f:
|
728
|
+
f.write(content_with_env)
|
729
|
+
console.print(f" [yellow]→ Test content saved to:[/yellow] {failed_filename}")
|
730
|
+
console.print(" [dim] (includes environment variables for reproduction)[/dim]")
|
731
|
+
except Exception as e:
|
732
|
+
console.print(f" [yellow]⚠ Failed to save test content: {e}[/yellow]")
|
733
|
+
|
734
|
+
console.print()
|
735
|
+
|
736
|
+
# Print summary table
|
737
|
+
console.print("\n" + "=" * 70)
|
738
|
+
console.print("[bold]Test Results Summary:[/bold]\n")
|
739
|
+
|
740
|
+
table = Table(title="Code Example Tests")
|
741
|
+
table.add_column("Service", style="cyan")
|
742
|
+
table.add_column("Provider", style="blue")
|
743
|
+
table.add_column("Example", style="white")
|
744
|
+
table.add_column("Status", style="green")
|
745
|
+
table.add_column("Exit Code", style="white")
|
746
|
+
|
747
|
+
total_tests = len(results)
|
748
|
+
passed = sum(1 for r in results if r["result"]["success"])
|
749
|
+
failed = total_tests - passed
|
750
|
+
|
751
|
+
for test in results:
|
752
|
+
status = "[green]✓ Pass[/green]" if test["result"]["success"] else "[red]✗ Fail[/red]"
|
753
|
+
# Use 'is not None' to properly handle exit_code of 0 (success)
|
754
|
+
exit_code = str(test["result"]["exit_code"]) if test["result"]["exit_code"] is not None else "N/A"
|
755
|
+
|
756
|
+
table.add_row(
|
757
|
+
test["service_name"],
|
758
|
+
test["provider"],
|
759
|
+
test["title"],
|
760
|
+
status,
|
761
|
+
exit_code,
|
762
|
+
)
|
763
|
+
|
764
|
+
console.print(table)
|
765
|
+
console.print(f"\n[green]✓ Passed: {passed}/{total_tests}[/green]")
|
766
|
+
console.print(f"[red]✗ Failed: {failed}/{total_tests}[/red]")
|
767
|
+
|
768
|
+
if failed > 0:
|
769
|
+
raise typer.Exit(code=1)
|