Python V1 · Deprecated
Registries
Registries in the Python V1 binding.The Python V1 registries reference continues the Python V1 overview.
Factory
from maelys_datalog import Engine
engine = Engine()Engine() loads the native library and returns a ready instance. Unlike
the WASM binding's MaelysPlayground.create(),
there is no factory module or WASM URL to pass — the shared libraries are
already resolved through $ORIGIN/@loader_path (see
Installation) before Python code runs at all.
More precisely: import maelys_datalog is what loads the compiled cffi
extension and its two native dependencies. At import it also runs a one-time
ABI self-check: it asks the compiled shim for its real struct sizes,
field offsets, and enum values (maelys_py_get_abi_layout /
maelys_py_get_abi_constants) and compares them against what the Python-side
cffi definitions expect. A shim rebuilt out of sync with the Python package
therefore fails loudly at import rather than silently misreading fields later.
All of that has already happened by the time Engine() is even called.
Engine() itself only creates the native engine handle and reads the build
limits into .limits (see Build limits below); it does not
repeat any loading or ABI work.
Build limits
engine = Engine()
engine.limits
# BuildLimits(max_symbols=..., string_pool_bytes=..., max_predicates=...,
# max_rules=..., max_arity=..., max_body_literals=...,
# max_depth=..., max_edb_facts=..., max_idb_facts=...,
# max_facts_per_pred=...)These ten fields are also available in WASM's buildLimits(); Wasm additionally exposes string and input-text bounds. Python V1 reads its limits once at Engine() construction
— the active build profile's observable capacities, compile-time constants,
not runtime occupancy. Unlike WASM, there is no separate call to make later:
.limits is already populated by the time Engine() returns.
Predicate kind flags
from maelys_datalog import PRED_EDB, PRED_IDB, PRED_QUERY, PRED_POLICY_FACT
PRED_EDB # 1 — runtime fact supplied by the caller
PRED_IDB # 2 — derived by Datalog rules
PRED_QUERY # 4 — inspectable through the query API
PRED_POLICY_FACT # 8 — declared by the policy sourceSame bit values as WASM's PredKind. These
constants are read from the engine's ABI constants at import time, not
written as literals in the Python source — so if the engine ever changed a flag
value, the Python constant would track it automatically instead of silently
disagreeing with the native side. Combine with |: PRED_IDB | PRED_QUERY.
Domain definition
Unlike the WASM dynamic builder — which is scoped to one MaelysPlayground
instance (see WASM bindings — Domain definition)
— the native domain registry is process-wide. Every Engine
instance in the same Python process shares it. Creating a second Engine()
does not give you a second, isolated domain space — unlike WASM, where a
new MaelysPlayground.create() gets its own WASM module and its own linear
memory, so the same domain name can be declared again independently:
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY
engine1 = Engine()
engine1.register_domain('document_access', [ # writes into the process-wide registry
Predicate('owns', 2, PRED_EDB),
Predicate('allow', 2, PRED_IDB | PRED_QUERY),
])
engine2 = Engine() # same process, same registry
engine2.register_domain('document_access', [ # 'document_access' already exists:
Predicate('owns', 2, PRED_EDB), # a safe no-op when the schema is
Predicate('allow', 2, PRED_IDB | PRED_QUERY), # identical — nothing new is created
])The current register_domain method declares predicates, not the string
atoms used as literals in policy source. A literal such as
"confidential" must belong to the domain's declared atom allowlist; merely
interning a runtime string does not declare a policy atom.
This Python registration method has no atoms argument. Unless a native
domain has declared the literal, loading such a policy fails explicitly
with an unknown-atom diagnostic; it does not silently produce a non-matching
rule. Runtime strings supplied through edb.add_fact() are a different case
and do not need to be listed as policy literals.
For low-cardinality categories, is_confidential(Document) can be a useful
alternative model to classification(Document, "confidential"). For explicit
Python-side atom declaration, see
Python Next's domain vocabulary.
In practice this rarely bites, because the two common cases are already fine:
# Case 1: re-running the same cell — by far the most common notebook
# workflow. Safe no-op, nothing to worry about.
engine.register_domain('document_access', [
Predicate('owns', 2, PRED_EDB),
Predicate('allow', 2, PRED_IDB | PRED_QUERY),
])
engine.register_domain('document_access', [
Predicate('owns', 2, PRED_EDB),
Predicate('allow', 2, PRED_IDB | PRED_QUERY),
])
# no error — identical table, safe no-op
# Case 2: changing the schema while iterating — raises, but the fix is
# the same "Restart Kernel" you would already reach for after changing
# imports or clearing corrupted variable state.
engine.register_domain('document_access', [
Predicate('owns', 2, PRED_EDB),
Predicate('allow', 3, PRED_IDB | PRED_QUERY), # arity changed 2 -> 3
])
# DomainAlreadyRegisteredError — restart the kernel, then re-runThe capacity applies to distinct registered domain names. Re-registering an identical schema consumes no slot; re-registering the same name with a different schema raises an error and does not allocate another slot. A service that invents a new domain name for every reload will eventually exhaust the 16 slots. Reuse a stable vocabulary, or restart the process when you intentionally change its definition.