gdk9-cli 0.3.0__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.
Files changed (82) hide show
  1. examples/01_analyze.py +28 -0
  2. examples/02_profile.py +24 -0
  3. examples/03_assign.py +19 -0
  4. examples/04_dcg.py +41 -0
  5. examples/05_tokenize.py +22 -0
  6. examples/06_synthesize.py +20 -0
  7. examples/07_api.py +57 -0
  8. examples/08_kernel.py +35 -0
  9. examples/09_kernel_search_walkthrough.py +102 -0
  10. examples/10_conserve_vs_naive.py +100 -0
  11. gdk9/__init__.py +5 -0
  12. gdk9/ansi.py +57 -0
  13. gdk9/cli.py +1186 -0
  14. gdk9/crdt.py +88 -0
  15. gdk9/crypto.py +132 -0
  16. gdk9/data/ninefold.json +40 -0
  17. gdk9/data/official.example.json +16 -0
  18. gdk9/data/official.json +132 -0
  19. gdk9/data/subs.json +436 -0
  20. gdk9/dcg.py +356 -0
  21. gdk9/egglog_bridge/__init__.py +6 -0
  22. gdk9/egglog_bridge/dr_egglog.py +107 -0
  23. gdk9/egglog_bridge/fallback.py +34 -0
  24. gdk9/energy.py +211 -0
  25. gdk9/errors.py +15 -0
  26. gdk9/fmt.py +212 -0
  27. gdk9/imply.py +83 -0
  28. gdk9/io_utils.py +24 -0
  29. gdk9/kernel/__init__.py +24 -0
  30. gdk9/kernel/engine.py +123 -0
  31. gdk9/kernel/errors.py +17 -0
  32. gdk9/kernel/expression.py +43 -0
  33. gdk9/kernel/principle.py +43 -0
  34. gdk9/kernel/proof.py +24 -0
  35. gdk9/kernel/rule.py +30 -0
  36. gdk9/kernel/symbol.py +19 -0
  37. gdk9/kernel_cli.py +195 -0
  38. gdk9/log.py +30 -0
  39. gdk9/optimize.py +288 -0
  40. gdk9/parser.py +35 -0
  41. gdk9/plugins/__init__.py +9 -0
  42. gdk9/plugins/loader.py +346 -0
  43. gdk9/plugins/registry.py +42 -0
  44. gdk9/principles.py +131 -0
  45. gdk9/saturate.py +89 -0
  46. gdk9/state.py +138 -0
  47. gdk9/subs.py +108 -0
  48. gdk9/tokenize.py +386 -0
  49. gdk9/tui.py +115 -0
  50. gdk9/utilization.py +15 -0
  51. gdk9_cli-0.3.0.dist-info/METADATA +295 -0
  52. gdk9_cli-0.3.0.dist-info/RECORD +82 -0
  53. gdk9_cli-0.3.0.dist-info/WHEEL +5 -0
  54. gdk9_cli-0.3.0.dist-info/entry_points.txt +3 -0
  55. gdk9_cli-0.3.0.dist-info/licenses/LICENSE +661 -0
  56. gdk9_cli-0.3.0.dist-info/top_level.txt +4 -0
  57. scripts/batch_analyze.py +109 -0
  58. scripts/export_json.py +125 -0
  59. scripts/make_zip.py +57 -0
  60. scripts/profile_compare.py +165 -0
  61. scripts/watch_file.py +124 -0
  62. tests/conftest.py +26 -0
  63. tests/experiment/__init__.py +1 -0
  64. tests/experiment/test_conserve_vs_naive.py +287 -0
  65. tests/kernel/test_kernel_energy.py +18 -0
  66. tests/kernel/test_kernel_rules.py +58 -0
  67. tests/kernel/test_kernel_search_bound.py +34 -0
  68. tests/test_crypto.py +36 -0
  69. tests/test_crypto_secure.py +26 -0
  70. tests/test_dcg.py +318 -0
  71. tests/test_dcg_golden.py +67 -0
  72. tests/test_egglog_bridge.py +41 -0
  73. tests/test_energy.py +34 -0
  74. tests/test_kernel_cli_handbook.py +101 -0
  75. tests/test_optimize.py +21 -0
  76. tests/test_plugins.py +64 -0
  77. tests/test_rules.py +43 -0
  78. tests/test_rules_commit.py +40 -0
  79. tests/test_rules_reversibility.py +50 -0
  80. tests/test_state_crdt.py +87 -0
  81. tests/test_tokenize.py +38 -0
  82. tests/test_tokenize_metrics.py +33 -0
