Python
Complete examples
Examples in the unified Python binding.The Python complete examples reference continues the Python overview.
Document-access example
The following three fragments build one program, in order. They use only methods already implemented in Python.
Step 1 — Declare the vocabulary
Our document-access example declares every predicate before loading the
policy. owns, delegated, blocked and user are runtime inputs.
can_read is an intermediate relation; allow and
has_any_document are queryable outputs.
Start documents_next.py with the imports and predicate declarations:
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY
predicates = [
Predicate("user", 1, PRED_EDB),
Predicate("owns", 2, PRED_EDB),
Predicate("delegated", 2, PRED_EDB),
Predicate("blocked", 1, PRED_EDB),
Predicate("can_read", 2, PRED_IDB),
Predicate("has_any_document", 1, PRED_IDB | PRED_QUERY),
Predicate("allow", 2, PRED_IDB | PRED_QUERY),
]Each Predicate supplies a name, an arity (number of arguments), and kind
flags. PRED_EDB marks input facts; PRED_IDB marks derived facts.
Add PRED_QUERY to a derived predicate when the application needs to query it.
The optional constructors above express these same declarations more briefly.
This list describes the vocabulary; Step 3 registers it with the engine.
A domain is process-wide, not private to an Engine. Reuse a stable
definition instead of inventing a new name for every request.
Step 2 — Express the policy
can_read(User, Doc) :-
owns(User, Doc) or delegated(User, Doc),
not(blocked(User)).
has_any_document(User) :- owns(User, _).
allow(User, Doc) :-
user(User),
can_read(User, Doc).
The or offers two positive access paths; not(blocked(User)) filters
both. The wildcard _ means that any document is enough for
has_any_document. All of these predicate names were declared in Step 1;
the language does not declare them implicitly.
Append the same rules as a Python multiline string named policy.
The triple quotes delimit the Python string; its contents are Datalog, not
Python. This step defines the source text — loading happens in Step 3.
policy = """
can_read(User, Doc) :-
owns(User, Doc) or delegated(User, Doc),
not(blocked(User)).
has_any_document(User) :- owns(User, _).
allow(User, Doc) :- user(User), can_read(User, Doc).
"""Step 3 — Add facts, solve and query
Append this block after predicates and policy. It creates the engine,
registers the vocabulary, loads the policy, then evaluates one batch of facts.
There is no need to redefine either variable.
with Engine() as engine:
engine.register_domain("documents_next", predicates)
ruleset = engine.load_inline_ruleset("documents_next", "documents.main", policy)
edb = ruleset.edb()
edb.add_fact("user", ["alice"])
edb.add_facts([
("user", ["bob"]),
("user", ["mallory"]),
("owns", ["alice", "roadmap.pdf"]),
("delegated", ["bob", "roadmap.pdf"]),
("owns", ["mallory", "roadmap.pdf"]),
("blocked", ["mallory"]),
])
result = ruleset.solve(edb)
for user in ("alice", "bob", "mallory"):
print(user, result.contains_fact("allow", [user, "roadmap.pdf"]))
print("allow:", sorted(result.enumerate_predicate_facts("allow", 2)))
print("derived:", result.derived_fact_count())register_domain() registers the declarations from Step 1.
load_inline_ruleset() compiles the source from Step 2 against that domain.
The additions buffer the runtime facts; solve() submits them to the
native engine, and the final calls inspect its result.
The with block closes the engine and its children when it exits.
Complete program
The three fragments above, in order, form this standalone
documents_next.py. Copy either the three fragments or the complete
program below — not both.
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY
predicates = [
Predicate("user", 1, PRED_EDB),
Predicate("owns", 2, PRED_EDB),
Predicate("delegated", 2, PRED_EDB),
Predicate("blocked", 1, PRED_EDB),
Predicate("can_read", 2, PRED_IDB),
Predicate("has_any_document", 1, PRED_IDB | PRED_QUERY),
Predicate("allow", 2, PRED_IDB | PRED_QUERY),
]
policy = """
can_read(User, Doc) :-
owns(User, Doc) or delegated(User, Doc),
not(blocked(User)).
has_any_document(User) :- owns(User, _).
allow(User, Doc) :- user(User), can_read(User, Doc).
"""
with Engine() as engine:
engine.register_domain("documents_next", predicates)
ruleset = engine.load_inline_ruleset("documents_next", "documents.main", policy)
edb = ruleset.edb()
edb.add_fact("user", ["alice"])
edb.add_facts([
("user", ["bob"]),
("user", ["mallory"]),
("owns", ["alice", "roadmap.pdf"]),
("delegated", ["bob", "roadmap.pdf"]),
("owns", ["mallory", "roadmap.pdf"]),
("blocked", ["mallory"]),
])
result = ruleset.solve(edb)
for user in ("alice", "bob", "mallory"):
print(user, result.contains_fact("allow", [user, "roadmap.pdf"]))
print("allow:", sorted(result.enumerate_predicate_facts("allow", 2)))
print("derived:", result.derived_fact_count())Run it from the repository root:
PYTHONPATH=bindings/python python documents_next.pyExpected output:
alice True
bob True
mallory False
allow: [('alice', 'roadmap.pdf'), ('bob', 'roadmap.pdf')]
derived: 6Alice owns the document. Bob has delegated access. Mallory owns it too,
but is blocked. The count of six includes two can_read facts, two
has_any_document facts and two allow facts — not just the two rows
returned by the allow enumeration.