Python
Loading & manifests
Load policies from inline source or verified manifests with the Python binding.Load a policy from source text or verified manifest files. Both methods return a Ruleset; its metadata and lifetime are documented in Rulesets.
Methods
| Method | Purpose |
|---|---|
Engine.load_inline_ruleset() | Load a policy from text |
Engine.load_manifest() | Load verified policy files |
Detailed API reference
Every class and method indexed at the start of this page has its detailed card here. A class’s method list also links to related operations documented on other topic pages; those links do not duplicate their cards.
Signatures and class declarations below come from the published binding inventory. The examples are fragments: engine, ruleset, edb, session and result refer to the live objects explained on this page and in the complete examples. Import the named classes before using them.
Load a policy from text
Use this path when the application already holds the Datalog source in memory. Register the domain first.
Engine.load_inline_ruleset(
domain: str,
policy_id: str,
source: str
) -> RulesetRulesetReturns a new Ruleset containing one policy, selected with policy_index=0. Invalid source or domain vocabulary raises MaelysDatalogError; loading does not solve runtime facts.
ruleset = engine.load_inline_ruleset(
"documents", "access",
"allow(User, Doc) :- owns(User, Doc).",
)Load verified policy files
Use a manifest to package policy identities, source digests and public queries. Permissions are independent, explicit keyword arguments.
Engine.load_manifest(
path: str | os.PathLike[str],
*,
allow_test_only: bool = False,
allow_undeclared_policy_atoms: bool = False
) -> RulesetRulesetReturns a new Ruleset containing the admitted policies. File, integrity or policy-validation failures raise MaelysDatalogError; no partially loaded set is returned.
ruleset = engine.load_manifest("policies/manifest.json")
print(ruleset.policy_count)Usage and examples
Choose a loading path, load inline source or verified manifest files, then explore filters written in the policy. Both loaders require the vocabulary described in Registries.
The reference above gives the exact declarations; the sections below explain their use.
Loading path
There are two ways to get a policy loaded: the Manifest path (JSON
manifest, SHA-256 verification, Public Query Whitelist) and the Inline
path (raw .dl text, no manifest, no manifest query whitelist). The original
reference distinguishes file/buffer manifest loading and static/dynamic inline
loading. Keep that distinction, but do not confuse the low-level C functions
with the public entry points used by Python:
| Path | Variant | Python / opaque C API |
|---|---|---|
| Manifest | file | engine.load_manifest(path) → maelys_datalog_policy_load_manifest; verifies manifest and policy files. |
| Manifest | buffer | Not exposed by this facade/binding. load_manifest takes a filesystem path, not JSON text. |
| Inline | static C table | No combined static-table Python entry point. Register the vocabulary from Python values, then load source. |
| Inline | dynamic | engine.register_domain(...) then engine.load_inline_ruleset(...) → maelys_datalog_domain_register + maelys_datalog_policy_load_inline. |
Python therefore supports Manifest, file and Inline, dynamic. The original Python binding supports only the latter. See the current C manifest reference for loading paths. The historical 0.9.1 registry discussion gives a wider native model; this table states the subset actually exposed here.
- Verified filesload_manifest(path)
- Source textload_inline_ruleset(...)
ruleset = engine.load_inline_ruleset(domain, policy_id, source) calls
maelys_datalog_policy_load_inline through CFFI. engine.register_domain(...)
calls maelys_datalog_domain_register — the registration step that must
already have declared the vocabulary. Together, the Python calls preserve
the same order as C: register, then load.
Why not the static variant too? A combined static loader solves a
C-specific problem: its predicate table already exists as a compile-time C
array. In Python, register_domain(name, predicates, atoms=...) accepts plain
Python values directly. There is no compile-time C table to skip past;
registration and loading remain explicit, separate operations.
What changes for manifests? SHA-256 verification and a Public Query
Whitelist are real additions over the original binding. load_manifest(path)
uses the native loader, not a Python reimplementation of its checks. It can
return a policy set containing several policies; subsequent evaluation selects
one by policy_index. The manifest example
shows packaging, hashing and loading the exact source bytes.
The manifest example covers verified files; the inline example below uses source text held in memory. Language filters below are written in the policy source, not exposed as Python callback methods.
Ruleset loading
ruleset = engine.load_inline_ruleset(
"document_access", "doc.main",
"allow(User, Document) :- owns(User, Document).\n",
)Here source is the .dl program text, not a filename. Loading parses and
validates it against the registered domain and returns a Ruleset; it does not
yet evaluate runtime inputs. The wrapper encodes the Python string as UTF-8
and passes its byte length to the C facade, so there is no manual
lengthBytesUTF8 bookkeeping. Parse and validation failures raise
MaelysDatalogError with the native diagnostic, not an empty ruleset.
The source accepts the full supported language, including bounded positive
or disjunction, stratified not(...),
wildcards and standard filters. Those are language features in the source,
not different Python loading methods. Each predicate must already exist in
the domain, and string literals must meet the vocabulary rules above.
engine.load_inline_ruleset(domain, policy_id, source) compiles one policy
against an already registered domain. Standard filters and language constructs
are expressed directly in policy source, not through special Python methods.
Invalid source or vocabulary raises MaelysDatalogError.
Loading a verified manifest
Use engine.load_manifest(path) for a native manifest. It verifies source
SHA-256 digests and query declarations in the native loader, atomically.
ruleset.policy_count reports the loaded count; policy_index selects a
zero-based policy from that set. The default is index zero.
allow_undeclared_policy_atoms=True explicitly admits policy-local string
constants without mutating the global domain. It does not make predicates
dynamic, affect runtime facts, or change inline loading. Referenced atoms
remain bounded: 256 distinct entries, each at most 63 UTF-8 bytes, subject to
native pool limits. Filter patterns are separate (256-byte maximum).
allow_test_only=True is an independent permission to load enabled manifest entries marked "mode": "test_only". Without it, the loader returns FORBIDDEN, surfaced as MaelysDatalogError. It is not a dry run: an admitted policy is evaluated normally. It does not relax atom declarations or integrity checks, and the engine does not detect whether the caller is running in production.
Both options default to False, can be combined, and apply only to load_manifest(). load_inline_ruleset() guarantees declared policy constants and exposes no permission to bypass that check. The C facade's corresponding permission mask rejects unknown bits with INVALID_ARGUMENT; since 0.7.1, MAELYS_DATALOG_PUBLIC_ALLOW_NONE names its permanent zero value. Python exposes named boolean arguments instead of an arbitrary mask, so the ordinary call with both defaults false is already the Python equivalent. See public loading permissions.
This complete example creates a temporary manifest and source, computes the digest from the exact source bytes, then loads and evaluates them. In production, use the manifest and sources produced by your policy packaging workflow.
import hashlib
import json
from pathlib import Path
from tempfile import TemporaryDirectory
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY
with TemporaryDirectory() as directory, Engine() as engine:
root = Path(directory)
engine.register_domain("manifest_docs", [
Predicate("user", 1, PRED_EDB),
Predicate("allow", 1, PRED_IDB | PRED_QUERY),
])
source = b'allow("alice") :- user("alice").\n'
(root / "access.dl").write_bytes(source)
manifest = {
"policy_set_id": "documents", "policy_set_version": "1",
"manifest_version": "1", "default_profile": "enforce",
"created_for": "example", "strict_loading": True,
"fail_closed": True, "capabilities": [],
"policies": [{
"policy_id": "access", "domain": "manifest_docs",
"file": "access.dl", "sha256": hashlib.sha256(source).hexdigest(),
"mode": "enforce", "enabled": True, "description": "Document access",
"queries": [{"name": "allow", "arity": 1}],
}],
}
path = root / "manifest.json"
path.write_text(json.dumps(manifest), encoding="utf-8")
ruleset = engine.load_manifest(path, allow_undeclared_policy_atoms=True)
print("policies:", ruleset.policy_count)
edb = ruleset.edb()
edb.add_fact("user", ["alice"])
with ruleset.solve(edb, policy_index=0) as result:
print(result.contains_fact("allow", ["alice"]))policies: 1
TrueHere "alice" is admitted only into this loaded policy. A subsequent inline
load using it still needs the closed domain whitelist. Altering access.dl
without updating its manifest digest makes loading fail, not execute different
rules silently.
Standard filters in policy source
Filters are not Python callbacks. The standard starts_with, ends_with
and contains operations are written in the Datalog body and evaluated by
the engine. They are non-binding: a positive atom must first bind the
value they inspect.
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY
with Engine() as engine:
engine.register_domain("filters_next_docs", [
Predicate("file", 1, PRED_EDB),
Predicate("allow", 1, PRED_IDB | PRED_QUERY),
])
ruleset = engine.load_inline_ruleset(
"filters_next_docs", "files.main",
'allow(Path) :- file(Path), starts_with(Path, "docs/"), '
'ends_with(Path, ".md"), contains(Path, "api").',
)
edb = ruleset.edb()
edb.add_facts([
("file", ["docs/api.md"]),
("file", ["docs/guide.md"]),
("file", ["src/api.md"]),
])
with ruleset.solve(edb) as result:
print(result.enumerate_predicate_facts("allow", 1))[('docs/api.md',)]Here the patterns "docs/", ".md" and "api" are filter parameters,
not policy atoms: no atoms=[...] list is needed for them. Moving a filter
before any positive binding, or omitting that binding, does not make it
generate values; an unsafe rule is rejected. Writing a custom native filter
implementation is a different task, covered by the extension SDK rather
than a Python callback registration API.