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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any