mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
feat: generate Python client wrappers for various host services
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
37f3b838d2
commit
08b952ef50
@ -19,8 +19,12 @@ hostgen -input <dir> -output <dir> -package <name> [-v] [-dry-run] [-host-only]
|
||||
| `-dry-run` | Parse and validate without writing files | `false` |
|
||||
| `-host-only` | Generate only host-side wrapper code | `false` |
|
||||
| `-plugin-only` | Generate only plugin/client-side wrapper code | `false` |
|
||||
| `-go` | Generate Go client wrappers | `true`* |
|
||||
| `-python` | Generate Python client wrappers | `false` |
|
||||
|
||||
By default, both host and plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type.
|
||||
\* `-go` is enabled by default when `-python` is not specified. Use both `-go -python` to generate both.
|
||||
|
||||
By default, both host and Go plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type. Use `-python` to generate Python wrappers.
|
||||
|
||||
### Example
|
||||
|
||||
@ -192,8 +196,68 @@ Generated files are named `nd_host_<servicename>.go` (lowercase) and placed in t
|
||||
```
|
||||
output/
|
||||
├── subsonicapi_gen.go # Host-side code (for Navidrome)
|
||||
└── go/
|
||||
└── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
|
||||
├── go/
|
||||
│ └── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
|
||||
└── python/
|
||||
└── nd_host_subsonicapi.py # Plugin-side code (for Python plugins)
|
||||
```
|
||||
|
||||
### Python Client Code (extism-py WASM)
|
||||
|
||||
Generated files are named `nd_host_<servicename>.py` (lowercase) and placed in the `python/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with extism-py. Each file includes:
|
||||
|
||||
- `# Code generated by hostgen. DO NOT EDIT.` header
|
||||
- Required imports (`dataclasses`, `typing`, `extism`, `json`)
|
||||
- `HostFunctionError` exception class for error handling
|
||||
- `@extism.import_fn` declarations for raw host functions
|
||||
- `@dataclass` types for methods with multiple return values
|
||||
- Wrapper functions with type hints, docstrings, and snake_case names
|
||||
|
||||
#### Python Type Mapping
|
||||
|
||||
| Go Type | Python Type |
|
||||
|-------------------------|-------------|
|
||||
| `string` | `str` |
|
||||
| `int`, `int32`, `int64` | `int` |
|
||||
| `float32`, `float64` | `float` |
|
||||
| `bool` | `bool` |
|
||||
| `[]byte` | `bytes` |
|
||||
| Unknown | `Any` |
|
||||
|
||||
#### Python Function Naming
|
||||
|
||||
Functions follow PEP 8 snake_case convention:
|
||||
|
||||
| Go Method | Python Function |
|
||||
|-------------------------------|----------------------------------|
|
||||
| `SubsonicAPI.Call` | `subsonicapi_call()` |
|
||||
| `Scheduler.ScheduleRecurring` | `scheduler_schedule_recurring()` |
|
||||
| `Cache.GetString` | `cache_get_string()` |
|
||||
|
||||
#### Multi-Value Returns
|
||||
|
||||
Methods with multiple return values use dataclasses:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class CacheGetStringResult:
|
||||
value: str
|
||||
exists: bool
|
||||
|
||||
def cache_get_string(key: str) -> CacheGetStringResult:
|
||||
...
|
||||
```
|
||||
|
||||
#### Example Python Plugin Usage
|
||||
|
||||
```python
|
||||
from nd_host_subsonicapi import subsonicapi_call, HostFunctionError
|
||||
|
||||
try:
|
||||
response = subsonicapi_call("getAlbumList2?type=random&size=10")
|
||||
data = json.loads(response)
|
||||
except HostFunctionError as e:
|
||||
print(f"API error: {e}")
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@ -205,15 +205,16 @@ type ServiceB interface {
|
||||
|
||||
Describe("code generation", func() {
|
||||
DescribeTable("generates correct host and client output",
|
||||
func(serviceFile, hostExpectedFile, clientExpectedFile string) {
|
||||
func(serviceFile, hostExpectedFile, goClientExpectedFile, pyClientExpectedFile string) {
|
||||
serviceCode := readTestdata(serviceFile)
|
||||
hostExpected := readTestdata(hostExpectedFile)
|
||||
clientExpected := readTestdata(clientExpectedFile)
|
||||
goClientExpected := readTestdata(goClientExpectedFile)
|
||||
pyClientExpected := readTestdata(pyClientExpectedFile)
|
||||
|
||||
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
||||
|
||||
// Generate both host and client code in one run
|
||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
|
||||
// Generate host and both Go and Python client code
|
||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-go", "-python")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
||||
|
||||
@ -223,7 +224,7 @@ type ServiceB interface {
|
||||
|
||||
var hostFiles []string
|
||||
for _, e := range entries {
|
||||
if e.Name() != "go" && !e.IsDir() {
|
||||
if e.Name() != "go" && e.Name() != "python" && !e.IsDir() {
|
||||
hostFiles = append(hostFiles, e.Name())
|
||||
}
|
||||
}
|
||||
@ -240,53 +241,64 @@ type ServiceB interface {
|
||||
|
||||
Expect(string(formattedHostActual)).To(Equal(string(formattedHostExpected)), "Host code mismatch")
|
||||
|
||||
// Verify client code
|
||||
// Verify Go client code
|
||||
goDir := filepath.Join(outputDir, "go")
|
||||
clientEntries, err := os.ReadDir(goDir)
|
||||
goClientEntries, err := os.ReadDir(goDir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(clientEntries).To(HaveLen(1), "Expected exactly one client file")
|
||||
Expect(goClientEntries).To(HaveLen(1), "Expected exactly one Go client file")
|
||||
|
||||
clientActual, err := os.ReadFile(filepath.Join(goDir, clientEntries[0].Name()))
|
||||
goClientActual, err := os.ReadFile(filepath.Join(goDir, goClientEntries[0].Name()))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
formattedClientActual, err := format.Source(clientActual)
|
||||
Expect(err).ToNot(HaveOccurred(), "Generated client code is not valid Go:\n%s", clientActual)
|
||||
formattedGoClientActual, err := format.Source(goClientActual)
|
||||
Expect(err).ToNot(HaveOccurred(), "Generated Go client code is not valid Go:\n%s", goClientActual)
|
||||
|
||||
formattedClientExpected, err := format.Source([]byte(clientExpected))
|
||||
Expect(err).ToNot(HaveOccurred(), "Expected client code is not valid Go")
|
||||
formattedGoClientExpected, err := format.Source([]byte(goClientExpected))
|
||||
Expect(err).ToNot(HaveOccurred(), "Expected Go client code is not valid Go")
|
||||
|
||||
Expect(string(formattedClientActual)).To(Equal(string(formattedClientExpected)), "Client code mismatch")
|
||||
Expect(string(formattedGoClientActual)).To(Equal(string(formattedGoClientExpected)), "Go client code mismatch")
|
||||
|
||||
// Verify Python client code
|
||||
pythonDir := filepath.Join(outputDir, "python")
|
||||
pyClientEntries, err := os.ReadDir(pythonDir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pyClientEntries).To(HaveLen(1), "Expected exactly one Python client file")
|
||||
|
||||
pyClientActual, err := os.ReadFile(filepath.Join(pythonDir, pyClientEntries[0].Name()))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(string(pyClientActual)).To(Equal(pyClientExpected), "Python client code mismatch")
|
||||
},
|
||||
|
||||
Entry("simple string params",
|
||||
"echo_service.go", "echo_expected.go", "echo_client_expected.go"),
|
||||
"echo_service.go", "echo_expected.go", "echo_client_expected.go", "echo_client_expected.py"),
|
||||
|
||||
Entry("multiple simple params (int32)",
|
||||
"math_service.go", "math_expected.go", "math_client_expected.go"),
|
||||
"math_service.go", "math_expected.go", "math_client_expected.go", "math_client_expected.py"),
|
||||
|
||||
Entry("struct param with request type",
|
||||
"store_service.go", "store_expected.go", "store_client_expected.go"),
|
||||
"store_service.go", "store_expected.go", "store_client_expected.go", "store_client_expected.py"),
|
||||
|
||||
Entry("mixed simple and complex params",
|
||||
"list_service.go", "list_expected.go", "list_client_expected.go"),
|
||||
"list_service.go", "list_expected.go", "list_client_expected.go", "list_client_expected.py"),
|
||||
|
||||
Entry("method without error",
|
||||
"counter_service.go", "counter_expected.go", "counter_client_expected.go"),
|
||||
"counter_service.go", "counter_expected.go", "counter_client_expected.go", "counter_client_expected.py"),
|
||||
|
||||
Entry("no params, error only",
|
||||
"ping_service.go", "ping_expected.go", "ping_client_expected.go"),
|
||||
"ping_service.go", "ping_expected.go", "ping_client_expected.go", "ping_client_expected.py"),
|
||||
|
||||
Entry("map and interface types",
|
||||
"meta_service.go", "meta_expected.go", "meta_client_expected.go"),
|
||||
"meta_service.go", "meta_expected.go", "meta_client_expected.go", "meta_client_expected.py"),
|
||||
|
||||
Entry("pointer types",
|
||||
"users_service.go", "users_expected.go", "users_client_expected.go"),
|
||||
"users_service.go", "users_expected.go", "users_client_expected.go", "users_client_expected.py"),
|
||||
|
||||
Entry("multiple returns",
|
||||
"search_service.go", "search_expected.go", "search_client_expected.go"),
|
||||
"search_service.go", "search_expected.go", "search_client_expected.go", "search_client_expected.py"),
|
||||
|
||||
Entry("bytes",
|
||||
"codec_service.go", "codec_expected.go", "codec_client_expected.go"),
|
||||
"codec_service.go", "codec_expected.go", "codec_client_expected.go", "codec_client_expected.py"),
|
||||
)
|
||||
|
||||
It("generates compilable host code for comprehensive service", func() {
|
||||
@ -398,6 +410,120 @@ type Filter2 struct {
|
||||
// Verify .wasm file was created
|
||||
Expect(filepath.Join(clientDir, "plugin.wasm")).To(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("generates Python client code with -python flag", func() {
|
||||
serviceCode := `package testpkg
|
||||
|
||||
import "context"
|
||||
|
||||
//nd:hostservice name=Test permission=test
|
||||
type TestService interface {
|
||||
//nd:hostfunc
|
||||
DoAction(ctx context.Context, input string) (output string, err error)
|
||||
}
|
||||
`
|
||||
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
||||
|
||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-python", "-plugin-only")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
||||
|
||||
// Verify Python client code exists
|
||||
pythonDir := filepath.Join(outputDir, "python")
|
||||
Expect(pythonDir).To(BeADirectory())
|
||||
|
||||
pythonFile := filepath.Join(pythonDir, "nd_host_test.py")
|
||||
Expect(pythonFile).To(BeAnExistingFile())
|
||||
|
||||
content, err := os.ReadFile(pythonFile)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
contentStr := string(content)
|
||||
Expect(contentStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
|
||||
Expect(contentStr).To(ContainSubstring("class HostFunctionError(Exception):"))
|
||||
Expect(contentStr).To(ContainSubstring(`@extism.import_fn("extism:host/user", "test_doaction")`))
|
||||
Expect(contentStr).To(ContainSubstring("def test_do_action(input: str) -> str:"))
|
||||
})
|
||||
|
||||
It("generates both Go and Python client code with -go -python flags", func() {
|
||||
serviceCode := `package testpkg
|
||||
|
||||
import "context"
|
||||
|
||||
//nd:hostservice name=Test permission=test
|
||||
type TestService interface {
|
||||
//nd:hostfunc
|
||||
DoAction(ctx context.Context, input string) (output string, err error)
|
||||
}
|
||||
`
|
||||
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
||||
|
||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-go", "-python", "-plugin-only")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
||||
|
||||
// Verify both Go and Python client code exist
|
||||
goDir := filepath.Join(outputDir, "go")
|
||||
Expect(goDir).To(BeADirectory())
|
||||
Expect(filepath.Join(goDir, "nd_host_test.go")).To(BeAnExistingFile())
|
||||
|
||||
pythonDir := filepath.Join(outputDir, "python")
|
||||
Expect(pythonDir).To(BeADirectory())
|
||||
Expect(filepath.Join(pythonDir, "nd_host_test.py")).To(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("generates Python code with dataclass for multi-value returns", func() {
|
||||
serviceCode := `package testpkg
|
||||
|
||||
import "context"
|
||||
|
||||
//nd:hostservice name=Cache permission=cache
|
||||
type CacheService interface {
|
||||
//nd:hostfunc
|
||||
GetString(ctx context.Context, key string) (value string, exists bool, err error)
|
||||
}
|
||||
`
|
||||
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
||||
|
||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-python", "-plugin-only")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(outputDir, "python", "nd_host_cache.py"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
contentStr := string(content)
|
||||
Expect(contentStr).To(ContainSubstring("@dataclass"))
|
||||
Expect(contentStr).To(ContainSubstring("class CacheGetStringResult:"))
|
||||
Expect(contentStr).To(ContainSubstring("value: str"))
|
||||
Expect(contentStr).To(ContainSubstring("exists: bool"))
|
||||
Expect(contentStr).To(ContainSubstring("def cache_get_string(key: str) -> CacheGetStringResult:"))
|
||||
})
|
||||
|
||||
It("generates Python code for methods with no parameters", func() {
|
||||
serviceCode := `package testpkg
|
||||
|
||||
import "context"
|
||||
|
||||
//nd:hostservice name=Test permission=test
|
||||
type TestService interface {
|
||||
//nd:hostfunc
|
||||
Ping(ctx context.Context) (status string, err error)
|
||||
}
|
||||
`
|
||||
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
||||
|
||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-python", "-plugin-only")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(outputDir, "python", "nd_host_test.py"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
contentStr := string(content)
|
||||
Expect(contentStr).To(ContainSubstring("def test_ping() -> str:"))
|
||||
Expect(contentStr).To(ContainSubstring(`request_bytes = b"{}"`))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -34,6 +34,17 @@ func clientFuncMap(svc Service) template.FuncMap {
|
||||
}
|
||||
}
|
||||
|
||||
// pythonFuncMap returns the template functions for Python client code generation.
|
||||
func pythonFuncMap(svc Service) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"lower": strings.ToLower,
|
||||
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
|
||||
"pythonFunc": func(m Method) string { return m.PythonFunctionName(svc.ExportPrefix()) },
|
||||
"pythonResultType": func(m Method) string { return m.PythonResultTypeName(svc.Name) },
|
||||
"pythonDefault": pythonDefaultValue,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateHost generates the host function wrapper code for a service.
|
||||
func GenerateHost(svc Service, pkgName string) ([]byte, error) {
|
||||
tmplContent, err := templatesFS.ReadFile("templates/host.go.tmpl")
|
||||
@ -101,3 +112,45 @@ func formatDoc(doc string) string {
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// GenerateClientPython generates Python client wrapper code for plugins.
|
||||
func GenerateClientPython(svc Service) ([]byte, error) {
|
||||
tmplContent, err := templatesFS.ReadFile("templates/client_py.py.tmpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading Python client template: %w", err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New("client_py").Funcs(pythonFuncMap(svc)).Parse(string(tmplContent))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing template: %w", err)
|
||||
}
|
||||
|
||||
data := templateData{
|
||||
Service: svc,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return nil, fmt.Errorf("executing template: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// pythonDefaultValue returns a Python default value for response.get() calls.
|
||||
func pythonDefaultValue(p Param) string {
|
||||
switch p.Type {
|
||||
case "string":
|
||||
return `, ""`
|
||||
case "int", "int32", "int64":
|
||||
return ", 0"
|
||||
case "float32", "float64":
|
||||
return ", 0.0"
|
||||
case "bool":
|
||||
return ", False"
|
||||
case "[]byte":
|
||||
return ", b\"\""
|
||||
default:
|
||||
return ", None"
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,6 +310,209 @@ var _ = Describe("Generator", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Python type and name helpers", func() {
|
||||
Describe("ToPythonType", func() {
|
||||
It("should map Go types to Python types", func() {
|
||||
Expect(ToPythonType("string")).To(Equal("str"))
|
||||
Expect(ToPythonType("int")).To(Equal("int"))
|
||||
Expect(ToPythonType("int32")).To(Equal("int"))
|
||||
Expect(ToPythonType("int64")).To(Equal("int"))
|
||||
Expect(ToPythonType("float32")).To(Equal("float"))
|
||||
Expect(ToPythonType("float64")).To(Equal("float"))
|
||||
Expect(ToPythonType("bool")).To(Equal("bool"))
|
||||
Expect(ToPythonType("[]byte")).To(Equal("bytes"))
|
||||
Expect(ToPythonType("unknown")).To(Equal("Any"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ToSnakeCase", func() {
|
||||
It("should convert PascalCase to snake_case", func() {
|
||||
Expect(ToSnakeCase("ScheduleRecurring")).To(Equal("schedule_recurring"))
|
||||
Expect(ToSnakeCase("GetString")).To(Equal("get_string"))
|
||||
Expect(ToSnakeCase("ID")).To(Equal("i_d"))
|
||||
Expect(ToSnakeCase("simple")).To(Equal("simple"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Method.PythonFunctionName", func() {
|
||||
It("should generate snake_case function name with service prefix", func() {
|
||||
m := Method{Name: "GetString"}
|
||||
Expect(m.PythonFunctionName("cache")).To(Equal("cache_get_string"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Param.PythonType", func() {
|
||||
It("should return Python type for parameter", func() {
|
||||
p := NewParam("value", "string")
|
||||
Expect(p.PythonType()).To(Equal("str"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Param.PythonName", func() {
|
||||
It("should return snake_case name for parameter", func() {
|
||||
p := NewParam("ttlSeconds", "int64")
|
||||
Expect(p.PythonName()).To(Equal("ttl_seconds"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GenerateClientPython", func() {
|
||||
It("should generate valid Python code for a simple service", func() {
|
||||
svc := Service{
|
||||
Name: "SubsonicAPI",
|
||||
Permission: "subsonicapi",
|
||||
Interface: "SubsonicAPIService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "Call",
|
||||
HasError: true,
|
||||
Params: []Param{NewParam("uri", "string")},
|
||||
Returns: []Param{NewParam("responseJSON", "string")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientPython(svc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Check for generated header
|
||||
Expect(codeStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
|
||||
|
||||
// Check for imports
|
||||
Expect(codeStr).To(ContainSubstring("from dataclasses import dataclass"))
|
||||
Expect(codeStr).To(ContainSubstring("import extism"))
|
||||
Expect(codeStr).To(ContainSubstring("import json"))
|
||||
|
||||
// Check for exception class
|
||||
Expect(codeStr).To(ContainSubstring("class HostFunctionError(Exception):"))
|
||||
|
||||
// Check for raw import function
|
||||
Expect(codeStr).To(ContainSubstring(`@extism.import_fn("extism:host/user", "subsonicapi_call")`))
|
||||
Expect(codeStr).To(ContainSubstring("def _subsonicapi_call(offset: int) -> int:"))
|
||||
|
||||
// Check for wrapper function with type hints
|
||||
Expect(codeStr).To(ContainSubstring("def subsonicapi_call(uri: str) -> str:"))
|
||||
|
||||
// Check for error handling
|
||||
Expect(codeStr).To(ContainSubstring("raise HostFunctionError(response["))
|
||||
})
|
||||
|
||||
It("should generate dataclass for multi-value returns", func() {
|
||||
svc := Service{
|
||||
Name: "Cache",
|
||||
Permission: "cache",
|
||||
Interface: "CacheService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "GetString",
|
||||
HasError: true,
|
||||
Params: []Param{NewParam("key", "string")},
|
||||
Returns: []Param{
|
||||
NewParam("value", "string"),
|
||||
NewParam("exists", "bool"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientPython(svc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Check for dataclass
|
||||
Expect(codeStr).To(ContainSubstring("@dataclass"))
|
||||
Expect(codeStr).To(ContainSubstring("class CacheGetStringResult:"))
|
||||
Expect(codeStr).To(ContainSubstring("value: str"))
|
||||
Expect(codeStr).To(ContainSubstring("exists: bool"))
|
||||
|
||||
// Check that function returns dataclass
|
||||
Expect(codeStr).To(ContainSubstring("def cache_get_string(key: str) -> CacheGetStringResult:"))
|
||||
Expect(codeStr).To(ContainSubstring("return CacheGetStringResult("))
|
||||
})
|
||||
|
||||
It("should handle methods with no parameters", func() {
|
||||
svc := Service{
|
||||
Name: "Test",
|
||||
Permission: "test",
|
||||
Interface: "TestService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "NoParams",
|
||||
HasError: true,
|
||||
Returns: []Param{NewParam("result", "string")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientPython(svc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Function with no params
|
||||
Expect(codeStr).To(ContainSubstring("def test_no_params() -> str:"))
|
||||
// Empty request
|
||||
Expect(codeStr).To(ContainSubstring(`request_bytes = b"{}"`))
|
||||
})
|
||||
|
||||
It("should handle methods with no return values", func() {
|
||||
svc := Service{
|
||||
Name: "Test",
|
||||
Permission: "test",
|
||||
Interface: "TestService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "NoReturn",
|
||||
HasError: true,
|
||||
Params: []Param{NewParam("input", "string")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientPython(svc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Function returns None
|
||||
Expect(codeStr).To(ContainSubstring("def test_no_return(input: str) -> None:"))
|
||||
})
|
||||
|
||||
It("should generate correct Python defaults for different types", func() {
|
||||
svc := Service{
|
||||
Name: "Test",
|
||||
Permission: "test",
|
||||
Interface: "TestService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "AllTypes",
|
||||
HasError: true,
|
||||
Returns: []Param{
|
||||
NewParam("strVal", "string"),
|
||||
NewParam("intVal", "int64"),
|
||||
NewParam("floatVal", "float64"),
|
||||
NewParam("boolVal", "bool"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientPython(svc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Check defaults in response.get() calls
|
||||
Expect(codeStr).To(ContainSubstring(`response.get("strVal", "")`))
|
||||
Expect(codeStr).To(ContainSubstring(`response.get("intVal", 0)`))
|
||||
Expect(codeStr).To(ContainSubstring(`response.get("floatVal", 0.0)`))
|
||||
Expect(codeStr).To(ContainSubstring(`response.get("boolVal", False)`))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Integration", func() {
|
||||
It("should generate compilable code from parsed source", func() {
|
||||
// This is an integration test that verifies the full pipeline
|
||||
|
||||
94
plugins/cmd/hostgen/internal/templates/client_py.py.tmpl
Normal file
94
plugins/cmd/hostgen/internal/templates/client_py.py.tmpl
Normal file
@ -0,0 +1,94 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the {{.Service.Name}} host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_{{.Service.Name | lower}} import {{range $i, $m := .Service.Methods}}{{if $i}}, {{end}}{{pythonFunc $m}}{{end}}
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
{{- /* Generate raw host function imports */ -}}
|
||||
{{range .Service.Methods}}
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "{{exportName .}}")
|
||||
def _{{exportName .}}(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
{{- end}}
|
||||
{{- /* Generate dataclasses for multi-value returns */ -}}
|
||||
{{range .Service.Methods}}
|
||||
{{- if .NeedsResultClass}}
|
||||
|
||||
|
||||
@dataclass
|
||||
class {{pythonResultType .}}:
|
||||
"""Result type for {{pythonFunc .}}."""
|
||||
{{- range .Returns}}
|
||||
{{.PythonName}}: {{.PythonType}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- /* Generate wrapper functions */ -}}
|
||||
{{range .Service.Methods}}
|
||||
|
||||
|
||||
def {{pythonFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.PythonName}}: {{$p.PythonType}}{{end}}){{if .NeedsResultClass}} -> {{pythonResultType .}}{{else if .HasReturns}} -> {{(index .Returns 0).PythonType}}{{else}} -> None{{end}}:
|
||||
"""{{if .Doc}}{{.Doc}}{{else}}Call the {{exportName .}} host function.{{end}}
|
||||
{{- if .HasParams}}
|
||||
|
||||
Args:
|
||||
{{- range .Params}}
|
||||
{{.PythonName}}: {{.PythonType}} parameter.
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- if .HasReturns}}
|
||||
|
||||
Returns:
|
||||
{{- if .NeedsResultClass}}
|
||||
{{pythonResultType .}} containing{{range .Returns}} {{.PythonName}},{{end}}.
|
||||
{{- else}}
|
||||
{{(index .Returns 0).PythonType}}: The result value.
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
{{- if .HasParams}}
|
||||
request = {
|
||||
{{- range .Params}}
|
||||
"{{.JSONName}}": {{.PythonName}},
|
||||
{{- end}}
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
{{- else}}
|
||||
request_bytes = b"{}"
|
||||
{{- end}}
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _{{exportName .}}(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
{{if .NeedsResultClass}}
|
||||
return {{pythonResultType .}}(
|
||||
{{- range .Returns}}
|
||||
{{.PythonName}}=response.get("{{.JSONName}}"{{pythonDefault .}}),
|
||||
{{- end}}
|
||||
)
|
||||
{{- else if .HasReturns}}
|
||||
return response.get("{{(index .Returns 0).JSONName}}"{{pythonDefault (index .Returns 0)}})
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@ -85,3 +85,58 @@ func toJSONName(name string) string {
|
||||
// Simple conversion: lowercase first letter
|
||||
return strings.ToLower(name[:1]) + name[1:]
|
||||
}
|
||||
|
||||
// ToPythonType converts a Go type to its Python equivalent.
|
||||
func ToPythonType(goType string) string {
|
||||
switch goType {
|
||||
case "string":
|
||||
return "str"
|
||||
case "int", "int32", "int64":
|
||||
return "int"
|
||||
case "float32", "float64":
|
||||
return "float"
|
||||
case "bool":
|
||||
return "bool"
|
||||
case "[]byte":
|
||||
return "bytes"
|
||||
default:
|
||||
return "Any"
|
||||
}
|
||||
}
|
||||
|
||||
// ToSnakeCase converts a PascalCase or camelCase string to snake_case.
|
||||
func ToSnakeCase(s string) string {
|
||||
var result strings.Builder
|
||||
for i, r := range s {
|
||||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||
result.WriteByte('_')
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
return strings.ToLower(result.String())
|
||||
}
|
||||
|
||||
// PythonFunctionName returns the Python function name for a method.
|
||||
func (m Method) PythonFunctionName(servicePrefix string) string {
|
||||
return ToSnakeCase(servicePrefix + m.Name)
|
||||
}
|
||||
|
||||
// PythonResultTypeName returns the Python dataclass name for multi-value returns.
|
||||
func (m Method) PythonResultTypeName(serviceName string) string {
|
||||
return serviceName + m.Name + "Result"
|
||||
}
|
||||
|
||||
// NeedsResultClass returns true if the method needs a dataclass for returns.
|
||||
func (m Method) NeedsResultClass() bool {
|
||||
return len(m.Returns) > 1
|
||||
}
|
||||
|
||||
// PythonType returns the Python type for this parameter.
|
||||
func (p Param) PythonType() string {
|
||||
return ToPythonType(p.Type)
|
||||
}
|
||||
|
||||
// PythonName returns the snake_case Python name for this parameter.
|
||||
func (p Param) PythonName() string {
|
||||
return ToSnakeCase(p.Name)
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@
|
||||
// -package Output package name (default: inferred from output directory)
|
||||
// -host-only Generate only host-side code (default: false)
|
||||
// -plugin-only Generate only plugin/client-side code (default: false)
|
||||
// -go Generate Go client wrappers (default: true when not using -python)
|
||||
// -python Generate Python client wrappers (default: false)
|
||||
// -v Verbose output
|
||||
// -dry-run Preview generated code without writing files
|
||||
package main
|
||||
@ -33,6 +35,8 @@ func main() {
|
||||
pkgName = flag.String("package", "", "Output package name (default: inferred from output directory)")
|
||||
hostOnly = flag.Bool("host-only", false, "Generate only host-side code")
|
||||
pluginOnly = flag.Bool("plugin-only", false, "Generate only plugin/client-side code")
|
||||
goClient = flag.Bool("go", false, "Generate Go client wrappers")
|
||||
pyClient = flag.Bool("python", false, "Generate Python client wrappers")
|
||||
verbose = flag.Bool("v", false, "Verbose output")
|
||||
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
|
||||
)
|
||||
@ -67,14 +71,20 @@ func main() {
|
||||
|
||||
// Determine what to generate
|
||||
generateHost := !*pluginOnly
|
||||
generateClient := !*hostOnly
|
||||
// Default: generate Go clients if no language flag is specified
|
||||
// If -python is specified without -go, only generate Python
|
||||
// If -go is specified, generate Go
|
||||
// If both are specified, generate both
|
||||
generateGoClient := !*hostOnly && (*goClient || !*pyClient)
|
||||
generatePyClient := !*hostOnly && *pyClient
|
||||
|
||||
if *verbose {
|
||||
fmt.Printf("Input directory: %s\n", absInput)
|
||||
fmt.Printf("Output directory: %s\n", absOutput)
|
||||
fmt.Printf("Package name: %s\n", *pkgName)
|
||||
fmt.Printf("Generate host code: %v\n", generateHost)
|
||||
fmt.Printf("Generate client code: %v\n", generateClient)
|
||||
fmt.Printf("Generate Go client code: %v\n", generateGoClient)
|
||||
fmt.Printf("Generate Python client code: %v\n", generatePyClient)
|
||||
}
|
||||
|
||||
// Parse source files
|
||||
@ -108,10 +118,18 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate client-side code
|
||||
if generateClient {
|
||||
if err := generateClientCode(svc, absOutput, *dryRun, *verbose); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating client code for %s: %v\n", svc.Name, err)
|
||||
// Generate Go client-side code
|
||||
if generateGoClient {
|
||||
if err := generateGoClientCode(svc, absOutput, *dryRun, *verbose); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating Go client code for %s: %v\n", svc.Name, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Python client-side code
|
||||
if generatePyClient {
|
||||
if err := generatePythonClientCode(svc, absOutput, *dryRun, *verbose); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating Python client code for %s: %v\n", svc.Name, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@ -147,8 +165,8 @@ func generateHostCode(svc internal.Service, pkgName, outputDir string, dryRun, v
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateClientCode generates client-side code for a service.
|
||||
func generateClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
|
||||
// generateGoClientCode generates Go client-side code for a service.
|
||||
func generateGoClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
|
||||
code, err := internal.GenerateClientGo(svc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating code: %w", err)
|
||||
@ -178,7 +196,38 @@ func generateClientCode(svc internal.Service, outputDir string, dryRun, verbose
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Generated client code: %s\n", clientFile)
|
||||
fmt.Printf("Generated Go client code: %s\n", clientFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generatePythonClientCode generates Python client-side code for a service.
|
||||
func generatePythonClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
|
||||
code, err := internal.GenerateClientPython(svc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating code: %w", err)
|
||||
}
|
||||
|
||||
// Python code goes in python/ subdirectory
|
||||
clientDir := filepath.Join(outputDir, "python")
|
||||
clientFile := filepath.Join(clientDir, "nd_host_"+strings.ToLower(svc.Name)+".py")
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("=== %s ===\n%s\n", clientFile, code)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create python/ subdirectory if needed
|
||||
if err := os.MkdirAll(clientDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating python client directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(clientFile, code, 0600); err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Generated Python client code: %s\n", clientFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
51
plugins/cmd/hostgen/testdata/codec_client_expected.py
vendored
Normal file
51
plugins/cmd/hostgen/testdata/codec_client_expected.py
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Codec host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_codec import codec_encode
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "codec_encode")
|
||||
def _codec_encode(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def codec_encode(data: bytes) -> bytes:
|
||||
"""Call the codec_encode host function.
|
||||
|
||||
Args:
|
||||
data: bytes parameter.
|
||||
|
||||
Returns:
|
||||
bytes: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"data": data,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _codec_encode(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("result", b"")
|
||||
51
plugins/cmd/hostgen/testdata/counter_client_expected.py
vendored
Normal file
51
plugins/cmd/hostgen/testdata/counter_client_expected.py
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Counter host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_counter import counter_count
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "counter_count")
|
||||
def _counter_count(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def counter_count(name: str) -> int:
|
||||
"""Call the counter_count host function.
|
||||
|
||||
Args:
|
||||
name: str parameter.
|
||||
|
||||
Returns:
|
||||
int: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"name": name,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _counter_count(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("value", 0)
|
||||
51
plugins/cmd/hostgen/testdata/echo_client_expected.py
vendored
Normal file
51
plugins/cmd/hostgen/testdata/echo_client_expected.py
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Echo host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_echo import echo_echo
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "echo_echo")
|
||||
def _echo_echo(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def echo_echo(message: str) -> str:
|
||||
"""Call the echo_echo host function.
|
||||
|
||||
Args:
|
||||
message: str parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"message": message,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _echo_echo(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("reply", "")
|
||||
53
plugins/cmd/hostgen/testdata/list_client_expected.py
vendored
Normal file
53
plugins/cmd/hostgen/testdata/list_client_expected.py
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the List host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_list import list_items
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "list_items")
|
||||
def _list_items(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def list_items(name: str, filter: Any) -> int:
|
||||
"""Call the list_items host function.
|
||||
|
||||
Args:
|
||||
name: str parameter.
|
||||
filter: Any parameter.
|
||||
|
||||
Returns:
|
||||
int: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"name": name,
|
||||
"filter": filter,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _list_items(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("count", 0)
|
||||
53
plugins/cmd/hostgen/testdata/math_client_expected.py
vendored
Normal file
53
plugins/cmd/hostgen/testdata/math_client_expected.py
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Math host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_math import math_add
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "math_add")
|
||||
def _math_add(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def math_add(a: int, b: int) -> int:
|
||||
"""Call the math_add host function.
|
||||
|
||||
Args:
|
||||
a: int parameter.
|
||||
b: int parameter.
|
||||
|
||||
Returns:
|
||||
int: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"a": a,
|
||||
"b": b,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _math_add(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("result", 0)
|
||||
80
plugins/cmd/hostgen/testdata/meta_client_expected.py
vendored
Normal file
80
plugins/cmd/hostgen/testdata/meta_client_expected.py
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Meta host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_meta import meta_get, meta_set
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "meta_get")
|
||||
def _meta_get(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "meta_set")
|
||||
def _meta_set(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def meta_get(key: str) -> Any:
|
||||
"""Call the meta_get host function.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
Any: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _meta_get(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("value", None)
|
||||
|
||||
|
||||
def meta_set(data: Any) -> None:
|
||||
"""Call the meta_set host function.
|
||||
|
||||
Args:
|
||||
data: Any parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"data": data,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _meta_set(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
41
plugins/cmd/hostgen/testdata/ping_client_expected.py
vendored
Normal file
41
plugins/cmd/hostgen/testdata/ping_client_expected.py
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Ping host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_ping import ping_ping
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "ping_ping")
|
||||
def _ping_ping(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def ping_ping() -> None:
|
||||
"""Call the ping_ping host function.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request_bytes = b"{}"
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _ping_ping(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
61
plugins/cmd/hostgen/testdata/search_client_expected.py
vendored
Normal file
61
plugins/cmd/hostgen/testdata/search_client_expected.py
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Search host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_search import search_find
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "search_find")
|
||||
def _search_find(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchFindResult:
|
||||
"""Result type for search_find."""
|
||||
results: Any
|
||||
total: int
|
||||
|
||||
|
||||
def search_find(query: str) -> SearchFindResult:
|
||||
"""Call the search_find host function.
|
||||
|
||||
Args:
|
||||
query: str parameter.
|
||||
|
||||
Returns:
|
||||
SearchFindResult containing results, total,.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"query": query,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _search_find(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return SearchFindResult(
|
||||
results=response.get("results", None),
|
||||
total=response.get("total", 0),
|
||||
)
|
||||
51
plugins/cmd/hostgen/testdata/store_client_expected.py
vendored
Normal file
51
plugins/cmd/hostgen/testdata/store_client_expected.py
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Store host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_store import store_save
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "store_save")
|
||||
def _store_save(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def store_save(item: Any) -> str:
|
||||
"""Call the store_save host function.
|
||||
|
||||
Args:
|
||||
item: Any parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"item": item,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _store_save(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("id", "")
|
||||
53
plugins/cmd/hostgen/testdata/users_client_expected.py
vendored
Normal file
53
plugins/cmd/hostgen/testdata/users_client_expected.py
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Users host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_users import users_get
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "users_get")
|
||||
def _users_get(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def users_get(id: Any, filter: Any) -> Any:
|
||||
"""Call the users_get host function.
|
||||
|
||||
Args:
|
||||
id: Any parameter.
|
||||
filter: Any parameter.
|
||||
|
||||
Returns:
|
||||
Any: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"id": id,
|
||||
"filter": filter,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _users_get(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("result", None)
|
||||
182
plugins/host/python/nd_host_artwork.py
Normal file
182
plugins/host/python/nd_host_artwork.py
Normal file
@ -0,0 +1,182 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Artwork host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_artwork import artwork_get_artist_url, artwork_get_album_url, artwork_get_track_url, artwork_get_playlist_url
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "artwork_getartisturl")
|
||||
def _artwork_getartisturl(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "artwork_getalbumurl")
|
||||
def _artwork_getalbumurl(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "artwork_gettrackurl")
|
||||
def _artwork_gettrackurl(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "artwork_getplaylisturl")
|
||||
def _artwork_getplaylisturl(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def artwork_get_artist_url(id: str, size: int) -> str:
|
||||
"""GetArtistUrl generates a public URL for an artist's artwork.
|
||||
|
||||
Parameters:
|
||||
- id: The artist's unique identifier
|
||||
- size: Desired image size in pixels (0 for original size)
|
||||
|
||||
Returns the public URL for the artwork, or an error if generation fails.
|
||||
|
||||
Args:
|
||||
id: str parameter.
|
||||
size: int parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"id": id,
|
||||
"size": size,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _artwork_getartisturl(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("url", "")
|
||||
|
||||
|
||||
def artwork_get_album_url(id: str, size: int) -> str:
|
||||
"""GetAlbumUrl generates a public URL for an album's artwork.
|
||||
|
||||
Parameters:
|
||||
- id: The album's unique identifier
|
||||
- size: Desired image size in pixels (0 for original size)
|
||||
|
||||
Returns the public URL for the artwork, or an error if generation fails.
|
||||
|
||||
Args:
|
||||
id: str parameter.
|
||||
size: int parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"id": id,
|
||||
"size": size,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _artwork_getalbumurl(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("url", "")
|
||||
|
||||
|
||||
def artwork_get_track_url(id: str, size: int) -> str:
|
||||
"""GetTrackUrl generates a public URL for a track's artwork.
|
||||
|
||||
Parameters:
|
||||
- id: The track's (media file) unique identifier
|
||||
- size: Desired image size in pixels (0 for original size)
|
||||
|
||||
Returns the public URL for the artwork, or an error if generation fails.
|
||||
|
||||
Args:
|
||||
id: str parameter.
|
||||
size: int parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"id": id,
|
||||
"size": size,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _artwork_gettrackurl(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("url", "")
|
||||
|
||||
|
||||
def artwork_get_playlist_url(id: str, size: int) -> str:
|
||||
"""GetPlaylistUrl generates a public URL for a playlist's artwork.
|
||||
|
||||
Parameters:
|
||||
- id: The playlist's unique identifier
|
||||
- size: Desired image size in pixels (0 for original size)
|
||||
|
||||
Returns the public URL for the artwork, or an error if generation fails.
|
||||
|
||||
Args:
|
||||
id: str parameter.
|
||||
size: int parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"id": id,
|
||||
"size": size,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _artwork_getplaylisturl(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("url", "")
|
||||
446
plugins/host/python/nd_host_cache.py
Normal file
446
plugins/host/python/nd_host_cache.py
Normal file
@ -0,0 +1,446 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Cache host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_cache import cache_set_string, cache_get_string, cache_set_int, cache_get_int, cache_set_float, cache_get_float, cache_set_bytes, cache_get_bytes, cache_has, cache_remove
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_setstring")
|
||||
def _cache_setstring(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_getstring")
|
||||
def _cache_getstring(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_setint")
|
||||
def _cache_setint(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_getint")
|
||||
def _cache_getint(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_setfloat")
|
||||
def _cache_setfloat(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_getfloat")
|
||||
def _cache_getfloat(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_setbytes")
|
||||
def _cache_setbytes(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_getbytes")
|
||||
def _cache_getbytes(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_has")
|
||||
def _cache_has(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "cache_remove")
|
||||
def _cache_remove(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheGetStringResult:
|
||||
"""Result type for cache_get_string."""
|
||||
value: str
|
||||
exists: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheGetIntResult:
|
||||
"""Result type for cache_get_int."""
|
||||
value: int
|
||||
exists: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheGetFloatResult:
|
||||
"""Result type for cache_get_float."""
|
||||
value: float
|
||||
exists: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheGetBytesResult:
|
||||
"""Result type for cache_get_bytes."""
|
||||
value: bytes
|
||||
exists: bool
|
||||
|
||||
|
||||
def cache_set_string(key: str, value: str, ttl_seconds: int) -> None:
|
||||
"""SetString stores a string value in the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
- value: The string value to store
|
||||
- ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
|
||||
Returns an error if the operation fails.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
value: str parameter.
|
||||
ttl_seconds: int parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
"value": value,
|
||||
"ttlSeconds": ttl_seconds,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_setstring(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
|
||||
|
||||
def cache_get_string(key: str) -> CacheGetStringResult:
|
||||
"""GetString retrieves a string value from the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
|
||||
Returns the value and whether the key exists. If the key doesn't exist
|
||||
or the stored value is not a string, exists will be false.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
CacheGetStringResult containing value, exists,.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_getstring(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return CacheGetStringResult(
|
||||
value=response.get("value", ""),
|
||||
exists=response.get("exists", False),
|
||||
)
|
||||
|
||||
|
||||
def cache_set_int(key: str, value: int, ttl_seconds: int) -> None:
|
||||
"""SetInt stores an integer value in the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
- value: The integer value to store
|
||||
- ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
|
||||
Returns an error if the operation fails.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
value: int parameter.
|
||||
ttl_seconds: int parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
"value": value,
|
||||
"ttlSeconds": ttl_seconds,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_setint(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
|
||||
|
||||
def cache_get_int(key: str) -> CacheGetIntResult:
|
||||
"""GetInt retrieves an integer value from the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
|
||||
Returns the value and whether the key exists. If the key doesn't exist
|
||||
or the stored value is not an integer, exists will be false.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
CacheGetIntResult containing value, exists,.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_getint(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return CacheGetIntResult(
|
||||
value=response.get("value", 0),
|
||||
exists=response.get("exists", False),
|
||||
)
|
||||
|
||||
|
||||
def cache_set_float(key: str, value: float, ttl_seconds: int) -> None:
|
||||
"""SetFloat stores a float value in the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
- value: The float value to store
|
||||
- ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
|
||||
Returns an error if the operation fails.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
value: float parameter.
|
||||
ttl_seconds: int parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
"value": value,
|
||||
"ttlSeconds": ttl_seconds,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_setfloat(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
|
||||
|
||||
def cache_get_float(key: str) -> CacheGetFloatResult:
|
||||
"""GetFloat retrieves a float value from the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
|
||||
Returns the value and whether the key exists. If the key doesn't exist
|
||||
or the stored value is not a float, exists will be false.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
CacheGetFloatResult containing value, exists,.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_getfloat(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return CacheGetFloatResult(
|
||||
value=response.get("value", 0.0),
|
||||
exists=response.get("exists", False),
|
||||
)
|
||||
|
||||
|
||||
def cache_set_bytes(key: str, value: bytes, ttl_seconds: int) -> None:
|
||||
"""SetBytes stores a byte slice in the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
- value: The byte slice to store
|
||||
- ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
|
||||
|
||||
Returns an error if the operation fails.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
value: bytes parameter.
|
||||
ttl_seconds: int parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
"value": value,
|
||||
"ttlSeconds": ttl_seconds,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_setbytes(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
|
||||
|
||||
def cache_get_bytes(key: str) -> CacheGetBytesResult:
|
||||
"""GetBytes retrieves a byte slice from the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
|
||||
Returns the value and whether the key exists. If the key doesn't exist
|
||||
or the stored value is not a byte slice, exists will be false.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
CacheGetBytesResult containing value, exists,.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_getbytes(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return CacheGetBytesResult(
|
||||
value=response.get("value", b""),
|
||||
exists=response.get("exists", False),
|
||||
)
|
||||
|
||||
|
||||
def cache_has(key: str) -> bool:
|
||||
"""Has checks if a key exists in the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
|
||||
Returns true if the key exists and has not expired.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
bool: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_has(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("exists", False)
|
||||
|
||||
|
||||
def cache_remove(key: str) -> None:
|
||||
"""Remove deletes a value from the cache.
|
||||
|
||||
Parameters:
|
||||
- key: The cache key (will be namespaced with plugin ID)
|
||||
|
||||
Returns an error if the operation fails. Does not return an error if the key doesn't exist.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _cache_remove(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
142
plugins/host/python/nd_host_scheduler.py
Normal file
142
plugins/host/python/nd_host_scheduler.py
Normal file
@ -0,0 +1,142 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Scheduler host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_scheduler import scheduler_schedule_one_time, scheduler_schedule_recurring, scheduler_cancel_schedule
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "scheduler_scheduleonetime")
|
||||
def _scheduler_scheduleonetime(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "scheduler_schedulerecurring")
|
||||
def _scheduler_schedulerecurring(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "scheduler_cancelschedule")
|
||||
def _scheduler_cancelschedule(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def scheduler_schedule_one_time(delay_seconds: int, payload: str, schedule_i_d: str) -> str:
|
||||
"""ScheduleOneTime schedules a one-time event to be triggered after the specified delay.
|
||||
Plugins that use this function must also implement the SchedulerCallback capability
|
||||
|
||||
Parameters:
|
||||
- delaySeconds: Number of seconds to wait before triggering the event
|
||||
- payload: Data to be passed to the scheduled event handler
|
||||
- scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
|
||||
|
||||
Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
|
||||
|
||||
Args:
|
||||
delay_seconds: int parameter.
|
||||
payload: str parameter.
|
||||
schedule_i_d: str parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"delaySeconds": delay_seconds,
|
||||
"payload": payload,
|
||||
"scheduleID": schedule_i_d,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _scheduler_scheduleonetime(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("newScheduleID", "")
|
||||
|
||||
|
||||
def scheduler_schedule_recurring(cron_expression: str, payload: str, schedule_i_d: str) -> str:
|
||||
"""ScheduleRecurring schedules a recurring event using a cron expression.
|
||||
Plugins that use this function must also implement the SchedulerCallback capability
|
||||
|
||||
Parameters:
|
||||
- cronExpression: Standard cron format expression (e.g., "0 0 * * *" for daily at midnight)
|
||||
- payload: Data to be passed to each scheduled event handler invocation
|
||||
- scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
|
||||
|
||||
Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
|
||||
|
||||
Args:
|
||||
cron_expression: str parameter.
|
||||
payload: str parameter.
|
||||
schedule_i_d: str parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"cronExpression": cron_expression,
|
||||
"payload": payload,
|
||||
"scheduleID": schedule_i_d,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _scheduler_schedulerecurring(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("newScheduleID", "")
|
||||
|
||||
|
||||
def scheduler_cancel_schedule(schedule_i_d: str) -> None:
|
||||
"""CancelSchedule cancels a scheduled job identified by its schedule ID.
|
||||
|
||||
This works for both one-time and recurring schedules. Once cancelled, the job will not trigger
|
||||
any future events.
|
||||
|
||||
Returns an error if the schedule ID is not found or if cancellation fails.
|
||||
|
||||
Args:
|
||||
schedule_i_d: str parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"scheduleID": schedule_i_d,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _scheduler_cancelschedule(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
54
plugins/host/python/nd_host_subsonicapi.py
Normal file
54
plugins/host/python/nd_host_subsonicapi.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the SubsonicAPI host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_subsonicapi import subsonicapi_call
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "subsonicapi_call")
|
||||
def _subsonicapi_call(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def subsonicapi_call(uri: str) -> str:
|
||||
"""Call executes a Subsonic API request and returns the JSON response.
|
||||
|
||||
The uri parameter should be the Subsonic API path without the server prefix,
|
||||
e.g., "getAlbumList2?type=random&size=10". The response is returned as raw JSON.
|
||||
|
||||
Args:
|
||||
uri: str parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"uri": uri,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _subsonicapi_call(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("responseJSON", "")
|
||||
180
plugins/host/python/nd_host_websocket.py
Normal file
180
plugins/host/python/nd_host_websocket.py
Normal file
@ -0,0 +1,180 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the WebSocket host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_websocket import websocket_connect, websocket_send_text, websocket_send_binary, websocket_close_connection
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "websocket_connect")
|
||||
def _websocket_connect(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "websocket_sendtext")
|
||||
def _websocket_sendtext(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "websocket_sendbinary")
|
||||
def _websocket_sendbinary(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "websocket_closeconnection")
|
||||
def _websocket_closeconnection(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def websocket_connect(url: str, headers: Any, connection_i_d: str) -> str:
|
||||
"""Connect establishes a WebSocket connection to the specified URL.
|
||||
|
||||
Plugins that use this function must also implement the WebSocketCallback capability
|
||||
to receive incoming messages and connection events.
|
||||
|
||||
Parameters:
|
||||
- url: The WebSocket URL to connect to (ws:// or wss://)
|
||||
- headers: Optional HTTP headers to include in the handshake request
|
||||
- connectionID: Optional unique identifier for the connection. If empty, one will be generated
|
||||
|
||||
Returns the connection ID that can be used to send messages or close the connection,
|
||||
or an error if the connection fails.
|
||||
|
||||
Args:
|
||||
url: str parameter.
|
||||
headers: Any parameter.
|
||||
connection_i_d: str parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"url": url,
|
||||
"headers": headers,
|
||||
"connectionID": connection_i_d,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _websocket_connect(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("newConnectionID", "")
|
||||
|
||||
|
||||
def websocket_send_text(connection_i_d: str, message: str) -> None:
|
||||
"""SendText sends a text message over an established WebSocket connection.
|
||||
|
||||
Parameters:
|
||||
- connectionID: The connection identifier returned by Connect
|
||||
- message: The text message to send
|
||||
|
||||
Returns an error if the connection is not found or if sending fails.
|
||||
|
||||
Args:
|
||||
connection_i_d: str parameter.
|
||||
message: str parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"connectionID": connection_i_d,
|
||||
"message": message,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _websocket_sendtext(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
|
||||
|
||||
def websocket_send_binary(connection_i_d: str, data: bytes) -> None:
|
||||
"""SendBinary sends binary data over an established WebSocket connection.
|
||||
|
||||
Parameters:
|
||||
- connectionID: The connection identifier returned by Connect
|
||||
- data: The binary data to send
|
||||
|
||||
Returns an error if the connection is not found or if sending fails.
|
||||
|
||||
Args:
|
||||
connection_i_d: str parameter.
|
||||
data: bytes parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"connectionID": connection_i_d,
|
||||
"data": data,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _websocket_sendbinary(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
|
||||
|
||||
def websocket_close_connection(connection_i_d: str, code: int, reason: str) -> None:
|
||||
"""CloseConnection gracefully closes a WebSocket connection.
|
||||
|
||||
Parameters:
|
||||
- connectionID: The connection identifier returned by Connect
|
||||
- code: WebSocket close status code (e.g., 1000 for normal closure)
|
||||
- reason: Optional human-readable reason for closing
|
||||
|
||||
Returns an error if the connection is not found or if closing fails.
|
||||
|
||||
Args:
|
||||
connection_i_d: str parameter.
|
||||
code: int parameter.
|
||||
reason: str parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"connectionID": connection_i_d,
|
||||
"code": code,
|
||||
"reason": reason,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _websocket_closeconnection(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user