yaqpy 0.6.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.
Files changed (130) hide show
  1. yaqpy-0.6.0/LICENSE +21 -0
  2. yaqpy-0.6.0/NOTICE +17 -0
  3. yaqpy-0.6.0/PKG-INFO +165 -0
  4. yaqpy-0.6.0/README.md +132 -0
  5. yaqpy-0.6.0/pyproject.toml +94 -0
  6. yaqpy-0.6.0/src/yaqpy/__init__.py +40 -0
  7. yaqpy-0.6.0/src/yaqpy/__main__.py +8 -0
  8. yaqpy-0.6.0/src/yaqpy/api.py +189 -0
  9. yaqpy-0.6.0/src/yaqpy/app/__init__.py +11 -0
  10. yaqpy-0.6.0/src/yaqpy/app/dto.py +58 -0
  11. yaqpy-0.6.0/src/yaqpy/app/examples.py +142 -0
  12. yaqpy-0.6.0/src/yaqpy/app/local.py +57 -0
  13. yaqpy-0.6.0/src/yaqpy/app/ports.py +75 -0
  14. yaqpy-0.6.0/src/yaqpy/app/printer.py +156 -0
  15. yaqpy-0.6.0/src/yaqpy/app/recipe_service.py +181 -0
  16. yaqpy-0.6.0/src/yaqpy/app/recipe_text.py +89 -0
  17. yaqpy-0.6.0/src/yaqpy/app/selfdoc.py +357 -0
  18. yaqpy-0.6.0/src/yaqpy/app/service.py +274 -0
  19. yaqpy-0.6.0/src/yaqpy/cli/__init__.py +5 -0
  20. yaqpy-0.6.0/src/yaqpy/cli/args.py +189 -0
  21. yaqpy-0.6.0/src/yaqpy/cli/describe_cli.py +40 -0
  22. yaqpy-0.6.0/src/yaqpy/cli/main.py +167 -0
  23. yaqpy-0.6.0/src/yaqpy/cli/parser.py +252 -0
  24. yaqpy-0.6.0/src/yaqpy/cli/recipe_cli.py +164 -0
  25. yaqpy-0.6.0/src/yaqpy/core/__init__.py +1 -0
  26. yaqpy-0.6.0/src/yaqpy/core/engine/__init__.py +7 -0
  27. yaqpy-0.6.0/src/yaqpy/core/engine/context.py +90 -0
  28. yaqpy-0.6.0/src/yaqpy/core/engine/helpers.py +149 -0
  29. yaqpy-0.6.0/src/yaqpy/core/engine/limits.py +33 -0
  30. yaqpy-0.6.0/src/yaqpy/core/engine/navigator.py +62 -0
  31. yaqpy-0.6.0/src/yaqpy/core/lang/__init__.py +11 -0
  32. yaqpy-0.6.0/src/yaqpy/core/lang/ast.py +92 -0
  33. yaqpy-0.6.0/src/yaqpy/core/lang/lex_rules.py +476 -0
  34. yaqpy-0.6.0/src/yaqpy/core/lang/lexer.py +116 -0
  35. yaqpy-0.6.0/src/yaqpy/core/lang/parser.py +83 -0
  36. yaqpy-0.6.0/src/yaqpy/core/lang/postfix.py +93 -0
  37. yaqpy-0.6.0/src/yaqpy/core/lang/prefs.py +85 -0
  38. yaqpy-0.6.0/src/yaqpy/core/lang/specs.py +143 -0
  39. yaqpy-0.6.0/src/yaqpy/core/lang/tokens.py +41 -0
  40. yaqpy-0.6.0/src/yaqpy/core/model/__init__.py +6 -0
  41. yaqpy-0.6.0/src/yaqpy/core/model/convert.py +85 -0
  42. yaqpy-0.6.0/src/yaqpy/core/model/datetime_util.py +573 -0
  43. yaqpy-0.6.0/src/yaqpy/core/model/depth.py +19 -0
  44. yaqpy-0.6.0/src/yaqpy/core/model/leading.py +38 -0
  45. yaqpy-0.6.0/src/yaqpy/core/model/node.py +444 -0
  46. yaqpy-0.6.0/src/yaqpy/core/model/tags.py +126 -0
  47. yaqpy-0.6.0/src/yaqpy/core/operators/__init__.py +32 -0
  48. yaqpy-0.6.0/src/yaqpy/core/operators/anchors.py +189 -0
  49. yaqpy-0.6.0/src/yaqpy/core/operators/arithmetic.py +307 -0
  50. yaqpy-0.6.0/src/yaqpy/core/operators/assign.py +52 -0
  51. yaqpy-0.6.0/src/yaqpy/core/operators/basic.py +124 -0
  52. yaqpy-0.6.0/src/yaqpy/core/operators/codecs.py +203 -0
  53. yaqpy-0.6.0/src/yaqpy/core/operators/collections.py +540 -0
  54. yaqpy-0.6.0/src/yaqpy/core/operators/datetime_ops.py +157 -0
  55. yaqpy-0.6.0/src/yaqpy/core/operators/documents.py +17 -0
  56. yaqpy-0.6.0/src/yaqpy/core/operators/logic.py +230 -0
  57. yaqpy-0.6.0/src/yaqpy/core/operators/meta.py +220 -0
  58. yaqpy-0.6.0/src/yaqpy/core/operators/multiply.py +140 -0
  59. yaqpy-0.6.0/src/yaqpy/core/operators/prune.py +55 -0
  60. yaqpy-0.6.0/src/yaqpy/core/operators/regex.py +220 -0
  61. yaqpy-0.6.0/src/yaqpy/core/operators/registry.py +91 -0
  62. yaqpy-0.6.0/src/yaqpy/core/operators/schema.py +206 -0
  63. yaqpy-0.6.0/src/yaqpy/core/operators/sequences.py +252 -0
  64. yaqpy-0.6.0/src/yaqpy/core/operators/slice.py +45 -0
  65. yaqpy-0.6.0/src/yaqpy/core/operators/strings.py +359 -0
  66. yaqpy-0.6.0/src/yaqpy/core/operators/structure.py +199 -0
  67. yaqpy-0.6.0/src/yaqpy/core/operators/traverse.py +205 -0
  68. yaqpy-0.6.0/src/yaqpy/errors.py +144 -0
  69. yaqpy-0.6.0/src/yaqpy/formats/__init__.py +5 -0
  70. yaqpy-0.6.0/src/yaqpy/formats/base.py +30 -0
  71. yaqpy-0.6.0/src/yaqpy/formats/csv_codec.py +294 -0
  72. yaqpy-0.6.0/src/yaqpy/formats/json_codec.py +207 -0
  73. yaqpy-0.6.0/src/yaqpy/formats/props_codec.py +402 -0
  74. yaqpy-0.6.0/src/yaqpy/formats/registry.py +205 -0
  75. yaqpy-0.6.0/src/yaqpy/formats/sniff.py +89 -0
  76. yaqpy-0.6.0/src/yaqpy/formats/toml_codec.py +804 -0
  77. yaqpy-0.6.0/src/yaqpy/formats/toon_codec.py +829 -0
  78. yaqpy-0.6.0/src/yaqpy/formats/xml_codec.py +601 -0
  79. yaqpy-0.6.0/src/yaqpy/formats/xml_tokens.py +480 -0
  80. yaqpy-0.6.0/src/yaqpy/formats/yaml/__init__.py +8 -0
  81. yaqpy-0.6.0/src/yaqpy/formats/yaml/codec.py +144 -0
  82. yaqpy-0.6.0/src/yaqpy/formats/yaml/emitter.py +404 -0
  83. yaqpy-0.6.0/src/yaqpy/formats/yaml/parser.py +1335 -0
  84. yaqpy-0.6.0/src/yaqpy/formats/yaml/resolver.py +31 -0
  85. yaqpy-0.6.0/src/yaqpy/gui/__init__.py +12 -0
  86. yaqpy-0.6.0/src/yaqpy/gui/_di.py +60 -0
  87. yaqpy-0.6.0/src/yaqpy/gui/_prefs.py +38 -0
  88. yaqpy-0.6.0/src/yaqpy/gui/_run.py +180 -0
  89. yaqpy-0.6.0/src/yaqpy/gui/_upload.py +145 -0
  90. yaqpy-0.6.0/src/yaqpy/gui/_web.py +126 -0
  91. yaqpy-0.6.0/src/yaqpy/gui/app.py +190 -0
  92. yaqpy-0.6.0/src/yaqpy/gui/assets/web/favicon.png +0 -0
  93. yaqpy-0.6.0/src/yaqpy/gui/assets/web/icons/loading-animation.png +0 -0
  94. yaqpy-0.6.0/src/yaqpy/gui/assets/yaqpy-logo.ico +0 -0
  95. yaqpy-0.6.0/src/yaqpy/gui/errors_ja.py +96 -0
  96. yaqpy-0.6.0/src/yaqpy/gui/intake.py +92 -0
  97. yaqpy-0.6.0/src/yaqpy/gui/logo.py +21 -0
  98. yaqpy-0.6.0/src/yaqpy/gui/pages/__init__.py +1 -0
  99. yaqpy-0.6.0/src/yaqpy/gui/pages/main_page.py +868 -0
  100. yaqpy-0.6.0/src/yaqpy/gui/pages/settings_page.py +156 -0
  101. yaqpy-0.6.0/src/yaqpy/gui/paths.py +111 -0
  102. yaqpy-0.6.0/src/yaqpy/gui/presenter.py +575 -0
  103. yaqpy-0.6.0/src/yaqpy/gui/state.py +217 -0
  104. yaqpy-0.6.0/src/yaqpy/gui/texts.py +300 -0
  105. yaqpy-0.6.0/src/yaqpy/gui/web_config.py +123 -0
  106. yaqpy-0.6.0/src/yaqpy/options.py +164 -0
  107. yaqpy-0.6.0/src/yaqpy/py.typed +0 -0
  108. yaqpy-0.6.0/src/yaqpy/recipes/__init__.py +17 -0
  109. yaqpy-0.6.0/src/yaqpy/recipes/analysis.py +83 -0
  110. yaqpy-0.6.0/src/yaqpy/recipes/builtin/anthropic-messages-request.schema.json +194 -0
  111. yaqpy-0.6.0/src/yaqpy/recipes/builtin/anthropic-to-gemini.recipe.yaml +229 -0
  112. yaqpy-0.6.0/src/yaqpy/recipes/builtin/anthropic-to-gemini.yaqpy +19 -0
  113. yaqpy-0.6.0/src/yaqpy/recipes/builtin/anthropic-to-openai.recipe.yaml +215 -0
  114. yaqpy-0.6.0/src/yaqpy/recipes/builtin/anthropic-to-openai.yaqpy +21 -0
  115. yaqpy-0.6.0/src/yaqpy/recipes/builtin/gemini-generate-content-request.schema.json +185 -0
  116. yaqpy-0.6.0/src/yaqpy/recipes/builtin/gemini-to-anthropic.recipe.yaml +243 -0
  117. yaqpy-0.6.0/src/yaqpy/recipes/builtin/gemini-to-anthropic.yaqpy +22 -0
  118. yaqpy-0.6.0/src/yaqpy/recipes/builtin/gemini-to-openai.recipe.yaml +239 -0
  119. yaqpy-0.6.0/src/yaqpy/recipes/builtin/gemini-to-openai.yaqpy +28 -0
  120. yaqpy-0.6.0/src/yaqpy/recipes/builtin/openai-chat-request.schema.json +404 -0
  121. yaqpy-0.6.0/src/yaqpy/recipes/builtin/openai-to-anthropic.recipe.yaml +247 -0
  122. yaqpy-0.6.0/src/yaqpy/recipes/builtin/openai-to-anthropic.yaqpy +25 -0
  123. yaqpy-0.6.0/src/yaqpy/recipes/builtin/openai-to-gemini.recipe.yaml +245 -0
  124. yaqpy-0.6.0/src/yaqpy/recipes/builtin/openai-to-gemini.yaqpy +36 -0
  125. yaqpy-0.6.0/src/yaqpy/recipes/catalog.py +63 -0
  126. yaqpy-0.6.0/src/yaqpy/recipes/conform.py +170 -0
  127. yaqpy-0.6.0/src/yaqpy/recipes/diff.py +108 -0
  128. yaqpy-0.6.0/src/yaqpy/recipes/loader.py +158 -0
  129. yaqpy-0.6.0/src/yaqpy/recipes/model.py +58 -0
  130. yaqpy-0.6.0/src/yaqpy/recipes/paths.py +156 -0
