dgconvert 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dgconvert-0.1.0/.gitignore +7 -0
- dgconvert-0.1.0/CHANGELOG.md +23 -0
- dgconvert-0.1.0/DESIGN.md +239 -0
- dgconvert-0.1.0/LICENSE +21 -0
- dgconvert-0.1.0/PKG-INFO +63 -0
- dgconvert-0.1.0/README.md +46 -0
- dgconvert-0.1.0/pyproject.toml +56 -0
- dgconvert-0.1.0/src/dgconvert/__init__.py +5 -0
- dgconvert-0.1.0/src/dgconvert/body_snatchers.py +39 -0
- dgconvert-0.1.0/src/dgconvert/dgpkm.py +62 -0
- dgconvert-0.1.0/src/dgconvert/frontmatter.py +121 -0
- dgconvert-0.1.0/src/dgconvert/incremental.py +117 -0
- dgconvert-0.1.0/src/dgconvert/links.py +59 -0
- dgconvert-0.1.0/src/dgconvert/slugs.py +20 -0
- dgconvert-0.1.0/tests/test_body_snatchers.py +36 -0
- dgconvert-0.1.0/tests/test_frontmatter.py +108 -0
- dgconvert-0.1.0/tests/test_incremental.py +132 -0
- dgconvert-0.1.0/tests/test_links.py +45 -0
- dgconvert-0.1.0/tests/test_slugs.py +29 -0
- dgconvert-0.1.0/uv.lock +158 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
|
|
3
|
+
|
|
4
|
+
- - -
|
|
5
|
+
## 0.1.0 - 2026-08-19
|
|
6
|
+
#### Features
|
|
7
|
+
- (**cog**) add cocogitto changelog generation - (1b31be7) - ScriptAutomate, *Claude Sonnet 5*
|
|
8
|
+
- add build/publish just recipes and trim the sdist - (0073681) - ScriptAutomate
|
|
9
|
+
- add PyPI package metadata (MIT license, urls, classifiers) - (08e6afc) - ScriptAutomate
|
|
10
|
+
- add body_snatchers module, sourced from docufig.seed.yaml - (f127c52) - ScriptAutomate
|
|
11
|
+
- extract generic helper modules from dg-tgdp - (be68511) - ScriptAutomate, *Claude Sonnet 5*
|
|
12
|
+
#### Documentation
|
|
13
|
+
- remove references to sibling repos, make dgconvert standalone - (cfc6172) - ScriptAutomate
|
|
14
|
+
- adopt helper-library architecture and drop nox-style runner - (dc1ca93) - ScriptAutomate, *Claude Sonnet 5*
|
|
15
|
+
#### Miscellaneous Chores
|
|
16
|
+
- drop authors from pyproject.toml - (dd2b421) - ScriptAutomate
|
|
17
|
+
- scaffold python project with spec-kit, uv, justfile, and devcontainer - (e1fef62) - ScriptAutomate, *Claude Sonnet 5*
|
|
18
|
+
#### Style
|
|
19
|
+
- normalize justfile formatting via just --fmt - (2d3f732) - ScriptAutomate
|
|
20
|
+
|
|
21
|
+
- - -
|
|
22
|
+
|
|
23
|
+
Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto).
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# dgconvert design — a helper library, not a runner
|
|
2
|
+
|
|
3
|
+
Status: extracted (2026-08-08) — the four generic modules plus the
|
|
4
|
+
incremental-conversion driver described below now live in
|
|
5
|
+
`dgconvert/src/dgconvert/`, per the extraction plan at the bottom of this
|
|
6
|
+
document. Seed repos depend on `dgconvert` as a real dependency rather
|
|
7
|
+
than owning their own copies. Originally drafted 2026-07-16 against a
|
|
8
|
+
still-inline reference implementation. Supersedes an earlier
|
|
9
|
+
fixed-pipeline architecture and an even earlier nox-style-runner draft
|
|
10
|
+
of this document. The compatibility contract, language decision
|
|
11
|
+
(Python, uv-managed), and verification strategy from that original plan
|
|
12
|
+
carry over unchanged.
|
|
13
|
+
|
|
14
|
+
## Terminology
|
|
15
|
+
|
|
16
|
+
- **Garden** — a directory of notes (`notes/*.md`).
|
|
17
|
+
- **Greenhouse** — a DocsGarden workspace rooted at `docufig.json`,
|
|
18
|
+
containing one or more gardens.
|
|
19
|
+
- **Seed** — a pre-created garden, ready to be planted into a greenhouse.
|
|
20
|
+
A seed is described by its own `docufig.seed.json` (license,
|
|
21
|
+
topHierarchy, source provenance, bodySnatchers).
|
|
22
|
+
- **Seedbank** — a registry of seeds (a separate, not-yet-built dgpkm
|
|
23
|
+
feature; not something dgconvert or a seed repo needs to know about).
|
|
24
|
+
|
|
25
|
+
`dgconvert` produces **seeds**. See dgpkm's own documentation for the
|
|
26
|
+
full terminology writeup.
|
|
27
|
+
|
|
28
|
+
## Core idea
|
|
29
|
+
|
|
30
|
+
Every seed repo owns its own conversion: its `docufig.seed.json`
|
|
31
|
+
(declarative — license, topHierarchy, source repo info) plus a plain
|
|
32
|
+
Python script that reads that config and drives the conversion
|
|
33
|
+
(imperative — fetch, transform, write, verify). **dgconvert is not a
|
|
34
|
+
task runner and does not discover or execute anything on a seed
|
|
35
|
+
repo's behalf** — an earlier draft of this document proposed a nox-style
|
|
36
|
+
`seedfile.py` + task-discovery model; that was rejected in favor of
|
|
37
|
+
dgconvert being nothing more than a plain library that a seed's own
|
|
38
|
+
script imports.
|
|
39
|
+
|
|
40
|
+
This mirrors the reasoning that was already settled before dgconvert had
|
|
41
|
+
any code: sources vary too widely (GitHub docs repos, MediaWiki, ebooks,
|
|
42
|
+
CSV data files) for a fixed pipeline or task-discovery convention to fit
|
|
43
|
+
all of them — a seed's own script is already the right level of control,
|
|
44
|
+
and dgconvert's job is only to make that script short.
|
|
45
|
+
|
|
46
|
+
## What a seed repo looks like
|
|
47
|
+
|
|
48
|
+
| File / dir | Role | Owned by |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `docufig.seed.json` | **What** the seed is: name, license, topHierarchy, source repo/owner info, bodySnatchers. dgpkm's schema; validated by `dgpkm doctor schema seed`. | seed maintainer, dgpkm-schema-shaped |
|
|
51
|
+
| `src/<pkg>/convert.py` (or similar) | **How** it's built: fetch, transform, write, verify. Imports `dgconvert` for the parts that are the same across every seed. | seed maintainer |
|
|
52
|
+
| `notes/*.md` | The output: the garden itself. | generated |
|
|
53
|
+
|
|
54
|
+
The first real seed repo built against this pattern is dgconvert's
|
|
55
|
+
reference implementation — its own conversion modules were the working
|
|
56
|
+
prototype this library gets generalized from, not a consumer built
|
|
57
|
+
against an already-finished dgconvert.
|
|
58
|
+
|
|
59
|
+
## Library API surface
|
|
60
|
+
|
|
61
|
+
Extracted 1:1 from a first seed's own generic modules (originally
|
|
62
|
+
prototyped, working, with zero source-specific imports — extraction was
|
|
63
|
+
a house-move, not a redesign):
|
|
64
|
+
|
|
65
|
+
- **`dgconvert.frontmatter`** — single-line frontmatter emission and a
|
|
66
|
+
parser that mirrors dgpkm's own. Two verified constraints drive this:
|
|
67
|
+
dgpkm's frontmatter delimiter split (`strings.SplitN(content, "---",
|
|
68
|
+
3)` in Go) is **not line-anchored**, so a value containing the literal
|
|
69
|
+
substring `"---"` truncates the frontmatter block early — every
|
|
70
|
+
emitted value is sanitized to a single line with `"---"` replaced; and
|
|
71
|
+
dgpkm requires `id:` to be present and `created`/`updated` to be
|
|
72
|
+
epoch-ms integers. No YAML library is used — round-tripping through a
|
|
73
|
+
real YAML parser reintroduces exactly the failure mode this avoids
|
|
74
|
+
(folded scalars, auto-quoting) and can't guarantee output matches
|
|
75
|
+
dgpkm's naive line-based reader.
|
|
76
|
+
- **`dgconvert.slugs`** — dotted-hierarchy slug joining and
|
|
77
|
+
wikilink/Dendron-link builders. Always **alias-after**
|
|
78
|
+
(`[[slug#anchor|Display]]`) — dgpkm cannot parse Dendron's alias-first
|
|
79
|
+
on-disk form.
|
|
80
|
+
- **`dgconvert.links`** — markdown-link scanning and rewriting via a
|
|
81
|
+
caller-supplied resolver callback, plus `defeat_md_regex`: dgpkm's
|
|
82
|
+
backlinks extractor matches **any** `](....md)` or `](....md#anchor)`,
|
|
83
|
+
including full external URLs, and does not skip fenced code blocks —
|
|
84
|
+
a link to unconverted or external content that bare-ends in `.md`
|
|
85
|
+
gets a harmless query string appended (`?ref_type=tags`) to defeat the
|
|
86
|
+
regex, confirmed necessary against real converted content (several
|
|
87
|
+
files reference other projects' `CHANGELOG.md`/`README.md` by full
|
|
88
|
+
URL with no query string).
|
|
89
|
+
- **`dgconvert.dgpkm`** — a `dgpkm` binary subprocess wrapper.
|
|
90
|
+
`dgpkm doctor backlinks` and `dgpkm doctor schema <type>` **always
|
|
91
|
+
exit 0**, even with findings — findings are only visible in
|
|
92
|
+
stdout/stderr text (`Found N broken link(s)`, `✗ ...`). This module
|
|
93
|
+
parses that text so a conversion script can fail loudly (non-zero
|
|
94
|
+
exit) when its output isn't doctor-clean.
|
|
95
|
+
- **`dgconvert.incremental`** — the create/skip/update/delete driver,
|
|
96
|
+
extracted once a second, independent consumer needed the same shape.
|
|
97
|
+
State lives **in each note's frontmatter** (a `sourceFileHash` sha256
|
|
98
|
+
key), not a separate state file. Re-running after new upstream
|
|
99
|
+
source: unchanged hash → skip (byte-identical, `created`/`updated`
|
|
100
|
+
both preserved); changed hash → rewrite in place (same `id`, same
|
|
101
|
+
`created`, bumped `updated`); source gone → delete the note and
|
|
102
|
+
report it. Callers supply a per-item `build()` closure (called only
|
|
103
|
+
for new/changed items, never for a skip) returning `(frontmatter,
|
|
104
|
+
body, extra)`; source-specific concerns (index-note generation, a
|
|
105
|
+
source's own path→slug table) stay in the seed repo, only the loop
|
|
106
|
+
itself is generic.
|
|
107
|
+
- **`dgconvert.body_snatchers`** — find/replace content transforms,
|
|
108
|
+
sourced directly from a seed's own `docufig.seed.yaml` (`bodySnatchers`
|
|
109
|
+
is a required field in dgpkm's own seed schema, not decorative), so a
|
|
110
|
+
seed's Python code never keeps a second, hand-maintained copy in sync
|
|
111
|
+
with that already-schema-validated manifest.
|
|
112
|
+
|
|
113
|
+
## Seed-specific code stays in the seed repo
|
|
114
|
+
|
|
115
|
+
Everything that only makes sense for one source format lives in the seed
|
|
116
|
+
repo, never in dgconvert: a source's own index/manifest model, its own
|
|
117
|
+
path→slug table, its attribution text, its kind-index-note builders have
|
|
118
|
+
no dgconvert equivalent and shouldn't get one — a future MediaWiki or
|
|
119
|
+
ebook seed will have an equally source-specific module of its own, built
|
|
120
|
+
the same way, using the same generic library pieces above.
|
|
121
|
+
|
|
122
|
+
## Non-Markdown sources: markitdown
|
|
123
|
+
|
|
124
|
+
Planned dependency (not yet added — no seed has needed it yet):
|
|
125
|
+
[`microsoft/markitdown`](https://github.com/microsoft/markitdown), for
|
|
126
|
+
converting non-Markdown sources (PDFs, Office docs, HTML scrapes, etc.)
|
|
127
|
+
into Markdown before the rest of the pipeline (frontmatter, slugs, link
|
|
128
|
+
rewriting) applies. Kept as an optional extra (`dgconvert[markitdown]`)
|
|
129
|
+
rather than a base dependency, since most seeds don't need it (e.g. one
|
|
130
|
+
real seed handles its CSV files by fencing them as code blocks rather
|
|
131
|
+
than converting them).
|
|
132
|
+
|
|
133
|
+
## Bundling the `dgpkm` binary
|
|
134
|
+
|
|
135
|
+
Open design question, not yet solved: `dgconvert.dgpkm` needs a `dgpkm`
|
|
136
|
+
binary to shell out to. Today this is solved by an environment variable
|
|
137
|
+
(`DGPKM_BIN`, falling back to `dgpkm` on `PATH`) and a documented
|
|
138
|
+
limitation that no devcontainer bundles one, because dgpkm has no
|
|
139
|
+
distributable releases yet.
|
|
140
|
+
|
|
141
|
+
**Recommendation once dgpkm does cut releases:** a separate,
|
|
142
|
+
platform-tagged PyPI package (e.g. `dgpkm-bin`) that vendors the compiled
|
|
143
|
+
binary as wheel data — one wheel per platform, built in dgpkm's own
|
|
144
|
+
release CI, the same pattern `ruff`/`uv` use for their own Rust binaries.
|
|
145
|
+
`dgconvert` would depend on it, with resolution order `DGPKM_BIN` env var
|
|
146
|
+
→ `dgpkm` on `PATH` → the bundled binary. This keeps dgconvert's own
|
|
147
|
+
dependency footprint honest (it depends on a real published artifact, not
|
|
148
|
+
a hand-rolled download-on-first-run step) and requires no changes to the
|
|
149
|
+
`dgconvert.dgpkm` module's public interface — only its binary-discovery
|
|
150
|
+
order gains a third fallback.
|
|
151
|
+
|
|
152
|
+
## Dependencies
|
|
153
|
+
|
|
154
|
+
Kept deliberately minimal. `pyyaml` is a base dependency:
|
|
155
|
+
`frontmatter.emit_note`'s `extra` passthrough (arbitrary
|
|
156
|
+
provenance/source fields a seed's own frontmatter model doesn't cover,
|
|
157
|
+
e.g. a task-tracking source format's own metadata or a nested custom
|
|
158
|
+
object) needs real YAML serialization to round-trip nested values
|
|
159
|
+
correctly — dgpkm's own frontmatter reader is a real YAML parser too
|
|
160
|
+
(`go.yaml.in/yaml/v4`, both directions), so this isn't working around a
|
|
161
|
+
naive reader the way the known-field emitter still does; also used by
|
|
162
|
+
`dgconvert.body_snatchers` to read a seed's own `docufig.seed.yaml`
|
|
163
|
+
directly. Slugs, link rewriting, hashing, and subprocess handling remain
|
|
164
|
+
stdlib-only. Planned additions, both as extras rather than base
|
|
165
|
+
dependencies:
|
|
166
|
+
|
|
167
|
+
- `markitdown` — non-Markdown source conversion (see above).
|
|
168
|
+
- A future `dgpkm-bin` platform wheel (see above), once it exists.
|
|
169
|
+
|
|
170
|
+
Dev dependencies: `pytest`, `ruff`.
|
|
171
|
+
|
|
172
|
+
## Toolchain parity
|
|
173
|
+
|
|
174
|
+
uv-managed, `.justfile` mirroring dgpkm's own
|
|
175
|
+
(`build-devcontainer` gated on `USE_CONTAINER_DEV`,
|
|
176
|
+
`sync`/`fmt`/`lint`/`test`/`check`, cocogitto conventional commits),
|
|
177
|
+
`.devcontainer/` (Python base image, uv, just, cocogitto). Unlike seed
|
|
178
|
+
repos, **dgconvert uses Spec Kit** (`.specify/`, this repo's `.claude/`
|
|
179
|
+
skills) for its own development — seed repos deliberately don't, since
|
|
180
|
+
they're conversion/data repos, not spec-driven feature work.
|
|
181
|
+
|
|
182
|
+
## Extraction plan — done (2026-08-08)
|
|
183
|
+
|
|
184
|
+
1. ~~Move the first seed's own generic modules into
|
|
185
|
+
`dgconvert/src/dgconvert/{frontmatter,slugs,links,dgpkm}.py`
|
|
186
|
+
verbatim~~ — done, plus one real addition: `frontmatter.emit_note`
|
|
187
|
+
gained an optional `extra` parameter (arbitrary passthrough YAML,
|
|
188
|
+
sorted-key, real-YAML-serialized) that the first seed doesn't use but
|
|
189
|
+
a second, independent consumer needs for fields with no dgpkm-native
|
|
190
|
+
equivalent. `dgpkm.verify`'s schema check also gained an off switch
|
|
191
|
+
(`check_schema=None`) since not every `dgconvert` consumer is itself
|
|
192
|
+
a seed with a `docufig.seed.json` to validate.
|
|
193
|
+
2. ~~Point the first seed's `pyproject.toml` at `dgconvert` as a
|
|
194
|
+
dependency~~ — done (a real dependency, editable during local
|
|
195
|
+
development). Verified behavior-preserving: its own source-specific
|
|
196
|
+
test suite (the tests that didn't move to `dgconvert`) still passes,
|
|
197
|
+
and a real re-run against its already-converted `notes/` garden
|
|
198
|
+
produces a byte-identical `git diff` (confirmed against the
|
|
199
|
+
pre-refactor code as a baseline, not just the tests).
|
|
200
|
+
3. ~~Extract `run_convert`'s incremental-diff shape into
|
|
201
|
+
`dgconvert.incremental`~~ — done, once a second, independent consumer
|
|
202
|
+
needed the same create/skip/update/delete-by-hash shape.
|
|
203
|
+
Source-specific concerns (index-note generation, a source's own
|
|
204
|
+
path→slug table) stayed in that seed's own repo as originally
|
|
205
|
+
planned; only the loop itself (a per-item `build()` closure plus
|
|
206
|
+
id/created/updated bookkeeping) is generic.
|
|
207
|
+
4. ~~Write the corresponding `dgconvert` test suite by moving~~ — done
|
|
208
|
+
(`tests/test_{frontmatter,slugs,links}.py`), plus a new
|
|
209
|
+
`tests/test_incremental.py` for the newly-generic driver (the first
|
|
210
|
+
seed's own incremental test stayed in place there, since it still
|
|
211
|
+
exercises source-specific behavior through the shared driver).
|
|
212
|
+
5. A `specs/` feature spec via `/speckit-specify` was **not** written for
|
|
213
|
+
this extraction — it happened as part of a larger, time-boxed
|
|
214
|
+
migration task rather than dgconvert's own spec-driven workflow.
|
|
215
|
+
Worth backfilling if dgconvert's process requires it retroactively.
|
|
216
|
+
6. ~~Extract `apply_body_snatchers` into `dgconvert.body_snatchers`,
|
|
217
|
+
sourced from `docufig.seed.yaml` directly~~ — done, once a second,
|
|
218
|
+
independent consumer needed the identical shape a first seed already
|
|
219
|
+
had. Went one step further than the other extractions: rather than
|
|
220
|
+
just deduplicating the apply function, `load()` reads a seed's
|
|
221
|
+
`bodySnatchers` directly from its already-schema-validated
|
|
222
|
+
`docufig.seed.yaml`, eliminating a real hand-sync bug class (a
|
|
223
|
+
seed's Python-side transform table drifting out of sync with its own
|
|
224
|
+
manifest, seen in practice) instead of just moving where the
|
|
225
|
+
duplication lived.
|
|
226
|
+
|
|
227
|
+
## Open questions
|
|
228
|
+
|
|
229
|
+
1. ~~Exact package/module names for the extracted pieces~~ — settled:
|
|
230
|
+
`dgconvert.frontmatter`, `dgconvert.slugs`, `dgconvert.links`,
|
|
231
|
+
`dgconvert.dgpkm`, `dgconvert.incremental`, `dgconvert.body_snatchers`.
|
|
232
|
+
2. Whether `dgconvert.dgpkm`'s binary-discovery fallback chain should be
|
|
233
|
+
configurable per-project or hardcoded to the `DGPKM_BIN` → `PATH` →
|
|
234
|
+
bundled order.
|
|
235
|
+
3. Whether the incremental-diff pattern (state-in-frontmatter,
|
|
236
|
+
create/skip/update/delete) generalizes cleanly to a source that isn't
|
|
237
|
+
a flat file tree (e.g. a paginated API), or needs a second, distinct
|
|
238
|
+
pattern for that case — genuinely unknown until a seed of that shape
|
|
239
|
+
exists.
|
dgconvert-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScriptAutomate
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dgconvert-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dgconvert
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Helper library for building dgpkm-compatible seeds (frontmatter, slugs, link rewriting, dgpkm doctor verification)
|
|
5
|
+
Project-URL: Homepage, https://github.com/ScriptAutomate/dgconvert
|
|
6
|
+
Project-URL: Repository, https://github.com/ScriptAutomate/dgconvert
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: pyyaml>=6
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# dgconvert
|
|
19
|
+
|
|
20
|
+
A small Python **helper library** for building
|
|
21
|
+
[dgpkm](https://github.com/ScriptAutomate/dgpkm)-compatible **seeds**
|
|
22
|
+
(pre-created gardens, ready to plant into a greenhouse).
|
|
23
|
+
|
|
24
|
+
`dgconvert` is not a task runner. Each seed repo owns its own
|
|
25
|
+
`docufig.seed.json` and its own plain-Python conversion script;
|
|
26
|
+
`dgconvert` supplies the pieces every seed needs regardless of source
|
|
27
|
+
format — dotted-slug file naming, single-line frontmatter with stable
|
|
28
|
+
ids, markdown-link → dgpkm-wikilink rewriting, and a wrapper that
|
|
29
|
+
verifies output with `dgpkm doctor`.
|
|
30
|
+
|
|
31
|
+
See `DESIGN.md` for the full architecture and the verified
|
|
32
|
+
dgpkm-compatibility constraints it's built against. The library was
|
|
33
|
+
generalized out of a first seed's own inline conversion code once a
|
|
34
|
+
second, independent seed needed the same pieces.
|
|
35
|
+
|
|
36
|
+
## Installing
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add dgconvert
|
|
40
|
+
# or
|
|
41
|
+
pip install dgconvert
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
Managed with [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/),
|
|
47
|
+
using [Spec Kit](https://github.com/github/spec-kit) for feature specs
|
|
48
|
+
(`specs/`, `.specify/`) — unlike seed repos, which don't use Spec Kit:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
just # list tasks
|
|
52
|
+
just sync # uv sync
|
|
53
|
+
just test # uv run pytest
|
|
54
|
+
just check # format + lint
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Container-based development mirrors dgpkm: set `USE_CONTAINER_DEV` to
|
|
58
|
+
`podman`, `docker`, or `container`, then `just build-devcontainer` once and
|
|
59
|
+
every build/test recipe runs inside the dev container automatically.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# dgconvert
|
|
2
|
+
|
|
3
|
+
A small Python **helper library** for building
|
|
4
|
+
[dgpkm](https://github.com/ScriptAutomate/dgpkm)-compatible **seeds**
|
|
5
|
+
(pre-created gardens, ready to plant into a greenhouse).
|
|
6
|
+
|
|
7
|
+
`dgconvert` is not a task runner. Each seed repo owns its own
|
|
8
|
+
`docufig.seed.json` and its own plain-Python conversion script;
|
|
9
|
+
`dgconvert` supplies the pieces every seed needs regardless of source
|
|
10
|
+
format — dotted-slug file naming, single-line frontmatter with stable
|
|
11
|
+
ids, markdown-link → dgpkm-wikilink rewriting, and a wrapper that
|
|
12
|
+
verifies output with `dgpkm doctor`.
|
|
13
|
+
|
|
14
|
+
See `DESIGN.md` for the full architecture and the verified
|
|
15
|
+
dgpkm-compatibility constraints it's built against. The library was
|
|
16
|
+
generalized out of a first seed's own inline conversion code once a
|
|
17
|
+
second, independent seed needed the same pieces.
|
|
18
|
+
|
|
19
|
+
## Installing
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv add dgconvert
|
|
23
|
+
# or
|
|
24
|
+
pip install dgconvert
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
Managed with [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/),
|
|
30
|
+
using [Spec Kit](https://github.com/github/spec-kit) for feature specs
|
|
31
|
+
(`specs/`, `.specify/`) — unlike seed repos, which don't use Spec Kit:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
just # list tasks
|
|
35
|
+
just sync # uv sync
|
|
36
|
+
just test # uv run pytest
|
|
37
|
+
just check # format + lint
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Container-based development mirrors dgpkm: set `USE_CONTAINER_DEV` to
|
|
41
|
+
`podman`, `docker`, or `container`, then `just build-devcontainer` once and
|
|
42
|
+
every build/test recipe runs inside the dev container automatically.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dgconvert"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Helper library for building dgpkm-compatible seeds (frontmatter, slugs, link rewriting, dgpkm doctor verification)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 3 - Alpha",
|
|
10
|
+
"License :: OSI Approved :: MIT License",
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pyyaml>=6",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/ScriptAutomate/dgconvert"
|
|
21
|
+
Repository = "https://github.com/ScriptAutomate/dgconvert"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8",
|
|
26
|
+
"ruff>=0.8",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/dgconvert"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
# Hatchling's sdist default is "everything git-tracks" -- excludes the
|
|
38
|
+
# repo's own dev/agent tooling (Claude Code skills, Spec Kit scaffolding,
|
|
39
|
+
# devcontainer, justfile) that a consumer installing this library from
|
|
40
|
+
# PyPI has no use for. The wheel (tool.hatch.build.targets.wheel above)
|
|
41
|
+
# was already correctly scoped to just src/dgconvert/ without any of
|
|
42
|
+
# this needing to be said twice.
|
|
43
|
+
exclude = [
|
|
44
|
+
".claude",
|
|
45
|
+
".specify",
|
|
46
|
+
".devcontainer",
|
|
47
|
+
".justfile",
|
|
48
|
+
"cog.toml",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
src = ["src", "tests"]
|
|
53
|
+
line-length = 100
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Body-snatcher (find/replace) content transforms, sourced from a
|
|
2
|
+
seed's own docufig.seed.yaml.
|
|
3
|
+
|
|
4
|
+
`bodySnatchers` is a required field in dgpkm's own seed config JSON
|
|
5
|
+
Schema (validated by `dgpkm doctor schema seed`), not a decorative
|
|
6
|
+
one -- so it's read directly from that already-schema-validated
|
|
7
|
+
manifest rather than requiring a seed's own Python code to keep a
|
|
8
|
+
second, hand-maintained copy in sync (a real, seen-in-practice failure
|
|
9
|
+
mode: a seed's manifest drifting out of sync with its own Python-side
|
|
10
|
+
transform table after a code-only change, caught only by a direct
|
|
11
|
+
question, not by any tooling).
|
|
12
|
+
|
|
13
|
+
Generalized out of two seeds' identical apply_body_snatchers once a
|
|
14
|
+
second seed needed the exact same shape a first one already had --
|
|
15
|
+
the same "second consumer" trigger this library's other modules were
|
|
16
|
+
each generalized under.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load(seed_config_path: Path) -> tuple[tuple[str, str], ...]:
|
|
27
|
+
"""Reads bodySnatchers from a seed's docufig.seed.yaml. Each entry's
|
|
28
|
+
{find, replace} shape matches dgpkm's own seed config JSON Schema
|
|
29
|
+
exactly. A missing bodySnatchers key (defensive -- the schema
|
|
30
|
+
requires it, but this shouldn't hard-fail a caller that hasn't
|
|
31
|
+
validated yet) behaves like an empty list."""
|
|
32
|
+
data = yaml.safe_load(seed_config_path.read_text(encoding="utf-8")) or {}
|
|
33
|
+
return tuple((s["find"], s["replace"]) for s in data.get("bodySnatchers", []))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def apply(text: str, snatchers: tuple[tuple[str, str], ...]) -> str:
|
|
37
|
+
for find, replace in snatchers:
|
|
38
|
+
text = text.replace(find, replace)
|
|
39
|
+
return text
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Wraps the `dgpkm` binary for verification.
|
|
2
|
+
|
|
3
|
+
`dgpkm doctor backlinks` and `dgpkm doctor schema <type>` always exit 0,
|
|
4
|
+
even when they report findings -- findings are only visible in
|
|
5
|
+
stdout/stderr text ("Found N broken link(s)", "✗ ..."). This module
|
|
6
|
+
parses that text so the converter can fail loudly (non-zero exit) when a
|
|
7
|
+
conversion isn't doctor-clean, which dgpkm itself won't do for you.
|
|
8
|
+
|
|
9
|
+
Generalized beyond its original prototype in one way: `verify`'s schema
|
|
10
|
+
check is now optional (`check_schema=None` skips it) since not every
|
|
11
|
+
consumer has a `docufig.seed.json` to validate -- a standalone
|
|
12
|
+
converter tool can write straight into a greenhouse's root garden,
|
|
13
|
+
not a separate seed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def dgpkm_bin() -> str:
|
|
24
|
+
return os.environ.get("DGPKM_BIN", "dgpkm")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
28
|
+
return subprocess.run(
|
|
29
|
+
[dgpkm_bin(), *args],
|
|
30
|
+
cwd=cwd,
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
check=False,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def verify(root: Path, *, check_schema: str | None = "seed") -> list[str]:
|
|
38
|
+
"""Run dgpkm doctor checks against the garden at `root`. Returns a list
|
|
39
|
+
of human-readable problem descriptions (empty = clean).
|
|
40
|
+
|
|
41
|
+
`check_schema` names the config type to pass to `dgpkm doctor schema
|
|
42
|
+
<type>` (e.g. "seed" for a `docufig.seed.json`); pass None to skip
|
|
43
|
+
that check entirely for a tool that has no such config file.
|
|
44
|
+
"""
|
|
45
|
+
problems: list[str] = []
|
|
46
|
+
|
|
47
|
+
backlinks = _run(["doctor", "backlinks"], root)
|
|
48
|
+
if backlinks.returncode != 0:
|
|
49
|
+
problems.append(f"dgpkm doctor backlinks failed to run:\n{backlinks.stderr}")
|
|
50
|
+
elif "All backlinks are valid" not in backlinks.stdout:
|
|
51
|
+
problems.append(f"backlinks: broken links found\n{backlinks.stdout}")
|
|
52
|
+
|
|
53
|
+
if check_schema:
|
|
54
|
+
schema = _run(["doctor", "schema", check_schema], root)
|
|
55
|
+
if schema.returncode != 0:
|
|
56
|
+
problems.append(f"dgpkm doctor schema {check_schema} failed to run:\n{schema.stderr}")
|
|
57
|
+
elif "✓" not in schema.stdout or "✗" in schema.stderr:
|
|
58
|
+
problems.append(
|
|
59
|
+
f"{check_schema} config schema invalid:\n{schema.stdout}{schema.stderr}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return problems
|