# 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`.