Skip to content

Commit f98b411

Browse files
committed
✨ Start of a CEL package
1 parent 4c74010 commit f98b411

File tree

4 files changed

+384
-0
lines changed

4 files changed

+384
-0
lines changed

.github/workflows/CI.yml

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# This file is autogenerated by maturin v1.5.1
2+
# To update, run
3+
#
4+
# maturin generate-ci github
5+
#
6+
name: CI
7+
8+
on:
9+
push:
10+
branches:
11+
- main
12+
- master
13+
tags:
14+
- '*'
15+
pull_request:
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
linux:
23+
runs-on: ${{ matrix.platform.runner }}
24+
strategy:
25+
matrix:
26+
platform:
27+
- runner: ubuntu-latest
28+
target: x86_64
29+
- runner: ubuntu-latest
30+
target: x86
31+
- runner: ubuntu-latest
32+
target: aarch64
33+
- runner: ubuntu-latest
34+
target: armv7
35+
- runner: ubuntu-latest
36+
target: s390x
37+
- runner: ubuntu-latest
38+
target: ppc64le
39+
steps:
40+
- uses: actions/checkout@v4
41+
- uses: actions/setup-python@v5
42+
with:
43+
python-version: '3.10'
44+
- name: Build wheels
45+
uses: PyO3/maturin-action@v1
46+
with:
47+
target: ${{ matrix.platform.target }}
48+
args: --release --out dist --find-interpreter
49+
sccache: 'true'
50+
manylinux: auto
51+
- name: Upload wheels
52+
uses: actions/upload-artifact@v4
53+
with:
54+
name: wheels-linux-${{ matrix.platform.target }}
55+
path: dist
56+
57+
windows:
58+
runs-on: ${{ matrix.platform.runner }}
59+
strategy:
60+
matrix:
61+
platform:
62+
- runner: windows-latest
63+
target: x64
64+
- runner: windows-latest
65+
target: x86
66+
steps:
67+
- uses: actions/checkout@v4
68+
- uses: actions/setup-python@v5
69+
with:
70+
python-version: '3.10'
71+
architecture: ${{ matrix.platform.target }}
72+
- name: Build wheels
73+
uses: PyO3/maturin-action@v1
74+
with:
75+
target: ${{ matrix.platform.target }}
76+
args: --release --out dist --find-interpreter
77+
sccache: 'true'
78+
- name: Upload wheels
79+
uses: actions/upload-artifact@v4
80+
with:
81+
name: wheels-windows-${{ matrix.platform.target }}
82+
path: dist
83+
84+
macos:
85+
runs-on: ${{ matrix.platform.runner }}
86+
strategy:
87+
matrix:
88+
platform:
89+
- runner: macos-latest
90+
target: x86_64
91+
- runner: macos-14
92+
target: aarch64
93+
steps:
94+
- uses: actions/checkout@v4
95+
- uses: actions/setup-python@v5
96+
with:
97+
python-version: '3.10'
98+
- name: Build wheels
99+
uses: PyO3/maturin-action@v1
100+
with:
101+
target: ${{ matrix.platform.target }}
102+
args: --release --out dist --find-interpreter
103+
sccache: 'true'
104+
- name: Upload wheels
105+
uses: actions/upload-artifact@v4
106+
with:
107+
name: wheels-macos-${{ matrix.platform.target }}
108+
path: dist
109+
110+
sdist:
111+
runs-on: ubuntu-latest
112+
steps:
113+
- uses: actions/checkout@v4
114+
- name: Build sdist
115+
uses: PyO3/maturin-action@v1
116+
with:
117+
command: sdist
118+
args: --out dist
119+
- name: Upload sdist
120+
uses: actions/upload-artifact@v4
121+
with:
122+
name: wheels-sdist
123+
path: dist
124+
125+
release:
126+
name: Release
127+
runs-on: ubuntu-latest
128+
if: "startsWith(github.ref, 'refs/tags/')"
129+
needs: [linux, windows, macos, sdist]
130+
steps:
131+
- uses: actions/download-artifact@v4
132+
- name: Publish to PyPI
133+
uses: PyO3/maturin-action@v1
134+
env:
135+
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
136+
with:
137+
command: upload
138+
args: --non-interactive --skip-existing wheels-*/*

Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "cel-python"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
[lib]
8+
name = "cel"
9+
crate-type = ["cdylib"]
10+
11+
[dependencies]
12+
pyo3 = { version = "0.21.1", features = ["chrono"]}
13+
cel-interpreter = "0.7.0"
14+
log = "0.4.21"

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[build-system]
2+
requires = ["maturin>=1.5,<2.0"]
3+
build-backend = "maturin"
4+
5+
[project]
6+
name = "cel-python"
7+
requires-python = ">=3.11"
8+
classifiers = [
9+
"Programming Language :: Rust",
10+
"Programming Language :: Python :: Implementation :: CPython",
11+
"Programming Language :: Python :: Implementation :: PyPy",
12+
]
13+
dynamic = ["version"]
14+
[tool.maturin]
15+
features = ["pyo3/extension-module"]
16+
17+
[project.scripts]
18+
cel = "cel:cli"

src/lib.rs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
use cel_interpreter::objects::{Key, TryIntoValue};
2+
use cel_interpreter::{Context, Program, Value};
3+
use log::debug;
4+
use pyo3::exceptions::PyValueError;
5+
use pyo3::prelude::*;
6+
use pyo3::types::{PyDict, PyList};
7+
use std::collections::HashMap;
8+
use std::error::Error;
9+
use std::fmt;
10+
11+
#[derive(Debug)]
12+
struct RustyCelType(Value);
13+
14+
impl IntoPy<PyObject> for RustyCelType {
15+
fn into_py(self, py: Python<'_>) -> PyObject {
16+
// Just use the native rust type's existing
17+
// IntoPy implementation
18+
match self {
19+
// Primitive Types
20+
RustyCelType(Value::Null) => py.None(),
21+
RustyCelType(Value::Bool(b)) => b.into_py(py),
22+
RustyCelType(Value::Int(i64)) => i64.into_py(py),
23+
RustyCelType(Value::UInt(u64)) => u64.into_py(py),
24+
RustyCelType(Value::Float(f)) => f.into_py(py),
25+
RustyCelType(Value::Timestamp(ts)) => ts.into_py(py),
26+
RustyCelType(Value::String(arcStr)) => arcStr.as_ref().into_py(py),
27+
RustyCelType(Value::List(val)) => {
28+
let list = val
29+
.as_ref()
30+
.into_iter()
31+
.map(|v| RustyCelType(v.clone()).into_py(py))
32+
.collect::<Vec<PyObject>>();
33+
list.into_py(py)
34+
}
35+
RustyCelType(Value::Bytes(val)) => {
36+
let bytes = val;
37+
bytes.as_ref().as_slice().into_py(py)
38+
}
39+
RustyCelType(Value::Duration(d)) => d.into_py(py),
40+
41+
RustyCelType(Value::Map(val)) => {
42+
// Create a PyDict with the converted Python key and values.
43+
let python_dict = PyDict::new_bound(py);
44+
45+
val.map.as_ref().into_iter().for_each(|(k, v)| {
46+
// Key is an enum with String, Uint, Int and Bool variants. Value is any RustyCelType
47+
let key = match k {
48+
Key::String(arcStr) => arcStr.as_ref().into_py(py),
49+
Key::Uint(u64) => u64.into_py(py),
50+
Key::Int(i64) => i64.into_py(py),
51+
Key::Bool(b) => b.into_py(py),
52+
_ => panic!("Invalid key type in Map"),
53+
};
54+
let value = RustyCelType(v.clone()).into_py(py);
55+
python_dict
56+
.set_item(key, value)
57+
.expect("Failed to set item in Python dict");
58+
});
59+
60+
python_dict.into()
61+
}
62+
63+
// Turn everything else into a String:
64+
nonprimitive => format!("{:?}", nonprimitive).into_py(py),
65+
}
66+
}
67+
}
68+
69+
#[derive(Debug)]
70+
struct RustyPyType<'a>(&'a PyAny);
71+
72+
#[derive(Debug, PartialEq, Clone)]
73+
pub enum CelError {
74+
ConversionError(String)
75+
}
76+
77+
impl fmt::Display for CelError {
78+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79+
write!(f, "Cel Error")
80+
}
81+
}
82+
impl Error for CelError {}
83+
84+
/// We can't implement TryIntoValue for PyAny, so we implement for our wrapper type
85+
impl TryIntoValue for RustyPyType<'_> {
86+
type Error = CelError;
87+
88+
fn try_into_value(self) -> Result<Value, Self::Error> {
89+
let val = match self {
90+
RustyPyType(pyobject) => {
91+
if let Ok(value) = pyobject.extract::<i64>() {
92+
Ok(Value::Int(value))
93+
} else if let Ok(value) = pyobject.extract::<f64>() {
94+
Ok(Value::Float(value))
95+
} else if let Ok(value) = pyobject.extract::<bool>() {
96+
Ok(Value::Bool(value))
97+
} else if let Ok(value) = pyobject.extract::<String>() {
98+
Ok(Value::String(value.into()))
99+
100+
// TODO Deal with container types (List, Dict etc)
101+
102+
// } else if let Ok(value) = pyobject.extract::<PyList>() {
103+
// let list = value
104+
// .iter()
105+
// .map(|item| RustyPyType((*item)).try_into_value().expect("Failed to convert PyList to Value"))
106+
// .collect::<Vec<Value>>();
107+
// Ok(Value::List(list.into()))
108+
// } else if let Ok(value) = pyobject.extract::<PyDict>() {
109+
// let mut map:HashMap<Key, Value> = HashMap::new();
110+
// for (key, value) in value.into_iter() {
111+
// let key = key.extract::<String>()?;
112+
//
113+
// map.insert(Key::String(key.into()), RustyPyType(*value).try_into_value().expect("Failed to convert PyDict to Value"));
114+
// }
115+
// Ok(Value::Map(map.into()))
116+
} else {
117+
Err(CelError::ConversionError("Failed to convert PyAny to Value".to_string()))
118+
}
119+
}
120+
};
121+
val
122+
123+
}
124+
}
125+
126+
/// Evaluate a CEL expression
127+
/// Returns a String representation of the result
128+
#[pyfunction]
129+
fn evaluate(src: String, context: Option<&PyDict>) -> PyResult<RustyCelType> {
130+
debug!("Evaluating CEL expression: {}", src);
131+
debug!("Context: {:?}", context);
132+
133+
let program = Program::compile(src.as_str());
134+
135+
// Handle the result of the compilation
136+
match program {
137+
Err(compile_error) => {
138+
println!("An error occurred during compilation");
139+
println!("compile_error: {:?}", compile_error);
140+
// compile_error
141+
// .into_iter()
142+
// .for_each(|e| println!("Parse error: {:?}", e));
143+
return Err(PyValueError::new_err("Parse Error"));
144+
}
145+
Ok(program) => {
146+
let mut environment = Context::default();
147+
environment.add_function("add", |a: i64, b: i64| a + b);
148+
149+
// Add any variables from the passed in Dict context
150+
if let Some(context) = context {
151+
for (key, value) in context {
152+
let key = key.extract::<String>().unwrap();
153+
// Each value is of type PyAny, we need to try to extract into a Value
154+
// and then add it to the CEL context
155+
156+
157+
let wrapped_value = RustyPyType(value);
158+
match wrapped_value.try_into_value() {
159+
Ok(value) => {
160+
environment
161+
.add_variable(key, value)
162+
.expect("Failed to add variable to context");
163+
}
164+
Err(error) => {
165+
println!("An error occurred during conversion");
166+
println!("Conversion error: {:?}", error);
167+
return Err(PyValueError::new_err("Conversion Error"));
168+
}
169+
}
170+
171+
// This direct way is a bit hacky, need to find a better way to do this with traits
172+
// if let Ok(value) = value.extract::<i64>() {
173+
// environment
174+
// .add_variable(key, Value::Int(value))
175+
// .expect("Failed to add variable to context");
176+
// } else if let Ok(value) = value.extract::<f64>() {
177+
// environment
178+
// .add_variable(key, value)
179+
// .expect("Failed to add variable to context");
180+
// } else if let Ok(value) = value.extract::<bool>() {
181+
// environment
182+
// .add_variable(key, value)
183+
// .expect("Failed to add variable to context");
184+
// } else if let Ok(value) = value.extract::<String>() {
185+
// environment
186+
// .add_variable(key, value)
187+
// .expect("Failed to add variable to context");
188+
// }
189+
}
190+
}
191+
192+
let result = program.execute(&environment);
193+
match result {
194+
Err(error) => {
195+
println!("An error occurred during execution");
196+
println!("Execution error: {:?}", error);
197+
// errors
198+
// .into_iter()
199+
// .for_each(|e| println!("Execution error: {:?}", e));
200+
Err(PyValueError::new_err("Execution Error"))
201+
}
202+
203+
Ok(value) => return Ok(RustyCelType(value)),
204+
}
205+
}
206+
}
207+
}
208+
209+
/// A Python module implemented in Rust.
210+
#[pymodule]
211+
fn cel(_py: Python, m: &PyModule) -> PyResult<()> {
212+
m.add_function(wrap_pyfunction!(evaluate, m)?)?;
213+
Ok(())
214+
}

0 commit comments

Comments
 (0)