unitysvc-services 0.1.5__py3-none-any.whl → 0.1.7__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/models/base.py +5 -16
- unitysvc_services/publisher.py +52 -2
- unitysvc_services/test.py +39 -12
- unitysvc_services/utils.py +65 -2
- {unitysvc_services-0.1.5.dist-info → unitysvc_services-0.1.7.dist-info}/METADATA +1 -1
- {unitysvc_services-0.1.5.dist-info → unitysvc_services-0.1.7.dist-info}/RECORD +10 -10
- {unitysvc_services-0.1.5.dist-info → unitysvc_services-0.1.7.dist-info}/WHEEL +0 -0
- {unitysvc_services-0.1.5.dist-info → unitysvc_services-0.1.7.dist-info}/entry_points.txt +0 -0
- {unitysvc_services-0.1.5.dist-info → unitysvc_services-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {unitysvc_services-0.1.5.dist-info → unitysvc_services-0.1.7.dist-info}/top_level.txt +0 -0
unitysvc_services/models/base.py
CHANGED
@@ -38,12 +38,13 @@ class DocumentContextEnum(StrEnum):
|
|
38
38
|
class DocumentCategoryEnum(StrEnum):
|
39
39
|
getting_started = "getting_started"
|
40
40
|
api_reference = "api_reference"
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
tutorial = "tutorial"
|
42
|
+
code_example = "code_example"
|
43
|
+
code_example_output = "code_example_output"
|
44
|
+
use_case = "use_case"
|
44
45
|
troubleshooting = "troubleshooting"
|
45
46
|
changelog = "changelog"
|
46
|
-
|
47
|
+
best_practice = "best_practice"
|
47
48
|
specification = "specification"
|
48
49
|
service_level_agreement = "service_level_agreement"
|
49
50
|
terms_of_service = "terms_of_service"
|
@@ -255,18 +256,6 @@ class Document(BaseModel):
|
|
255
256
|
default=False,
|
256
257
|
description="Whether document is publicly accessible without authentication",
|
257
258
|
)
|
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
|
-
)
|
270
259
|
|
271
260
|
|
272
261
|
class RateLimit(BaseModel):
|
unitysvc_services/publisher.py
CHANGED
@@ -67,6 +67,7 @@ class ServiceDataPublisher(UnitySvcAPI):
|
|
67
67
|
offering: dict[str, Any] | None = None,
|
68
68
|
provider: dict[str, Any] | None = None,
|
69
69
|
seller: dict[str, Any] | None = None,
|
70
|
+
listing_filename: str | None = None,
|
70
71
|
) -> dict[str, Any]:
|
71
72
|
"""Recursively resolve file references and include content in data.
|
72
73
|
|
@@ -80,6 +81,7 @@ class ServiceDataPublisher(UnitySvcAPI):
|
|
80
81
|
offering: Offering data for template rendering (optional)
|
81
82
|
provider: Provider data for template rendering (optional)
|
82
83
|
seller: Seller data for template rendering (optional)
|
84
|
+
listing_filename: Listing filename for constructing output filenames (optional)
|
83
85
|
|
84
86
|
Returns:
|
85
87
|
Data with file references resolved and content loaded
|
@@ -90,14 +92,26 @@ class ServiceDataPublisher(UnitySvcAPI):
|
|
90
92
|
if isinstance(value, dict):
|
91
93
|
# Recursively process nested dictionaries
|
92
94
|
result[key] = self.resolve_file_references(
|
93
|
-
value,
|
95
|
+
value,
|
96
|
+
base_path,
|
97
|
+
listing=listing,
|
98
|
+
offering=offering,
|
99
|
+
provider=provider,
|
100
|
+
seller=seller,
|
101
|
+
listing_filename=listing_filename,
|
94
102
|
)
|
95
103
|
elif isinstance(value, list):
|
96
104
|
# Process lists
|
97
105
|
result[key] = [
|
98
106
|
(
|
99
107
|
self.resolve_file_references(
|
100
|
-
item,
|
108
|
+
item,
|
109
|
+
base_path,
|
110
|
+
listing=listing,
|
111
|
+
offering=offering,
|
112
|
+
provider=provider,
|
113
|
+
seller=seller,
|
114
|
+
listing_filename=listing_filename,
|
101
115
|
)
|
102
116
|
if isinstance(item, dict)
|
103
117
|
else item
|
@@ -135,6 +149,41 @@ class ServiceDataPublisher(UnitySvcAPI):
|
|
135
149
|
else:
|
136
150
|
result[key] = value
|
137
151
|
|
152
|
+
# After processing all fields, check if this is a code_examples document
|
153
|
+
# If so, try to load corresponding .out file and add to meta.output
|
154
|
+
if result.get("category") == "code_examples" and result.get("file_content") and listing_filename:
|
155
|
+
# Get the actual filename (after .j2 stripping if applicable)
|
156
|
+
# If file_path was updated (e.g., from "test.py.j2" to "test.py"), use that
|
157
|
+
# Otherwise, extract basename from original file_path
|
158
|
+
output_base_filename: str | None = None
|
159
|
+
|
160
|
+
# Check if file_path was modified (original might have had .j2)
|
161
|
+
file_path_value = result.get("file_path", "")
|
162
|
+
if file_path_value:
|
163
|
+
output_base_filename = Path(file_path_value).name
|
164
|
+
|
165
|
+
if output_base_filename:
|
166
|
+
# Construct output filename: {listing_stem}_{output_base_filename}.out
|
167
|
+
# e.g., "svclisting_test.py.out" for svclisting.json and test.py
|
168
|
+
listing_stem = Path(listing_filename).stem
|
169
|
+
output_filename = f"{listing_stem}_{output_base_filename}.out"
|
170
|
+
|
171
|
+
# Try to find the .out file in base_path (listing directory)
|
172
|
+
output_path = base_path / output_filename
|
173
|
+
|
174
|
+
if output_path.exists():
|
175
|
+
try:
|
176
|
+
with open(output_path, encoding="utf-8") as f:
|
177
|
+
output_content = f.read()
|
178
|
+
|
179
|
+
# Add output to meta field
|
180
|
+
if "meta" not in result or result["meta"] is None:
|
181
|
+
result["meta"] = {}
|
182
|
+
result["meta"]["output"] = output_content
|
183
|
+
except Exception:
|
184
|
+
# Don't fail if output file can't be read, just skip it
|
185
|
+
pass
|
186
|
+
|
138
187
|
return result
|
139
188
|
|
140
189
|
async def post( # type: ignore[override]
|
@@ -401,6 +450,7 @@ class ServiceDataPublisher(UnitySvcAPI):
|
|
401
450
|
offering=service_data,
|
402
451
|
provider=provider_data,
|
403
452
|
seller=seller_data,
|
453
|
+
listing_filename=listing_file.name,
|
404
454
|
)
|
405
455
|
|
406
456
|
# Post to the endpoint using retry helper
|
unitysvc_services/test.py
CHANGED
@@ -70,15 +70,18 @@ def extract_code_examples_from_listing(listing_data: dict[str, Any], listing_fil
|
|
70
70
|
documents = interface.get("documents", [])
|
71
71
|
|
72
72
|
for doc in documents:
|
73
|
-
#
|
73
|
+
# Check if this is a code example document
|
74
74
|
category = doc.get("category", "")
|
75
|
-
if category == DocumentCategoryEnum.
|
75
|
+
if category == DocumentCategoryEnum.code_example:
|
76
76
|
# Resolve file path relative to listing file
|
77
77
|
file_path = doc.get("file_path")
|
78
78
|
if file_path:
|
79
79
|
# Resolve relative path
|
80
80
|
absolute_path = (listing_file.parent / file_path).resolve()
|
81
81
|
|
82
|
+
# Extract meta fields for code examples (expect, requirements, etc.)
|
83
|
+
meta = doc.get("meta", {}) or {}
|
84
|
+
|
82
85
|
code_example = {
|
83
86
|
"service_name": service_name,
|
84
87
|
"title": doc.get("title", "Untitled"),
|
@@ -86,7 +89,8 @@ def extract_code_examples_from_listing(listing_data: dict[str, Any], listing_fil
|
|
86
89
|
"file_path": str(absolute_path),
|
87
90
|
"listing_data": listing_data, # Full listing data for templates
|
88
91
|
"listing_file": listing_file, # Path to listing file for loading related data
|
89
|
-
"expect":
|
92
|
+
"expect": meta.get("expect"), # Expected output substring for validation (from meta)
|
93
|
+
"requirements": meta.get("requirements"), # Required packages (from meta)
|
90
94
|
}
|
91
95
|
code_examples.append(code_example)
|
92
96
|
|
@@ -200,6 +204,8 @@ def execute_code_example(code_example: dict[str, Any], credentials: dict[str, st
|
|
200
204
|
"stderr": None,
|
201
205
|
"rendered_content": None,
|
202
206
|
"file_suffix": None,
|
207
|
+
"listing_file": None,
|
208
|
+
"actual_filename": None,
|
203
209
|
}
|
204
210
|
|
205
211
|
file_path = code_example.get("file_path")
|
@@ -237,6 +243,8 @@ def execute_code_example(code_example: dict[str, Any], credentials: dict[str, st
|
|
237
243
|
# Store rendered content and file suffix for later use (e.g., writing failed tests)
|
238
244
|
result["rendered_content"] = file_content
|
239
245
|
result["file_suffix"] = file_suffix
|
246
|
+
result["listing_file"] = listing_file
|
247
|
+
result["actual_filename"] = actual_filename
|
240
248
|
|
241
249
|
# Determine interpreter to use
|
242
250
|
lines = file_content.split("\n")
|
@@ -686,6 +694,27 @@ def run(
|
|
686
694
|
console.print(f" [green]✓ Success[/green] (exit code: {result['exit_code']})")
|
687
695
|
if verbose and result["stdout"]:
|
688
696
|
console.print(f" [dim]stdout:[/dim] {result['stdout'][:200]}")
|
697
|
+
|
698
|
+
# Save successful test output to .out file
|
699
|
+
if result.get("stdout") and result.get("listing_file") and result.get("actual_filename"):
|
700
|
+
listing_file = Path(result["listing_file"])
|
701
|
+
actual_filename = result["actual_filename"]
|
702
|
+
|
703
|
+
# Create filename: {listing_stem}_{actual_filename}.out
|
704
|
+
# e.g., "svclisting_test.py.out" for svclisting.json and test.py
|
705
|
+
listing_stem = listing_file.stem
|
706
|
+
output_filename = f"{listing_stem}_{actual_filename}.out"
|
707
|
+
|
708
|
+
# Save to listing directory
|
709
|
+
output_path = listing_file.parent / output_filename
|
710
|
+
|
711
|
+
# Write stdout to .out file
|
712
|
+
try:
|
713
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
714
|
+
f.write(result["stdout"])
|
715
|
+
console.print(f" [dim]→ Output saved to:[/dim] {output_path}")
|
716
|
+
except Exception as e:
|
717
|
+
console.print(f" [yellow]⚠ Failed to save output: {e}[/yellow]")
|
689
718
|
else:
|
690
719
|
console.print(f" [red]✗ Failed[/red] - {result['error']}")
|
691
720
|
if verbose:
|
@@ -695,15 +724,13 @@ def run(
|
|
695
724
|
console.print(f" [dim]stderr:[/dim] {result['stderr'][:200]}")
|
696
725
|
|
697
726
|
# Write failed test content to current directory
|
698
|
-
if result.get("rendered_content"):
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
# Create filename
|
706
|
-
failed_filename = f"failed_{safe_service}_{safe_title}{file_suffix}"
|
727
|
+
if result.get("rendered_content") and result.get("listing_file") and result.get("actual_filename"):
|
728
|
+
listing_file = Path(result["listing_file"])
|
729
|
+
actual_filename = result["actual_filename"]
|
730
|
+
listing_stem = listing_file.stem
|
731
|
+
|
732
|
+
# Create filename: failed_{listing_stem}_{actual_filename}
|
733
|
+
failed_filename = f"failed_{listing_stem}_{actual_filename}"
|
707
734
|
|
708
735
|
# Prepare content with environment variables as header comments
|
709
736
|
content_with_env = result["rendered_content"]
|
unitysvc_services/utils.py
CHANGED
@@ -10,10 +10,47 @@ import tomli_w
|
|
10
10
|
from jinja2 import Template
|
11
11
|
|
12
12
|
|
13
|
+
def deep_merge_dicts(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
14
|
+
"""
|
15
|
+
Deep merge two dictionaries, with override values taking precedence.
|
16
|
+
|
17
|
+
For nested dictionaries, performs recursive merge. For all other types
|
18
|
+
(lists, primitives), the override value completely replaces the base value.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
base: Base dictionary
|
22
|
+
override: Override dictionary (values take precedence)
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
Merged dictionary
|
26
|
+
"""
|
27
|
+
result = base.copy()
|
28
|
+
|
29
|
+
for key, value in override.items():
|
30
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
31
|
+
# Recursively merge nested dictionaries
|
32
|
+
result[key] = deep_merge_dicts(result[key], value)
|
33
|
+
else:
|
34
|
+
# For all other types (lists, primitives, etc.), override completely
|
35
|
+
result[key] = value
|
36
|
+
|
37
|
+
return result
|
38
|
+
|
39
|
+
|
13
40
|
def load_data_file(file_path: Path) -> tuple[dict[str, Any], str]:
|
14
41
|
"""
|
15
42
|
Load a data file (JSON or TOML) and return (data, format).
|
16
43
|
|
44
|
+
Automatically checks for and merges override files with the pattern:
|
45
|
+
<base_name>.override.<extension>
|
46
|
+
|
47
|
+
For example:
|
48
|
+
- service.json -> service.override.json
|
49
|
+
- provider.toml -> provider.override.toml
|
50
|
+
|
51
|
+
If an override file exists, it will be deep-merged with the base file,
|
52
|
+
with override values taking precedence.
|
53
|
+
|
17
54
|
Args:
|
18
55
|
file_path: Path to the data file
|
19
56
|
|
@@ -23,15 +60,41 @@ def load_data_file(file_path: Path) -> tuple[dict[str, Any], str]:
|
|
23
60
|
Raises:
|
24
61
|
ValueError: If file format is not supported
|
25
62
|
"""
|
63
|
+
# Load the base file
|
26
64
|
if file_path.suffix == ".json":
|
27
65
|
with open(file_path, encoding="utf-8") as f:
|
28
|
-
|
66
|
+
data = json.load(f)
|
67
|
+
file_format = "json"
|
29
68
|
elif file_path.suffix == ".toml":
|
30
69
|
with open(file_path, "rb") as f:
|
31
|
-
|
70
|
+
data = tomllib.load(f)
|
71
|
+
file_format = "toml"
|
32
72
|
else:
|
33
73
|
raise ValueError(f"Unsupported file format: {file_path.suffix}")
|
34
74
|
|
75
|
+
# Check for override file
|
76
|
+
# Pattern: <stem>.override.<suffix>
|
77
|
+
# Example: service.json -> service.override.json
|
78
|
+
override_path = file_path.with_stem(f"{file_path.stem}.override")
|
79
|
+
|
80
|
+
if override_path.exists():
|
81
|
+
# Load the override file (same format as base file)
|
82
|
+
if override_path.suffix == ".json":
|
83
|
+
with open(override_path, encoding="utf-8") as f:
|
84
|
+
override_data = json.load(f)
|
85
|
+
elif override_path.suffix == ".toml":
|
86
|
+
with open(override_path, "rb") as f:
|
87
|
+
override_data = tomllib.load(f)
|
88
|
+
else:
|
89
|
+
# This shouldn't happen since we're using the same suffix as base
|
90
|
+
# But handle it gracefully
|
91
|
+
override_data = {}
|
92
|
+
|
93
|
+
# Deep merge the override data into the base data
|
94
|
+
data = deep_merge_dicts(data, override_data)
|
95
|
+
|
96
|
+
return data, file_format
|
97
|
+
|
35
98
|
|
36
99
|
def write_data_file(file_path: Path, data: dict[str, Any], format: str) -> None:
|
37
100
|
"""
|
@@ -4,23 +4,23 @@ unitysvc_services/cli.py,sha256=omHzrWk8iZJUjZTE5KCmEK1wnRGGnQk_BNV-hKqucnA,725
|
|
4
4
|
unitysvc_services/format_data.py,sha256=Jl9Vj3fRX852fHSUa5DzO-oiFQwuQHC3WMCDNIlo1Lc,5460
|
5
5
|
unitysvc_services/list.py,sha256=QDp9BByaoeFeJxXJN9RQ-jU99mH9Guq9ampfXCbpZmI,7033
|
6
6
|
unitysvc_services/populate.py,sha256=jiqS2D3_widV6siPe3OBvw7ZdG9MuddEIOuTUCtMM5c,7605
|
7
|
-
unitysvc_services/publisher.py,sha256=
|
7
|
+
unitysvc_services/publisher.py,sha256=_r6wqJJkC-9RtyKcxiegPoSRDi2-nRL16M8OJ-vi7eE,56237
|
8
8
|
unitysvc_services/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
unitysvc_services/query.py,sha256=q0_g5YAl9cPlHpW7k7Y6A-t4fQSdI-_4Jl6g2KgkEm0,24049
|
10
10
|
unitysvc_services/scaffold.py,sha256=Y73IX8vskImxSvxDgR0mvEFuAMYnBKfttn3bjcz3jmQ,40331
|
11
|
-
unitysvc_services/test.py,sha256=
|
11
|
+
unitysvc_services/test.py,sha256=nZp6O_j2Y-gNPNWJ55QG4kI50BsbS_faUFGupWK1VMI,29734
|
12
12
|
unitysvc_services/update.py,sha256=K9swocTUnqqiSgARo6GmuzTzUySSpyqqPPW4xF7ZU-g,9659
|
13
|
-
unitysvc_services/utils.py,sha256=
|
13
|
+
unitysvc_services/utils.py,sha256=4tEBdO90XpkS6j73ZXnz5dNLVXJaPUILWMwzk5_fUQ4,15276
|
14
14
|
unitysvc_services/validator.py,sha256=sasMpdDhoWZJZ-unrnDhaJfyxHSm3IgQjEdmPvXrFEE,29525
|
15
15
|
unitysvc_services/models/__init__.py,sha256=hJCc2KSZmIHlKWKE6GpLGdeVB6LIpyVUKiOKnwmKvCs,200
|
16
|
-
unitysvc_services/models/base.py,sha256=
|
16
|
+
unitysvc_services/models/base.py,sha256=ofdxWkzSxCB3JqMCnNHL83KfWlXZ9A4HbNRtKG6jcMQ,18333
|
17
17
|
unitysvc_services/models/listing_v1.py,sha256=PPb9hIdWQp80AWKLxFXYBDcWXzNcDrO4v6rqt5_i2qo,3083
|
18
18
|
unitysvc_services/models/provider_v1.py,sha256=76EK1i0hVtdx_awb00-ZMtSj4Oc9Zp4xZ-DeXmG3iTY,2701
|
19
19
|
unitysvc_services/models/seller_v1.py,sha256=oll2ZZBPBDX8wslHrbsCKf_jIqHNte2VEj5RJ9bawR4,3520
|
20
20
|
unitysvc_services/models/service_v1.py,sha256=Xpk-K-95M1LRqYM8nNJcll8t-lsW9Xdi2_bVbYNs8-M,3019
|
21
|
-
unitysvc_services-0.1.
|
22
|
-
unitysvc_services-0.1.
|
23
|
-
unitysvc_services-0.1.
|
24
|
-
unitysvc_services-0.1.
|
25
|
-
unitysvc_services-0.1.
|
26
|
-
unitysvc_services-0.1.
|
21
|
+
unitysvc_services-0.1.7.dist-info/licenses/LICENSE,sha256=_p8V6A8OMPu2HIztn3O01v0-urZFwk0Dd3Yk_PTIlL8,1065
|
22
|
+
unitysvc_services-0.1.7.dist-info/METADATA,sha256=WrTTofhHZReTM3ArgJ8E76Cb-tgJpJikNfL8bBg58Go,7234
|
23
|
+
unitysvc_services-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
24
|
+
unitysvc_services-0.1.7.dist-info/entry_points.txt,sha256=RBhVHKky3rsOly4jVa29c7UAw5ZwNNWnttmtzozr5O0,97
|
25
|
+
unitysvc_services-0.1.7.dist-info/top_level.txt,sha256=GIotQj-Ro2ruR7eupM1r58PWqIHTAq647ORL7E2kneo,18
|
26
|
+
unitysvc_services-0.1.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|