Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ png = "0.17.13"
predicates = "3.0.2"
pyo3 = { version = "0.25.0", features = ["extension-module", "anyhow", "abi3-py37"] }
pyo3-async-runtimes = { version = "0.25.0", features = ["tokio-runtime"] }
pyo3-log = "0.12"
pythonize = "0.25.0"
regex = "1"
reqwest = { version = "0.11.20", default-features = false, features = ["rustls-tls"] }
Expand All @@ -63,6 +64,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.106"
shellexpand = "3.1.0"
svg2pdf = "0.13.0"
svgtypes = "0.15"
tempfile = "3.8.0"
tiny-skia = "0.11.4"
tinyvec = "1"
Expand Down
68 changes: 68 additions & 0 deletions thirdparty_rust.yaml

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions vl-convert-fontsource/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,40 @@ impl FontsourceClient {
))
}

/// Check whether a font exists on Fontsource without downloading blobs (async).
///
/// Returns `Ok(true)` if the font metadata was successfully fetched (from
/// cache or API), `Ok(false)` if the API returned 404, and `Err` for
/// transient/network errors.
pub async fn is_known_font(&self, font_id: &str) -> Result<bool, FontsourceError> {
if self.try_read_cached_metadata(font_id).is_some() {
return Ok(true);
}
match self.fetch_metadata_async(font_id).await {
Ok(metadata) => {
self.cache_metadata(font_id, &metadata)?;
Ok(true)
}
Err(FontsourceError::FontNotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}

/// Check whether a font exists on Fontsource without downloading blobs (blocking).
pub fn is_known_font_blocking(&self, font_id: &str) -> Result<bool, FontsourceError> {
if self.try_read_cached_metadata(font_id).is_some() {
return Ok(true);
}
match self.fetch_metadata_blocking(font_id) {
Ok(metadata) => {
self.cache_metadata(font_id, &metadata)?;
Ok(true)
}
Err(FontsourceError::FontNotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}

/// Validate load arguments and return the normalized font ID.
fn validate_load_request(
family: &str,
Expand Down
1 change: 1 addition & 0 deletions vl-convert-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ futures = { workspace = true }
pythonize = { workspace = true }
tokio = { workspace = true }
pyo3-async-runtimes = { workspace = true }
pyo3-log = { workspace = true }
2 changes: 1 addition & 1 deletion vl-convert-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ By default, `vl-convert-python` uses `1` converter worker. You can configure thi
```python
import vl_convert as vlc

cfg = vlc.get_converter_config()
cfg = vlc.get_config()
print(cfg["num_workers"]) # 1

vlc.configure(num_workers=4) # enable parallel worker pool
Expand Down
65 changes: 54 additions & 11 deletions vl-convert-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use std::str::FromStr;
use std::sync::{Arc, RwLock};
use vl_convert_rs::configure_font_cache as configure_font_cache_rs;
use vl_convert_rs::converter::{
FormatLocale, Renderer, TimeFormatLocale, ValueOrString, VgOpts, VlConverterConfig, VlOpts,
ACCESS_DENIED_MARKER,
FormatLocale, MissingFontsPolicy, Renderer, TimeFormatLocale, ValueOrString, VgOpts,
VlConverterConfig, VlOpts, ACCESS_DENIED_MARKER,
};
use vl_convert_rs::module_loader::import_map::{
VlVersion, VEGA_EMBED_VERSION, VEGA_THEMES_VERSION, VEGA_VERSION, VL_VERSIONS,
Expand Down Expand Up @@ -63,6 +63,12 @@ fn converter_config_json(config: &VlConverterConfig) -> serde_json::Value {
.as_ref()
.map(|root| root.to_string_lossy().to_string()),
"allowed_base_urls": config.allowed_base_urls,
"auto_fontsource": config.auto_fontsource,
"missing_fonts": match config.missing_fonts {
MissingFontsPolicy::Fallback => "fallback",
MissingFontsPolicy::Warn => "warn",
MissingFontsPolicy::Error => "error",
},
"fontsource_cache_dir": vl_convert_rs::fontsource_cache_dir()
.map(|p| p.to_string_lossy().into_owned()),
})
Expand All @@ -77,6 +83,8 @@ struct ConverterConfigOverrides {
// None => no change, Some(None) => clear, Some(Some(urls)) => set
allowed_base_urls: Option<Option<Vec<String>>>,
fontsource_cache_size_mb: Option<u64>,
auto_fontsource: Option<bool>,
missing_fonts: Option<MissingFontsPolicy>,
}

fn parse_config_overrides(
Expand Down Expand Up @@ -145,8 +153,36 @@ fn parse_config_overrides(
})?);
}
}
// Read-only config fields returned by get_converter_config() are
// silently ignored so that `configure(**get_converter_config())` works.
"auto_fontsource" => {
if !value.is_none() {
overrides.auto_fontsource = Some(value.extract::<bool>().map_err(|err| {
vl_convert_rs::anyhow::anyhow!(
"Invalid auto_fontsource value for configure: {err}"
)
})?);
}
}
"missing_fonts" => {
if !value.is_none() {
let s = value.extract::<String>().map_err(|err| {
vl_convert_rs::anyhow::anyhow!(
"Invalid missing_fonts value for configure: {err}"
)
})?;
overrides.missing_fonts = Some(match s.as_str() {
"fallback" => MissingFontsPolicy::Fallback,
"warn" => MissingFontsPolicy::Warn,
"error" => MissingFontsPolicy::Error,
_ => {
return Err(vl_convert_rs::anyhow::anyhow!(
"Invalid missing_fonts value: {s}. Expected 'fallback', 'warn', or 'error'"
));
}
});
}
}
// Read-only config fields returned by get_config() are
// silently ignored so that `configure(**get_config())` works.
"fontsource_cache_dir" => {}
other => {
return Err(vl_convert_rs::anyhow::anyhow!(
Expand Down Expand Up @@ -176,6 +212,12 @@ fn apply_config_overrides(config: &mut VlConverterConfig, overrides: ConverterCo
let bytes = mb.saturating_mul(1024 * 1024);
configure_font_cache_rs(Some(bytes));
}
if let Some(auto_fontsource) = overrides.auto_fontsource {
config.auto_fontsource = auto_fontsource;
}
if let Some(missing_fonts) = overrides.missing_fonts {
config.missing_fonts = missing_fonts;
}
}

fn configure_converter_with_config_overrides(
Expand Down Expand Up @@ -1310,9 +1352,9 @@ fn configure(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
}

/// Get the currently configured converter options.
#[pyfunction]
#[pyfunction(name = "get_config")]
#[pyo3(signature = ())]
fn get_converter_config() -> PyResult<PyObject> {
fn get_config() -> PyResult<PyObject> {
let config = converter_config()
.map_err(|err| prefixed_py_error("Failed to read converter config", err))?;
Python::with_gil(|py| {
Expand Down Expand Up @@ -2233,10 +2275,10 @@ fn configure_asyncio<'py>(
})
}

#[doc = async_variant_doc!("get_converter_config")]
#[pyfunction(name = "get_converter_config")]
#[doc = async_variant_doc!("get_config")]
#[pyfunction(name = "get_config")]
#[pyo3(signature = ())]
fn get_converter_config_asyncio<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
fn get_config_asyncio<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
future_into_py_object(py, async move {
let config = converter_config()
.map_err(|err| prefixed_py_error("Failed to read converter config", err))?;
Expand Down Expand Up @@ -2468,7 +2510,7 @@ fn add_asyncio_submodule(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()
&asyncio
)?)?;
asyncio.add_function(wrap_pyfunction!(configure_asyncio, &asyncio)?)?;
asyncio.add_function(wrap_pyfunction!(get_converter_config_asyncio, &asyncio)?)?;
asyncio.add_function(wrap_pyfunction!(get_config_asyncio, &asyncio)?)?;
asyncio.add_function(wrap_pyfunction!(warm_up_workers_asyncio, &asyncio)?)?;
asyncio.add_function(wrap_pyfunction!(get_local_tz_asyncio, &asyncio)?)?;
asyncio.add_function(wrap_pyfunction!(get_themes_asyncio, &asyncio)?)?;
Expand All @@ -2490,6 +2532,7 @@ fn add_asyncio_submodule(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()
/// Convert Vega-Lite specifications to other formats
#[pymodule]
fn vl_convert(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
pyo3_log::init();
m.add_function(wrap_pyfunction!(vegalite_to_vega, m)?)?;
m.add_function(wrap_pyfunction!(vegalite_to_svg, m)?)?;
m.add_function(wrap_pyfunction!(vegalite_to_scenegraph, m)?)?;
Expand All @@ -2511,7 +2554,7 @@ fn vl_convert(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(register_font_directory, m)?)?;
m.add_function(wrap_pyfunction!(register_fontsource_font, m)?)?;
m.add_function(wrap_pyfunction!(configure, m)?)?;
m.add_function(wrap_pyfunction!(get_converter_config, m)?)?;
m.add_function(wrap_pyfunction!(get_config, m)?)?;
m.add_function(wrap_pyfunction!(warm_up_workers, m)?)?;
m.add_function(wrap_pyfunction!(get_local_tz, m)?)?;
m.add_function(wrap_pyfunction!(get_themes, m)?)?;
Expand Down
6 changes: 3 additions & 3 deletions vl-convert-python/tests/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def public_callable_names(module):

@pytest.fixture(autouse=True)
def reset_worker_count():
original = vlc.get_converter_config()
original = vlc.get_config()
vlc.configure(num_workers=1)
try:
yield
Expand Down Expand Up @@ -93,7 +93,7 @@ async def scenario():
def test_asyncio_worker_lifecycle_calls():
async def scenario():
await vlca.configure(num_workers=3)
config = await vlca.get_converter_config()
config = await vlca.get_config()
assert config["num_workers"] == 3
svg = await vlca.vegalite_to_svg(SIMPLE_VL_SPEC, "v5_16")
assert svg.lstrip().startswith("<svg")
Expand All @@ -110,7 +110,7 @@ async def scenario():
allow_http_access=False,
filesystem_root=str(root),
)
config = await vlca.get_converter_config()
config = await vlca.get_config()
assert config["num_workers"] == 2
assert config["allow_http_access"] is False
assert config["filesystem_root"] == str(root.resolve())
Expand Down
14 changes: 7 additions & 7 deletions vl-convert-python/tests/test_workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@

@pytest.fixture(autouse=True)
def reset_worker_count():
original = vlc.get_converter_config()
original = vlc.get_config()
vlc.configure(num_workers=1)
try:
yield
finally:
vlc.configure(**original)


def test_get_converter_config_reports_default_num_workers():
assert vlc.get_converter_config()["num_workers"] == 1
def test_get_config_reports_default_num_workers():
assert vlc.get_config()["num_workers"] == 1


def test_configure_rejects_zero_num_workers():
Expand Down Expand Up @@ -94,7 +94,7 @@ def test_configure_round_trip(tmp_path):
allowed_base_urls=None,
)

config = vlc.get_converter_config()
config = vlc.get_config()
assert config["num_workers"] == 2
assert config["allow_http_access"] is False
assert config["filesystem_root"] == str(root.resolve())
Expand All @@ -112,7 +112,7 @@ def test_configure_num_workers_preserves_access_policy(tmp_path):
allowed_base_urls=None,
)
vlc.configure(num_workers=3)
config = vlc.get_converter_config()
config = vlc.get_config()

assert config["num_workers"] == 3
assert config["allow_http_access"] is False
Expand All @@ -126,7 +126,7 @@ def test_configure_noop_when_called_without_args():
filesystem_root=None,
allowed_base_urls=["https://example.com/"],
)
before = vlc.get_converter_config()
before = vlc.get_config()
vlc.configure()
after = vlc.get_converter_config()
after = vlc.get_config()
assert after == before
Loading
Loading