Files
navidrome/plugins/pdk/python/host/nd_host_cache.py
Deluan Quintão bd8032b327 fix(plugins): add base64 handling for []byte and remove raw=true (#5121)
* fix(plugins): add base64 handling for []byte and remove raw=true

Go's json.Marshal automatically base64-encodes []byte fields, but Rust's
serde_json serializes Vec<u8> as a JSON array and Python's json.dumps
raises TypeError on bytes. This fixes both directions of plugin
communication by adding proper base64 encoding/decoding in generated
client code.

For Rust templates (client and capability): adds a base64_bytes serde
helper module with #[serde(with = "base64_bytes")] on all Vec<u8> fields,
and adds base64 as a dependency. For Python templates: wraps bytes params
with base64.b64encode() and responses with base64.b64decode().

Also removes the raw=true binary framing protocol from all templates,
the parser, and the Method type. The raw mechanism added complexity that
is no longer needed once []byte works properly over JSON.

* fix(plugins): update production code and tests for base64 migration

Remove raw=true annotation from SubsonicAPI.CallRaw, delete all raw
test fixtures, remove raw-related test cases from parser, generator, and
integration tests, and add new test cases validating base64 handling
for Rust and Python templates.

* fix(plugins): update golden files and regenerate production code

Update golden test fixtures for codec and comprehensive services to
include base64 handling for []byte fields. Regenerate all production
PDK code (Go, Rust, Python) and host wrappers to use standard JSON
with base64-encoded byte fields instead of binary framing protocol.

* refactor: remove base64 helper duplication from rust template

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(plugins): add base64 dependency to capabilities' Cargo.toml

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-27 19:00:19 -05:00

449 lines
12 KiB
Python

# Code generated by ndpgen. 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.
#
# IMPORTANT: Due to a limitation in extism-py, you cannot import this file directly.
# The @extism.import_fn decorators are only detected when defined in the plugin's
# main __init__.py file. Copy the needed functions from this file into your plugin.
from dataclasses import dataclass
from typing import Any
import extism
import json
import base64
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": base64.b64encode(value).decode("ascii"),
"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=base64.b64decode(response.get("value", "")),
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"])