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.
@@ -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
- tutorials = "tutorials"
42
- code_examples = "code_examples"
43
- use_cases = "use_cases"
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
- best_practices = "best_practices"
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):
@@ -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, base_path, listing=listing, offering=offering, provider=provider, seller=seller
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, base_path, listing=listing, offering=offering, provider=provider, seller=seller
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
- # Match both "code_example" and "code_examples"
73
+ # Check if this is a code example document
74
74
  category = doc.get("category", "")
75
- if category == DocumentCategoryEnum.code_examples:
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": doc.get("expect"), # Expected output substring for validation
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
- # 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}"
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"]
@@ -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
- return json.load(f), "json"
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
- return tomllib.load(f), "toml"
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
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-services
3
- Version: 0.1.5
3
+ Version: 0.1.7
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>
@@ -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=mKlY7-zEP8dtldcO8DJtbXVrdOACZCYsu49yP2iSD4w,53855
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=ZwWr2WCfzRBY39tuijK5P2v0VZ7PXwg3tFtJkTOsGrU,28341
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=9htuxyzEjjbbAi704td9mOrtPzSOJzY46ALuup6bJW0,13093
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=cSxOYs_YSrqkQLBKIGVZmK627MyMft7cDn0WnrQlzPM,18736
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.5.dist-info/licenses/LICENSE,sha256=_p8V6A8OMPu2HIztn3O01v0-urZFwk0Dd3Yk_PTIlL8,1065
22
- unitysvc_services-0.1.5.dist-info/METADATA,sha256=LZrIRYvKsl0t2mptoidJVkjOeNJFbx_a-QgUxEJLsgY,7234
23
- unitysvc_services-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- unitysvc_services-0.1.5.dist-info/entry_points.txt,sha256=RBhVHKky3rsOly4jVa29c7UAw5ZwNNWnttmtzozr5O0,97
25
- unitysvc_services-0.1.5.dist-info/top_level.txt,sha256=GIotQj-Ro2ruR7eupM1r58PWqIHTAq647ORL7E2kneo,18
26
- unitysvc_services-0.1.5.dist-info/RECORD,,
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,,