Skip to content

Commit 4fdda97

Browse files
committed
feat: update rust to pip install code block requirements
Signed-off-by: Nick Mitchell <[email protected]>
1 parent ea8004d commit 4fdda97

File tree

9 files changed

+144
-30
lines changed

9 files changed

+144
-30
lines changed

examples/talk/6-code-json.pdl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ text:
2626
- def: EVAL
2727
contribute: []
2828
lang: python
29+
requirements:
30+
- textdistance
2931
code:
3032
|
3133
import textdistance

pdl-live-react/src-tauri/src/cli/run.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@ use ::std::path::Path;
22
use duct::cmd;
33
use futures::executor::block_on;
44
use yaml_rust2::yaml::LoadError;
5+
use yaml_rust2::{ScanError, Yaml, YamlLoader};
56

6-
use crate::interpreter::pip::pip_install_interpreter_if_needed;
7+
use crate::interpreter::pip::{
8+
pip_install_code_blocks_if_needed, pip_install_interpreter_if_needed,
9+
};
710
use crate::interpreter::pull::pull_if_needed;
811

12+
/// Read the given filesystem path and produce a potentially multi-document Yaml
13+
fn from_path(path: &String) -> Result<Vec<Yaml>, ScanError> {
14+
let content = std::fs::read_to_string(path).unwrap();
15+
YamlLoader::load_from_str(&content)
16+
}
17+
918
#[cfg(desktop)]
1019
pub fn run_pdl_program(
1120
source_file_path: String,
@@ -20,8 +29,10 @@ pub fn run_pdl_program(
2029
);
2130

2231
// async the model pull and pip installs
23-
let pull_future = pull_if_needed(&source_file_path);
24-
let bin_path_future = pip_install_interpreter_if_needed(app_handle);
32+
let program = &from_path(&source_file_path).unwrap()[0];
33+
let pull_future = pull_if_needed(&program);
34+
let reqs_future = pip_install_code_blocks_if_needed(&app_handle, &program);
35+
let bin_path_future = pip_install_interpreter_if_needed(&app_handle);
2536

2637
// wait for any model pulls to finish
2738
block_on(pull_future).map_err(|e| match e {
@@ -32,6 +43,7 @@ pub fn run_pdl_program(
3243

3344
// wait for any pip installs to finish
3445
let bin_path = block_on(bin_path_future)?;
46+
block_on(reqs_future)?;
3547

3648
let mut args = vec![
3749
source_file_path,

pdl-live-react/src-tauri/src/interpreter/extract.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
use yaml_rust2::Yaml;
22

3-
/// Extract models referenced by the programs
4-
pub fn extract_models(programs: Vec<Yaml>) -> Vec<String> {
5-
extract_values(programs, "model")
3+
/// Extract models referenced by the program
4+
pub fn extract_models(program: &Yaml) -> Vec<String> {
5+
extract_values(program, "model")
6+
}
7+
8+
/// Extract requirements.txt referenced by the program
9+
pub fn extract_requirements(program: &Yaml) -> Vec<String> {
10+
extract_values(program, "requirements")
611
}
712

813
/// Take a list of Yaml fragments and produce a vector of the string-valued entries of the given field
9-
pub fn extract_values(programs: Vec<Yaml>, field: &str) -> Vec<String> {
10-
let mut values = programs
11-
.into_iter()
12-
.flat_map(|p| extract_one_values(p, field))
13-
.collect::<Vec<String>>();
14+
pub fn extract_values(program: &Yaml, field: &str) -> Vec<String> {
15+
let mut values = extract_one_values(program, field);
1416

1517
// A single program may specify the same model more than once. Dedup!
1618
values.sort();
@@ -20,7 +22,7 @@ pub fn extract_values(programs: Vec<Yaml>, field: &str) -> Vec<String> {
2022
}
2123

2224
/// Take one Yaml fragment and produce a vector of the string-valued entries of the given field
23-
fn extract_one_values(program: Yaml, field: &str) -> Vec<String> {
25+
fn extract_one_values(program: &Yaml, field: &str) -> Vec<String> {
2426
let mut values: Vec<String> = Vec::new();
2527

2628
match program {
@@ -31,6 +33,10 @@ fn extract_one_values(program: Yaml, field: &str) -> Vec<String> {
3133
Yaml::String(m) => {
3234
values.push(m.to_string());
3335
}
36+
Yaml::Array(a) => a.into_iter().for_each(|v| match v {
37+
Yaml::String(m) => values.push(m.to_string()),
38+
_ => {}
39+
}),
3440
_ => {}
3541
},
3642
_ => {}
Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
use ::std::fs::{copy, create_dir_all};
1+
use ::std::fs::{copy, create_dir_all, write};
22
use ::std::path::{Path, PathBuf};
33

44
use duct::cmd;
5+
use rayon::prelude::*;
56
use tauri::path::BaseDirectory;
67
use tauri::Manager;
8+
use tempfile::Builder;
9+
use yaml_rust2::Yaml;
710

11+
use crate::interpreter::extract;
812
use crate::interpreter::shasum;
913

1014
#[cfg(desktop)]
11-
pub async fn pip_install_if_needed(
15+
fn pip_install_if_needed_with_hash(
1216
cache_path: &Path,
1317
requirements_path: &Path,
18+
hash: String,
19+
force: bool,
1420
) -> Result<PathBuf, tauri::Error> {
1521
create_dir_all(&cache_path)?;
1622

17-
let hash = shasum::sha256sum(&requirements_path)?;
1823
let venv_path = cache_path.join("venvs").join(hash);
1924
let bin_path = venv_path.join(if cfg!(windows) { "Scripts" } else { "bin" });
2025

26+
// re: force, this is part of the short-term hack to install all
27+
// code block dependencies in the main interpreter venv. Once we
28+
// figure out how to support a separate venv for each code block
29+
// (that needs it), we can undo this hack.
2130
if !venv_path.exists() {
2231
println!("Creating virtual environment...");
2332
let python = if cfg!(target_os = "macos") {
@@ -27,18 +36,65 @@ pub async fn pip_install_if_needed(
2736
};
2837
cmd!(python, "-mvenv", &venv_path).run()?;
2938

30-
cmd!(bin_path.join("pip"), "install", "-r", &requirements_path,).run()?;
39+
if !force {
40+
cmd!(bin_path.join("pip"), "install", "-r", &requirements_path).run()?;
41+
42+
let cached_requirements_path = venv_path.join("requirements.txt");
43+
copy(requirements_path, cached_requirements_path)?;
44+
}
45+
}
3146

32-
let cached_requirements_path = venv_path.join("requirements.txt");
33-
copy(requirements_path, cached_requirements_path)?;
47+
if force {
48+
cmd!(bin_path.join("pip"), "install", "-r", &requirements_path).run()?;
3449
}
3550

3651
Ok(bin_path.to_path_buf())
3752
}
3853

54+
#[cfg(desktop)]
55+
fn pip_install_if_needed(
56+
cache_path: &Path,
57+
requirements_path: &Path,
58+
) -> Result<PathBuf, tauri::Error> {
59+
let hash = shasum::sha256sum(&requirements_path)?;
60+
pip_install_if_needed_with_hash(cache_path, requirements_path, hash, false)
61+
}
62+
63+
#[cfg(desktop)]
64+
pub async fn pip_install_code_blocks_if_needed(
65+
app_handle: &tauri::AppHandle,
66+
program: &Yaml,
67+
) -> Result<(), tauri::Error> {
68+
let cache_path = app_handle.path().cache_dir()?.join("pdl");
69+
70+
// for now, install the requirements in the main interpreter venv
71+
let requirements_path = app_handle
72+
.path()
73+
.resolve("interpreter/requirements.txt", BaseDirectory::Resource)?;
74+
75+
extract::extract_requirements(program)
76+
.into_par_iter()
77+
.try_for_each(|req| -> Result<(), tauri::Error> {
78+
let req_path = Builder::new()
79+
.prefix("pdl-requirements-")
80+
.suffix(".txt")
81+
.tempfile()?;
82+
// This is part of the "force" hack described above, where
83+
// we force the code block dependencies to be installed in
84+
// the main interpreter venv.
85+
let hash = shasum::sha256sum(&requirements_path)?;
86+
write(&req_path, req)?;
87+
pip_install_if_needed_with_hash(&cache_path, &req_path.path(), hash, true)?;
88+
Ok(())
89+
})
90+
.expect("code block requirements installed");
91+
92+
Ok(())
93+
}
94+
3995
#[cfg(desktop)]
4096
pub async fn pip_install_interpreter_if_needed(
41-
app_handle: tauri::AppHandle,
97+
app_handle: &tauri::AppHandle,
4298
) -> Result<PathBuf, tauri::Error> {
4399
// the interpreter requirements.txt
44100
let requirements_path = app_handle
@@ -47,5 +103,5 @@ pub async fn pip_install_interpreter_if_needed(
47103

48104
let cache_path = app_handle.path().cache_dir()?.join("pdl");
49105

50-
pip_install_if_needed(&cache_path, &requirements_path).await
106+
pip_install_if_needed(&cache_path, &requirements_path)
51107
}

pdl-live-react/src-tauri/src/interpreter/pull.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
use duct::cmd;
22
use rayon::prelude::*;
33
use yaml_rust2::yaml::LoadError;
4-
use yaml_rust2::{ScanError, Yaml, YamlLoader};
4+
use yaml_rust2::Yaml;
55

66
use crate::interpreter::extract;
77

8-
/// Read the given filesystem path and produce a potentially multi-document Yaml
9-
fn from_path(path: &String) -> Result<Vec<Yaml>, ScanError> {
10-
let content = std::fs::read_to_string(path).unwrap();
11-
YamlLoader::load_from_str(&content)
12-
}
13-
148
/// Pull models (in parallel) from the PDL program in the given filepath.
15-
pub async fn pull_if_needed(path: &String) -> Result<(), LoadError> {
16-
extract::extract_models(from_path(path).unwrap())
9+
pub async fn pull_if_needed(program: &Yaml) -> Result<(), LoadError> {
10+
extract::extract_models(program)
1711
.into_par_iter()
1812
.try_for_each(|model| match model {
1913
m if model.starts_with("ollama/") => ollama_pull_if_needed(&m[7..]),

pdl-live-react/src/pdl_ast.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,11 @@ export type Kind16 = "code"
20382038
*
20392039
*/
20402040
export type Lang1 = "python" | "command" | "jinja" | "pdl"
2041+
/**
2042+
* Pip requirements.txt
2043+
*
2044+
*/
2045+
export type Requirements = string | string[] | null
20412046
/**
20422047
* Code to execute.
20432048
*
@@ -2984,6 +2989,7 @@ export interface CodeBlock {
29842989
pdl__is_leaf?: PdlIsLeaf16
29852990
kind?: Kind16
29862991
lang: Lang1
2992+
requirements?: Requirements
29872993
code: Code
29882994
}
29892995
/**

src/pdl/pdl-schema.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,25 @@
16991699
"title": "Lang",
17001700
"type": "string"
17011701
},
1702+
"requirements": {
1703+
"anyOf": [
1704+
{
1705+
"type": "string"
1706+
},
1707+
{
1708+
"items": {
1709+
"type": "string"
1710+
},
1711+
"type": "array"
1712+
},
1713+
{
1714+
"type": "null"
1715+
}
1716+
],
1717+
"default": null,
1718+
"description": "Pip requirements.txt\n ",
1719+
"title": "Requirements"
1720+
},
17021721
"code": {
17031722
"anyOf": [
17041723
{

src/pdl/pdl_ast.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@
1515
Union,
1616
)
1717

18-
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, RootModel
18+
from pydantic import (
19+
BaseModel,
20+
BeforeValidator,
21+
ConfigDict,
22+
Field,
23+
RootModel,
24+
model_validator,
25+
)
1926
from pydantic.json_schema import SkipJsonSchema
2027

2128
from .pdl_lazy import PdlDict, PdlLazy
@@ -479,10 +486,21 @@ class CodeBlock(BaseCodeBlock):
479486
]
480487
"""Programming language of the code.
481488
"""
489+
requirements: Optional[str | list[str]] = None
490+
"""Pip requirements.txt
491+
"""
482492
code: "BlockType"
483493
"""Code to execute.
484494
"""
485495

496+
@model_validator(mode="after")
497+
def lang_is_python(self):
498+
if self.requirements is not None and self.lang != "python":
499+
raise ValueError(
500+
"CodeBlock requirements field provided for non-python block"
501+
)
502+
return self
503+
486504

487505
class ArgsBlock(BaseCodeBlock):
488506
"""

src/pdl/pdl_dumper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ def block_to_dict( # noqa: C901
144144
case CodeBlock():
145145
d["lang"] = block.lang
146146
d["code"] = block_to_dict(block.code, json_compatible)
147+
d["requirements"] = block.requirements
147148
case GetBlock():
148149
d["get"] = block.get
149150
case DataBlock():

0 commit comments

Comments
 (0)