aardvark_core/persistent/
inline.rs

1use std::collections::HashSet;
2use std::io::{Cursor, Write};
3
4use serde_json::Value;
5use zip::write::FileOptions;
6use zip::CompressionMethod;
7
8use crate::bundle::Bundle;
9use crate::bundle_manifest::{
10    BundleManifest, ManifestResources, ManifestRuntime, MANIFEST_BASENAME, MANIFEST_SCHEMA_VERSION,
11};
12use crate::error::{PyRunnerError, Result};
13use crate::runtime_language::RuntimeLanguage;
14
15/// Options configuring inline Python execution without a prebuilt bundle.
16#[derive(Debug, Clone, Default)]
17pub struct InlinePythonOptions {
18    /// Optional entrypoint override (defaults to `"main:handler"`).
19    pub entrypoint: Option<String>,
20    /// Optional package hints passed through the manifest.
21    pub packages: Vec<String>,
22    /// Optional runtime overrides copied into the manifest.
23    pub runtime: Option<ManifestRuntime>,
24    /// Optional sandbox resource policies copied into the manifest.
25    pub resources: Option<ManifestResources>,
26}
27
28impl InlinePythonOptions {
29    /// Returns the entrypoint, falling back to `main:handler`.
30    pub fn entrypoint(&self) -> &str {
31        self.entrypoint.as_deref().unwrap_or("main:handler")
32    }
33
34    /// Builds a bundle from the inline code and returns it with the normalised entrypoint.
35    pub(crate) fn build_bundle(&self, code: &str) -> Result<(Bundle, String)> {
36        let (manifest, manifest_bytes) = self.build_manifest()?;
37        let entrypoint = manifest.entrypoint().to_owned();
38        let (module_path, init_modules) = module_path_for_entrypoint(&entrypoint)?;
39        let bundle = assemble_inline_bundle(code, &module_path, &init_modules, &manifest_bytes)?;
40        Ok((bundle, entrypoint))
41    }
42
43    fn build_manifest(&self) -> Result<(BundleManifest, Vec<u8>)> {
44        if let Some(runtime) = &self.runtime {
45            if matches!(runtime.language, Some(RuntimeLanguage::JavaScript)) {
46                return Err(PyRunnerError::Manifest(
47                    "inline python options must target the python runtime".into(),
48                ));
49            }
50        }
51
52        let mut map = serde_json::Map::new();
53        map.insert(
54            "schemaVersion".to_string(),
55            Value::String(MANIFEST_SCHEMA_VERSION.to_string()),
56        );
57        map.insert(
58            "entrypoint".to_string(),
59            Value::String(self.entrypoint().to_string()),
60        );
61        if !self.packages.is_empty() {
62            map.insert(
63                "packages".to_string(),
64                serde_json::to_value(&self.packages).map_err(|err| {
65                    PyRunnerError::Manifest(format!("failed to encode inline package list: {err}"))
66                })?,
67            );
68        }
69        if let Some(runtime) = &self.runtime {
70            map.insert(
71                "runtime".to_string(),
72                serde_json::to_value(runtime).map_err(|err| {
73                    PyRunnerError::Manifest(format!("failed to encode inline runtime block: {err}"))
74                })?,
75            );
76        }
77        if let Some(resources) = &self.resources {
78            map.insert(
79                "resources".to_string(),
80                serde_json::to_value(resources).map_err(|err| {
81                    PyRunnerError::Manifest(format!(
82                        "failed to encode inline resources block: {err}"
83                    ))
84                })?,
85            );
86        }
87
88        let manifest_value = Value::Object(map);
89        let manifest_bytes = serde_json::to_vec(&manifest_value).map_err(|err| {
90            PyRunnerError::Manifest(format!("failed to serialise inline manifest: {err}"))
91        })?;
92        let manifest = BundleManifest::from_bytes(&manifest_bytes)?;
93        let manifest_bytes = serde_json::to_vec(&manifest).map_err(|err| {
94            PyRunnerError::Manifest(format!(
95                "failed to serialise normalised inline manifest: {err}"
96            ))
97        })?;
98        Ok((manifest, manifest_bytes))
99    }
100}
101
102fn module_path_for_entrypoint(entrypoint: &str) -> Result<(String, Vec<String>)> {
103    let (module, _) = entrypoint
104        .split_once(':')
105        .ok_or_else(|| PyRunnerError::Manifest("entrypoint must include module:function".into()))?;
106    let module = module.trim();
107    if module.is_empty() {
108        return Err(PyRunnerError::Manifest(
109            "entrypoint must specify a module".into(),
110        ));
111    }
112
113    let mut components = Vec::new();
114    for token in module.split('.') {
115        let trimmed = token.trim();
116        if trimmed.is_empty() {
117            return Err(PyRunnerError::Manifest(
118                "entrypoint module cannot contain empty components".into(),
119            ));
120        }
121        components.push(trimmed);
122    }
123    let mut init_modules = Vec::new();
124    if components.len() > 1 {
125        let mut prefix = String::new();
126        for component in &components[..components.len() - 1] {
127            if !prefix.is_empty() {
128                prefix.push('/');
129            }
130            prefix.push_str(component);
131            init_modules.push(format!("{prefix}/__init__.py"));
132        }
133    }
134    let module_path = format!(
135        "{}{}.py",
136        if components.len() > 1 {
137            components[..components.len() - 1].join("/") + "/"
138        } else {
139            String::new()
140        },
141        components.last().unwrap()
142    );
143    Ok((module_path, init_modules))
144}
145
146fn assemble_inline_bundle(
147    code: &str,
148    module_path: &str,
149    init_modules: &[String],
150    manifest_bytes: &[u8],
151) -> Result<Bundle> {
152    let cursor = Cursor::new(Vec::new());
153    let mut writer = zip::ZipWriter::new(cursor);
154    let options = FileOptions::default().compression_method(CompressionMethod::Stored);
155
156    let mut seen = HashSet::new();
157    for init in init_modules {
158        if seen.insert(init.clone()) {
159            writer
160                .start_file(init, options)
161                .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
162            writer
163                .write_all(b"")
164                .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
165        }
166    }
167
168    writer
169        .start_file(module_path, options)
170        .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
171    writer
172        .write_all(code.as_bytes())
173        .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
174
175    writer
176        .start_file(MANIFEST_BASENAME, options)
177        .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
178    writer
179        .write_all(manifest_bytes)
180        .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
181
182    let cursor = writer
183        .finish()
184        .map_err(|err| PyRunnerError::Bundle(err.to_string()))?;
185    Bundle::from_zip_bytes(cursor.into_inner())
186}