examples/01_analyze.py ADDED
@@ -0,0 +1,28 @@
1
+ """Example 01 — analyze: break text into energy units.
2
+
3
+ gdk9 analyze decomposes text from document → paragraph → sentence → word,
4
+ reporting the A1Z26 digital-root energy at each level.
5
+ """
6
+ import subprocess, sys, os
7
+
8
+ _ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
9
+
10
+ def run(*args):
11
+ result = subprocess.run(
12
+ [sys.executable, "-m", "gdk9.cli", *args],
13
+ capture_output=True, text=True,
14
+ cwd=_ROOT,
15
+ )
16
+ print(result.stdout or result.stderr)
17
+
18
+ # Basic analysis
19
+ print("=== analyze a phrase ===")
20
+ run("--color", "analyze", "Signal 123 ehdxkcit 3245768")
21
+
22
+ # Table format
23
+ print("=== analyze — table format ===")
24
+ run("--color", "analyze", "FWEM dark swan protocol", "--format", "table")
25
+
26
+ # Compare two phrases
27
+ print("=== compare two strings ===")
28
+ run("--color", "compare", "dark swan", "dark swan protocol")
examples/02_profile.py ADDED
@@ -0,0 +1,24 @@
1
+ """Example 02 — profile: energy histogram and DR distribution.
2
+
3
+ profile shows how energy is distributed across digital roots 1-9,
4
+ rendered as a coloured block-bar chart.
5
+ """
6
+ import subprocess, sys
7
+
8
+ def run(*args):
9
+ result = subprocess.run(
10
+ [sys.executable, "-m", "gdk9.cli", *args],
11
+ capture_output=True, text=True,
12
+ )
13
+ print(result.stdout or result.stderr)
14
+
15
+ texts = [
16
+ "The quick brown fox jumps over the lazy dog",
17
+ "FWEM",
18
+ "dark swan protocol identity",
19
+ ]
20
+
21
+ for text in texts:
22
+ print(f'=== profile: {text!r} ===')
23
+ run("--color", "profile", text)
24
+ print()
examples/03_assign.py ADDED
@@ -0,0 +1,19 @@
1
+ """Example 03 — assign: per-character energy breakdown.
2
+
3
+ assign lists every character with its individual energy value and DR,
4
+ useful for auditing which letters drive the total energy of a word.
5
+ """
6
+ import subprocess, sys
7
+
8
+ def run(*args):
9
+ result = subprocess.run(
10
+ [sys.executable, "-m", "gdk9.cli", *args],
11
+ capture_output=True, text=True,
12
+ )
13
+ print(result.stdout or result.stderr)
14
+
15
+ print("=== assign: FWEM ===")
16
+ run("--color", "assign", "FWEM")
17
+
18
+ print("=== assign: dark swan ===")
19
+ run("--color", "assign", "dark swan")
examples/04_dcg.py ADDED
@@ -0,0 +1,41 @@
1
+ """Example 04 — DCG: Directed Cognition Graph.
2
+
3
+ The DCG models letters as nodes in a directed graph with three edge types:
4
+ DC↔AC uppercase ↔ lowercase (cost 0.5 / 2.0)
5
+ within all-pairs within symmetry class (cost 1.0)
6
+ cross energy cycle asym → biph → idemp → invol → asym (cost 2.0)
7
+
8
+ Demonstrates: classify, path, homotopy, shortest, vector, info.
9
+ """
10
+ import subprocess, sys
11
+
12
+ def run(*args):
13
+ result = subprocess.run(
14
+ [sys.executable, "-m", "gdk9.cli", *args],
15
+ capture_output=True, text=True,
16
+ )
17
+ print(result.stdout or result.stderr)
18
+
19
+ # Symmetry class of individual letters
20
+ print("=== dcg classify ===")
21
+ run("--color", "dcg", "classify", "FWEM")
22
+
23
+ # Full word path — symmetry class + energy per letter
24
+ print("=== dcg path: FWEM ===")
25
+ run("--color", "dcg", "path", "FWEM")
26
+
27
+ # Shortest path between two nodes
28
+ print("=== dcg shortest: F → M ===")
29
+ run("--color", "dcg", "shortest", "F", "M")
30
+
31
+ # Homotopy equivalence: same letters reordered
32
+ print("=== dcg homotopy: FWEM vs MWEF ===")
33
+ run("--color", "dcg", "homotopy", "FWEM", "MWEF")
34
+
35
+ # 4D vector encoding
36
+ print("=== dcg vector: FWEM ===")
37
+ run("--color", "dcg", "vector", "FWEM")
38
+
39
+ # Graph info
40
+ print("=== dcg info ===")
41
+ run("--color", "dcg", "info")
@@ -0,0 +1,22 @@
1
+ """Example 05 — tokenize: split text by symbolic energy delimiters.
2
+
3
+ Delimiters are characters whose symbol-energy equals the requested level.
4
+ With -F table the output includes per-token DR, energy totals, and a
5
+ metrics footer (DR histogram, top tokens, class breakdown).
6
+ """
7
+ import subprocess, sys
8
+
9
+ def run(*args):
10
+ result = subprocess.run(
11
+ [sys.executable, "-m", "gdk9.cli", *args],
12
+ capture_output=True, text=True,
13
+ )
14
+ print(result.stdout or result.stderr)
15
+
16
+ text = "alpha|beta:gamma/delta=epsilon"
17
+
18
+ print("=== tokenize at energy-1 delimiters (table) ===")
19
+ run("--color", "tokenize", text, "--energy", "1", "-F", "table")
20
+
21
+ print("=== tokenize JSON payload ===")
22
+ run("tokenize", "FWEM dark-swan protocol", "-F", "json")
@@ -0,0 +1,20 @@
1
+ """Example 06 — synthesize: build a sigil from an energy profile.
2
+
3
+ synthesize takes a word and renders a bordered sigil with per-letter
4
+ DR coloring, the total SymPhi energy, and the digital root.
5
+ """
6
+ import subprocess, sys
7
+
8
+ def run(*args):
9
+ result = subprocess.run(
10
+ [sys.executable, "-m", "gdk9.cli", *args],
11
+ capture_output=True, text=True,
12
+ )
13
+ print(result.stdout or result.stderr)
14
+
15
+ words = ["FWEM", "dark", "swan", "protocol"]
16
+
17
+ for word in words:
18
+ print(f"=== synthesize: {word} ===")
19
+ run("--color", "synthesize", word)
20
+ print()
examples/07_api.py ADDED
@@ -0,0 +1,57 @@
1
+ """Example 07 — Python API: use gdk9 modules directly.
2
+
3
+ All CLI commands are thin wrappers around importable Python functions.
4
+ This example exercises the core API without subprocess.
5
+ """
6
+ import sys, os
7
+ # Allow running from the examples/ directory
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
9
+
10
+ from gdk9.energy import string_energy, char_energy
11
+ from gdk9.dcg import (
12
+ get_dcg, symmetry_class, sym_energy,
13
+ word_sym_energy, homotopy_equivalent,
14
+ )
15
+ from gdk9.fmt import fmt_class, fmt_dr, fmt_float_e, Box, section, kv
16
+ from gdk9.principles import Principle
17
+
18
+ principle = Principle.default()
19
+
20
+ # ── Energy model ──────────────────────────────────────────────────────────────
21
+ print(section("energy model", enabled=False))
22
+
23
+ for word in ["FWEM", "dark", "swan"]:
24
+ total, dr = string_energy(word, principle)
25
+ print(f' {word:<10} total={total:>6} dr={dr}')
26
+
27
+ # ── DCG ───────────────────────────────────────────────────────────────────────
28
+ print()
29
+ print(section("dcg", enabled=False))
30
+
31
+ g = get_dcg()
32
+ print(f' nodes={g.node_count()} edges={g.edge_count()}')
33
+
34
+ for ch in "FWEM":
35
+ cls = symmetry_class(ch)
36
+ e = sym_energy(ch)
37
+ print(f' {ch} {cls:<12} e={e:.4f}')
38
+
39
+ print()
40
+ path = g.shortest_path("F", "M")
41
+ print(f' shortest F→M: {" → ".join(path)}')
42
+
43
+ equiv, detail = homotopy_equivalent("FWEM", "MWEF", tol=1.0)
44
+ print(f' homotopy FWEM≅MWEF: {equiv} '
45
+ f'(Δe={detail["energy_diff"]:.4f} dist={detail["vector_distance"]:.4f})')
46
+
47
+ # ── fmt helpers ───────────────────────────────────────────────────────────────
48
+ print()
49
+ print(section("fmt — Box table", enabled=False))
50
+
51
+ box = Box(["char", "class", "energy", "dr"], col_align=["c", "l", "r", "c"], enabled=False)
52
+ for ch in "FWEM":
53
+ e = sym_energy(ch)
54
+ cls = symmetry_class(ch)
55
+ _, dr = string_energy(ch, principle)
56
+ box.row([ch, cls, f"{e:.4f}", str(dr)])
57
+ print(box.render())
examples/08_kernel.py ADDED
@@ -0,0 +1,35 @@
1
+ """Example 08 — Pure kernel smoke: evaluate, fuse, and bounded search.
2
+
3
+ The kernel package has no CLI/state/plugin I/O. This script uses the library
4
+ surface (and optionally the thin ``gdk9 kernel`` CLI adapter) for research.
5
+ """
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
11
+
12
+ from gdk9.kernel import Expression, ImplicationEngine, KernelPrinciple
13
+ from gdk9.kernel.engine import fusion_rule
14
+ from gdk9.kernel.symbol import Symbol
15
+ from gdk9.kernel_cli import expression_payload, judgment_payload, proof_steps_payload, principle_to_kernel
16
+ from gdk9.principles import Principle
17
+
18
+ principle = Principle.default()
19
+ kp = principle_to_kernel(principle)
20
+ engine = ImplicationEngine(kp, (fusion_rule(),))
21
+
22
+ # Evaluate
23
+ expr = Expression.from_text("ABC", kp)
24
+ total, root = engine.evaluate(expr)
25
+ print(json.dumps({"eval": {"expression": expression_payload(expr), "total": total, "digital_root": root}}, indent=2))
26
+
27
+ # Apply fuse
28
+ source = Expression.from_names(["A", "B"], kp)
29
+ judgment = engine.apply("fuse", source)
30
+ print(json.dumps({"apply": judgment_payload(judgment)}, indent=2))
31
+
32
+ # Bounded search A,B -> AB
33
+ target = Expression((Symbol("AB", source.total_energy()),))
34
+ path = engine.infer(source, target, max_depth=2)
35
+ print(json.dumps({"search": {"found": path is not None, "steps": proof_steps_payload(path or ())}}, indent=2))
@@ -0,0 +1,102 @@
1
+ """Example 09 — Kernel search walkthrough (handbook voice).
2
+
3
+ Three short scenes. Each conserves energy. Each can fail if the depth bound
4
+ is too tight or the target energy does not match — that failure is the point.
5
+
6
+ Run::
7
+
8
+ python examples/09_kernel_search_walkthrough.py
9
+
10
+ Or mirror with the CLI::
11
+
12
+ gdk9 kernel eval A B C
13
+ gdk9 kernel apply fuse A B
14
+ gdk9 kernel search A B --target AB --max-depth 2 --rules fuse
15
+ gdk9 kernel apply split AB --parts A,B --energies 1,2
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+
23
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
24
+
25
+ from gdk9.kernel import Expression, ImplicationEngine
26
+ from gdk9.kernel.engine import fusion_rule, split_rule
27
+ from gdk9.kernel.symbol import Symbol
28
+ from gdk9.kernel_cli import (
29
+ judgment_payload,
30
+ principle_to_kernel,
31
+ proof_steps_payload,
32
+ )
33
+ from gdk9.principles import Principle
34
+
35
+
36
+ def scene(title: str, payload: dict) -> None:
37
+ print(f"\n## {title}")
38
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
39
+
40
+
41
+ def main() -> int:
42
+ kp = principle_to_kernel(Principle.default())
43
+ fuse = fusion_rule()
44
+ a_e = Expression.from_names(["A"], kp).total_energy()
45
+ b_e = Expression.from_names(["B"], kp).total_energy()
46
+ split = split_rule(("A", "B"), (a_e, b_e))
47
+
48
+ # Scene 1 — weigh the letters (eval)
49
+ eng = ImplicationEngine(kp, (fuse,))
50
+ abc = Expression.from_names(["A", "B", "C"], kp)
51
+ total, root = eng.evaluate(abc)
52
+ scene(
53
+ "1. Weigh — eval ABC",
54
+ {
55
+ "names": list(abc.names()),
56
+ "total_energy": total,
57
+ "digital_root": root,
58
+ "note": "A=1, B=2, C=3 under the default principle; sum=6, DR=6.",
59
+ },
60
+ )
61
+
62
+ # Scene 2 — one conserved fuse, then find it by bounded search
63
+ src = Expression.from_names(["A", "B"], kp)
64
+ judgment = eng.apply("fuse", src)
65
+ target = Expression((Symbol("AB", src.total_energy()),))
66
+ path = eng.infer(src, target, max_depth=2)
67
+ scene(
68
+ "2. Fuse — then search A,B → AB (max_depth=2)",
69
+ {
70
+ "apply": judgment_payload(judgment),
71
+ "search_found": path is not None,
72
+ "steps": proof_steps_payload(path or ()),
73
+ "note": "Search is not magic: it is bounded BFS over conserving rules.",
74
+ },
75
+ )
76
+
77
+ # Scene 3 — reverse the arrow (split) with matching energies
78
+ eng2 = ImplicationEngine(kp, (fuse, split))
79
+ ab = judgment.after
80
+ back = eng2.apply("split", ab)
81
+ scene(
82
+ "3. Split — AB → A,B with matching energies (conserved)",
83
+ {
84
+ "judgment": judgment_payload(back),
85
+ "note": "Split needs explicit part energies that sum to the whole.",
86
+ },
87
+ )
88
+
89
+ # Scene 4 — deliberate miss (prove it can fail)
90
+ miss = eng.infer(src, Expression((Symbol("ZZ", 99.0),)), max_depth=2)
91
+ scene(
92
+ "4. Miss — impossible target (energy 99) within depth 2",
93
+ {
94
+ "found": miss is not None,
95
+ "note": "A honest research tool must be able to say no.",
96
+ },
97
+ )
98
+ return 0
99
+
100
+
101
+ if __name__ == "__main__":
102
+ raise SystemExit(main())
@@ -0,0 +1,100 @@
1
+ """Example 10 — Conserved search vs naive name-join/split (research move 4).
2
+
3
+ Prints a short JSON summary. Full fail bar lives in pytest::
4
+
5
+ python -m pytest -q tests/experiment/test_conserve_vs_naive.py
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
14
+
15
+ from gdk9.kernel import Expression, ImplicationEngine, KernelPrinciple
16
+ from gdk9.kernel.engine import fusion_rule, split_rule
17
+ from gdk9.kernel.symbol import Symbol
18
+ from tests.experiment.test_conserve_vs_naive import (
19
+ MISMATCHED_AB_ENERGY,
20
+ MISMATCHED_A_ENERGY,
21
+ MISMATCHED_B_ENERGY,
22
+ conserved_kernel_proof,
23
+ naive_join_search,
24
+ naive_split_search,
25
+ )
26
+
27
+
28
+ def main() -> int:
29
+ principle = KernelPrinciple.default()
30
+ ea = principle.energy_of("A")
31
+ eb = principle.energy_of("B")
32
+ source = Expression.from_names(["A", "B"], principle)
33
+ conserved_target = Expression((Symbol("AB", source.total_energy()),))
34
+ mismatched = Expression((Symbol("AB", MISMATCHED_AB_ENERGY),))
35
+ fuse_engine = ImplicationEngine(principle, (fusion_rule(),))
36
+
37
+ ab_source = Expression((Symbol("AB", ea + eb),))
38
+ split_target = Expression.from_names(["A", "B"], principle)
39
+ split_mismatched = Expression(
40
+ (Symbol("A", MISMATCHED_A_ENERGY), Symbol("B", MISMATCHED_B_ENERGY))
41
+ )
42
+ split_engine = ImplicationEngine(principle, (split_rule(("A", "B"), (ea, eb)),))
43
+
44
+ conserved_path = conserved_kernel_proof(fuse_engine, source, conserved_target, max_depth=2)
45
+ mismatch_kernel = conserved_kernel_proof(fuse_engine, source, mismatched, max_depth=2)
46
+ naive_ok = naive_join_search(source.names(), ("AB",), max_depth=2)
47
+
48
+ split_path = conserved_kernel_proof(split_engine, ab_source, split_target, max_depth=2)
49
+ split_mismatch_kernel = conserved_kernel_proof(
50
+ split_engine, ab_source, split_mismatched, max_depth=2
51
+ )
52
+ naive_split_ok = naive_split_search(ab_source.names(), ("A", "B"), max_depth=2)
53
+
54
+ summary = {
55
+ "experiment": "conserve_vs_naive",
56
+ "approach": "A",
57
+ "move": 4,
58
+ "claim": "beats naive join/split on proof validity under conservation",
59
+ "source_names": list(source.names()),
60
+ "source_energy": source.total_energy(),
61
+ "conserved_ab": {
62
+ "kernel_found": conserved_path is not None,
63
+ "steps": len(conserved_path or ()),
64
+ "all_conserved": bool(
65
+ conserved_path
66
+ and all(s.judgment.conserved for s in conserved_path)
67
+ ),
68
+ },
69
+ "mismatched_ab_energy": MISMATCHED_AB_ENERGY,
70
+ "mismatched_ab": {
71
+ "naive_found": naive_ok is not None,
72
+ "kernel_found": mismatch_kernel is not None,
73
+ },
74
+ "conserved_split": {
75
+ "kernel_found": split_path is not None,
76
+ "steps": len(split_path or ()),
77
+ "all_conserved": bool(
78
+ split_path and all(s.judgment.conserved for s in split_path)
79
+ ),
80
+ },
81
+ "mismatched_split": {
82
+ "naive_found": naive_split_ok is not None,
83
+ "kernel_found": split_mismatch_kernel is not None,
84
+ "part_energies": [MISMATCHED_A_ENERGY, MISMATCHED_B_ENERGY],
85
+ },
86
+ "validity_beat_fuse": naive_ok is not None and mismatch_kernel is None,
87
+ "validity_beat_split": naive_split_ok is not None and split_mismatch_kernel is None,
88
+ }
89
+ print(json.dumps(summary, indent=2))
90
+ ok = (
91
+ summary["validity_beat_fuse"]
92
+ and summary["validity_beat_split"]
93
+ and summary["conserved_ab"]["kernel_found"]
94
+ and summary["conserved_split"]["kernel_found"]
95
+ )
96
+ return 0 if ok else 1
97
+
98
+
99
+ if __name__ == "__main__":
100
+ raise SystemExit(main())
gdk9/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ __all__ = [
2
+ "__version__",
3
+ ]
4
+
5
+ __version__ = "0.3.0"
gdk9/ansi.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import sys
6
+
7
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
8
+
9
+ _CODES: dict[str, str] = {
10
+ # styles
11
+ "bold": "\033[1m",
12
+ "dim": "\033[2m",
13
+ "italic": "\033[3m",
14
+ "underline": "\033[4m",
15
+ "reverse": "\033[7m",
16
+ # standard colours
17
+ "red": "\033[31m",
18
+ "green": "\033[32m",
19
+ "yellow": "\033[33m",
20
+ "blue": "\033[34m",
21
+ "magenta": "\033[35m",
22
+ "cyan": "\033[36m",
23
+ "white": "\033[37m",
24
+ # bright variants
25
+ "bright_black": "\033[90m",
26
+ "bright_red": "\033[91m",
27
+ "bright_green": "\033[92m",
28
+ "bright_yellow": "\033[93m",
29
+ "bright_blue": "\033[94m",
30
+ "bright_magenta": "\033[95m",
31
+ "bright_cyan": "\033[96m",
32
+ "bright_white": "\033[97m",
33
+ }
34
+
35
+ _RESET = "\033[0m"
36
+
37
+
38
+ def supports_color() -> bool:
39
+ if os.getenv("NO_COLOR") == "1":
40
+ return False
41
+ term = os.getenv("TERM", "")
42
+ if term in ("dumb", ""):
43
+ return False
44
+ return sys.stdout.isatty()
45
+
46
+
47
+ def colorize(text: str, color: str | None, enabled: bool) -> str:
48
+ if not enabled or not color:
49
+ return text
50
+ code = _CODES.get(color, "")
51
+ return f"{code}{text}{_RESET}" if code else text
52
+
53
+
54
+ def strip_ansi(s: str) -> str:
55
+ """Remove all ANSI escape codes from *s*."""
56
+ return _ANSI_RE.sub("", s)
57
+