# dkepy
Write facts into a [DKE](https://dke.langsyn.com/) store, from Python, for
kinds you define yourself.
Distribution name `dkepy`, import name `dke` — they differ because `dke` is
taken on PyPI, and the package provides the module `dke` so that
`from dke import ...` reads the way it should.
**Where to get it.** The source is served, file by file and as a reproducible
tarball, at . It is **not on PyPI**: the
distribution name is unregistered there as of 2026-08-25, so `pip install dkepy`
installs nothing of ours and would install whatever a stranger registers under
that name. Put the `src/` directory on your path, or install from the tarball.
## What it is for
A DKE reasoning module supplies the reasoning — the schema, the standing
rules, the queries. You supply the facts. This package is the second half: it
turns whatever you have read into a DKE Python program that records it, which
you submit to your store through whichever DKE client you already use.
```python
from dke import FactProgram
prog = FactProgram("inventory", source="warehouse-scan")
prog.comment("Stock levels, read 2026-08-21.")
prog.open()
prog.table(
"items",
['("widget", "Widget", 12)', '("sprocket", "Sprocket", 0)'],
[prog.put("Item.s.label", "r[1]"),
prog.put("Item.s.count", "r[2]")],
)
print(prog.render())
```
The output is a program you can read before you run it. Every fact it would
record is a line you can see.
## Why not just write the program by hand
You can, and for a handful of facts you should. What this package is for is
the case where a machine produces them, because three things are easy to get
wrong there and wrong in the direction that stays quiet:
- **A subject that moves between runs.** Re-ingesting then records a second
set of facts beside the first instead of revising it, and every query that
joined on the old subject still answers — with stale numbers.
- **The write verb.** A producer means "replace my own earlier reading", and
that is what `update` says. `put()` emits it.
- **An optional cell written as an empty string.** A rule that concludes from
the *absence* of a cell is silenced by a value stored everywhere, including
a falsy one. `table()` is shaped so a row can carry a placeholder while the
write stays behind a guard.
A producer with a broken reader records nothing, which is loud. A producer
with a broken fact-writer records plausible facts, and every query still
answers.
## The code readers
`dke.ingest` holds producers for one domain — source code, for the `code`
reasoning module:
```
dke-ingest-python ./src > facts.dpy
dke-ingest-cpp ./src ./include > facts.dpy
```
Both are lower bounds by construction. Every edge they record is one the
source makes plain; a call through a variable, a dict of handlers, `getattr`
or any dynamic dispatch is not visible to a reader and is not recorded. Each
module documents its own limits — read those before trusting a count.
They are also the worked example. Your own reader supplies the rows, and
everything from the rows onward is the same code.
## The quantum and pqc readers
`dke.quantum` writes facts for the `quantum` reasoning module, and `dke.pqc`
for the `pqc` module:
```
dke-quantum-device ./torino --max-bytes 40000 > device.dpy
dke-pqc-cbom ./cbom.json --max-bytes 40000 > cbom.dpy
```
`dke-quantum-device` reads a device's calibration snapshot: a directory named
for the device, holding the `conf_.json` and `props_.json` that
Qiskit's fake backends ship. It writes the device, its physical qubits and the
operations it runs natively, each with its error, its duration and when it was
measured, and the snapshot's digest, which a result computed for the device
records as the calibration it read. `dke.quantum.qasm` reads and writes
OpenQASM 2.0 as the gate lists the module runs, keeping an angle that is an
exact multiple of pi exact, and `dke.quantum.provenance` writes what produced a
result, with the digests the module's rules compare. None of them imports
Qiskit or any compiler: they read files.
`dke-pqc-cbom` reads a cryptographic bill of materials, a CycloneDX document of
version 1.6 or later. It writes each algorithm's standing against a quantum
computer and its classical strength, as the document states them, and each
use of an algorithm by a system. What the document does not hold — what each
use protects, for how long, how long a system takes to migrate — are your own
facts, recorded beside these. Each program's header counts what the document
held and was not read.
Both load more facts than one program may hold, so both split the load, and the
size to split at is your store's: pass it as `--max-bytes`, from the store's
documentation or its operator. Each program records a load marker, as
"Telling whether a load finished" describes.
## Submitting what it produced
What comes out is a PROGRAM — statements at top level — so it goes to your
store through the same verb as any other question you ask. Nothing needs
compiling: `compile` stores a definition, and a fact load defines nothing.
Over MCP, that is one call:
```
run(source=)
```
and this package can make it for you. The client is a separate import, on
purpose — see **No network, no subprocess** below:
```python
from dke.client import Client
client = Client.from_env() # reads DKE_API_KEY
result = client.run(program).raise_for_status()
print(result.text, result.ops)
```
Either way the reply lists the claims it wrote, which is the same list you
could read in the file first.
Before 0.5.0 the only routes were your own MCP client or hand-built JSON,
which is what `dke.client` replaces.
The reasoning module is the other half and goes in the other way, because a
module IS a definition — its schema, rules and queries are meant to persist:
```
compile(name="code", source=)
```
Do that once. Re-running a producer afterwards revises the facts underneath it
and leaves the reasoning alone, which is what `update` is for.
## Making a store key
`dke.keys` makes a store key on your own machine, the way the dashboard makes
one in your browser and the `dkekey` tool makes one at a terminal:
```python
import os
from pathlib import Path
from dke.keys import mint
made = mint("notes", label="nightly", level="read",
token=os.environ["DKEKEY_TOKEN"],
open_with=Path("notes.key").read_text())
fd = os.open("nightly.key", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as out:
out.write(made.key + "\n")
```
It needs one package the rest of `dkepy` does not: `cryptography`, 38 or
later, for AES-GCM, which the standard library does not have. That is the
`keys` extra, and the only dependency anything here has. Without it,
`import dke` and everything else still work, and `import dke.keys` says what
to install.
**What it sends, and what it does not.** The key is made here, from the
operating system's random source. To add it to a store, `mint` opens the store
with a key that already opens it, wraps the store's data key under the new key,
and sends the account service two things: the new key's SHA-256 hash, and that
wrap. The key itself is never sent: it is in the `MintedKey` that `mint`
returns, and left out of that object's `repr`, so a log line that prints it
does not carry the key. The key that opened the store is never sent either,
nor your password: it derives, here, the proof the account checks and the key
your customer key is kept under, as the dashboard does.
**Signing in**, one of two ways:
- `email=` and `password=`, as you sign in to the dashboard. With nothing else
given, the store is opened with your customer key, which the password opens.
`getpass.getpass()` reads a password without showing it.
- `token=`, an account token made in the dashboard, under Account tokens. A
token can only add keys to stores that exist, and only up to the level it
was made with. It opens no store by itself, so give `open_with=` too.
**Opening the store.** `open_with=` is a key that already opens it: one of its
store keys (`dke_s_...`) or your customer key (`dke_c_...`).
**Levels.** `level=` says what the new key may do:
| Level | The key may |
|---|---|
| `read` (the default) | answer questions, and change nothing |
| `read-add` | add facts, and change or delete none already recorded |
| `read-write` | add facts and revise them, and delete nothing |
| `read-write-delete` | add, revise and delete; what it deletes is gone for good |
**When it fails.** An argument that cannot be right raises `ValueError` before
anything is sent. Anything the account service refuses, and a key that does
not open the store, raise `dke.keys.MintError`, whose message says what to do;
`status` and `code` carry the account service's answer when it gave one.
`MintError` is a `dke.client.DkeError`, so one `except` catches both modules'
failures.
## Telling whether a load finished
A fact load is not all-or-nothing. If a submission stops part-way — a dropped
connection, a service restart, a refusal on one slot — the claims it already
made stay made. Re-running the producer repairs that, because it writes
`update` and revises its own earlier reading rather than adding beside it. What
re-running cannot do is tell you there was anything to repair: a partially
loaded store looks exactly like a complete one, and every query answers from it
with the same confidence.
So each program records a marker for the source it loads:
```
Load..started when the load began
Load..declared how many rows it meant to write
Load..completed written last, and only if the load reached the end
```
`completed` is the final statement of the program, so an interrupted load
simply never writes it. That is the point: you are told a load finished only by
a load that finished, and a run that died says nothing at all. A load is
complete when `completed` is at least `started`; if it is older, or missing,
the last load of that source did not finish and `declared` says how much it was
going to write.
```python
st = current(Load.python.started)
sv = st.value
fi = current(Load.python.completed)
fv = fi.value
match sv:
case int as a:
match fv:
case int as b:
print(b >= a)
case default:
print(False)
case default:
print(False)
```
A load that legitimately wrote fewer facts — because your codebase shrank — is
not the same thing and does not read as one: it finished, so `completed` is
current and `declared` is simply smaller.
`Load` is deliberately not under the reasoning module's name, so you can ask
this question without importing the module first — which matters most in the
case where a load may have failed. If your own module declares a `Load` of its
own, `FactProgram.load_marker` takes the kind name as an argument.
## A load too big for one program
A store refuses a program larger than a size it is set up with. A load that
outgrows one program goes through `FactLoad` instead: the same tables, the same
`put` and `q`, and as many programs as the size you pass needs. Take that size
from your store's documentation or its operator, and stay well inside it.
```python
from dke import FactLoad
load = FactLoad("inventory", source="warehouse-scan", max_bytes=130_000)
load.comment("Stock levels, read 2026-10-08.")
load.table("items", rows,
[load.put("Item.s.label", "r[1]"),
load.put("Item.s.count", "r[2]")])
load.load_marker("warehouse", stamp)
for program in load.render():
client.run(program).raise_for_status()
```
`rows` are your rows exactly as `FactProgram.table` takes them, `stamp` is any
number that grows from one load to the next (epoch milliseconds is the obvious
one), and `client` is a `dke.client.Client`.
Every program is whole: its own import, its own tables, its own loops, so each
runs on its own. A split falls between rows and never inside a table's loop.
Rows that belong together — a sentence and its words — go in through
`load.add({"sentences": [...], "words": [...]})` as one group, and no split
falls inside a group. A group too big for a program of its own is refused
rather than split.
Its rows are written into the program's text, so the program's size is the one
limit a load meets.
With a load marker, each program carries its own, under `warehouse_01`,
`warehouse_02` and so on, and each also records `parts`: how many programs the
load was split into. Each program commits on its own, so each needs its own
proof that it reached its end. The last load finished when the programs whose
`started` holds the newest stamp number `parts`, and every one of them has
`completed` at least that stamp. A program that never ran shows up as one
missing from that count. A load that fits in one program is still written as
part `_01` of 1, so the marker keeps its shape on the day the load grows.
## No network, no subprocess
**Producing facts never needs your key.** `import dke`, `dke.factwriter` and
everything under `dke.ingest`, `dke.quantum` and `dke.pqc` open no socket and
spawn no process, so you can
point a reader at your own source without granting it anything, and read the
program it wrote before deciding to send it.
`dke.client` and `dke.keys` are the exceptions, and each is one you have to
ask for: they are the only modules here that talk to a network, they are
reached by nothing the producing half imports, and importing one is what
grants it. Naming it IS the grant.
Neither follows a redirect. Each request carries your key or your account
token, and following a redirect would send it on to whatever address the
redirect names, so a redirect is raised instead: a `TransportError` from
`dke.client` and a `MintError` from `dke.keys`, each with the redirect's
status. Until 0.7.1 `dke.client` followed one, key included; `dke.keys` has
refused them since it arrived in 0.7.0.
None of that is promised in prose alone. LangSyn's tests for the package, which
are not part of this release, assert it three ways, because they fail
differently: every producing module is read for a
networking import; the import GRAPH is walked from each producing root to show
neither network module is reachable; and a fresh interpreter imports the
producing half and is asked what it actually loaded. A file scan cannot see an
edge, and a graph walk cannot see an import performed at run time.
The exemption for each network module is checked in **both** directions — a
module that is exempt and imports no networking module fails too — so it
cannot outlive the reason it was granted. The same suite holds the
dependencies to their names: no module imports anything outside the standard
library and this package except `dke.keys`, which imports `cryptography`, and
that listing fails too the day `dke.keys` stops needing it.
## Requirements
Python 3.10 or later. No runtime dependencies, except that `dke.keys` needs
`cryptography` 38 or later (the `keys` extra).
## Licence
MIT. See `LICENSE`.