unitysvc-services 0.1.5__tar.gz → 0.1.6__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.1.5/src/unitysvc_services.egg-info → unitysvc_services-0.1.6}/PKG-INFO +1 -1
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/code-examples.md +144 -30
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/data-structure.md +142 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/pyproject.toml +1 -1
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/base.py +0 -12
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/publisher.py +52 -2
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/test.py +37 -10
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/utils.py +65 -2
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6/src/unitysvc_services.egg-info}/PKG-INFO +1 -1
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/test_utils.py +178 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/CONTRIBUTING.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/HISTORY.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/LICENSE +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/MANIFEST.in +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/README.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/api-reference.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/cli-reference.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/contributing.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/development.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/documenting-services.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/file-schemas.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/getting-started.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/index.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/installation.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/usage.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/docs/workflows.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/setup.cfg +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/__init__.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/api.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/cli.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/format_data.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/list.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/__init__.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/listing_v1.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/provider_v1.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/seller_v1.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/service_v1.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/populate.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/py.typed +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/query.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/scaffold.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/update.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/validator.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/SOURCES.txt +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/dependency_links.txt +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/entry_points.txt +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/requires.txt +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/top_level.txt +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/__init__.py +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/README.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider1/README.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider1/provider.toml +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider1/services/service1/code-example.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider1/services/service1/service.toml +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider1/services/service1/svcreseller.toml +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider1/terms-of-service.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider2/README.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider2/provider.json +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider2/services/service2/code-example.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider2/services/service2/service.json +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider2/services/service2/svcreseller.json +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider2/terms-of-service.md +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/seller.json +0 -0
- {unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/test_validator.py +0 -0
@@ -77,8 +77,10 @@ Code examples are referenced in your `listing.json` or `listing.toml` file under
|
|
77
77
|
"file_path": "../../docs/example.py.j2",
|
78
78
|
"mime_type": "python",
|
79
79
|
"is_public": true,
|
80
|
-
"
|
81
|
-
|
80
|
+
"meta": {
|
81
|
+
"requirements": ["httpx"],
|
82
|
+
"expect": "✓ Test passed"
|
83
|
+
}
|
82
84
|
}
|
83
85
|
]
|
84
86
|
}
|
@@ -94,10 +96,25 @@ Code examples are referenced in your `listing.json` or `listing.toml` file under
|
|
94
96
|
- **`mime_type`**: File type (`python`, `javascript`, `shell`, etc.)
|
95
97
|
- **`is_public`**: Should be `true` for code examples
|
96
98
|
|
97
|
-
**Optional but Recommended Fields:**
|
99
|
+
**Optional but Recommended Fields (in `meta` object):**
|
98
100
|
|
99
|
-
- **`requirements`**: Package dependencies
|
100
|
-
-
|
101
|
+
- **`meta.requirements`**: _(User-maintained)_ Package dependencies needed to run the code example
|
102
|
+
- For Python: PyPI packages (e.g., `["httpx", "openai"]`)
|
103
|
+
- For JavaScript: npm packages (e.g., `["node-fetch"]`)
|
104
|
+
- For Shell scripts: commands (e.g., `["curl"]`)
|
105
|
+
- Helps users understand what to install before running the example
|
106
|
+
- **`meta.expect`**: _(User-maintained)_ Expected substring in stdout for validation
|
107
|
+
- Examples: `"✓ Test passed"`, `"\"choices\""`, `"Status: 200"`
|
108
|
+
- If specified, test passes only if stdout contains this string
|
109
|
+
- Without this field, tests only check exit code (0 = pass, non-zero = fail)
|
110
|
+
|
111
|
+
**System-Maintained Fields (in `meta` object):**
|
112
|
+
|
113
|
+
- **`meta.output`**: _(System-maintained)_ Actual output from successful test execution
|
114
|
+
- Automatically populated by `usvc test run` when a test passes
|
115
|
+
- Contains the stdout from the last successful test run
|
116
|
+
- Included in your service listing during `usvc publish`
|
117
|
+
- Displayed alongside code examples for documentation
|
101
118
|
|
102
119
|
### 4. Use Relative Paths
|
103
120
|
|
@@ -220,8 +237,11 @@ usvc test run --verbose
|
|
220
237
|
3. Sets environment variables (`API_KEY`, `API_ENDPOINT`) from provider credentials
|
221
238
|
4. Executes the code example using appropriate interpreter (python3, node, bash)
|
222
239
|
5. Validates results:
|
223
|
-
- Test passes if exit code is 0 AND (no `expect` field OR expected string found in stdout)
|
240
|
+
- Test passes if exit code is 0 AND (no `meta.expect` field OR expected string found in stdout)
|
224
241
|
- Test fails if exit code is non-zero OR expected string not found
|
242
|
+
6. **Saves output**: When a test passes, stdout is saved to listing directory as `{listing_stem}_{code_filename}.out`
|
243
|
+
- Example: For `svclisting.json` with code file `test.py`, output is saved as `svclisting_test.py.out`
|
244
|
+
- Saved in the same directory as the listing file for easy version control
|
225
245
|
|
226
246
|
**Failed test debugging:**
|
227
247
|
|
@@ -382,8 +402,10 @@ Reference the code example in your `listing.json` file.
|
|
382
402
|
"file_path": "../../docs/test.py.j2",
|
383
403
|
"mime_type": "python",
|
384
404
|
"is_public": true,
|
385
|
-
"
|
386
|
-
|
405
|
+
"meta": {
|
406
|
+
"requirements": ["httpx"],
|
407
|
+
"expect": "✓ Test passed"
|
408
|
+
}
|
387
409
|
}
|
388
410
|
]
|
389
411
|
}
|
@@ -398,17 +420,23 @@ Reference the code example in your `listing.json` file.
|
|
398
420
|
- `file_path`: Relative path from listing file to your `.j2` template
|
399
421
|
- `mime_type`: File type (`python`, `javascript`, `shell`, etc.)
|
400
422
|
- `is_public`: Should be `true` for code examples
|
401
|
-
- `
|
402
|
-
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
-
|
407
|
-
-
|
408
|
-
-
|
409
|
-
|
410
|
-
|
411
|
-
|
423
|
+
- `meta`: **[Optional]** Metadata object containing:
|
424
|
+
- `requirements`: _(User-maintained)_ List of package dependencies needed to run the code example
|
425
|
+
- For Python: PyPI packages (e.g., `["httpx", "openai"]`)
|
426
|
+
- For JavaScript: npm packages (e.g., `["node-fetch"]`)
|
427
|
+
- For Shell scripts: commands (e.g., `["curl"]`)
|
428
|
+
- Helps users understand what to install before running the example
|
429
|
+
- `expect`: _(User-maintained, strongly recommended)_ Expected substring that should appear in stdout when the test passes
|
430
|
+
- Examples:
|
431
|
+
- `"✓ Test passed"` - Explicit success message
|
432
|
+
- `"\"choices\""` - Check for JSON field in API response
|
433
|
+
- `"Status: 200"` - Check for HTTP status
|
434
|
+
- Without this field, tests only check exit code (0 = pass, non-zero = fail), which is unreliable
|
435
|
+
- `output`: _(System-maintained)_ Automatically populated by `usvc test run`
|
436
|
+
- Contains stdout from the last successful test execution
|
437
|
+
- Saved to `{listing_stem}_{code_filename}.out` file during test run
|
438
|
+
- Embedded into `meta.output` during `usvc publish`
|
439
|
+
- Displayed alongside code examples in your service listing
|
412
440
|
|
413
441
|
### Step 5: Validate and Test Before Publishing
|
414
442
|
|
@@ -500,8 +528,14 @@ curl ${API_ENDPOINT}/chat/completions \
|
|
500
528
|
|
501
529
|
```json
|
502
530
|
{
|
531
|
+
"category": "code_examples",
|
532
|
+
"title": "Shell Example",
|
503
533
|
"file_path": "test.sh.j2",
|
504
|
-
"
|
534
|
+
"mime_type": "bash",
|
535
|
+
"is_public": true,
|
536
|
+
"meta": {
|
537
|
+
"expect": "\"choices\""
|
538
|
+
}
|
505
539
|
}
|
506
540
|
```
|
507
541
|
|
@@ -532,9 +566,15 @@ if "choices" in data:
|
|
532
566
|
|
533
567
|
```json
|
534
568
|
{
|
569
|
+
"category": "code_examples",
|
570
|
+
"title": "Python Example",
|
535
571
|
"file_path": "test.py.j2",
|
536
|
-
"
|
537
|
-
"
|
572
|
+
"mime_type": "python",
|
573
|
+
"is_public": true,
|
574
|
+
"meta": {
|
575
|
+
"requirements": ["httpx"],
|
576
|
+
"expect": "✓ Validation passed"
|
577
|
+
}
|
538
578
|
}
|
539
579
|
```
|
540
580
|
|
@@ -568,8 +608,14 @@ if (data.choices) {
|
|
568
608
|
|
569
609
|
```json
|
570
610
|
{
|
611
|
+
"category": "code_examples",
|
612
|
+
"title": "JavaScript Example",
|
571
613
|
"file_path": "test.js.j2",
|
572
|
-
"
|
614
|
+
"mime_type": "javascript",
|
615
|
+
"is_public": true,
|
616
|
+
"meta": {
|
617
|
+
"expect": "✓ Success"
|
618
|
+
}
|
573
619
|
}
|
574
620
|
```
|
575
621
|
|
@@ -625,12 +671,12 @@ If a field might not exist, use Jinja2 defaults:
|
|
625
671
|
## Tips for Effective Code Examples
|
626
672
|
|
627
673
|
1. **Keep examples short and focused** - Test one thing at a time
|
628
|
-
2. **Use `expect` field** - Makes validation automatic and reliable
|
674
|
+
2. **Use `meta.expect` field** - Makes validation automatic and reliable
|
629
675
|
3. **Print clear success messages** - Makes debugging easier
|
630
676
|
4. **Handle errors gracefully** - Exit with non-zero code on failure
|
631
677
|
5. **Test locally first** - Always verify with hardcoded values before templating
|
632
678
|
6. **Use meaningful output** - Print enough info to understand what happened
|
633
|
-
7. **Add requirements** - List all dependencies in
|
679
|
+
7. **Add requirements** - List all dependencies in `meta.requirements` field
|
634
680
|
|
635
681
|
## Workflow Summary
|
636
682
|
|
@@ -648,21 +694,89 @@ python3 test.py
|
|
648
694
|
mv test.py test.py.j2
|
649
695
|
vim test.py.j2 # Replace with {{ offering.name }}, etc.
|
650
696
|
|
651
|
-
# 4. Add to listing.json
|
652
|
-
vim listing.json # Add document entry
|
697
|
+
# 4. Add to listing.json with meta fields
|
698
|
+
vim listing.json # Add document entry with meta.requirements and meta.expect
|
653
699
|
|
654
700
|
# 5. Validate and test
|
655
701
|
usvc validate
|
656
702
|
usvc test list
|
657
703
|
usvc test run --provider your-provider
|
704
|
+
# ✓ Successful tests create .out files (e.g., listing_test.py.out in listing directory)
|
658
705
|
|
659
706
|
# 6. Debug if needed
|
660
|
-
cat failed_* # Check saved test files
|
707
|
+
cat failed_* # Check saved test files (in current directory)
|
708
|
+
cat services/*/listing_*.out # Review successful test outputs (in listing directories)
|
661
709
|
|
662
|
-
# 7. Publish
|
710
|
+
# 7. Publish - embeds .out files into meta.output
|
663
711
|
usvc publish
|
712
|
+
# ✓ Reads .out files from listing directories and adds content to meta.output
|
713
|
+
# ✓ Output will appear alongside code examples in your service listing
|
664
714
|
```
|
665
715
|
|
716
|
+
## Understanding meta.output Workflow
|
717
|
+
|
718
|
+
The `meta.output` field follows an automated workflow from test execution to publication:
|
719
|
+
|
720
|
+
### 1. Testing Phase: `usvc test run`
|
721
|
+
|
722
|
+
When you run tests, successful executions generate `.out` files:
|
723
|
+
|
724
|
+
```bash
|
725
|
+
$ usvc test run --provider fireworks
|
726
|
+
|
727
|
+
Testing: llama-3-1-405b - Python code example
|
728
|
+
✓ Success (exit code: 0)
|
729
|
+
→ Output saved to: /path/to/fireworks/services/llama-3-1-405b/listing_test.py.out
|
730
|
+
```
|
731
|
+
|
732
|
+
**Output file naming:** `{listing_stem}_{code_filename}.out`
|
733
|
+
|
734
|
+
- `listing_stem`: The listing filename without extension (e.g., "listing" from "listing.json")
|
735
|
+
- `code_filename`: The code filename after template expansion (e.g., "test.py" from "test.py.j2")
|
736
|
+
- Example: `listing_test.py.out`, `svclisting_example.sh.out`
|
737
|
+
|
738
|
+
**File location:** Same directory as the listing file that references the code example
|
739
|
+
|
740
|
+
### 2. Publishing Phase: `usvc publish`
|
741
|
+
|
742
|
+
During publish, the SDK automatically:
|
743
|
+
|
744
|
+
1. Expands `.j2` templates for each model if a template is used
|
745
|
+
2. Looks for matching `.out` files in the listing's base directory
|
746
|
+
3. Reads the output content and embeds it into `meta.output`
|
747
|
+
|
748
|
+
**Example published document:**
|
749
|
+
|
750
|
+
```json
|
751
|
+
{
|
752
|
+
"category": "code_examples",
|
753
|
+
"title": "Python code example",
|
754
|
+
"file_path": "chat-completion.py",
|
755
|
+
"file_content": "#!/usr/bin/env python3\nimport httpx...",
|
756
|
+
"mime_type": "python",
|
757
|
+
"meta": {
|
758
|
+
"requirements": ["httpx"],
|
759
|
+
"expect": "✓ Test passed",
|
760
|
+
"output": "{'id': 'chatcmpl-...', 'choices': [{'message': {'content': 'Hello!'}}]}\n✓ Test passed"
|
761
|
+
}
|
762
|
+
}
|
763
|
+
```
|
764
|
+
|
765
|
+
### 3. Display in Service Listing
|
766
|
+
|
767
|
+
After publishing, the output will automatically appear alongside the code example in your service listing documentation, allowing users to see both the code and its expected output together.
|
768
|
+
|
769
|
+
### 4. Key Points
|
770
|
+
|
771
|
+
- **`.out` files are model-specific**: Since templates expand per model, each model gets its own output file
|
772
|
+
- **`.out` files location**: Saved in the **same directory as the listing file**, making them easy to find and version control
|
773
|
+
- **`.out` file naming**: Format is `{listing_stem}_{code_filename}.out` to clearly associate output with both listing and code
|
774
|
+
- **Version control**: You **can** commit `.out` files to version control since they're co-located with listings
|
775
|
+
- **Publishing is flexible**: `usvc publish` works even if `.out` files are missing (gracefully skips)
|
776
|
+
- **User vs System fields**:
|
777
|
+
- `meta.requirements` and `meta.expect` are **user-maintained** (you write these)
|
778
|
+
- `meta.output` is **system-maintained** (auto-generated by `usvc test run` and `usvc publish`)
|
779
|
+
|
666
780
|
## Interpreter Detection
|
667
781
|
|
668
782
|
The test framework automatically detects the appropriate interpreter based on file extension:
|
@@ -710,7 +824,7 @@ If the required interpreter is not found, the test will fail with a clear error
|
|
710
824
|
|
711
825
|
**Problem:** Exit code is 0 but test still fails
|
712
826
|
|
713
|
-
**Solution:** Check the `expect` field - test requires the expected string to appear in stdout
|
827
|
+
**Solution:** Check the `meta.expect` field - test requires the expected string to appear in stdout
|
714
828
|
|
715
829
|
### Validation Errors
|
716
830
|
|
@@ -114,6 +114,148 @@ display_name = "My Service"
|
|
114
114
|
description = "A high-performance digital service"
|
115
115
|
```
|
116
116
|
|
117
|
+
## Override Files
|
118
|
+
|
119
|
+
Override files provide a way to complement and customize data files without modifying the base files. This is particularly useful when base files are auto-generated by scripts, and you need to manually curate specific values like status, logo URLs, or other metadata.
|
120
|
+
|
121
|
+
### Naming Pattern
|
122
|
+
|
123
|
+
Override files follow the pattern: `<base_name>.override.<extension>`
|
124
|
+
|
125
|
+
Examples:
|
126
|
+
- `service.json` → `service.override.json`
|
127
|
+
- `provider.toml` → `provider.override.toml`
|
128
|
+
- `listing-premium.json` → `listing-premium.override.json`
|
129
|
+
|
130
|
+
### How Override Files Work
|
131
|
+
|
132
|
+
When any data file is loaded by the SDK:
|
133
|
+
|
134
|
+
1. The base file is loaded first
|
135
|
+
2. The system checks for a corresponding `.override.*` file
|
136
|
+
3. If found, the override file is loaded and **deep-merged** into the base data
|
137
|
+
4. Override values take precedence over base values
|
138
|
+
|
139
|
+
This merge happens automatically and transparently - you don't need to do anything special.
|
140
|
+
|
141
|
+
### Merge Behavior
|
142
|
+
|
143
|
+
**Nested Dictionaries**: Recursively merged - override values complement and replace base values
|
144
|
+
|
145
|
+
**Example**:
|
146
|
+
```json
|
147
|
+
// Base: service.json
|
148
|
+
{
|
149
|
+
"name": "my-service",
|
150
|
+
"config": {
|
151
|
+
"host": "localhost",
|
152
|
+
"port": 8080,
|
153
|
+
"timeout": 30
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
// Override: service.override.json
|
158
|
+
{
|
159
|
+
"config": {
|
160
|
+
"port": 9000,
|
161
|
+
"ssl": true
|
162
|
+
},
|
163
|
+
"status": "active"
|
164
|
+
}
|
165
|
+
|
166
|
+
// Merged Result
|
167
|
+
{
|
168
|
+
"name": "my-service",
|
169
|
+
"config": {
|
170
|
+
"host": "localhost", // preserved from base
|
171
|
+
"port": 9000, // overridden
|
172
|
+
"timeout": 30, // preserved from base
|
173
|
+
"ssl": true // added from override
|
174
|
+
},
|
175
|
+
"status": "active" // added from override
|
176
|
+
}
|
177
|
+
```
|
178
|
+
|
179
|
+
**Lists and Primitives**: Completely replaced (not merged)
|
180
|
+
|
181
|
+
```json
|
182
|
+
// Base: service.json
|
183
|
+
{
|
184
|
+
"tags": ["python", "web", "api"],
|
185
|
+
"version": 1
|
186
|
+
}
|
187
|
+
|
188
|
+
// Override: service.override.json
|
189
|
+
{
|
190
|
+
"tags": ["backend", "production"]
|
191
|
+
}
|
192
|
+
|
193
|
+
// Merged Result
|
194
|
+
{
|
195
|
+
"tags": ["backend", "production"], // completely replaced
|
196
|
+
"version": 1
|
197
|
+
}
|
198
|
+
```
|
199
|
+
|
200
|
+
This replacement behavior for lists is intentional and predictable - if you want to change a list, simply specify the complete desired list in the override file.
|
201
|
+
|
202
|
+
### Common Use Cases
|
203
|
+
|
204
|
+
**1. Manual curation of auto-generated data**
|
205
|
+
```json
|
206
|
+
// service.json (auto-generated by script)
|
207
|
+
{
|
208
|
+
"schema": "service_v1",
|
209
|
+
"name": "gpt-4",
|
210
|
+
"description": "Auto-generated description",
|
211
|
+
"status": "draft"
|
212
|
+
}
|
213
|
+
|
214
|
+
// service.override.json (manually maintained)
|
215
|
+
{
|
216
|
+
"status": "active",
|
217
|
+
"logo_url": "https://example.com/custom-logo.png",
|
218
|
+
"featured": true
|
219
|
+
}
|
220
|
+
```
|
221
|
+
|
222
|
+
**2. Environment-specific configuration**
|
223
|
+
```toml
|
224
|
+
# provider.toml (base configuration)
|
225
|
+
schema = "provider_v1"
|
226
|
+
name = "my-provider"
|
227
|
+
api_endpoint = "https://api.example.com"
|
228
|
+
|
229
|
+
# provider.override.toml (local testing overrides)
|
230
|
+
api_endpoint = "http://localhost:8000"
|
231
|
+
debug_mode = true
|
232
|
+
```
|
233
|
+
|
234
|
+
**3. Maintaining custom metadata**
|
235
|
+
```json
|
236
|
+
// listing-premium.json (generated from template)
|
237
|
+
{
|
238
|
+
"schema": "listing_v1",
|
239
|
+
"name": "premium-tier",
|
240
|
+
"pricing": { /* auto-generated */ }
|
241
|
+
}
|
242
|
+
|
243
|
+
// listing-premium.override.json (manual customization)
|
244
|
+
{
|
245
|
+
"featured": true,
|
246
|
+
"promotional_badge": "Best Value",
|
247
|
+
"custom_cta": "Try Premium Now!"
|
248
|
+
}
|
249
|
+
```
|
250
|
+
|
251
|
+
### Version Control
|
252
|
+
|
253
|
+
Override files should be committed to version control alongside base files. They are part of your data and represent intentional manual customizations that should be preserved and tracked.
|
254
|
+
|
255
|
+
### File Location
|
256
|
+
|
257
|
+
Override files must be in the same directory as their corresponding base files, following the directory structure rules described above.
|
258
|
+
|
117
259
|
## Schema Requirements
|
118
260
|
|
119
261
|
Each file must include a `schema` field identifying its type:
|
@@ -255,18 +255,6 @@ 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
|
-
)
|
270
258
|
|
271
259
|
|
272
260
|
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,
|
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
|
@@ -79,6 +79,9 @@ def extract_code_examples_from_listing(listing_data: dict[str, Any], listing_fil
|
|
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"]
|
@@ -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
|
"""
|
@@ -6,6 +6,8 @@ import pytest
|
|
6
6
|
|
7
7
|
from unitysvc_services.utils import (
|
8
8
|
convert_convenience_fields_to_documents,
|
9
|
+
deep_merge_dicts,
|
10
|
+
load_data_file,
|
9
11
|
resolve_provider_name,
|
10
12
|
resolve_service_name_for_listing,
|
11
13
|
)
|
@@ -221,3 +223,179 @@ def test_convert_mime_type_detection(tmp_path: Path) -> None:
|
|
221
223
|
data = {"logo": file_path}
|
222
224
|
result = convert_convenience_fields_to_documents(data, tmp_path, terms_field=None)
|
223
225
|
assert result["documents"][0]["mime_type"] == expected_mime
|
226
|
+
|
227
|
+
|
228
|
+
def test_deep_merge_dicts_simple() -> None:
|
229
|
+
"""Test simple dictionary merge."""
|
230
|
+
base = {"a": 1, "b": 2, "c": 3}
|
231
|
+
override = {"b": 20, "d": 4}
|
232
|
+
|
233
|
+
result = deep_merge_dicts(base, override)
|
234
|
+
|
235
|
+
assert result == {"a": 1, "b": 20, "c": 3, "d": 4}
|
236
|
+
|
237
|
+
|
238
|
+
def test_deep_merge_dicts_nested() -> None:
|
239
|
+
"""Test deep merge with nested dictionaries."""
|
240
|
+
base = {"config": {"host": "localhost", "port": 8080}, "name": "service1"}
|
241
|
+
override = {"config": {"port": 9000, "ssl": True}, "status": "active"}
|
242
|
+
|
243
|
+
result = deep_merge_dicts(base, override)
|
244
|
+
|
245
|
+
assert result == {
|
246
|
+
"config": {"host": "localhost", "port": 9000, "ssl": True},
|
247
|
+
"name": "service1",
|
248
|
+
"status": "active",
|
249
|
+
}
|
250
|
+
|
251
|
+
|
252
|
+
def test_deep_merge_dicts_lists_replaced() -> None:
|
253
|
+
"""Test that lists are replaced, not merged."""
|
254
|
+
base = {"tags": ["python", "web"], "name": "service1"}
|
255
|
+
override = {"tags": ["backend"]}
|
256
|
+
|
257
|
+
result = deep_merge_dicts(base, override)
|
258
|
+
|
259
|
+
# Lists should be replaced, not merged
|
260
|
+
assert result == {"tags": ["backend"], "name": "service1"}
|
261
|
+
|
262
|
+
|
263
|
+
def test_deep_merge_dicts_empty_override() -> None:
|
264
|
+
"""Test merge with empty override."""
|
265
|
+
base = {"a": 1, "b": 2}
|
266
|
+
override: dict[str, int] = {}
|
267
|
+
|
268
|
+
result = deep_merge_dicts(base, override)
|
269
|
+
|
270
|
+
assert result == {"a": 1, "b": 2}
|
271
|
+
|
272
|
+
|
273
|
+
def test_deep_merge_dicts_deeply_nested() -> None:
|
274
|
+
"""Test deeply nested dictionary merge."""
|
275
|
+
base = {"level1": {"level2": {"level3": {"value": "old", "keep": True}}}}
|
276
|
+
override = {"level1": {"level2": {"level3": {"value": "new"}}}}
|
277
|
+
|
278
|
+
result = deep_merge_dicts(base, override)
|
279
|
+
|
280
|
+
assert result == {"level1": {"level2": {"level3": {"value": "new", "keep": True}}}}
|
281
|
+
|
282
|
+
|
283
|
+
def test_load_data_file_json_no_override(tmp_path: Path) -> None:
|
284
|
+
"""Test loading JSON file without override."""
|
285
|
+
import json
|
286
|
+
|
287
|
+
# Create a base JSON file
|
288
|
+
base_file = tmp_path / "test.json"
|
289
|
+
base_data = {"schema": "test_v1", "name": "test", "value": 100}
|
290
|
+
with open(base_file, "w", encoding="utf-8") as f:
|
291
|
+
json.dump(base_data, f)
|
292
|
+
|
293
|
+
data, file_format = load_data_file(base_file)
|
294
|
+
|
295
|
+
assert file_format == "json"
|
296
|
+
assert data == base_data
|
297
|
+
|
298
|
+
|
299
|
+
def test_load_data_file_json_with_override(tmp_path: Path) -> None:
|
300
|
+
"""Test loading JSON file with override."""
|
301
|
+
import json
|
302
|
+
|
303
|
+
# Create base JSON file
|
304
|
+
base_file = tmp_path / "service.json"
|
305
|
+
base_data = {"schema": "service_v1", "name": "my-service", "status": "draft", "version": 1}
|
306
|
+
with open(base_file, "w", encoding="utf-8") as f:
|
307
|
+
json.dump(base_data, f)
|
308
|
+
|
309
|
+
# Create override JSON file
|
310
|
+
override_file = tmp_path / "service.override.json"
|
311
|
+
override_data = {"status": "active", "logo_url": "https://example.com/logo.png"}
|
312
|
+
with open(override_file, "w", encoding="utf-8") as f:
|
313
|
+
json.dump(override_data, f)
|
314
|
+
|
315
|
+
data, file_format = load_data_file(base_file)
|
316
|
+
|
317
|
+
assert file_format == "json"
|
318
|
+
assert data == {
|
319
|
+
"schema": "service_v1",
|
320
|
+
"name": "my-service",
|
321
|
+
"status": "active", # overridden
|
322
|
+
"version": 1,
|
323
|
+
"logo_url": "https://example.com/logo.png", # added from override
|
324
|
+
}
|
325
|
+
|
326
|
+
|
327
|
+
def test_load_data_file_toml_no_override(tmp_path: Path) -> None:
|
328
|
+
"""Test loading TOML file without override."""
|
329
|
+
import tomli_w
|
330
|
+
|
331
|
+
# Create a base TOML file
|
332
|
+
base_file = tmp_path / "test.toml"
|
333
|
+
base_data = {"schema": "test_v1", "name": "test", "value": 100}
|
334
|
+
with open(base_file, "wb") as f:
|
335
|
+
tomli_w.dump(base_data, f)
|
336
|
+
|
337
|
+
data, file_format = load_data_file(base_file)
|
338
|
+
|
339
|
+
assert file_format == "toml"
|
340
|
+
assert data == base_data
|
341
|
+
|
342
|
+
|
343
|
+
def test_load_data_file_toml_with_override(tmp_path: Path) -> None:
|
344
|
+
"""Test loading TOML file with override."""
|
345
|
+
import tomli_w
|
346
|
+
|
347
|
+
# Create base TOML file
|
348
|
+
base_file = tmp_path / "provider.toml"
|
349
|
+
base_data = {"schema": "provider_v1", "name": "my-provider", "tier": "free"}
|
350
|
+
with open(base_file, "wb") as f:
|
351
|
+
tomli_w.dump(base_data, f)
|
352
|
+
|
353
|
+
# Create override TOML file
|
354
|
+
override_file = tmp_path / "provider.override.toml"
|
355
|
+
override_data = {"tier": "premium", "featured": True}
|
356
|
+
with open(override_file, "wb") as f:
|
357
|
+
tomli_w.dump(override_data, f)
|
358
|
+
|
359
|
+
data, file_format = load_data_file(base_file)
|
360
|
+
|
361
|
+
assert file_format == "toml"
|
362
|
+
assert data == {
|
363
|
+
"schema": "provider_v1",
|
364
|
+
"name": "my-provider",
|
365
|
+
"tier": "premium", # overridden
|
366
|
+
"featured": True, # added from override
|
367
|
+
}
|
368
|
+
|
369
|
+
|
370
|
+
def test_load_data_file_with_nested_override(tmp_path: Path) -> None:
|
371
|
+
"""Test loading file with nested dictionary override."""
|
372
|
+
import json
|
373
|
+
|
374
|
+
# Create base file with nested config
|
375
|
+
base_file = tmp_path / "config.json"
|
376
|
+
base_data = {
|
377
|
+
"schema": "config_v1",
|
378
|
+
"database": {"host": "localhost", "port": 5432, "name": "testdb"},
|
379
|
+
"cache": {"enabled": False},
|
380
|
+
}
|
381
|
+
with open(base_file, "w", encoding="utf-8") as f:
|
382
|
+
json.dump(base_data, f)
|
383
|
+
|
384
|
+
# Create override file with partial nested override
|
385
|
+
override_file = tmp_path / "config.override.json"
|
386
|
+
override_data = {"database": {"port": 3306, "ssl": True}, "cache": {"enabled": True}}
|
387
|
+
with open(override_file, "w", encoding="utf-8") as f:
|
388
|
+
json.dump(override_data, f)
|
389
|
+
|
390
|
+
data, file_format = load_data_file(base_file)
|
391
|
+
|
392
|
+
assert data == {
|
393
|
+
"schema": "config_v1",
|
394
|
+
"database": {
|
395
|
+
"host": "localhost", # preserved from base
|
396
|
+
"port": 3306, # overridden
|
397
|
+
"name": "testdb", # preserved from base
|
398
|
+
"ssl": True, # added from override
|
399
|
+
},
|
400
|
+
"cache": {"enabled": True}, # overridden
|
401
|
+
}
|
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
|
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/__init__.py
RENAMED
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/listing_v1.py
RENAMED
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/provider_v1.py
RENAMED
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services/models/seller_v1.py
RENAMED
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/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.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/SOURCES.txt
RENAMED
File without changes
|
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/entry_points.txt
RENAMED
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/requires.txt
RENAMED
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/src/unitysvc_services.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider1/provider.toml
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider1/terms-of-service.md
RENAMED
File without changes
|
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider2/provider.json
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{unitysvc_services-0.1.5 → unitysvc_services-0.1.6}/tests/example_data/provider2/terms-of-service.md
RENAMED
File without changes
|
File without changes
|
File without changes
|