yaqpy-0.6.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sgtao
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.
yaqpy-0.6.0/NOTICE ADDED
@@ -0,0 +1,17 @@
1
+ yaqpy
2
+ =====
3
+
4
+ yaqpy is an independent, pure-Python re-implementation of the expression
5
+ language and core behaviour of yq (https://github.com/mikefarah/yq), a Go
6
+ program by Mike Farah and contributors, released under the MIT License.
7
+
8
+ The design of the operator set, the precedence table, the lexer rule order and
9
+ the test scenarios under tests/golden/operators/ were derived from the yq
10
+ source tree (v4.53.6). The extracted scenarios remain subject to the yq MIT
11
+ License:
12
+
13
+ MIT License
14
+ Copyright (c) 2017 Mike Farah
15
+
16
+ No code from yq is compiled into or linked with yaqpy; yaqpy has zero runtime
17
+ dependencies beyond the Python 3.13 standard library.
yaqpy-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: yaqpy
3
+ Version: 0.6.0
4
+ Summary: YAML and more—Query editor in Python: query, update and convert YAML / JSON / XML / CSV / TOML / properties / TOON with the mikefarah/yq expression language, in pure Python (stdlib only)
5
+ Keywords: yaml,json,xml,csv,toml,toon,yq,jq,query,cli,json-schema
6
+ Author: sgtao
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Text Processing :: Markup
20
+ Classifier: Topic :: Utilities
21
+ Classifier: Typing :: Typed
22
+ Requires-Dist: flet>=1.0,<2 ; extra == 'gui'
23
+ Requires-Dist: flet[web]>=1.0,<2 ; extra == 'web'
24
+ Requires-Python: >=3.13
25
+ Project-URL: Homepage, https://github.com/sgtao/yaqpy
26
+ Project-URL: Repository, https://github.com/sgtao/yaqpy
27
+ Project-URL: Documentation, https://github.com/sgtao/yaqpy/blob/main/USAGE.ja.md
28
+ Project-URL: Changelog, https://github.com/sgtao/yaqpy/blob/main/CHANGELOG.md
29
+ Project-URL: Issues, https://github.com/sgtao/yaqpy/issues
30
+ Provides-Extra: gui
31
+ Provides-Extra: web
32
+ Description-Content-Type: text/markdown
33
+
34
+ **English** | [日本語](https://github.com/sgtao/yaqpy/blob/main/README.ja.md)
35
+
36
+ <p align="center">
37
+ <img src="https://raw.githubusercontent.com/sgtao/yaqpy/main/logo.svg" alt="yaqpy — YAML and more, Query editor in Python" width="600">
38
+ </p>
39
+
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/sgtao/yaqpy/blob/main/LICENSE)
41
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue.svg)](https://www.python.org/downloads/)
42
+ [![GitHub release](https://img.shields.io/github/v/release/sgtao/yaqpy.svg)](https://github.com/sgtao/yaqpy/releases)
43
+
44
+ # yaqpy — YAML and more, Query editor in Python
45
+
46
+ A lightweight tool to **query, update and convert** YAML, JSON and more with expressions, from the command line or from Python.
47
+
48
+ - It follows the expression language of the popular CLI tool [mikefarah/yq](https://github.com/mikefarah/yq) (Go, v4.53.6)
49
+ - It is re-implemented with **nothing but the Python standard library**
50
+ - The name stands for **Y**AML **A**nd more, **Q**uery editor in **PY**thon
51
+
52
+ ```console
53
+ $ yaqpy '.server.port' config.yaml
54
+ 8080
55
+ $ yaqpy -i '.server.port = 9090' config.yaml # comments and key order are kept
56
+ ```
57
+
58
+ ## Features
59
+
60
+ - **Keeps your formatting**: comments, key order, anchors (`&` / `*`) and the way numbers and quotes were written (`0x1F`, `1.50`, `'yes'`) survive an update
61
+ - **No dependencies**: all it needs is Python 3.13 or later. The GUI is an optional extra (Flet); the web version adds flet-web
62
+ - **Four ways to use it**: the `yaqpy` command, the Python library (`import yaqpy`), a desktop GUI (`yaqpy-gui`) and the same GUI in a web browser (`yaqpy-web`)
63
+ - **Formats**: YAML, JSON, XML, CSV / TSV, TOML, properties and TOON (a token-saving format for LLMs), both in and out. TOML comments are not kept, so `-i` on TOML is refused by default
64
+ - **Compatible with yq**: 1,091 test scenarios of the Go version are run as compatibility tests (1,047 of the 1,051 comparable ones match). Every operator works except `load` and friends, `eval`, `envsubst`, `system` and `error`
65
+ - **Beyond yq** (not in the Go version):
66
+ - the input format is detected from the content when the file extension does not tell it
67
+ - `yaqpy --schema data.yaml` prints a JSON Schema (Draft 2020-12) of the data, as JSON or YAML
68
+ - `yaqpy --recipe openai-to-gemini request.json` converts request bodies between OpenAI, Gemini and Anthropic, and reports what was dropped, added, or does not fit the target schema (results go to stdout; files are written only with `--apply --out-dir`; no API is called)
69
+ - yaqpy describes itself for people and AI: `--print-spec` (the operators that work and those that do not), `--example`, `--guide-prompt` (a prompt that lets an AI write yaqpy expressions) and `--skill-md` (a Claude Code skill)
70
+ - **Safe defaults**: as a library, operators that read files or environment variables or run commands are all off. The CLI, like the Go version, allows environment variables and file reads (commands stay off). The web version always turns them off
71
+
72
+ ## Installation
73
+
74
+ Python 3.13 or later is required.
75
+
76
+ ```bash
77
+ pip install yaqpy # the yaqpy command and the library (no dependencies)
78
+ pip install "yaqpy[gui]" # + the desktop GUI (adds Flet)
79
+ pip install "yaqpy[web]" # + the GUI in a web browser (adds Flet and flet-web)
80
+ ```
81
+
82
+ With [uv](https://docs.astral.sh/uv/):
83
+
84
+ ```bash
85
+ uv tool install yaqpy # install the yaqpy command
86
+ uv tool install "yaqpy[gui,web]" # ... with what yaqpy-gui and yaqpy-web need
87
+ uvx yaqpy '.server.port' config.yaml # run it once without installing
88
+ uv add yaqpy # use it as a library in a uv project
89
+ ```
90
+
91
+ **From GitHub Releases** (for example, a version that is not on PyPI): pick a version on [Releases](https://github.com/sgtao/yaqpy/releases) and replace `0.6.0` / `v0.6.0` below with it.
92
+
93
+ ```bash
94
+ pip install "yaqpy[gui] @ https://github.com/sgtao/yaqpy/releases/download/v0.6.0/yaqpy-0.6.0-py3-none-any.whl"
95
+ pip install "yaqpy[gui] @ git+https://github.com/sgtao/yaqpy@v0.6.0"
96
+ ```
97
+
98
+ To work on the source, see [DEVELOPMENT.md](https://github.com/sgtao/yaqpy/blob/main/DEVELOPMENT.md) (Japanese).
99
+
100
+ ## Quick start
101
+
102
+ Take this `config.yaml`:
103
+
104
+ ```yaml
105
+ # server settings
106
+ server:
107
+ port: 8080 # dev
108
+ hosts: [a, b]
109
+ items:
110
+ - {name: pen, price: 120}
111
+ - {name: book, price: 980}
112
+ ```
113
+
114
+ **Command line**
115
+
116
+ ```bash
117
+ yaqpy '.server.port' config.yaml # read -> 8080
118
+ yaqpy '.items[] | select(.price > 500) | .name' config.yaml # filter -> book
119
+ yaqpy -i '.server.port = 9090' config.yaml # update (comments and key order are kept)
120
+ yaqpy -o json '.server' config.yaml # convert (yaml / json / xml / csv / tsv / toml / props / toon)
121
+ ```
122
+
123
+ **Python library**
124
+
125
+ ```python
126
+ import yaqpy
127
+
128
+ yaqpy.evaluate(".server.port", "server:\n port: 8080\n") # '8080\n'
129
+ yaqpy.query(".items[] | select(.price > 500)", {"items": [{"price": 1}, {"price": 900}]}) # [{'price': 900}]
130
+ yaqpy.update(".server.port = 9090", {"server": {"port": 8080}}) # {'server': {'port': 9090}}
131
+ ```
132
+
133
+ **GUI**
134
+
135
+ ```bash
136
+ yaqpy-gui # desktop window (or: yaqpy --gui)
137
+ yaqpy-web # the same screens in your browser at http://127.0.0.1:8550/ (or: yaqpy --web)
138
+ yaqpy-web --port 9000 # another port; see yaqpy-web --help for the options
139
+ ```
140
+
141
+ The web version listens on this PC only by default and has no authentication. Files are uploaded from the browser and results come back as downloads.
142
+
143
+ ## Documentation
144
+
145
+ The detailed guides are written in **Japanese**.
146
+
147
+ | Contents | File |
148
+ |---|---|
149
+ | This README in Japanese | [README.ja.md](https://github.com/sgtao/yaqpy/blob/main/README.ja.md) |
150
+ | The command, each format, JSON Schema output, recipes, the library, the operators, and the differences from Go yq | [USAGE.ja.md](https://github.com/sgtao/yaqpy/blob/main/USAGE.ja.md) |
151
+ | The GUI (desktop and web) | [USAGE-GUI.ja.md](https://github.com/sgtao/yaqpy/blob/main/USAGE-GUI.ja.md) |
152
+ | For developers (setup, design, tests, repository layout) | [DEVELOPMENT.md](https://github.com/sgtao/yaqpy/blob/main/DEVELOPMENT.md) |
153
+ | Changes in each version (features, known limitations) | [CHANGELOG.md](https://github.com/sgtao/yaqpy/blob/main/CHANGELOG.md) |
154
+
155
+ `yaqpy --help`, `yaqpy-gui --help` and `yaqpy-web --help` are in English.
156
+
157
+ ## License
158
+
159
+ [MIT License](https://github.com/sgtao/yaqpy/blob/main/LICENSE)
160
+
161
+ The design and the test scenarios draw on Go yq (MIT); see [NOTICE](https://github.com/sgtao/yaqpy/blob/main/NOTICE).
162
+
163
+ ---
164
+
165
+ 🤖 Built with [Claude Code](https://claude.com/claude-code)
yaqpy-0.6.0/README.md ADDED
@@ -0,0 +1,132 @@
1
+ **English** | [日本語](https://github.com/sgtao/yaqpy/blob/main/README.ja.md)
2
+
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/sgtao/yaqpy/main/logo.svg" alt="yaqpy — YAML and more, Query editor in Python" width="600">
5
+ </p>
6
+
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/sgtao/yaqpy/blob/main/LICENSE)
8
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue.svg)](https://www.python.org/downloads/)
9
+ [![GitHub release](https://img.shields.io/github/v/release/sgtao/yaqpy.svg)](https://github.com/sgtao/yaqpy/releases)
10
+
11
+ # yaqpy — YAML and more, Query editor in Python
12
+
13
+ A lightweight tool to **query, update and convert** YAML, JSON and more with expressions, from the command line or from Python.
14
+
15
+ - It follows the expression language of the popular CLI tool [mikefarah/yq](https://github.com/mikefarah/yq) (Go, v4.53.6)
16
+ - It is re-implemented with **nothing but the Python standard library**
17
+ - The name stands for **Y**AML **A**nd more, **Q**uery editor in **PY**thon
18
+
19
+ ```console
20
+ $ yaqpy '.server.port' config.yaml
21
+ 8080
22
+ $ yaqpy -i '.server.port = 9090' config.yaml # comments and key order are kept
23
+ ```
24
+
25
+ ## Features
26
+
27
+ - **Keeps your formatting**: comments, key order, anchors (`&` / `*`) and the way numbers and quotes were written (`0x1F`, `1.50`, `'yes'`) survive an update
28
+ - **No dependencies**: all it needs is Python 3.13 or later. The GUI is an optional extra (Flet); the web version adds flet-web
29
+ - **Four ways to use it**: the `yaqpy` command, the Python library (`import yaqpy`), a desktop GUI (`yaqpy-gui`) and the same GUI in a web browser (`yaqpy-web`)
30
+ - **Formats**: YAML, JSON, XML, CSV / TSV, TOML, properties and TOON (a token-saving format for LLMs), both in and out. TOML comments are not kept, so `-i` on TOML is refused by default
31
+ - **Compatible with yq**: 1,091 test scenarios of the Go version are run as compatibility tests (1,047 of the 1,051 comparable ones match). Every operator works except `load` and friends, `eval`, `envsubst`, `system` and `error`
32
+ - **Beyond yq** (not in the Go version):
33
+ - the input format is detected from the content when the file extension does not tell it
34
+ - `yaqpy --schema data.yaml` prints a JSON Schema (Draft 2020-12) of the data, as JSON or YAML
35
+ - `yaqpy --recipe openai-to-gemini request.json` converts request bodies between OpenAI, Gemini and Anthropic, and reports what was dropped, added, or does not fit the target schema (results go to stdout; files are written only with `--apply --out-dir`; no API is called)
36
+ - yaqpy describes itself for people and AI: `--print-spec` (the operators that work and those that do not), `--example`, `--guide-prompt` (a prompt that lets an AI write yaqpy expressions) and `--skill-md` (a Claude Code skill)
37
+ - **Safe defaults**: as a library, operators that read files or environment variables or run commands are all off. The CLI, like the Go version, allows environment variables and file reads (commands stay off). The web version always turns them off
38
+
39
+ ## Installation
40
+
41
+ Python 3.13 or later is required.
42
+
43
+ ```bash
44
+ pip install yaqpy # the yaqpy command and the library (no dependencies)
45
+ pip install "yaqpy[gui]" # + the desktop GUI (adds Flet)
46
+ pip install "yaqpy[web]" # + the GUI in a web browser (adds Flet and flet-web)
47
+ ```
48
+
49
+ With [uv](https://docs.astral.sh/uv/):
50
+
51
+ ```bash
52
+ uv tool install yaqpy # install the yaqpy command
53
+ uv tool install "yaqpy[gui,web]" # ... with what yaqpy-gui and yaqpy-web need
54
+ uvx yaqpy '.server.port' config.yaml # run it once without installing
55
+ uv add yaqpy # use it as a library in a uv project
56
+ ```
57
+
58
+ **From GitHub Releases** (for example, a version that is not on PyPI): pick a version on [Releases](https://github.com/sgtao/yaqpy/releases) and replace `0.6.0` / `v0.6.0` below with it.
59
+
60
+ ```bash
61
+ pip install "yaqpy[gui] @ https://github.com/sgtao/yaqpy/releases/download/v0.6.0/yaqpy-0.6.0-py3-none-any.whl"
62
+ pip install "yaqpy[gui] @ git+https://github.com/sgtao/yaqpy@v0.6.0"
63
+ ```
64
+
65
+ To work on the source, see [DEVELOPMENT.md](https://github.com/sgtao/yaqpy/blob/main/DEVELOPMENT.md) (Japanese).
66
+
67
+ ## Quick start
68
+
69
+ Take this `config.yaml`:
70
+
71
+ ```yaml
72
+ # server settings
73
+ server:
74
+ port: 8080 # dev
75
+ hosts: [a, b]
76
+ items:
77
+ - {name: pen, price: 120}
78
+ - {name: book, price: 980}
79
+ ```
80
+
81
+ **Command line**
82
+
83
+ ```bash
84
+ yaqpy '.server.port' config.yaml # read -> 8080
85
+ yaqpy '.items[] | select(.price > 500) | .name' config.yaml # filter -> book
86
+ yaqpy -i '.server.port = 9090' config.yaml # update (comments and key order are kept)
87
+ yaqpy -o json '.server' config.yaml # convert (yaml / json / xml / csv / tsv / toml / props / toon)
88
+ ```
89
+
90
+ **Python library**
91
+
92
+ ```python
93
+ import yaqpy
94
+
95
+ yaqpy.evaluate(".server.port", "server:\n port: 8080\n") # '8080\n'
96
+ yaqpy.query(".items[] | select(.price > 500)", {"items": [{"price": 1}, {"price": 900}]}) # [{'price': 900}]
97
+ yaqpy.update(".server.port = 9090", {"server": {"port": 8080}}) # {'server': {'port': 9090}}
98
+ ```
99
+
100
+ **GUI**
101
+
102
+ ```bash
103
+ yaqpy-gui # desktop window (or: yaqpy --gui)
104
+ yaqpy-web # the same screens in your browser at http://127.0.0.1:8550/ (or: yaqpy --web)
105
+ yaqpy-web --port 9000 # another port; see yaqpy-web --help for the options
106
+ ```
107
+
108
+ The web version listens on this PC only by default and has no authentication. Files are uploaded from the browser and results come back as downloads.
109
+
110
+ ## Documentation
111
+
112
+ The detailed guides are written in **Japanese**.
113
+
114
+ | Contents | File |
115
+ |---|---|
116
+ | This README in Japanese | [README.ja.md](https://github.com/sgtao/yaqpy/blob/main/README.ja.md) |
117
+ | The command, each format, JSON Schema output, recipes, the library, the operators, and the differences from Go yq | [USAGE.ja.md](https://github.com/sgtao/yaqpy/blob/main/USAGE.ja.md) |
118
+ | The GUI (desktop and web) | [USAGE-GUI.ja.md](https://github.com/sgtao/yaqpy/blob/main/USAGE-GUI.ja.md) |
119
+ | For developers (setup, design, tests, repository layout) | [DEVELOPMENT.md](https://github.com/sgtao/yaqpy/blob/main/DEVELOPMENT.md) |
120
+ | Changes in each version (features, known limitations) | [CHANGELOG.md](https://github.com/sgtao/yaqpy/blob/main/CHANGELOG.md) |
121
+
122
+ `yaqpy --help`, `yaqpy-gui --help` and `yaqpy-web --help` are in English.
123
+
124
+ ## License
125
+
126
+ [MIT License](https://github.com/sgtao/yaqpy/blob/main/LICENSE)
127
+
128
+ The design and the test scenarios draw on Go yq (MIT); see [NOTICE](https://github.com/sgtao/yaqpy/blob/main/NOTICE).
129
+
130
+ ---
131
+
132
+ 🤖 Built with [Claude Code](https://claude.com/claude-code)
@@ -0,0 +1,94 @@
1
+ [project]
2
+ name = "yaqpy"
3
+ version = "0.6.0"
4
+ description = "YAML and more—Query editor in Python: query, update and convert YAML / JSON / XML / CSV / TOML / properties / TOON with the mikefarah/yq expression language, in pure Python (stdlib only)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE", "NOTICE"]
8
+ authors = [{ name = "sgtao" }]
9
+ keywords = ["yaml", "json", "xml", "csv", "toml", "toon", "yq", "jq", "query", "cli", "json-schema"]
10
+ requires-python = ">=3.13"
11
+ dependencies = []
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Environment :: Web Environment",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: System Administrators",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Text Processing :: Markup",
23
+ "Topic :: Utilities",
24
+ "Typing :: Typed",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/sgtao/yaqpy"
29
+ Repository = "https://github.com/sgtao/yaqpy"
30
+ Documentation = "https://github.com/sgtao/yaqpy/blob/main/USAGE.ja.md"
31
+ Changelog = "https://github.com/sgtao/yaqpy/blob/main/CHANGELOG.md"
32
+ Issues = "https://github.com/sgtao/yaqpy/issues"
33
+
34
+ [project.scripts]
35
+ yaqpy = "yaqpy.cli.main:main"
36
+ yaqpy-gui = "yaqpy.gui.app:cli_entry"
37
+ yaqpy-web = "yaqpy.gui.app:web_cli_entry"
38
+
39
+ [project.optional-dependencies]
40
+ gui = [
41
+ "flet>=1.0,<2",
42
+ ]
43
+ web = [
44
+ "flet[web]>=1.0,<2",
45
+ ]
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "pytest>=9.1.1",
50
+ "pytest-cov>=7.1.0",
51
+ "pytest-timeout>=2.4.0",
52
+ "pytest-xdist>=3.8.0",
53
+ "python-toon>=0.1.3",
54
+ "tzdata>=2026.4",
55
+ ]
56
+
57
+ [build-system]
58
+ requires = ["uv_build>=0.11.17,<0.12.0"]
59
+ build-backend = "uv_build"
60
+
61
+ [tool.ruff]
62
+ target-version = "py313"
63
+ line-length = 100
64
+
65
+ [tool.pytest.ini_options]
66
+ minversion = "9.0"
67
+ testpaths = ["tests"]
68
+ python_files = ["test_*.py"]
69
+ # The test classes are named XxxTests (and a few TestXxx); helper base classes such as CliTestCase
70
+ # match neither, so they are not collected.
71
+ python_classes = ["Test*", "*Tests"]
72
+ addopts = ["-ra", "--strict-markers", "--strict-config"]
73
+ timeout = 300
74
+ markers = [
75
+ "golden: scenarios taken from the Go yq tests (tests/golden)",
76
+ "acceptance: runs the CLI in a subprocess (tests/acceptance)",
77
+ "gui: needs the optional Flet package",
78
+ "slow: takes several seconds",
79
+ ]
80
+
81
+ [tool.coverage.run]
82
+ source = ["yaqpy"]
83
+ branch = true
84
+ patch = ["subprocess"]
85
+
86
+ [tool.coverage.report]
87
+ show_missing = true
88
+ skip_empty = true
89
+ exclude_also = [
90
+ "if TYPE_CHECKING:",
91
+ "raise NotImplementedError",
92
+ "if __name__ == .__main__.:",
93
+ '^\s*\.\.\.$',
94
+ ]
@@ -0,0 +1,40 @@
1
+ """yaqpy - a pure-Python (standard library only) implementation of mikefarah/yq.
2
+
3
+ >>> import yaqpy
4
+ >>> yaqpy.evaluate(".a.b", "a:\\n b: 3\\n")
5
+ '3\\n'
6
+ >>> yaqpy.query(".items[] | select(. > 1)", {"items": [1, 2, 3]})
7
+ [2, 3]
8
+ """
9
+
10
+ from yaqpy.api import (
11
+ Yq, apply_recipe, compile, detect_format, dump, evaluate, evaluate_all, list_recipes, load,
12
+ query, update,
13
+ )
14
+ from yaqpy.app.recipe_service import RecipeRun
15
+ from yaqpy.core.lang.parser import Expression
16
+ from yaqpy.core.model.node import Kind, Node, Style
17
+ from yaqpy.errors import (
18
+ EvaluationError, EvaluationLimitError, ExpressionSyntaxError, FormatError, RecipeError,
19
+ SecurityError, UnknownFormatError, YamlSyntaxError, YqError,
20
+ )
21
+ from yaqpy.options import (
22
+ CsvOptions, JsonOptions, Limits, Options, PropertiesOptions, SchemaOptions, SecurityPolicy,
23
+ TomlOptions, ToonOptions, XmlOptions, YamlOptions,
24
+ )
25
+ from yaqpy.recipes import Recipe, build_recipe
26
+ from yaqpy.recipes.analysis import RecipeReport
27
+
28
+ __version__ = "0.6.0"
29
+
30
+ __all__ = [
31
+ "Yq", "compile", "dump", "evaluate", "evaluate_all", "load", "query", "update",
32
+ "apply_recipe", "list_recipes", "build_recipe", "Recipe", "RecipeReport", "RecipeRun",
33
+ "detect_format",
34
+ "Expression", "Kind", "Node", "Style",
35
+ "EvaluationError", "EvaluationLimitError", "ExpressionSyntaxError", "FormatError", "RecipeError",
36
+ "SecurityError", "UnknownFormatError", "YamlSyntaxError", "YqError",
37
+ "CsvOptions", "JsonOptions", "Limits", "Options", "PropertiesOptions", "SchemaOptions",
38
+ "SecurityPolicy", "TomlOptions", "ToonOptions", "XmlOptions", "YamlOptions",
39
+ "__version__",
40
+ ]
@@ -0,0 +1,8 @@
1
+ """``python -m yaqpy`` entry point."""
2
+
3
+ import sys
4
+
5
+ from yaqpy.cli.main import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,189 @@
1
+ """Public library API (design doc section 10)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ from yaqpy.app.dto import EvalMode, EvaluateRequest, InputSource
10
+ from yaqpy.app.ports import SandboxFileSystem, StaticEnvironment
11
+ from yaqpy.app.printer import MemorySink
12
+ from yaqpy.app.recipe_service import RecipeRun, RecipeService
13
+ from yaqpy.app.service import YqService
14
+ from yaqpy.core.lang.parser import Expression
15
+ from yaqpy.core.model.convert import from_python, to_python
16
+ from yaqpy.core.model.node import Node
17
+ from yaqpy.core.operators import OperatorRegistry, builtin_registry
18
+ from yaqpy.formats.registry import FormatRegistry, builtin_formats
19
+ from yaqpy.options import Options
20
+ from yaqpy.recipes import Recipe, builtin_recipes
21
+
22
+
23
+ class Yq:
24
+ """Stateless facade. Safe to share between threads."""
25
+
26
+ def __init__(
27
+ self,
28
+ options: Options | None = None,
29
+ *,
30
+ operators: OperatorRegistry | None = None,
31
+ formats: FormatRegistry | None = None,
32
+ environ: Mapping[str, str] | None = None,
33
+ clock: Callable[[], datetime] | None = None,
34
+ ) -> None:
35
+ self.options = options or Options()
36
+ if environ is None:
37
+ import os
38
+
39
+ environ = os.environ
40
+ self._service = YqService(
41
+ SandboxFileSystem(), StaticEnvironment(environ),
42
+ operators=operators or builtin_registry(), formats=formats or builtin_formats(),
43
+ clock=clock,
44
+ )
45
+
46
+ # ------------------------------------------------------------------ compile
47
+
48
+ def compile(self, expression: str) -> Expression:
49
+ return self._service.compile(expression)
50
+
51
+ # ------------------------------------------------------------------ text in / text out
52
+
53
+ def _request(self, expression: str | Expression, texts: Iterable[str], mode: EvalMode,
54
+ options: Options | None) -> EvaluateRequest:
55
+ source = expression.source if isinstance(expression, Expression) else expression
56
+ inputs = tuple(InputSource("<text>", text) for text in texts)
57
+ return EvaluateRequest(expression=source, inputs=inputs, mode=mode,
58
+ options=options or self.options)
59
+
60
+ def evaluate(self, expression: str | Expression, text: str = "", *,
61
+ options: Options | None = None) -> str:
62
+ request = self._request(expression, [text] if text != "" else [], EvalMode.STREAM, options)
63
+ result = self._service.evaluate(request, MemorySink())
64
+ return result.output or ""
65
+
66
+ def evaluate_all(self, expression: str | Expression, texts: Iterable[str], *,
67
+ options: Options | None = None) -> str:
68
+ request = self._request(expression, texts, EvalMode.ALL, options)
69
+ result = self._service.evaluate(request, MemorySink())
70
+ return result.output or ""
71
+
72
+ # ------------------------------------------------------------------ nodes / python objects
73
+
74
+ def evaluate_nodes(self, expression: str | Expression, documents: Sequence[Node], *,
75
+ options: Options | None = None) -> list[Node]:
76
+ return self._service.evaluate_nodes(expression, documents, options or self.options)
77
+
78
+ def iter_results(self, expression: str | Expression, text: str, *,
79
+ options: Options | None = None) -> Iterator[Node]:
80
+ options = options or self.options
81
+ decoder = self._service.formats.decoder_for(options.input_format, options)
82
+ for doc in decoder.decode_documents(text):
83
+ yield from self.evaluate_nodes(expression, [doc], options=options)
84
+
85
+ def query(self, expression: str | Expression, data: Any, *,
86
+ options: Options | None = None) -> list[Any]:
87
+ root = from_python(data)
88
+ root.evaluate_together = True
89
+ results = self.evaluate_nodes(expression, [root], options=options)
90
+ return [to_python(n) for n in results]
91
+
92
+ def update(self, expression: str | Expression, data: Any, *,
93
+ options: Options | None = None) -> Any:
94
+ root = from_python(data)
95
+ root.evaluate_together = True
96
+ self.evaluate_nodes(expression, [root], options=options)
97
+ return to_python(root)
98
+
99
+ def load(self, text: str, *, format: str = "yaml", options: Options | None = None) -> list[Node]:
100
+ options = options or self.options
101
+ decoder = self._service.formats.decoder_for(format, options)
102
+ return list(decoder.decode_documents(text))
103
+
104
+ def dump(self, documents: Iterable[Node], *, format: str = "yaml",
105
+ options: Options | None = None) -> str:
106
+ from yaqpy.app.printer import ResultPrinter
107
+
108
+ options = options or self.options
109
+ spec = self._service.formats.get(format)
110
+ unwrap = options.unwrap_scalar if options.unwrap_scalar is not None else spec.unwrap_scalar_default
111
+ encoder = self._service.formats.encoder_for(format, options, unwrap)
112
+ sink = MemorySink()
113
+ ResultPrinter(encoder, sink).print_results(list(documents))
114
+ return sink.finish() or ""
115
+
116
+
117
+ # ------------------------------------------------------------------ recipes (a yaqpy extension)
118
+
119
+ def apply_recipe(self, recipe: str | Recipe, text: str, *, input_format: str = "json",
120
+ output_format: str = "json", prune_null: bool = False, prune_empty: bool = False,
121
+ options: Options | None = None) -> RecipeRun:
122
+ """Convert ``text`` with a recipe: a bundled one by name, or a ``Recipe`` you built.
123
+
124
+ Never reads files or environment variables, whatever the options say. The result holds the
125
+ converted text (``output``) and a ``report`` of what was dropped, added or does not fit the
126
+ target schema.
127
+ """
128
+ service = RecipeService(self._service)
129
+ if isinstance(recipe, str):
130
+ recipe = service.load(recipe)
131
+ return service.run(recipe, InputSource("<text>", text), options or self.options,
132
+ input_format=service.input_format_for(recipe, "", input_format),
133
+ output_format=service.output_format_for(recipe, output_format),
134
+ prune_null=prune_null, prune_empty=prune_empty)
135
+
136
+
137
+ _DEFAULT = Yq()
138
+
139
+
140
+ def compile(expression: str) -> Expression: # noqa: A001 - mirrors the design doc
141
+ return _DEFAULT.compile(expression)
142
+
143
+
144
+ def evaluate(expression: str, text: str = "", *, options: Options | None = None) -> str:
145
+ return _DEFAULT.evaluate(expression, text, options=options)
146
+
147
+
148
+ def evaluate_all(expression: str, texts: Iterable[str], *, options: Options | None = None) -> str:
149
+ return _DEFAULT.evaluate_all(expression, texts, options=options)
150
+
151
+
152
+ def query(expression: str, data: Any, *, options: Options | None = None) -> list[Any]:
153
+ return _DEFAULT.query(expression, data, options=options)
154
+
155
+
156
+ def update(expression: str, data: Any, *, options: Options | None = None) -> Any:
157
+ return _DEFAULT.update(expression, data, options=options)
158
+
159
+
160
+ def load(text: str, *, format: str = "yaml", options: Options | None = None) -> list[Node]:
161
+ return _DEFAULT.load(text, format=format, options=options)
162
+
163
+
164
+ def dump(documents: Iterable[Node], *, format: str = "yaml", options: Options | None = None) -> str:
165
+ return _DEFAULT.dump(documents, format=format, options=options)
166
+
167
+
168
+ def detect_format(text: str) -> str:
169
+ """Guess the format of ``text`` from its content alone (a yaqpy extension; Go yq has no such
170
+ thing - it only ever looks at a file's extension).
171
+
172
+ Meant for text with no filename to go by, or none whose extension names a format: the same
173
+ guess ``Options(input_format="auto")`` falls back to once a filename's extension gives no
174
+ answer. Returns a format name ``Options(input_format=...)`` accepts; "yaml" is the fallback
175
+ when nothing in the content looks confident enough (never an error).
176
+ """
177
+ return builtin_formats().guess("", text).name
178
+
179
+
180
+ def list_recipes() -> dict[str, Recipe]:
181
+ """The bundled recipes by name."""
182
+ return dict(builtin_recipes())
183
+
184
+
185
+ def apply_recipe(recipe: str | Recipe, text: str, *, input_format: str = "json",
186
+ output_format: str = "json", prune_null: bool = False, prune_empty: bool = False,
187
+ options: Options | None = None) -> RecipeRun:
188
+ return _DEFAULT.apply_recipe(recipe, text, input_format=input_format, output_format=output_format,
189
+ prune_null=prune_null, prune_empty=prune_empty, options=options)