Python asset parser
This commit is contained in:
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@@ -15064,6 +15064,7 @@ version = "1.502.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
"rustpython-ast",
|
||||
"rustpython-parser",
|
||||
"serde_json",
|
||||
"windmill-parser",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
81
backend/parsers/windmill-parser-py/src/asset_parser.rs
Normal file
81
backend/parsers/windmill-parser-py/src/asset_parser.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user