mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Python bindings (#115)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
055bdd295f
commit
8ca863c661
113
.github/workflows/publish-python.yml
vendored
Normal file
113
.github/workflows/publish-python.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
# This file is autogenerated by maturin v1.4.0
|
||||
# To update, run
|
||||
#
|
||||
# maturin generate-ci github --zig
|
||||
#
|
||||
name: publish-python
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter --zig
|
||||
sccache: 'true'
|
||||
manylinux: auto
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x64, x86]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
architecture: ${{ matrix.target }}
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [x86_64, aarch64]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist
|
||||
|
||||
sdist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build sdist
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
- name: Upload sdist
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
if: "startsWith(github.ref, 'refs/tags/')"
|
||||
needs: [linux, windows, macos, sdist]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
- name: Publish to PyPI
|
||||
uses: PyO3/maturin-action@v1
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
with:
|
||||
command: upload
|
||||
args: --non-interactive --skip-existing *
|
||||
10
.github/workflows/publish-wasm.yml
vendored
10
.github/workflows/publish-wasm.yml
vendored
@@ -15,9 +15,17 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- name: Build
|
||||
run: wasm-pack build --target nodejs -r
|
||||
- name: Publish
|
||||
run: # TODO
|
||||
run: wasm-pack publish --target nodejs
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
workspace = { members = ["bindings/wasm"] }
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"bindings/python",
|
||||
"bindings/wasm"
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "regorus"
|
||||
description = "A fast, lightweight Rego (OPA policy language) interpreter"
|
||||
|
||||
@@ -65,7 +65,8 @@ builtins. See [OPA Conformance](#opa-conformance) below.
|
||||
|
||||
Regorus can be used from a variety of languages:
|
||||
|
||||
- Javascript (nodejs): Via npm package `regorus-wasm`. This package is Regorus compiled into WASM.
|
||||
- Javascript: Via npm package `regorusjs`. This package is Regorus compiled into WASM.
|
||||
- Python: Via `regorus` package.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
1
bindings/python/.python-version
Normal file
1
bindings/python/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
pyo3
|
||||
20
bindings/python/Cargo.toml
Normal file
20
bindings/python/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "regoruspy"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/microsoft/regorus/bindings/python"
|
||||
description = "Python bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
|
||||
keywords = ["interpreter", "opa", "policy-as-code", "rego"]
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.79"
|
||||
ordered-float = "4.2.0"
|
||||
pyo3 = {version = "0.20.2", features = ["anyhow", "extension-module"] }
|
||||
regorus = { path = "../.." }
|
||||
serde_json = "1.0.112"
|
||||
|
||||
16
bindings/python/pyproject.toml
Normal file
16
bindings/python/pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[build-system]
|
||||
requires = ["maturin>=1.4,<2.0"]
|
||||
build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "regorus"
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Programming Language :: Rust",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[tool.maturin]
|
||||
features = ["pyo3/extension-module"]
|
||||
279
bindings/python/src/lib.rs
Normal file
279
bindings/python/src/lib.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
use anyhow::{anyhow, Result};
|
||||
use pyo3::exceptions::PyTypeError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::*;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use regorus::Value;
|
||||
|
||||
/// Python wrapper for [`regorus::Engine`]
|
||||
#[pyclass(unsendable)]
|
||||
pub struct Engine {
|
||||
engine: regorus::Engine,
|
||||
}
|
||||
|
||||
impl Default for Engine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Engine {
|
||||
/// Clone a [`Engine`]
|
||||
///
|
||||
/// To avoid having to parse same policy again, the engine can be cloned
|
||||
/// after policies and data have been added.
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
engine: self.engine.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from<'source>(ob: &'source PyAny) -> Result<Value, PyErr> {
|
||||
// dicts
|
||||
Ok(if let Ok(dict) = ob.downcast::<PyDict>() {
|
||||
let mut map = BTreeMap::new();
|
||||
for (k, v) in dict {
|
||||
map.insert(from(k)?, from(v)?);
|
||||
}
|
||||
map.into()
|
||||
}
|
||||
// set
|
||||
else if let Ok(pset) = ob.downcast::<PySet>() {
|
||||
let mut set = BTreeSet::new();
|
||||
for v in pset {
|
||||
set.insert(from(v)?);
|
||||
}
|
||||
set.into()
|
||||
}
|
||||
// frozen set
|
||||
else if let Ok(pfset) = ob.downcast::<PyFrozenSet>() {
|
||||
//
|
||||
let mut set = BTreeSet::new();
|
||||
for v in pfset {
|
||||
set.insert(from(v)?);
|
||||
}
|
||||
set.into()
|
||||
}
|
||||
// lists and tuples
|
||||
else if let Ok(plist) = ob.downcast::<PyList>() {
|
||||
let mut array = Vec::new();
|
||||
for v in plist {
|
||||
array.push(from(v)?);
|
||||
}
|
||||
array.into()
|
||||
} else if let Ok(ptuple) = ob.downcast::<PyTuple>() {
|
||||
let mut array = Vec::new();
|
||||
for v in ptuple {
|
||||
array.push(from(v)?);
|
||||
}
|
||||
array.into()
|
||||
}
|
||||
// String
|
||||
else if let Ok(s) = String::extract(ob) {
|
||||
s.into()
|
||||
}
|
||||
// Numeric
|
||||
else if let Ok(v) = i64::extract(ob) {
|
||||
v.into()
|
||||
} else if let Ok(v) = u64::extract(ob) {
|
||||
v.into()
|
||||
} else if let Ok(v) = f64::extract(ob) {
|
||||
v.into()
|
||||
}
|
||||
// Boolean
|
||||
else if let Ok(b) = bool::extract(ob) {
|
||||
b.into()
|
||||
}
|
||||
// None
|
||||
else if ob.downcast::<PyNone>().is_ok() {
|
||||
Value::Null
|
||||
}
|
||||
// Anything that is a sequence
|
||||
else if let Ok(pseq) = ob.downcast::<PySequence>() {
|
||||
let mut array = Vec::new();
|
||||
for i in 0..pseq.len()? {
|
||||
array.push(from(pseq.get_item(i)?)?);
|
||||
}
|
||||
array.into()
|
||||
}
|
||||
// Anything that is a map
|
||||
else if let Ok(pmap) = ob.downcast::<PyMapping>() {
|
||||
let mut map = BTreeMap::new();
|
||||
let keys = pmap.keys()?;
|
||||
let values = pmap.values()?;
|
||||
for i in 0..keys.len()? {
|
||||
let key = keys.get_item(i)?;
|
||||
let value = values.get_item(i)?;
|
||||
map.insert(from(key)?, from(value)?);
|
||||
}
|
||||
map.into()
|
||||
} else {
|
||||
return Err(PyErr::new::<PyTypeError, _>(
|
||||
"object cannot be converted to RegoValue",
|
||||
));
|
||||
})
|
||||
}
|
||||
|
||||
fn to(mut v: Value, py: Python<'_>) -> Result<PyObject> {
|
||||
Ok(match v {
|
||||
Value::Null => None::<u64>.to_object(py),
|
||||
|
||||
// TODO: Revisit this mapping
|
||||
Value::Undefined => None::<u64>.to_object(py),
|
||||
|
||||
Value::Bool(b) => b.to_object(py),
|
||||
Value::String(s) => s.to_object(py),
|
||||
|
||||
Value::Number(_) => {
|
||||
if let Ok(f) = v.as_f64() {
|
||||
f.to_object(py)
|
||||
} else if let Ok(u) = v.as_u64() {
|
||||
u.to_object(py)
|
||||
} else {
|
||||
v.as_i64()?.to_object(py)
|
||||
}
|
||||
}
|
||||
|
||||
Value::Array(_) => {
|
||||
let list = PyList::empty(py);
|
||||
for v in std::mem::replace(v.as_array_mut()?, Vec::new()) {
|
||||
list.append(to(v, py)?)?;
|
||||
}
|
||||
list.into()
|
||||
}
|
||||
|
||||
Value::Set(_) => {
|
||||
let set = PySet::empty(py)?;
|
||||
for v in std::mem::replace(v.as_set_mut()?, BTreeSet::new()) {
|
||||
set.add(to(v, py)?)?;
|
||||
}
|
||||
set.into()
|
||||
}
|
||||
|
||||
Value::Object(_) => {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in std::mem::replace(v.as_object_mut()?, BTreeMap::new()) {
|
||||
dict.set_item(to(k, py)?, to(v, py)?)?;
|
||||
}
|
||||
dict.into()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Engine {
|
||||
/// Construct a new Engine
|
||||
#[new]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
engine: regorus::Engine::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a policy
|
||||
///
|
||||
/// The policy is parsed into AST.
|
||||
///
|
||||
/// * `path`: A filename to be associated with the policy.
|
||||
/// * `rego`: Rego policy.
|
||||
pub fn add_policy(&mut self, path: String, rego: String) -> Result<()> {
|
||||
self.engine.add_policy(path, rego)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add policy data.
|
||||
///
|
||||
/// * `data`: Rego value. A Rego value is a number, bool, string, None
|
||||
/// or a list/set/map whose items themselves are Rego values.
|
||||
pub fn add_data(&mut self, data: &PyAny) -> Result<()> {
|
||||
let data = from(data)?;
|
||||
self.engine.add_data(data)
|
||||
}
|
||||
|
||||
/// Add policy data.
|
||||
///
|
||||
/// * `data`: JSON encoded value to be used as policy data.
|
||||
pub fn add_data_json(&mut self, data: String) -> Result<()> {
|
||||
let data = regorus::Value::from_json_str(&data)?;
|
||||
self.engine.add_data(data)
|
||||
}
|
||||
|
||||
/// Clear policy data.
|
||||
pub fn clear_data(&mut self) -> Result<()> {
|
||||
self.engine.clear_data();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set input.
|
||||
///
|
||||
/// * `input`: Rego value. A Rego value is a number, bool, string, None
|
||||
/// or a list/set/map whose items themselves are Rego values.
|
||||
pub fn set_input(&mut self, input: &PyAny) -> Result<()> {
|
||||
let input = from(input)?;
|
||||
self.engine.set_input(input);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set input.
|
||||
///
|
||||
/// * `input`: JSON encoded value to be used as input to query.
|
||||
pub fn set_input_json(&mut self, input: String) -> Result<()> {
|
||||
let input = regorus::Value::from_json_str(&input)?;
|
||||
self.engine.set_input(input);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Evaluate query.
|
||||
///
|
||||
/// * `query`: Rego expression to be evaluate.
|
||||
pub fn eval_query(&mut self, query: String, py: Python<'_>) -> Result<PyObject> {
|
||||
let results = self.engine.eval_query(query, false)?;
|
||||
|
||||
let rlist = PyList::empty(py);
|
||||
for result in results.result.into_iter() {
|
||||
let rdict = PyDict::new(py);
|
||||
|
||||
let elist = PyList::empty(py);
|
||||
for expr in result.expressions.into_iter() {
|
||||
let edict = PyDict::new(py);
|
||||
edict.set_item("value".to_object(py), to(expr.value, py)?)?;
|
||||
edict.set_item("text".to_object(py), expr.text.as_ref().to_object(py))?;
|
||||
|
||||
let ldict = PyDict::new(py);
|
||||
ldict.set_item("row".to_object(py), expr.location.row.to_object(py))?;
|
||||
ldict.set_item("col".to_object(py), expr.location.col.to_object(py))?;
|
||||
|
||||
edict.set_item("location".to_object(py), ldict)?;
|
||||
elist.append(edict)?;
|
||||
}
|
||||
|
||||
rdict.set_item("expressions".to_object(py), elist)?;
|
||||
rdict.set_item("bindings".to_object(py), to(result.bindings, py)?)?;
|
||||
rlist.append(rdict)?;
|
||||
}
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("result".to_object(py), rlist)?;
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
/// Evaluate query. Returns result as JSON.
|
||||
///
|
||||
/// * `query`: Rego expression to be evaluate.
|
||||
pub fn eval_query_as_json(&mut self, query: String) -> Result<String> {
|
||||
let results = self.engine.eval_query(query, false)?;
|
||||
serde_json::to_string_pretty(&results).map_err(|e| anyhow!("{e}"))
|
||||
}
|
||||
}
|
||||
|
||||
mod export {
|
||||
use pyo3::prelude::*;
|
||||
#[pymodule]
|
||||
fn regorus(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<crate::Engine>()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "regorus-wasm"
|
||||
name = "regorusjs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/microsoft/regorus/bindings/wasm"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# regorus-wasm
|
||||
# regorusjs
|
||||
|
||||
**Regorus** is
|
||||
|
||||
@@ -8,21 +8,70 @@
|
||||
|
||||
See [Repository](https://github.com/microsoft/regorus).
|
||||
|
||||
`regorus-wasm` is Regorus compiled into WASM.
|
||||
`regorusjs` is Regorus compiled into WASM.
|
||||
|
||||
## Usage
|
||||
|
||||
In nodejs,
|
||||
```javascript
|
||||
|
||||
``javascript
|
||||
|
||||
var regorus = require('regorus-wasm')
|
||||
var regorus = require('regorusjs')
|
||||
|
||||
// Create an engine.
|
||||
var engine = new regorus.Engine();
|
||||
|
||||
// Add Rego policy.
|
||||
engine.add_policy()
|
||||
engine.add_policy(
|
||||
// Associate this file name with policy
|
||||
'hello.rego',
|
||||
|
||||
// Rego policy
|
||||
`
|
||||
package test
|
||||
|
||||
# Join messages
|
||||
message = concat(", ", [input.message, data.message])
|
||||
`)
|
||||
|
||||
// Set policy data
|
||||
engine.add_data_json(`
|
||||
{
|
||||
"message" : "World!"
|
||||
}
|
||||
`)
|
||||
|
||||
// Set policy input
|
||||
engine.set_input_json(`
|
||||
{
|
||||
"message" : "Hello"
|
||||
}
|
||||
`)
|
||||
|
||||
// Eval query
|
||||
results = engine.eval_query('data.test.message')
|
||||
|
||||
// Display
|
||||
console.log(results)
|
||||
// {
|
||||
// "result": [
|
||||
// {
|
||||
// "expressions": [
|
||||
// {
|
||||
// "value": "Hello, World!",
|
||||
// "text": "data.test.message",
|
||||
// "location": {
|
||||
// "row": 1,
|
||||
// "col": 1
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
// Convert results to object
|
||||
results = JSON.parse(results)
|
||||
|
||||
// Process result
|
||||
console.log(results.result[0].expressions[0].value)
|
||||
// Hello, World!
|
||||
```
|
||||
|
||||
@@ -63,6 +63,12 @@ impl Engine {
|
||||
self.engine.add_data(data).map_err(error_to_jsvalue)
|
||||
}
|
||||
|
||||
/// Clear policy data.
|
||||
pub fn clear_data(&mut self) -> Result<(), JsValue> {
|
||||
self.engine.clear_data();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set input.
|
||||
///
|
||||
/// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.set_input
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::QueryResults;
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
/// The Rego evaluation engine.
|
||||
///
|
||||
@@ -175,6 +175,9 @@ impl Engine {
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn add_data(&mut self, data: Value) -> Result<()> {
|
||||
if data.as_object().is_err() {
|
||||
bail!("data must be object");
|
||||
}
|
||||
self.prepared = false;
|
||||
self.interpreter.get_data_mut().merge(data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user