opa-golib-python-bindings 0.1.2__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/PKG-INFO +15 -1
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/README.md +14 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/go/bridge.go +32 -6
- opa_golib_python_bindings-0.2.0/go/trace.go +78 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/pyproject.toml +1 -1
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/src/opa_bindings/_native.py +14 -2
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/src/opa_bindings/engine.py +52 -6
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/.gitignore +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/LICENSE +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/Makefile +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/go/bridge_call.c +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/go/builtins.go +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/go/go.mod +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/go/go.sum +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/go/merge.go +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/hatch_build.py +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/src/opa_bindings/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opa-golib-python-bindings
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python bindings for the OPA (Open Policy Agent) Rego engine via a Go c-shared library
|
|
5
5
|
Project-URL: Homepage, https://github.com/phi1010/opa-golib-python-bindings
|
|
6
6
|
Project-URL: Repository, https://github.com/phi1010/opa-golib-python-bindings
|
|
@@ -68,6 +68,20 @@ Notes:
|
|
|
68
68
|
- Builtin arguments and return values are JSON-compatible objects. A callback exception
|
|
69
69
|
becomes an evaluation error; returning is fine.
|
|
70
70
|
- An undefined document raises `OpaUndefinedError`.
|
|
71
|
+
- Pass `coverage=True` to `eval_document` / `eval_query` to capture a coverage
|
|
72
|
+
report (OPA's `cover` tracer) in `engine.last_coverage`: per-file `covered` /
|
|
73
|
+
`not_covered` line ranges plus line counts and a coverage percentage over all
|
|
74
|
+
added policies. Evaluating without `coverage=True` resets it to `None`.
|
|
75
|
+
- Pass `trace=True` to capture the full evaluation trace in `engine.last_trace`:
|
|
76
|
+
a list of event dicts (`op`, `location`, `node`, `locals`, ...) in evaluation
|
|
77
|
+
order. `locals` holds the plugged variable bindings live at each step, so the
|
|
78
|
+
value a statement produced is visible (e.g. `{"x": 6}` after `x := input.n * 2`);
|
|
79
|
+
a false condition appears as a `Fail` event at its location. An undefined
|
|
80
|
+
document still carries its trace — the main way to see which condition failed. Note that `node`
|
|
81
|
+
shows the compiler-rewritten expression (temporaries like `__local0__`), a
|
|
82
|
+
statement may appear multiple times (`Redo` on backtracking), and tracing
|
|
83
|
+
slows evaluation, so keep it opt-in per call. Coverage only records *which*
|
|
84
|
+
statements were evaluated; traces are how to see their results.
|
|
71
85
|
- Rego `print(...)` output is captured per evaluation: set `engine.print_handler`
|
|
72
86
|
to a `callable(message, location)` to receive it (default: written to stderr);
|
|
73
87
|
`engine.last_prints` holds the `(message, location)` pairs of the last eval.
|
|
@@ -43,6 +43,20 @@ Notes:
|
|
|
43
43
|
- Builtin arguments and return values are JSON-compatible objects. A callback exception
|
|
44
44
|
becomes an evaluation error; returning is fine.
|
|
45
45
|
- An undefined document raises `OpaUndefinedError`.
|
|
46
|
+
- Pass `coverage=True` to `eval_document` / `eval_query` to capture a coverage
|
|
47
|
+
report (OPA's `cover` tracer) in `engine.last_coverage`: per-file `covered` /
|
|
48
|
+
`not_covered` line ranges plus line counts and a coverage percentage over all
|
|
49
|
+
added policies. Evaluating without `coverage=True` resets it to `None`.
|
|
50
|
+
- Pass `trace=True` to capture the full evaluation trace in `engine.last_trace`:
|
|
51
|
+
a list of event dicts (`op`, `location`, `node`, `locals`, ...) in evaluation
|
|
52
|
+
order. `locals` holds the plugged variable bindings live at each step, so the
|
|
53
|
+
value a statement produced is visible (e.g. `{"x": 6}` after `x := input.n * 2`);
|
|
54
|
+
a false condition appears as a `Fail` event at its location. An undefined
|
|
55
|
+
document still carries its trace — the main way to see which condition failed. Note that `node`
|
|
56
|
+
shows the compiler-rewritten expression (temporaries like `__local0__`), a
|
|
57
|
+
statement may appear multiple times (`Redo` on backtracking), and tracing
|
|
58
|
+
slows evaluation, so keep it opt-in per call. Coverage only records *which*
|
|
59
|
+
statements were evaluated; traces are how to see their results.
|
|
46
60
|
- Rego `print(...)` output is captured per evaluation: set `engine.print_handler`
|
|
47
61
|
to a `callable(message, location)` to receive it (default: written to stderr);
|
|
48
62
|
`engine.last_prints` holds the `(message, location)` pairs of the last eval.
|
|
@@ -21,6 +21,7 @@ import (
|
|
|
21
21
|
"unsafe"
|
|
22
22
|
|
|
23
23
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
24
|
+
"github.com/open-policy-agent/opa/v1/cover"
|
|
24
25
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
25
26
|
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
|
26
27
|
"github.com/open-policy-agent/opa/v1/topdown/print"
|
|
@@ -187,21 +188,21 @@ func OpaRegisterBuiltin(h C.ulonglong, name *C.char, arity C.int, cb C.opa_callb
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
//export OpaEvalQuery
|
|
190
|
-
func OpaEvalQuery(h C.ulonglong, query, inputJson *C.char) *C.char {
|
|
191
|
-
return evalCommon(h, C.GoString(query), inputJson)
|
|
191
|
+
func OpaEvalQuery(h C.ulonglong, query, inputJson *C.char, coverage, trace C.int) *C.char {
|
|
192
|
+
return evalCommon(h, C.GoString(query), inputJson, coverage != 0, trace != 0)
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
//export OpaEvalDocument
|
|
195
|
-
func OpaEvalDocument(h C.ulonglong, docPath, inputJson *C.char) *C.char {
|
|
196
|
+
func OpaEvalDocument(h C.ulonglong, docPath, inputJson *C.char, coverage, trace C.int) *C.char {
|
|
196
197
|
p := C.GoString(docPath)
|
|
197
198
|
q := "data"
|
|
198
199
|
if p != "" {
|
|
199
200
|
q = "data." + p
|
|
200
201
|
}
|
|
201
|
-
return evalCommon(h, q, inputJson)
|
|
202
|
+
return evalCommon(h, q, inputJson, coverage != 0, trace != 0)
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
func evalCommon(h C.ulonglong, query string, inputJson *C.char) *C.char {
|
|
205
|
+
func evalCommon(h C.ulonglong, query string, inputJson *C.char, coverage, trace bool) *C.char {
|
|
205
206
|
e, err := getEngine(h)
|
|
206
207
|
if err != nil {
|
|
207
208
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -243,11 +244,36 @@ func evalCommon(h C.ulonglong, query string, inputJson *C.char) *C.char {
|
|
|
243
244
|
}
|
|
244
245
|
collector := &printCollector{}
|
|
245
246
|
evalOpts = append(evalOpts, rego.EvalPrintHook(collector))
|
|
247
|
+
var cov *cover.Cover
|
|
248
|
+
if coverage {
|
|
249
|
+
cov = cover.New()
|
|
250
|
+
evalOpts = append(evalOpts, rego.EvalQueryTracer(cov))
|
|
251
|
+
}
|
|
252
|
+
var tracer *traceCollector
|
|
253
|
+
if trace {
|
|
254
|
+
tracer = &traceCollector{}
|
|
255
|
+
evalOpts = append(evalOpts, rego.EvalQueryTracer(tracer))
|
|
256
|
+
}
|
|
246
257
|
rs, err := pq.Eval(context.Background(), evalOpts...)
|
|
247
258
|
if err != nil {
|
|
248
259
|
return errorJSON("eval_error", err.Error())
|
|
249
260
|
}
|
|
250
|
-
|
|
261
|
+
envelope := map[string]any{"result": rs, "prints": collector.msgs}
|
|
262
|
+
if cov != nil {
|
|
263
|
+
e.mu.Lock()
|
|
264
|
+
parsed := map[string]*ast.Module{}
|
|
265
|
+
for path, src := range e.modules {
|
|
266
|
+
if m, perr := ast.ParseModule(path, src); perr == nil {
|
|
267
|
+
parsed[path] = m
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
e.mu.Unlock()
|
|
271
|
+
envelope["coverage"] = cov.Report(parsed)
|
|
272
|
+
}
|
|
273
|
+
if tracer != nil {
|
|
274
|
+
envelope["trace"] = tracer.events
|
|
275
|
+
}
|
|
276
|
+
b, err := json.Marshal(envelope)
|
|
251
277
|
if err != nil {
|
|
252
278
|
return errorJSON("internal", err.Error())
|
|
253
279
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
"sync"
|
|
7
|
+
|
|
8
|
+
"github.com/open-policy-agent/opa/v1/ast"
|
|
9
|
+
"github.com/open-policy-agent/opa/v1/topdown"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type traceEvent struct {
|
|
13
|
+
Op string `json:"op"`
|
|
14
|
+
QueryID uint64 `json:"query_id"`
|
|
15
|
+
ParentID uint64 `json:"parent_id"`
|
|
16
|
+
Location string `json:"location,omitempty"`
|
|
17
|
+
Node string `json:"node,omitempty"`
|
|
18
|
+
Locals map[string]any `json:"locals,omitempty"`
|
|
19
|
+
Message string `json:"message,omitempty"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// traceCollector implements topdown.QueryTracer, capturing every evaluation
|
|
23
|
+
// event together with the local variable bindings live at that point.
|
|
24
|
+
type traceCollector struct {
|
|
25
|
+
mu sync.Mutex
|
|
26
|
+
events []traceEvent
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func (t *traceCollector) Enabled() bool { return true }
|
|
30
|
+
|
|
31
|
+
func (t *traceCollector) Config() topdown.TraceConfig {
|
|
32
|
+
return topdown.TraceConfig{PlugLocalVars: true}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func (t *traceCollector) TraceEvent(evt topdown.Event) {
|
|
36
|
+
te := traceEvent{
|
|
37
|
+
Op: string(evt.Op),
|
|
38
|
+
QueryID: evt.QueryID,
|
|
39
|
+
ParentID: evt.ParentID,
|
|
40
|
+
Message: evt.Message,
|
|
41
|
+
}
|
|
42
|
+
if evt.Location != nil {
|
|
43
|
+
te.Location = evt.Location.String()
|
|
44
|
+
}
|
|
45
|
+
if evt.Node != nil {
|
|
46
|
+
te.Node = fmt.Sprintf("%v", evt.Node)
|
|
47
|
+
}
|
|
48
|
+
if evt.Locals != nil {
|
|
49
|
+
locals := map[string]any{}
|
|
50
|
+
evt.Locals.Iter(func(k, v ast.Value) bool {
|
|
51
|
+
kv, ok := k.(ast.Var)
|
|
52
|
+
if !ok {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
name := string(kv)
|
|
56
|
+
// Compiler-generated vars carry the user-facing name in the
|
|
57
|
+
// event metadata; drop them if no such name exists.
|
|
58
|
+
if md, found := evt.LocalMetadata[kv]; found {
|
|
59
|
+
name = string(md.Name)
|
|
60
|
+
}
|
|
61
|
+
if strings.HasPrefix(name, "__local") || strings.HasPrefix(name, "$") {
|
|
62
|
+
return false // compiler temporaries and wildcards
|
|
63
|
+
}
|
|
64
|
+
if j, err := ast.JSON(v); err == nil {
|
|
65
|
+
locals[name] = j
|
|
66
|
+
} else {
|
|
67
|
+
locals[name] = v.String()
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
})
|
|
71
|
+
if len(locals) > 0 {
|
|
72
|
+
te.Locals = locals
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
t.mu.Lock()
|
|
76
|
+
t.events = append(t.events, te)
|
|
77
|
+
t.mu.Unlock()
|
|
78
|
+
}
|
{opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/src/opa_bindings/_native.py
RENAMED
|
@@ -43,9 +43,21 @@ def load():
|
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
lib.OpaEvalQuery.restype = ctypes.c_void_p
|
|
46
|
-
lib.OpaEvalQuery.argtypes = [
|
|
46
|
+
lib.OpaEvalQuery.argtypes = [
|
|
47
|
+
ctypes.c_uint64,
|
|
48
|
+
ctypes.c_char_p,
|
|
49
|
+
ctypes.c_char_p,
|
|
50
|
+
ctypes.c_int, # coverage
|
|
51
|
+
ctypes.c_int, # trace
|
|
52
|
+
]
|
|
47
53
|
|
|
48
54
|
lib.OpaEvalDocument.restype = ctypes.c_void_p
|
|
49
|
-
lib.OpaEvalDocument.argtypes = [
|
|
55
|
+
lib.OpaEvalDocument.argtypes = [
|
|
56
|
+
ctypes.c_uint64,
|
|
57
|
+
ctypes.c_char_p,
|
|
58
|
+
ctypes.c_char_p,
|
|
59
|
+
ctypes.c_int, # coverage
|
|
60
|
+
ctypes.c_int, # trace
|
|
61
|
+
]
|
|
50
62
|
|
|
51
63
|
return lib
|
{opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/src/opa_bindings/engine.py
RENAMED
|
@@ -50,6 +50,17 @@ class OpaEngine:
|
|
|
50
50
|
self.print_handler = None
|
|
51
51
|
#: Prints captured by the most recent eval, as (message, location).
|
|
52
52
|
self.last_prints = []
|
|
53
|
+
#: Coverage report of the most recent eval with ``coverage=True``:
|
|
54
|
+
#: {"files": {path: {"covered": [...], "not_covered": [...], ...}},
|
|
55
|
+
#: "covered_lines": int, "not_covered_lines": int, "coverage": float}.
|
|
56
|
+
#: None if the last eval did not capture coverage.
|
|
57
|
+
self.last_coverage = None
|
|
58
|
+
#: Evaluation trace of the most recent eval with ``trace=True``: a
|
|
59
|
+
#: list of event dicts {"op", "query_id", "parent_id", "location",
|
|
60
|
+
#: "node", "locals", "message"} in evaluation order, where "locals"
|
|
61
|
+
#: holds the variable bindings live at that point. None if the last
|
|
62
|
+
#: eval did not capture a trace.
|
|
63
|
+
self.last_trace = None
|
|
53
64
|
|
|
54
65
|
# -- lifecycle -----------------------------------------------------
|
|
55
66
|
|
|
@@ -90,7 +101,16 @@ class OpaEngine:
|
|
|
90
101
|
raise OpaError(err.get("code", "unknown"), err.get("message", ""))
|
|
91
102
|
return envelope
|
|
92
103
|
|
|
104
|
+
def _eval_reset(self):
|
|
105
|
+
# Clear per-eval state up front so a failed eval never leaves stale
|
|
106
|
+
# results from a previous evaluation behind.
|
|
107
|
+
self.last_coverage = None
|
|
108
|
+
self.last_trace = None
|
|
109
|
+
self.last_prints = []
|
|
110
|
+
|
|
93
111
|
def _eval_result(self, envelope):
|
|
112
|
+
self.last_coverage = envelope.get("coverage")
|
|
113
|
+
self.last_trace = envelope.get("trace")
|
|
94
114
|
self.last_prints = [
|
|
95
115
|
(p["message"], p["location"]) for p in envelope.get("prints") or []
|
|
96
116
|
]
|
|
@@ -166,24 +186,50 @@ class OpaEngine:
|
|
|
166
186
|
del self._functions[name]
|
|
167
187
|
raise
|
|
168
188
|
|
|
169
|
-
def eval_query(
|
|
170
|
-
|
|
189
|
+
def eval_query(
|
|
190
|
+
self, query: str, input=None, *, coverage: bool = False, trace: bool = False
|
|
191
|
+
):
|
|
192
|
+
"""Evaluate a Rego query; returns a list of binding dicts.
|
|
193
|
+
|
|
194
|
+
With ``coverage=True`` the evaluation is traced and a coverage report
|
|
195
|
+
over all added policies is stored in ``self.last_coverage``. With
|
|
196
|
+
``trace=True`` the full event trace, including the variable bindings
|
|
197
|
+
at each step, is stored in ``self.last_trace``.
|
|
198
|
+
"""
|
|
171
199
|
self._check_open()
|
|
200
|
+
self._eval_reset()
|
|
172
201
|
rs = self._eval_result(
|
|
173
|
-
self._call(
|
|
202
|
+
self._call(
|
|
203
|
+
self._lib.OpaEvalQuery,
|
|
204
|
+
query.encode(),
|
|
205
|
+
_encode_input(input),
|
|
206
|
+
int(coverage),
|
|
207
|
+
int(trace),
|
|
208
|
+
)
|
|
174
209
|
)
|
|
175
210
|
if not rs:
|
|
176
211
|
return []
|
|
177
212
|
return [r.get("bindings", {}) for r in rs]
|
|
178
213
|
|
|
179
|
-
def eval_document(
|
|
214
|
+
def eval_document(
|
|
215
|
+
self, path: str, input=None, *, coverage: bool = False, trace: bool = False
|
|
216
|
+
):
|
|
180
217
|
"""Evaluate the document at ``data.<path>`` and return its value.
|
|
181
218
|
|
|
182
|
-
Raises OpaUndefinedError if the document is undefined.
|
|
219
|
+
Raises OpaUndefinedError if the document is undefined. With
|
|
220
|
+
``coverage=True`` a coverage report is stored in ``self.last_coverage``;
|
|
221
|
+
with ``trace=True`` the event trace is stored in ``self.last_trace``.
|
|
183
222
|
"""
|
|
184
223
|
self._check_open()
|
|
224
|
+
self._eval_reset()
|
|
185
225
|
rs = self._eval_result(
|
|
186
|
-
self._call(
|
|
226
|
+
self._call(
|
|
227
|
+
self._lib.OpaEvalDocument,
|
|
228
|
+
path.encode(),
|
|
229
|
+
_encode_input(input),
|
|
230
|
+
int(coverage),
|
|
231
|
+
int(trace),
|
|
232
|
+
)
|
|
187
233
|
)
|
|
188
234
|
if not rs or not rs[0].get("expressions"):
|
|
189
235
|
raise OpaUndefinedError(f"data.{path}" if path else "data")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.2.0}/src/opa_bindings/__init__.py
RENAMED
|
File without changes
|