fux-decoder-config 4.1.1__py3-none-any.whl
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.
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""`fux_decoder_config` — the fux-decoder-config package (W-289, SR-PLUGINS).
|
|
2
|
+
|
|
3
|
+
One fux decoder module per format it reads: `fux_decoder_config.toml`, `fux_decoder_config.yaml`, `fux_decoder_config.ini`. Each is the
|
|
4
|
+
decoder protocol of SR-DECODE — `EXTENSIONS`, `VERSION`, `USES` and
|
|
5
|
+
`decode(raw, rel_path)` — moved here unchanged from `src/fux/decode/` in
|
|
6
|
+
W-289 phase 2.
|
|
7
|
+
|
|
8
|
+
**fux loads a module from here only when `.fux/plugins.toml` declares this
|
|
9
|
+
package**, after verifying its version and digest (SR-PLUGINS P3, P4), by its
|
|
10
|
+
fixed import name (P7). A same-named file in `.fux/decoders/` overrides it
|
|
11
|
+
(P8). This file runs nothing: importing the package imports no decoder.
|
|
12
|
+
"""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""INI and Java `.properties` -> Markdown.
|
|
2
|
+
|
|
3
|
+
`configparser` is stdlib and handles INI. `.properties` is close but not the
|
|
4
|
+
same — `key=value` with no sections at all — so it is normalised into a single
|
|
5
|
+
implicit section rather than given its own module: they are one format with two
|
|
6
|
+
spellings, and splitting them would give a consumer two things to override to
|
|
7
|
+
change one behaviour.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import configparser
|
|
13
|
+
|
|
14
|
+
# Imports are ABSOLUTE, not relative, and that is what makes this file work in
|
|
15
|
+
# both places it runs: as a package module, and as a consumer copy in
|
|
16
|
+
# `.fux/decoders/` loaded by path. A path-loaded file has no parent package, so
|
|
17
|
+
# `from . import _xml` raises `attempted relative import with no known parent
|
|
18
|
+
# package` — the copy would be dead on arrival. Absolute imports mean the file
|
|
19
|
+
# fux ships and the file you edit are byte-identical (SR-DECODE decision 11).
|
|
20
|
+
from fux_decoder_json.json import _prose
|
|
21
|
+
from fux.constants import fixed
|
|
22
|
+
|
|
23
|
+
#: **The reuse key's handle on this decoder** (W-166). Bump it by hand in the
|
|
24
|
+
#: same change as any edit that can change what `decode()` returns, and the next
|
|
25
|
+
#: `fux ingest` re-extracts the documents bound to THIS decoder and no others.
|
|
26
|
+
#: Leaving it alone is the claim that the edit cannot move a byte of output.
|
|
27
|
+
#: `tests/decode/test_decoder_versions.py` fails on a changed module that did
|
|
28
|
+
#: not bump it. [SR-DECODE](../../../../records/0139_decode.md) decision 11a.
|
|
29
|
+
VERSION = fixed("decoders.ini", "version") # not bumped by W-234: law handles in comments only; not bumped by W-288 (imports moved to decode/ext, same output; USES now carries the extensions' VERSIONs)
|
|
30
|
+
|
|
31
|
+
EXTENSIONS = tuple(fixed("decoders.ini", "extensions"))
|
|
32
|
+
#: The decoder extensions this decoder calls, read by the reuse key (SR-DECODE decision 24).
|
|
33
|
+
USES = tuple(fixed("decoders.ini", "uses"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decode(raw: bytes, rel_path: str) -> str | None:
|
|
37
|
+
text = raw.decode("utf-8-sig", errors="replace")
|
|
38
|
+
parser = configparser.ConfigParser(
|
|
39
|
+
# A `.properties` file is full of duplicate-looking keys and colons;
|
|
40
|
+
# strict mode would reject the file entirely over one repeat, which
|
|
41
|
+
# turns a readable document into a skipped one for no gain.
|
|
42
|
+
strict=False,
|
|
43
|
+
interpolation=None, # `%(x)s` in a value is text here, not a template
|
|
44
|
+
delimiters=("=", ":"),
|
|
45
|
+
allow_no_value=True,
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
parser.read_string(text)
|
|
49
|
+
except (configparser.Error, UnicodeDecodeError):
|
|
50
|
+
# `.properties` has no section header at all, which configparser
|
|
51
|
+
# rejects. Give it the implicit one the format assumes.
|
|
52
|
+
try:
|
|
53
|
+
parser.read_string("[properties]\n" + text)
|
|
54
|
+
except configparser.Error:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
blocks: list[str] = []
|
|
58
|
+
# `parser.sections()` is document order; sorting makes two files that
|
|
59
|
+
# differ only in section order decode identically (L4).
|
|
60
|
+
for section in sorted(parser.sections()):
|
|
61
|
+
blocks.append("## " + section)
|
|
62
|
+
for key in sorted(parser[section]):
|
|
63
|
+
value = parser[section][key]
|
|
64
|
+
text_value = _prose(value) if value is not None else ""
|
|
65
|
+
blocks.append(f"**{key}:** {text_value}" if text_value else f"**{key}**")
|
|
66
|
+
body = "\n\n".join(blocks)
|
|
67
|
+
return body if body.strip() else None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""TOML -> Markdown. Free: `tomllib` is stdlib on the Python L7 already requires.
|
|
2
|
+
|
|
3
|
+
Same shape as `json` and for the same reason — a table name is a heading, a
|
|
4
|
+
string value is prose, a number is not a word. It is a separate module rather
|
|
5
|
+
than a branch inside `json` because the override seam works **by module
|
|
6
|
+
name**: a consumer who wants different TOML handling must be able to take that
|
|
7
|
+
and nothing else.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import tomllib
|
|
13
|
+
from fux.decode.ext.limits import limit
|
|
14
|
+
|
|
15
|
+
# Imports are ABSOLUTE, not relative, and that is what makes this file work in
|
|
16
|
+
# both places it runs: as a package module, and as a consumer copy in
|
|
17
|
+
# `.fux/decoders/` loaded by path. A path-loaded file has no parent package, so
|
|
18
|
+
# `from . import _xml` raises `attempted relative import with no known parent
|
|
19
|
+
# package` — the copy would be dead on arrival. Absolute imports mean the file
|
|
20
|
+
# fux ships and the file you edit are byte-identical (SR-DECODE decision 11).
|
|
21
|
+
from fux_decoder_json.json import _prose
|
|
22
|
+
from fux.constants import fixed
|
|
23
|
+
|
|
24
|
+
#: Markdown has six heading levels; a deeper one is clamped, never invented.
|
|
25
|
+
_MAX_HEADING = fixed("markdown", "max_heading")
|
|
26
|
+
|
|
27
|
+
#: **The reuse key's handle on this decoder** (W-166). Bump it by hand in the
|
|
28
|
+
#: same change as any edit that can change what `decode()` returns, and the next
|
|
29
|
+
#: `fux ingest` re-extracts the documents bound to THIS decoder and no others.
|
|
30
|
+
#: Leaving it alone is the claim that the edit cannot move a byte of output.
|
|
31
|
+
#: `tests/decode/test_decoder_versions.py` fails on a changed module that did
|
|
32
|
+
#: not bump it. [SR-DECODE](../../../../records/0139_decode.md) decision 11a.
|
|
33
|
+
VERSION = fixed("decoders.toml", "version") # not bumped by W-225 4a (caps moved, same values) nor 5b (numerals moved, same output); not bumped by W-288 (imports moved to decode/ext, same output; USES now carries the extensions' VERSIONs)
|
|
34
|
+
|
|
35
|
+
EXTENSIONS = tuple(fixed("decoders.toml", "extensions"))
|
|
36
|
+
#: The decoder extensions this decoder calls, read by the reuse key (SR-DECODE decision 24).
|
|
37
|
+
USES = tuple(fixed("decoders.toml", "uses"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def decode(raw: bytes, rel_path: str) -> str | None:
|
|
41
|
+
try:
|
|
42
|
+
data = tomllib.loads(raw.decode("utf-8-sig"))
|
|
43
|
+
except (UnicodeDecodeError, tomllib.TOMLDecodeError):
|
|
44
|
+
return None
|
|
45
|
+
lines: list[str] = []
|
|
46
|
+
_walk(data, lines, depth=1, label=None)
|
|
47
|
+
body = "\n\n".join(lines)
|
|
48
|
+
return body if body.strip() else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _walk(node, out: list[str], *, depth: int, label: str | None) -> None:
|
|
52
|
+
if depth > limit("json", "max_depth"):
|
|
53
|
+
return
|
|
54
|
+
if isinstance(node, dict):
|
|
55
|
+
if label:
|
|
56
|
+
out.append("#" * min(depth, _MAX_HEADING) + " " + label)
|
|
57
|
+
for key in sorted(node, key=str):
|
|
58
|
+
_walk(node[key], out, depth=depth + 1, label=str(key))
|
|
59
|
+
return
|
|
60
|
+
if isinstance(node, list):
|
|
61
|
+
if label:
|
|
62
|
+
out.append("#" * min(depth, _MAX_HEADING) + " " + label)
|
|
63
|
+
for item in node:
|
|
64
|
+
_walk(item, out, depth=depth + 1, label=None)
|
|
65
|
+
return
|
|
66
|
+
# `tomllib` returns real `datetime` objects for dates, so unlike JSON the
|
|
67
|
+
# timestamp noise never reaches `_prose` as a string — it is dropped here
|
|
68
|
+
# by not being a `str` at all.
|
|
69
|
+
text = _prose(node)
|
|
70
|
+
if text:
|
|
71
|
+
out.append(f"**{label}:** {text}" if label else text)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""YAML -> Markdown. **A deliberate subset, and the subset is the correct
|
|
2
|
+
option rather than the cheap one.**
|
|
3
|
+
|
|
4
|
+
The extra 80 % of the YAML 1.2 spec is **type resolution** — is `NO` the string
|
|
5
|
+
"NO" or the boolean false, is `1:30` a sexagesimal, is `2024-01-01` a date.
|
|
6
|
+
Fux consumes **words**, not types, so none of that changes a single term while
|
|
7
|
+
all of it is L2 surface owned forever.
|
|
8
|
+
|
|
9
|
+
Worse, one full-spec behaviour is **actively wrong here**:
|
|
10
|
+
|
|
11
|
+
* **Anchors and aliases (`&a` / `*a`).** A conformant parser expands an alias
|
|
12
|
+
everywhere it appears, which **duplicates the anchored text and inflates
|
|
13
|
+
`tf`** — a document mentioning a term once would rank as though it mentioned
|
|
14
|
+
it five times. This module reads the anchor's text once and never expands.
|
|
15
|
+
* **Nested aliases** are the billion-laughs bomb in YAML clothing: exponential
|
|
16
|
+
expansion, unbounded memory, from a file sitting in a git repo.
|
|
17
|
+
|
|
18
|
+
So a "full YAML parser" for fux would be a conformant parser that then
|
|
19
|
+
deliberately violates the spec at the one place that matters. The subset is
|
|
20
|
+
what is left when you keep only what carries text: indentation, `key:`,
|
|
21
|
+
`- ` items, quoting, block scalars `|` and `>`, and multi-document `---`.
|
|
22
|
+
|
|
23
|
+
⚠ **`frontmatter.py` is fux's other hand-rolled YAML subset.** Two dialects in
|
|
24
|
+
one codebase is the `_MdParser` defect again, so
|
|
25
|
+
`tests/decode/test_decode.py` asserts the two agree on the shapes both accept.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from fux.decode.ext.limits import limit
|
|
32
|
+
|
|
33
|
+
# Imports are ABSOLUTE, not relative, and that is what makes this file work in
|
|
34
|
+
# both places it runs: as a package module, and as a consumer copy in
|
|
35
|
+
# `.fux/decoders/` loaded by path. A path-loaded file has no parent package, so
|
|
36
|
+
# `from . import _xml` raises `attempted relative import with no known parent
|
|
37
|
+
# package` — the copy would be dead on arrival. Absolute imports mean the file
|
|
38
|
+
# fux ships and the file you edit are byte-identical (SR-DECODE decision 11).
|
|
39
|
+
from fux_decoder_json.json import _label, _prose
|
|
40
|
+
from fux.constants import fixed
|
|
41
|
+
|
|
42
|
+
#: **The reuse key's handle on this decoder** (W-166). Bump it by hand in the
|
|
43
|
+
#: same change as any edit that can change what `decode()` returns, and the next
|
|
44
|
+
#: `fux ingest` re-extracts the documents bound to THIS decoder and no others.
|
|
45
|
+
#: Leaving it alone is the claim that the edit cannot move a byte of output.
|
|
46
|
+
#: `tests/decode/test_decoder_versions.py` fails on a changed module that did
|
|
47
|
+
#: not bump it. [SR-DECODE](../../../../records/0139_decode.md) decision 11a.
|
|
48
|
+
VERSION = fixed("decoders.yaml", "version") # not bumped by W-234: law handles in comments only; W-225 4a (caps moved, same values) nor 5b (numerals moved, same output); not bumped by W-288 (imports moved to decode/ext, same output; USES now carries the extensions' VERSIONs)
|
|
49
|
+
|
|
50
|
+
EXTENSIONS = tuple(fixed("decoders.yaml", "extensions"))
|
|
51
|
+
#: The decoder extensions this decoder calls, read by the reuse key (SR-DECODE decision 24).
|
|
52
|
+
USES = tuple(fixed("decoders.yaml", "uses"))
|
|
53
|
+
|
|
54
|
+
#: ⚠ **`MAX_DEPTH` is `[limits.yaml] max_depth` in .fux/formats.toml** since W-225 stage 4a
|
|
55
|
+
#: (SR-LAW-12 decision 9b): read per call through `limit()`, in the extract-config digest.
|
|
56
|
+
|
|
57
|
+
#: `key:` or `key: value`, capturing indentation. Anchors and tags are stripped
|
|
58
|
+
#: from the value rather than interpreted — `&id001`, `*id001`, `!!str` are
|
|
59
|
+
#: type/graph machinery, and none of them is a word.
|
|
60
|
+
_KEY_RE = re.compile(r"^(?P<indent>\s*)(?P<key>[^\s#][^:]*?)\s*:\s*(?P<value>.*)$")
|
|
61
|
+
_ITEM_RE = re.compile(r"^(?P<indent>\s*)-\s*(?P<value>.*)$")
|
|
62
|
+
_NOISE_PREFIX = re.compile(r"^(?:[&*][^\s]+|![^\s]*)\s*")
|
|
63
|
+
_BLOCK_SCALAR = re.compile(r"^[|>][-+]?\d*$")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def decode(raw: bytes, rel_path: str) -> str | None:
|
|
67
|
+
text = raw.decode("utf-8-sig", errors="replace")
|
|
68
|
+
out: list[str] = []
|
|
69
|
+
block_indent: int | None = None
|
|
70
|
+
block_lines: list[str] = []
|
|
71
|
+
block_lines_indent: list[int | None] = [None]
|
|
72
|
+
|
|
73
|
+
for line in text.splitlines():
|
|
74
|
+
if block_indent is not None:
|
|
75
|
+
# Inside a block scalar: content is literal text until the
|
|
76
|
+
# indentation drops back. This is the one place YAML carries real
|
|
77
|
+
# prose in bulk — a description, a script, a runbook step.
|
|
78
|
+
if not line.strip() or _indent(line) > block_indent:
|
|
79
|
+
# Dedent by the block's OWN first-line indentation, not by the
|
|
80
|
+
# key's + 1. YAML lets a block be indented any amount past its
|
|
81
|
+
# key, so a fixed guess leaves a ragged leading space on every
|
|
82
|
+
# line — invisible in a diff and present in every term's context.
|
|
83
|
+
if block_lines_indent[0] is None and line.strip():
|
|
84
|
+
block_lines_indent[0] = _indent(line)
|
|
85
|
+
strip = block_lines_indent[0] or 0
|
|
86
|
+
block_lines.append(line[strip:] if line.strip() else "")
|
|
87
|
+
continue
|
|
88
|
+
_flush_block(block_lines, out)
|
|
89
|
+
block_indent, block_lines = None, []
|
|
90
|
+
block_lines_indent[0] = None
|
|
91
|
+
|
|
92
|
+
stripped = line.strip()
|
|
93
|
+
if not stripped or stripped.startswith("#"):
|
|
94
|
+
continue
|
|
95
|
+
if stripped in ("---", "..."):
|
|
96
|
+
continue # a document boundary; every document in the file is indexed
|
|
97
|
+
|
|
98
|
+
item = _ITEM_RE.match(line)
|
|
99
|
+
if item and not _KEY_RE.match(item.group("value")):
|
|
100
|
+
value = _clean(item.group("value"))
|
|
101
|
+
if value:
|
|
102
|
+
out.append("- " + value)
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
match = _KEY_RE.match(line)
|
|
106
|
+
if not match:
|
|
107
|
+
value = _clean(stripped)
|
|
108
|
+
if value:
|
|
109
|
+
out.append(value)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
key = _clean(match.group("key")).strip("\"'")
|
|
113
|
+
value = match.group("value").strip()
|
|
114
|
+
if _BLOCK_SCALAR.match(value):
|
|
115
|
+
if key:
|
|
116
|
+
out.append(f"**{key}:**")
|
|
117
|
+
block_indent = _indent(line)
|
|
118
|
+
block_lines = []
|
|
119
|
+
block_lines_indent[0] = None
|
|
120
|
+
continue
|
|
121
|
+
value = _clean(value)
|
|
122
|
+
depth = _indent(line) // limit("yaml", "indent_step") + 1
|
|
123
|
+
if not value:
|
|
124
|
+
if key:
|
|
125
|
+
out.append(_label(key, min(depth, limit("yaml", "max_depth"))))
|
|
126
|
+
else:
|
|
127
|
+
prose = _prose(value) or value if len(value) >= limit("yaml", "min_value_len") else ""
|
|
128
|
+
if prose:
|
|
129
|
+
out.append(f"**{key}:** {prose}")
|
|
130
|
+
|
|
131
|
+
if block_indent is not None:
|
|
132
|
+
_flush_block(block_lines, out)
|
|
133
|
+
|
|
134
|
+
body = "\n\n".join(o for o in out if o.strip())
|
|
135
|
+
return body if body.strip() else None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _flush_block(lines: list[str], out: list[str]) -> None:
|
|
139
|
+
text = "\n".join(lines).strip("\n")
|
|
140
|
+
if text.strip():
|
|
141
|
+
out.append(text)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _indent(line: str) -> int:
|
|
145
|
+
return len(line) - len(line.lstrip())
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _clean(value: str) -> str:
|
|
149
|
+
"""Strip anchors, aliases, tags, quotes and trailing comments.
|
|
150
|
+
|
|
151
|
+
An alias is dropped rather than resolved — see the module docstring. This
|
|
152
|
+
is the single most consequential line in the file: resolving it would
|
|
153
|
+
inflate `tf` and quietly change every ranking over a YAML corpus.
|
|
154
|
+
"""
|
|
155
|
+
value = _NOISE_PREFIX.sub("", value.strip())
|
|
156
|
+
if value.startswith(("'", '"')) and len(value) > 1:
|
|
157
|
+
quote = value[0]
|
|
158
|
+
end = value.rfind(quote)
|
|
159
|
+
if end > 0:
|
|
160
|
+
return value[1:end]
|
|
161
|
+
hash_at = value.find(" #")
|
|
162
|
+
if hash_at >= 0:
|
|
163
|
+
value = value[:hash_at]
|
|
164
|
+
return value.strip()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fux-decoder-config
|
|
3
|
+
Version: 4.1.1
|
|
4
|
+
Summary: Fux decoder package: configuration files — TOML, YAML, INI.
|
|
5
|
+
Project-URL: Homepage, https://github.com/arpitarya/fux
|
|
6
|
+
Project-URL: Repository, https://github.com/arpitarya/fux
|
|
7
|
+
Author-email: arpit arya <arpitarya.me@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: fux-core==4.1.1
|
|
11
|
+
Requires-Dist: fux-decoder-json==4.1.1
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# fux-decoder-config
|
|
15
|
+
|
|
16
|
+
Part of [fux](https://github.com/arpitarya/fux) — rank your written knowledge
|
|
17
|
+
from a small index committed to git; fetch content from the systems that own
|
|
18
|
+
it; verify freshness at answer time.
|
|
19
|
+
|
|
20
|
+
**`fux-decoder-config` is a fux decoder package: configuration files — TOML, YAML, INI.** Only `fux-engine` is mandatory;
|
|
21
|
+
everything else is declared per repository in the committed
|
|
22
|
+
`.fux/plugins.toml`, pinned to one version and digest, and refused loudly when
|
|
23
|
+
it is missing or differs, so two machines on one repository never build
|
|
24
|
+
different indexes.
|
|
25
|
+
|
|
26
|
+
It reads `.toml` `.yaml` `.yml` `.ini` `.cfg` `.properties`. Install it and declare it in `.fux/plugins.toml` --
|
|
27
|
+
`fux setup` proposes it when your sources hold those files -- or fux does not
|
|
28
|
+
read them, and `fux doctor` says so.
|
|
29
|
+
|
|
30
|
+
It is released on fux's own version, together with every other fux package.
|
|
31
|
+
Documentation: <https://github.com/arpitarya/fux#readme>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
fux_decoder_config/__init__.py,sha256=d5bkwLFQAYfM2332fRkkKg9wlsLIrBqkQkpjnxfOm18,682
|
|
2
|
+
fux_decoder_config/ini.py,sha256=NLgU0snwXHql8lKjioLbdPPBUmH9COZfNKuyFxJJwjw,3309
|
|
3
|
+
fux_decoder_config/toml.py,sha256=muFtUY2t-uH-Gc4EciiLeo5ykGRoEwttNKtf7dz8qhA,3302
|
|
4
|
+
fux_decoder_config/yaml.py,sha256=uuh2mmxWmIXb_zTdM-fvIdRccjO-VU8Gpw4E2PVN_v4,7392
|
|
5
|
+
fux_decoder_config-4.1.1.dist-info/METADATA,sha256=WcnYMdjXqsitkCsOs9by5GleLKF9z_u7KUWX9F9FeH8,1390
|
|
6
|
+
fux_decoder_config-4.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
fux_decoder_config-4.1.1.dist-info/RECORD,,
|