Python asset parser

This commit is contained in:
Diego Imbert
2025-07-03 23:34:45 +02:00
parent f34467ca62
commit ea9487e3cf
10 changed files with 124 additions and 18 deletions

1
backend/Cargo.lock generated
View File

@@ -15064,6 +15064,7 @@ version = "1.502.2"
dependencies = [
"anyhow",
"itertools 0.14.0",
"rustpython-ast",
"rustpython-parser",
"serde_json",
"windmill-parser",

View File

@@ -13,4 +13,5 @@ windmill-parser.workspace = true
rustpython-parser.workspace = true
itertools.workspace = true
serde_json.workspace = true
anyhow.workspace = true
anyhow.workspace = true
rustpython-ast = { version = "0.4.0", features = ["visitor"] }

View File

@@ -0,0 +1,81 @@
use rustpython_ast::{Constant, Expr, ExprConstant, Visitor};
use rustpython_parser::{ast::Suite, Parse};
use windmill_parser::asset_parser::{
merge_assets, parse_asset_syntax, AssetKind, AssetUsageAccessType, ParseAssetsResult,
};
use AssetUsageAccessType::*;
pub fn parse_assets<'a>(
input: &'a str,
paths_storage: &'a mut Vec<String>,
) -> anyhow::Result<Vec<ParseAssetsResult<'a>>> {
let ast = Suite::parse(&input, "main.py")
.map_err(|e| anyhow::anyhow!("Error parsing code: {}", e.to_string()))?;
let mut assets_finder = AssetsFinder { assets: vec![], paths_storage };
ast.into_iter()
.for_each(|stmt| assets_finder.visit_stmt(stmt));
for (asset, path) in assets_finder
.assets
.iter_mut()
.zip(assets_finder.paths_storage.iter_mut())
{
asset.path = path;
}
Ok(merge_assets(assets_finder.assets))
}
struct AssetsFinder<'a> {
assets: Vec<ParseAssetsResult<'a>>,
// We have to store paths separately because of lifetime concerns
paths_storage: &'a mut Vec<String>,
}
impl<'a> Visitor for AssetsFinder<'a> {
fn visit_expr_call(&mut self, node: rustpython_ast::ExprCall) {
match self.visit_expr_call_inner(&node) {
Ok(_) => {}
Err(_) => self.generic_visit_expr_call(node),
}
}
}
impl<'a> AssetsFinder<'a> {
fn visit_expr_call_inner(&mut self, node: &rustpython_ast::ExprCall) -> Result<(), ()> {
let ident: String = node
.func
.as_name_expr()
.and_then(|o| o.id.parse().ok())
.or_else(|| {
node.func
.as_attribute_expr()
.and_then(|attr| attr.value.as_name_expr().and_then(|o| o.id.parse().ok()))
})
.ok_or(())?;
let (kind, access_type) = match ident.as_str() {
"get_resource" => (AssetKind::Resource, None),
"load_s3_file" => (AssetKind::S3Object, Some(R)),
"write_s3_file" => (AssetKind::S3Object, Some(W)),
_ => return Err(()),
};
if node.args.len() < 1 {
return Err(());
}
match &node.args[0] {
Expr::Constant(ExprConstant { value: Constant::Str(value), .. }) => {
if let Some((k, path)) = parse_asset_syntax(value.as_str()) {
if k != kind {
return Err(());
}
self.paths_storage.push(path.to_string());
self.assets
.push(ParseAssetsResult { kind, path: "", access_type });
}
}
_ => return Err(()),
};
Ok(())
}
}

View File

@@ -20,6 +20,9 @@ use rustpython_parser::{
Parse,
};
pub mod asset_parser;
pub use asset_parser::parse_assets;
const FUNCTION_CALL: &str = "<function call>";
fn filter_non_main(code: &str, main_name: &str) -> String {

View File

@@ -189,4 +189,15 @@ pub fn parse_assets_ts(code: &str) -> String {
}
}
#[cfg(feature = "py-parser")]
#[wasm_bindgen]
pub fn parse_assets_py(code: &str) -> String {
let mut paths_storage = vec![];
if let Ok(r) = windmill_parser_py::parse_assets(code, &mut paths_storage) {
return serde_json::to_string(&r).unwrap();
} else {
return "Invalid".to_string();
}
}
// for related places search: ADD_NEW_LANG

View File

@@ -135,7 +135,7 @@
dispatch('change', { code, schema })
})
let assets = usePromise(() => inferAssets(lang, code))
let assets = usePromise(() => inferAssets(lang, code), { clearValueOnRefresh: false })
$effect(() => {
untrack(() => assets.refresh()), [lang, code]
})

View File

@@ -299,10 +299,12 @@
}
})
let assets = usePromise(async () =>
flowModule.value.type === 'rawscript'
? await inferAssets(flowModule.value.language, flowModule.value.content)
: undefined
let assets = usePromise(
async () =>
flowModule.value.type === 'rawscript'
? await inferAssets(flowModule.value.language, flowModule.value.content)
: undefined,
{ clearValueOnRefresh: false }
)
$effect(() => {
if (flowModule.value.type !== 'rawscript') return

View File

@@ -749,15 +749,17 @@
key={[v.content, v.asset_alternative_access_types]}
runFirstEffect
onChange={() =>
inferAssets(v.language, v.content).then((assets) => {
for (const override of v.asset_alternative_access_types ?? []) {
assets = assets.map((asset) => {
if (assetEq(asset, override)) return { ...asset, access_type: override.access_type }
return asset
})
}
if (assetsMap && !deepEqual(assetsMap[mod.id], assets)) assetsMap[mod.id] = assets
})}
inferAssets(v.language, v.content)
.then((assets) => {
for (const override of v.asset_alternative_access_types ?? []) {
assets = assets.map((asset) => {
if (assetEq(asset, override)) return { ...asset, access_type: override.access_type }
return asset
})
}
if (assetsMap && !deepEqual(assetsMap[mod.id], assets)) assetsMap[mod.id] = assets
})
.catch((e) => {})}
/>
{/if}
{/each}

View File

@@ -19,7 +19,7 @@ import initRegexParsers, {
parse_powershell,
parse_assets_sql
} from 'windmill-parser-wasm-regex'
import initPythonParser, { parse_python } from 'windmill-parser-wasm-py'
import initPythonParser, { parse_assets_py, parse_python } from 'windmill-parser-wasm-py'
import initGoParser, { parse_go } from 'windmill-parser-wasm-go'
import initPhpParser, { parse_php } from 'windmill-parser-wasm-php'
import initRustParser, { parse_rust } from 'windmill-parser-wasm-rust'
@@ -92,6 +92,10 @@ export async function inferAssets(
await initWasmTs()
return JSON.parse(parse_assets_ts(code))
}
if (language === 'python3') {
await initWasmPython()
return JSON.parse(parse_assets_py(code))
}
return []
}

View File

@@ -28,11 +28,12 @@ export type UsePromiseResult<T> = (
export type UsePromiseOptions = {
loadInit?: boolean
clearValueOnRefresh?: boolean
}
export function usePromise<T>(
createPromise: () => Promise<T>,
{ loadInit = true }: UsePromiseOptions = {}
{ loadInit = true, clearValueOnRefresh = true }: UsePromiseOptions = {}
): UsePromiseResult<T> {
const ret: any = $state({
status: 'loading',
@@ -41,7 +42,7 @@ export function usePromise<T>(
let promise = createPromise()
ret.__promise = promise
ret.status = 'loading'
ret.value = undefined
if (clearValueOnRefresh) ret.value = undefined
ret.error = undefined
promise