A schemaless query engine and editor for directories of frontmatter files.
Project description
fmql — FrontMatter Utilities
A schemaless query engine and editor for directories of frontmatter (markdown + YAML) files.
Point it at any directory of markdown/YAML files. Query with filters, traversal, aggregation, and graph patterns. Edit properties across single files or entire result sets. No configuration, no schema, no setup.
Installation
pip install fmql
From source:
git clone https://github.com/buyuk-dev/fmql.git
cd fmql
uv sync # or: pip install -e '.[dev]'
Requires Python 3.11+.
Quickstart
CLI:
fmql query ./project 'status = "active" AND priority > 2'
fmql query ./project 'due_date < today' --format json
Python:
from fmql import Workspace, Query
ws = Workspace("./project")
q = Query(ws).where(status="active", priority__gt=2)
for packet in q:
print(packet.id)
Features
- Filter DSL — SQL-ish string queries, case-insensitive keywords, typed comparisons, date literals.
- Python kwargs API — Django-style
field__op=valuewith a full operator registry. - Edit operations —
set,remove,rename,append,toggleon single files or bulk result sets, with diff preview and confirmation. - Format-preserving YAML — round-trip via
ruamel.yaml; edits preserve comments, key order, and quoting of untouched fields. - Traversal —
follow()resolves reference fields (paths, UUIDs, slugs) forward or reverse, bounded or transitive. - Aggregation —
group_by(...).aggregate(Count, Sum, Avg, Min, Max). - Describe — workspace introspection: observed fields, types, distinct-value samples.
- Cypher subset — graph patterns for dependency chains, cycle detection, multi-hop traversal.
- Pluggable search — minimal
SearchIndexprotocol; ships with a text-scan fallback.
CLI reference
| Command | Purpose | Example |
|---|---|---|
query |
Run a filter query against a workspace | fmql query ./project 'status = "active"' |
set |
Set frontmatter fields | fmql set ./tasks/task-42.md status=done priority=1 |
remove |
Remove frontmatter fields | fmql remove ./tasks/task-42.md temp_notes |
rename |
Rename frontmatter fields | fmql rename ./tasks/task-42.md assignee=assigned_to |
append |
Append to list-valued fields | fmql append ./tasks/task-42.md tags=urgent |
toggle |
Toggle boolean fields | fmql toggle ./tasks/task-42.md flagged |
describe |
Workspace introspection | fmql describe ./project |
cypher |
Graph pattern query (Cypher subset) | fmql cypher ./project 'MATCH (a)-[:blocked_by]->(b) RETURN a, b' |
Common flags:
--format {paths,json,rows}— output format (default:pathsforquery,rowsforcypher).--follow FIELD,--depth N|'*',--direction {forward,reverse}— traversal onquery.--resolver {path,uuid,slug}— reference resolution strategy for traversal/Cypher.--search QUERY,--index NAME— pluggable search stage.--dry-run,--yes— preview or auto-confirm for edit commands.--workspace ROOT— explicit workspace root when piping paths into edit commands.
Run fmql <command> --help for the full flag list on any command.
Query syntax
Filter DSL (query command and qlang)
Logical operators (case-insensitive):
AND OR NOT ( ... )
Comparisons:
= != > >= < <=
CONTAINS — substring match on strings/lists
MATCHES — regex match on strings
IN [v1, v2] — membership test
IS EMPTY — field missing or empty
IS NOT EMPTY
IS NULL
Values: quoted strings ("active"), numbers (42, 3.14), booleans (true, false), ISO dates (2026-05-01), and date sentinels:
today now
yesterday tomorrow
today-7d now+1h today+30d
Examples:
fmql query ./project 'status = "active" AND priority > 2'
fmql query ./project 'due_date < today AND status != "done"'
fmql query ./project 'tags CONTAINS "urgent" OR priority >= 3'
fmql query ./project 'status IN ["todo", "in_progress"]'
fmql query ./project 'NOT (assigned_to IS EMPTY)'
fmql query ./project 'title MATCHES "^\\[WIP\\]"'
Python kwargs API
field__op=value — everything before the final __ is the field, everything after is the operator. No __ means eq.
| Operator | Matches when field value… |
|---|---|
eq (default) |
equals the expected value (booleans stay distinct from ints) |
ne / not |
is present and does not equal |
gt, gte, lt, lte |
is a comparable type and ordered accordingly |
in |
is in the given list/tuple/set |
not_in |
is present and not in the list |
contains |
is a string containing the substring, or a list containing the value |
icontains |
same as contains, case-insensitive for strings |
startswith, endswith |
string prefix / suffix match |
matches |
matches the given regex |
exists |
field is present (any value, truthy flag) |
not_empty |
field is present and not empty / zero-length |
is_null |
field value is explicitly null |
type |
field value's type name equals the expected (int, str, list, date, …) |
Type-honest: non-comparable values are silently excluded, not coerced. priority > 2 matches packets where priority is an int > 2; packets where it's a string or missing are just not in the result.
from fmql import Query, Workspace
ws = Workspace("./project")
Query(ws).where(status="active", priority__gt=2)
Query(ws).where(tags__contains="urgent")
Query(ws).where(status__in=["todo", "in_progress"])
Query(ws).where(assigned_to__not_empty=True)
Query(ws).where(title__matches=r"^\[WIP\]")
Cypher subset (cypher command)
MATCH (a)-[:field]->(b) # single hop
MATCH (a)-[:field*]->(b) # transitive
MATCH (a)-[:field*1..5]->(b) # bounded depth
MATCH (a)-[:blocked_by*]->(a) # cycle detection
WHERE a.status = "active" AND b.priority > 2
RETURN a
RETURN a, b
RETURN a.title
RETURN count(a)
Node labels parse but are ignored (schemaless). The WHERE clause uses the same operators as the filter DSL.
fmql cypher ./project 'MATCH (a)-[:blocked_by*]->(a) RETURN a'
fmql cypher ./project 'MATCH (a)-[:belongs_to]->(e) WHERE e.type = "epic" RETURN a, e'
Traversal & resolvers
--follow FIELD turns the result set into the starting seeds for a graph walk along that field. --depth N bounds the walk (use * for transitive). --direction reverse walks incoming edges instead of outgoing.
# Direct dependencies of one task
fmql query ./project 'uuid = "task-42"' --follow blocked_by --depth 1
# Full transitive dependency chain
fmql query ./project 'uuid = "task-42"' --follow blocked_by --depth '*'
# What does task-42 unblock? (reverse edge)
fmql query ./project 'uuid = "task-42"' --follow blocked_by --direction reverse
References in frontmatter fields are resolved by the selected resolver:
path(default) — relative filesystem paths, e.g.blocked_by: ../tasks/task-41.md.uuid— matches auuidfrontmatter field on other packets.slug— matches aslugfrontmatter field on other packets.
Pass --resolver uuid / --resolver slug to switch. Unresolvable references are dropped silently.
Aggregation & describe
Group-and-aggregate returns one row per group:
from fmql import Query, Workspace
from fmql import Count, Sum, Avg
ws = Workspace("./project")
(
Query(ws)
.where(type="task", in_sprint="sprint-3")
.group_by("status")
.aggregate(count=Count(), points=Sum("points"))
)
describe summarises a workspace — fields observed, types seen per field, and a sample of distinct values:
fmql describe ./project
fmql describe ./project --format json --top 10
Editing & safety
Every edit is previewable, confirmable, and preserves comments, key order, quoting, and body bytes.
# Single file
fmql set ./project/tasks/task-42.md status=escalated priority=1
fmql remove ./project/tasks/task-42.md temp_notes
fmql rename ./project/tasks/task-42.md assignee=assigned_to
fmql append ./project/tasks/task-42.md tags=urgent
fmql toggle ./project/tasks/task-42.md flagged
# Bulk: pipe query results into edits
fmql query ./project 'status != "done" AND due_date < today' \
| fmql set status=escalated --workspace ./project --yes
# Preview without writing
fmql set ./project/tasks/task-42.md status=done --dry-run
Python equivalent:
from fmql import Workspace, Query
ws = Workspace("./project")
plan = Query(ws).where(status="active").set(status="reviewed")
print(plan.dry_run()) # unified diff
plan.apply(confirm=False) # write
Value coercion from CLI strings: true/false → bool; integers and floats parsed as numbers; ISO dates (2026-05-01) parsed as dates; null → None. Quote to force string: label='"123"'.
Safety model. Bulk edits print a unified diff and prompt before writing. --dry-run shows the diff without writing; --yes skips the prompt. When stdin is piped (fmql query ... | fmql set ...), the prompt reopens /dev/tty — on systems without a tty (CI, containers), pass --yes.
Formatting. fmql re-emits edited YAML with 2-space mapping indent and 4-space sequence offset (ruamel defaults with explicit offset). Files that don't conform can still be parsed; only edited files are re-emitted, and untouched keys round-trip byte-for-byte.
Development
uv sync --extra dev
make test # run pytest
make lint # ruff + black --check
make cov # pytest with coverage (fails under 84%)
make format # black
Status
v0.1.0 — first public release. All five design phases shipped: read path (filters, qlang, Python API), edit path (surgical YAML edits + bulk pipe), relationships & traversal (follow + resolvers), aggregation & describe, and the Cypher subset with pluggable search.
Links
- Design document — rationale, comparisons, target surface.
- Implementation plan — milestone map (frozen; written under the working name
fmq). - LICENSE — MIT.
- GitHub — source, issues, releases.
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 fmql-0.1.0.tar.gz.
File metadata
- Download URL: fmql-0.1.0.tar.gz
- Upload date:
- Size: 51.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a58286c2330754afcc10c5061cca077d1503b33c48300336a97427cc74904d5
|
|
| MD5 |
205a81c87ccabc56e3d5f82c9eaa87d9
|
|
| BLAKE2b-256 |
63c0a9e3be7603da4354787593fe3c29203e55471dcfd86cdb6f4e54f4aadf26
|
Provenance
The following attestation bundles were made for fmql-0.1.0.tar.gz:
Publisher:
publish.yml on buyuk-dev/fmql
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fmql-0.1.0.tar.gz -
Subject digest:
8a58286c2330754afcc10c5061cca077d1503b33c48300336a97427cc74904d5 - Sigstore transparency entry: 1323322673
- Sigstore integration time:
-
Permalink:
buyuk-dev/fmql@22dd9b0b473b3b56a0a25c63e3321ab8bf8fde98 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/buyuk-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@22dd9b0b473b3b56a0a25c63e3321ab8bf8fde98 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fmql-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fmql-0.1.0-py3-none-any.whl
- Upload date:
- Size: 46.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2bd9b61fa7bd725ffd446ffa1bd9d342acece0b0cb34838033d781cd5d3e4f7b
|
|
| MD5 |
261afcdd4a23284cd663772429ed415f
|
|
| BLAKE2b-256 |
5e1afc73ca62e172666275f8f35f3f88315cb7edf2ff3107b30931ea772a7e00
|
Provenance
The following attestation bundles were made for fmql-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on buyuk-dev/fmql
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fmql-0.1.0-py3-none-any.whl -
Subject digest:
2bd9b61fa7bd725ffd446ffa1bd9d342acece0b0cb34838033d781cd5d3e4f7b - Sigstore transparency entry: 1323322785
- Sigstore integration time:
-
Permalink:
buyuk-dev/fmql@22dd9b0b473b3b56a0a25c63e3321ab8bf8fde98 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/buyuk-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@22dd9b0b473b3b56a0a25c63e3321ab8bf8fde98 -
Trigger Event:
push
-
Statement type: