KDL 2.0 query tool
Project description
kdlquery
A pure Python KDL 2.0 parser with a CSS3-like selector API.
kdlquery provides a lossless CST parser, an immutable node tree with parent/sibling navigation, a Reader API for transforming KDL documents into arbitrary Python objects, and a selector engine for querying nodes by name, type annotation, properties, arguments, combinators, and pseudo-classes.
Designed as a foundation for building DSLs — KDL is a good fit for configuration, schemas, and structured data. The parser and selector API together cover the common cases of parsing, validating, and linting KDL documents.
Parser test cases are borrowed from the official KDL test suite.
Requirements
Python 3.10+, no external dependencies.
Installation
pip install kdlquery
Quick start
Parsing
from kdlquery import parse
doc = parse("""
app "my-service" version="1.0.0" {
(network)server "primary" port=8080 tls=#true {
host "localhost"
host "127.0.0.1"
timeout idle=30 connect=5
}
(network)server "replica" port=8081 tls=#false {
host "replica.local"
timeout idle=60 connect=5
}
router {
route "GET" "/api/users" handler="users.list" auth=#true
route "POST" "/api/users" handler="users.create" auth=#true
route "GET" "/api/health" handler="health.check" auth=#false
route "GET" "/static/*" handler="static.serve" auth=#false
}
plugins {
plugin "auth" enabled=#true {
(jwt)secret "hs256" key=(regex)"hs(256|512)"
expires (i32)3600
}
plugin "cache" enabled=#true {
backend "redis" host="cache.local" port=(u16)6379
}
plugin "debug" enabled=#false
}
(i32)workers 4
(i32)timeout 30
limits max-conn=(u32)1000 max-req=(u32)500
}
""")
parse() returns a KdlDocument — an immutable tree of KdlNode objects with parent, depth, sibling, and index maps pre-built.
Node access
# Top-level nodes
app = doc.nodes[0]
app.name # "app"
app.get_arg(0) # "my-service"
app.get_prop("version") # "1.0.0"
# Children
for server in app.children:
print(server.get_arg(0), server.get_prop("port"))
# Tree navigation
doc.parent_of(app.children[0]) is app # True
doc.depth_of(app) # 0
doc.index_of(app.children[1]) # 1
doc.siblings_of(app.children[0]) # (child_0, child_1, ...)
Selector API
CSS3-like selectors for querying the node tree.
# By name
doc.select("server")
# → [server "primary", server "replica"]
# By type annotation
doc.select("(network)")
# → [server "primary", server "replica"]
# Property filters
doc.select("server[tls=#true]")
# → [server "primary"]
doc.select('route[handler^="users"]')
# → [route "GET" "/api/users", route "POST" "/api/users"]
# Argument filters
doc.select('route[0="GET"]')
# → [route "GET" "/api/users", route "GET" "/api/health", route "GET" "/static/*"]
doc.select('route[*="POST"]')
# → [route "POST" "/api/users"]
# Type-annotated properties
doc.select("backend[(u16)port]")
# → [backend "redis"]
# Type-annotated arguments
doc.select("expires[(i32)0]")
# → [expires (i32)3600]
# Combinators: child (>), descendant (space), adjacent sibling (+), general sibling (~)
doc.select("app > server")
# → [server "primary", server "replica"]
doc.select("app route")
# → all four routes
doc.select('route[0="GET"] + route')
# → [route "POST" ..., route "GET" ...]
# Pseudo-classes
doc.select("route:first-child")
# → [route "GET" "/api/users"]
doc.select("(i32):last-child")
# → [timeout 30]
doc.select("host:only-child")
# → [host "replica.local"]
doc.select("app:root")
# → [app]
# :not()
doc.select("server:not([port=8080])")
# → [server "replica"]
doc.select("plugin:not(:empty)")
# → [plugin "auth", plugin "cache"]
# :has()
doc.select("plugin:has(backend)")
# → [plugin "cache"]
doc.select("app:has(> router)")
# → [app]
doc.select("plugin:has(> secret[(regex)key])")
# → [plugin "auth"]
# Comma (union) — deduplicates by node identity
doc.select("app, router")
# → [app, router]
doc.select("server, server")
# → [server "primary", server "replica"] (no duplicates)
# select_one — lazy, returns first match or None
doc.select_one("server[tls=#true]")
# → server "primary"
Reader API
The Reader API lets you transform a KDL document into arbitrary Python objects by walking the node tree.
from kdlquery import KDL2CSTParser, DictReader, parse_into
cst = KDL2CSTParser().parse("""
database "primary" {
host "db.example.com"
port 5432
}
database "replica" {
host "db-replica.example.com"
port 5433
}
""")
result, diagnostics = parse_into(cst, DictReader())
# result is a list of plain dicts
# [
# {
# "name": "database",
# "args": ("primary",),
# "props": {},
# "children": [
# {"name": "host", "args": ("db.example.com",), "props": {}, "children": []},
# {"name": "port", "args": (5432,), "props": {}, "children": []},
# ],
# },
# ...
# ]
DictReader is a built-in reader that produces nested dicts. To build a custom reader, subclass Reader and implement on_node:
from kdlquery import KdlNode, Reader, WalkContext
class ConfigReader(Reader[dict, dict]):
def on_node(self, node: KdlNode, ctx: WalkContext[dict]) -> dict:
if node.name == "server":
children = ctx.walk_children()
return {
"id": node.get_arg(0),
"port": node.get_prop("port"),
"tls": node.get_prop("tls", False),
"hosts": [c["host"] for c in children if "host" in c],
}
if node.name == "host":
return {"host": node.get_arg(0)}
return ctx.walk_children() # recurse by default
def finalize(self, nodes, diagnostics):
return {n["id"]: n for n in nodes if "id" in n}
CST parser
For cases where you need the raw parse tree with full source spans:
from kdlquery import KDL2CSTParser
cst = KDL2CSTParser().parse('node "value" key=42')
# cst.nodes[0].entries — raw CST entries with exact positions
Selector reference
# Node
name # by name
* # any node
(type) # by type annotation on node
(type)name # type annotation + name
# Properties
[key] # property exists
[key=val] # equals
[key^=val] # starts with
[key$=val] # ends with
[key~=val] # contains
[(type)key] # property with type-annotated value
[(type)key=val] # type-annotated + value match
# Arguments
[N] # argument at position N exists
[N=val] # equals
[N^=val] # starts with
[N$=val] # ends with
[N~=val] # contains
[(type)N] # argument with type annotation
[*=val] # any argument equals val
# Combinators
A B # descendant
A > B # direct child
A + B # adjacent sibling
A ~ B # general sibling
A, B # union (deduplicated)
# Pseudo-classes
:root
:first-child
:last-child
:nth-child(n)
:nth-child(2n)
:nth-child(2n+1)
:only-child
:empty
:not(compound)
:has(complex)
:has(> complex)
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kdlquery-1.0.0.tar.gz.
File metadata
- Download URL: kdlquery-1.0.0.tar.gz
- Upload date:
- Size: 91.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9df1c6019c4533828891aed6f81295aacbc373e11222ca5cfa3375d3dc63d3db
|
|
| MD5 |
8ae33c1d41e03c761cb7aef0eb46333f
|
|
| BLAKE2b-256 |
5bec931fd458697516ae3547d994f6e7b4f1a7952eb50af96eeed4c0d3fb6332
|
File details
Details for the file kdlquery-1.0.0-py3-none-any.whl.
File metadata
- Download URL: kdlquery-1.0.0-py3-none-any.whl
- Upload date:
- Size: 27.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e7100d8047ace525abd42936edb2811d6103495f23fafa67a8e92ee8cf2e1e3
|
|
| MD5 |
6e681813c4e961e7414b5a641dd04a6a
|
|
| BLAKE2b-256 |
76337b5e7f326e87dbe2e0afa5107a407eb11102a1e1a023cd39a6fc64e889df
|