pqtools 0.4.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.
- pqtools-0.4.0/LICENSE +21 -0
- pqtools-0.4.0/NOTICE +9 -0
- pqtools-0.4.0/PKG-INFO +357 -0
- pqtools-0.4.0/README.md +321 -0
- pqtools-0.4.0/pyproject.toml +74 -0
- pqtools-0.4.0/setup.cfg +4 -0
- pqtools-0.4.0/src/pqtools/THIRD_PARTY_NOTICES.txt +34 -0
- pqtools-0.4.0/src/pqtools/__init__.py +47 -0
- pqtools-0.4.0/src/pqtools/_bridge.cjs +45584 -0
- pqtools-0.4.0/src/pqtools/cli.py +307 -0
- pqtools-0.4.0/src/pqtools/containers.py +496 -0
- pqtools-0.4.0/src/pqtools/core.py +685 -0
- pqtools-0.4.0/src/pqtools/evaluate.py +1310 -0
- pqtools-0.4.0/src/pqtools/fabric.py +219 -0
- pqtools-0.4.0/src/pqtools/pqtest.py +93 -0
- pqtools-0.4.0/src/pqtools/py.typed +0 -0
- pqtools-0.4.0/src/pqtools.egg-info/PKG-INFO +357 -0
- pqtools-0.4.0/src/pqtools.egg-info/SOURCES.txt +27 -0
- pqtools-0.4.0/src/pqtools.egg-info/dependency_links.txt +1 -0
- pqtools-0.4.0/src/pqtools.egg-info/entry_points.txt +2 -0
- pqtools-0.4.0/src/pqtools.egg-info/requires.txt +12 -0
- pqtools-0.4.0/src/pqtools.egg-info/top_level.txt +1 -0
- pqtools-0.4.0/tests/test_adapters.py +475 -0
- pqtools-0.4.0/tests/test_cli.py +178 -0
- pqtools-0.4.0/tests/test_containers.py +692 -0
- pqtools-0.4.0/tests/test_core.py +451 -0
- pqtools-0.4.0/tests/test_corpus.py +46 -0
- pqtools-0.4.0/tests/test_evaluate.py +557 -0
- pqtools-0.4.0/tests/test_rules.py +15 -0
pqtools-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gopal Bagaswar
|
|
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.
|
pqtools-0.4.0/NOTICE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Unofficial project, not affiliated with Microsoft. The bundled JavaScript
|
|
2
|
+
components retain their copyright and MIT permission notices in
|
|
3
|
+
`pqtools/THIRD_PARTY_NOTICES.txt`.
|
|
4
|
+
The test corpus includes a short Power Query M let-expression example from the
|
|
5
|
+
Microsoft M language specification:
|
|
6
|
+
https://learn.microsoft.com/en-us/powerquery-m/m-spec-let
|
|
7
|
+
|
|
8
|
+
The vendored DataConnectors samples remain Copyright (c) Microsoft Corporation
|
|
9
|
+
and are redistributed under the MIT License included beside those fixtures.
|
pqtools-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pqtools
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Power Query M from Python: lint, format and refactor .pq or the queries inside .pbix/.xlsx, and run the transformation chain on your own data
|
|
5
|
+
Author: Gopal Bagaswar
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/GopalGB/pqtools
|
|
8
|
+
Project-URL: Repository, https://github.com/GopalGB/pqtools
|
|
9
|
+
Project-URL: Issues, https://github.com/GopalGB/pqtools/issues
|
|
10
|
+
Keywords: power-query,m-language,power-bi,fabric,linter,formatter,pq
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
Provides-Extra: fabric
|
|
26
|
+
Requires-Dist: pyarrow>=14; extra == "fabric"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
32
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
33
|
+
Requires-Dist: twine>=6.1; extra == "dev"
|
|
34
|
+
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# pqtools
|
|
38
|
+
|
|
39
|
+
Offline command-line and Python tooling for Power Query M source: parse, format,
|
|
40
|
+
lint (`check`), safely rename a `let` binding, and run (`eval`) the
|
|
41
|
+
transformation chain of a query against data you supply.
|
|
42
|
+
|
|
43
|
+
> **Unofficial.** Not affiliated with or endorsed by Microsoft. Not a Power Query
|
|
44
|
+
> runtime - `pq eval` runs the transformation chain of a query locally; it never
|
|
45
|
+
> runs a connector (`Web.Contents`, `Sql.Database`, `Csv.Document`, ...). See
|
|
46
|
+
> [Running M](#running-m) below.
|
|
47
|
+
|
|
48
|
+
> **Renamed.** Published as `mquery-toolkit` 0.1.0 on 2026-09-03 and renamed the
|
|
49
|
+
> same day to `pqtools` to avoid a CLI name collision with the existing `mquery`
|
|
50
|
+
> package on PyPI (a Yara malware-query tool). `mquery-toolkit` 0.1.0 is yanked.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install pqtools
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requires **Node.js 22 or newer** on `PATH`, or point `MQUERY_NODE` at a Node
|
|
59
|
+
binary. The Microsoft parser and formatter packages are bundled inside the
|
|
60
|
+
wheel (`_bridge.cjs`) - no `npm install` needed.
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Parse to deterministic JSON (tokens, root kind, bindings/references)
|
|
66
|
+
pq parse query.pq
|
|
67
|
+
|
|
68
|
+
# Format - dry run prints a unified diff, nothing is written
|
|
69
|
+
pq format query.pq
|
|
70
|
+
|
|
71
|
+
# Format and write in place (atomic replace, preserves mode/newline/encoding)
|
|
72
|
+
pq format query.pq --write
|
|
73
|
+
|
|
74
|
+
# Lint, machine-readable output; exit code 2 if any diagnostic is severity=error
|
|
75
|
+
pq check query.pq --json
|
|
76
|
+
|
|
77
|
+
# Rename one top-level let binding - dry run first
|
|
78
|
+
pq rename query.pq --old OldName --new NewName
|
|
79
|
+
|
|
80
|
+
# Run a query's transformation chain locally, against your own data
|
|
81
|
+
pq eval report.pq --bind Source=data.csv
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Python API
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from pqtools import check, format_source, parse, rename, update_file
|
|
88
|
+
|
|
89
|
+
parsed = parse(source_text) # dict: tokens, rootKind, analysis
|
|
90
|
+
formatted = format_source(source_text) # formatted M source, same encoding
|
|
91
|
+
diagnostics = check(source_text, "query.pq") # list[Diagnostic]
|
|
92
|
+
renamed = rename(source_text, "OldName", "NewName")
|
|
93
|
+
|
|
94
|
+
from pqtools.evaluate import evaluate
|
|
95
|
+
|
|
96
|
+
result = evaluate(source_text, bindings={"Source": [{"a": "1"}, {"a": "2"}]})
|
|
97
|
+
|
|
98
|
+
# File-level edit with the same dry-run/--write safety model as the CLI
|
|
99
|
+
diff = update_file(path, format_source) # dry run: unified diff
|
|
100
|
+
diff = update_file(path, format_source, write=True) # atomic write
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Diagnostics
|
|
104
|
+
|
|
105
|
+
| Code | Severity | Meaning |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `M_PARSE_ERROR` | error | source does not parse |
|
|
108
|
+
| `M001` | error | duplicate `let` binding name |
|
|
109
|
+
| `M002` | warning | `Web.Contents` called with a non-literal (dynamic) URL |
|
|
110
|
+
| `M003` | warning | credential-like literal (`password`/`token`/`secret` = `"..."`) |
|
|
111
|
+
| `M004` | warning | `let` binding unreachable from the result |
|
|
112
|
+
| `M005` | warning | unresolved unqualified reference |
|
|
113
|
+
| `M006` | info | source-function inventory (`*.Contents` dependency) |
|
|
114
|
+
|
|
115
|
+
`M002` and `M003` are token-based checks over the parsed source, so they no
|
|
116
|
+
longer fire inside comments or strings. Every matching occurrence is
|
|
117
|
+
reported, one diagnostic per call site or literal.
|
|
118
|
+
|
|
119
|
+
`check --json` emits stable objects; `check` without `--json` prints
|
|
120
|
+
`file:line:column: severity code: message` per diagnostic. The CLI exits `2`
|
|
121
|
+
when any diagnostic has severity `error`, `0` otherwise.
|
|
122
|
+
|
|
123
|
+
## Running M
|
|
124
|
+
|
|
125
|
+
pandas does not run Excel's formulas; it replaces Excel's data connections with
|
|
126
|
+
your data, in Python. `pq eval` does the same for Power Query. A real M query is
|
|
127
|
+
a `Source = <connector>(...)` step followed by a chain of `Table.*`
|
|
128
|
+
transformations. `pqtools` cannot run the connector step - that is Microsoft's
|
|
129
|
+
proprietary Mashup Engine, and this project does not reimplement it. But if
|
|
130
|
+
*you* supply the source table, the rest of the transformation chain runs
|
|
131
|
+
locally, offline, in Python:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pq eval report.pq --bind Source=data.csv
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`--bind NAME=PATH` loads `PATH` (a `.csv`, read as a list of records with
|
|
138
|
+
`csv.DictReader` - every value stays text, or a `.json` file, loaded as
|
|
139
|
+
whatever it holds) and, wherever `NAME` is used as a `let` binding in the
|
|
140
|
+
query, substitutes it directly - the binding's own right-hand-side expression
|
|
141
|
+
(the connector call) is never evaluated, which is exactly what makes it
|
|
142
|
+
irrelevant that `pqtools` cannot run it.
|
|
143
|
+
|
|
144
|
+
A **table** is simply `list[dict[str, Any]]` - a list of records. A record is
|
|
145
|
+
`dict[str, Any]`. A list is `list[Any]`. That is the whole data model.
|
|
146
|
+
|
|
147
|
+
**Worked example.** Given `report.pq`:
|
|
148
|
+
|
|
149
|
+
```m
|
|
150
|
+
let
|
|
151
|
+
Source = Csv.Document(File.Contents("ignored.csv")),
|
|
152
|
+
Kept = Table.SelectRows(Source, each [b] <> "y"),
|
|
153
|
+
Renamed = Table.RenameColumns(Kept, {{"a", "id"}})
|
|
154
|
+
in
|
|
155
|
+
Renamed
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
and `data.csv`:
|
|
159
|
+
|
|
160
|
+
```csv
|
|
161
|
+
a,b
|
|
162
|
+
1,x
|
|
163
|
+
2,y
|
|
164
|
+
3,z
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
$ pq eval report.pq --bind Source=data.csv
|
|
169
|
+
[{"b": "x", "id": "1"}, {"b": "z", "id": "3"}]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`Csv.Document(File.Contents("ignored.csv"))` is never called - `ignored.csv` is
|
|
173
|
+
never opened. `Source` is the CSV you bound, `Kept` drops the `b = "y"` row, and
|
|
174
|
+
`Renamed` renames `a` to `id`. Without `--bind`, the same query fails with a
|
|
175
|
+
typed, exit-`2` error naming the connector:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
$ pq eval report.pq
|
|
179
|
+
error M_EVAL_UNSUPPORTED: Csv.Document is a connector - Power Query's Mashup
|
|
180
|
+
Engine runs it (Fabric or PQTest is the host that can); pqtools evaluates only
|
|
181
|
+
the transformation chain after you supply its result table with --bind
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Supported:** number/text/logical/null literals; `+ - * /`; `= <> < <= > >=`;
|
|
185
|
+
`and or not`; text `&`; `if/then/else`; `let/in` (lazy, memoised, correctly
|
|
186
|
+
shadowed - a binding's expression is only ever evaluated once, and only if
|
|
187
|
+
something actually references it); records (`[a = 1]`) and field access
|
|
188
|
+
(`r[a]`, `r[a]?`, and the `each`-scoped `[a]` shorthand for `_[a]`); lists
|
|
189
|
+
(`{1, 2}`) and index access (`l{0}`, `l{0}?`); `each` and `(x) => ...` lambdas
|
|
190
|
+
and calling them; `try ... otherwise ...`; and this builtin set (verbatim from
|
|
191
|
+
`pqtools.evaluate.BUILTINS`, so it cannot drift out of sync with the code):
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
Text.From Text.Upper Text.Lower Text.Length Text.Combine Text.Contains
|
|
195
|
+
Text.Replace Text.Split Text.Start Text.End Text.Trim
|
|
196
|
+
Number.From Number.Round Number.Abs
|
|
197
|
+
List.Count List.Sum List.Max List.Min List.Average List.Transform List.Select
|
|
198
|
+
List.First List.Last List.Reverse List.Sort List.Contains List.Distinct
|
|
199
|
+
List.Range
|
|
200
|
+
Record.Field Record.FieldNames Record.HasFields Record.AddField
|
|
201
|
+
Record.RemoveFields
|
|
202
|
+
Table.FromRecords Table.ToRecords Table.RowCount Table.ColumnNames
|
|
203
|
+
Table.SelectRows Table.SelectColumns Table.RemoveColumns Table.RenameColumns
|
|
204
|
+
Table.AddColumn Table.TransformColumns Table.Sort Table.FirstN Table.LastN
|
|
205
|
+
Table.Distinct
|
|
206
|
+
Json.Document (text only - not the binary overload)
|
|
207
|
+
Logical.From
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Everything else raises a typed `UnsupportedError` (`M_EVAL_UNSUPPORTED`)
|
|
211
|
+
naming the exact construct** - never approximated, never guessed at. That
|
|
212
|
+
includes: any connector (`Web.Contents`, `Sql.Database`, `File.Contents`,
|
|
213
|
+
`Excel.Workbook`, `Csv.Document`, `Binary.*` - the error names the construct
|
|
214
|
+
and says it needs Fabric or PQTest, the two hosts that can actually run it);
|
|
215
|
+
`#shared`; `meta`; type ascription (`as`, `is`, parameter/return types,
|
|
216
|
+
`type ...`); `??`; field projection (`r[[a],[b]]`); any identifier this
|
|
217
|
+
evaluator does not know; and any builtin call with an argument shape not
|
|
218
|
+
listed above. A wrong number would be worse than a refusal, so `pqtools` never
|
|
219
|
+
approximates a connector's result or a builtin's documented behaviour - it
|
|
220
|
+
either runs the real, documented semantics or it stops and tells you exactly
|
|
221
|
+
where. `max_steps` (default 1,000,000, an `evaluate()` keyword argument) bounds
|
|
222
|
+
the total number of AST nodes visited, so a runaway query cannot hang the
|
|
223
|
+
caller either.
|
|
224
|
+
|
|
225
|
+
`pq eval` does not replace Power Query - it replaces the connector's *data*,
|
|
226
|
+
the same trade pandas makes when it replaces a spreadsheet's data connections.
|
|
227
|
+
|
|
228
|
+
## Safety model
|
|
229
|
+
|
|
230
|
+
- **Dry-run by default.** Every edit command (`format`, `rename`,
|
|
231
|
+
`replace-source`) prints a unified diff and touches nothing unless `--write`
|
|
232
|
+
is passed.
|
|
233
|
+
- **`--write` is an atomic replace**: the file is written to a sibling temp
|
|
234
|
+
file, `fsync`'d, `chmod`'d to match the original, then moved into place with
|
|
235
|
+
`os.replace`, after which the parent directory is `fsync`'d so the rename
|
|
236
|
+
itself is durable.
|
|
237
|
+
- **Layout is preserved**: UTF-8 encoding, a leading BOM (present in every
|
|
238
|
+
Power Query SDK connector file), newline convention (`\n` vs `\r\n`),
|
|
239
|
+
final-newline state, and file mode all round-trip unchanged.
|
|
240
|
+
- **Refuses symlinks and hardlinks** - writes require a regular, single-link
|
|
241
|
+
file.
|
|
242
|
+
- **Detects concurrent change**: the source is snapshotted before the
|
|
243
|
+
transform and re-checked immediately before the atomic replace - this final
|
|
244
|
+
snapshot check, not the lock, is the guarantee against lost updates; a
|
|
245
|
+
change in that microsecond window raises `SafeWriteError`.
|
|
246
|
+
- **Advisory lock while writing only** - a `--write` call takes a
|
|
247
|
+
cross-process advisory lock (`fcntl`/`msvcrt`) for the duration of the
|
|
248
|
+
write and removes the lock file afterward, best-effort. It only serialises
|
|
249
|
+
cooperating `pq` processes and is not a correctness guarantee: because
|
|
250
|
+
the lock file is removed after use, a waiting process and a freshly
|
|
251
|
+
started one can end up locking different inodes. Dry-run calls take no
|
|
252
|
+
lock and create no lock file.
|
|
253
|
+
- This is **not mandatory locking** - no OS provides a portable mandatory
|
|
254
|
+
lock, and the advisory lock is not itself the correctness guard. Use
|
|
255
|
+
source control or external exclusive ownership for concurrent editors.
|
|
256
|
+
|
|
257
|
+
## Limits
|
|
258
|
+
|
|
259
|
+
- Input and output are capped at **10 MiB**.
|
|
260
|
+
- The Node subprocess is bounded to a **30 second** timeout.
|
|
261
|
+
- Supported extensions: `.pq`, `.m`, `.pqm`, and any `*.query.pq` file.
|
|
262
|
+
- `rename` scope: exactly **one unquoted top-level `let` binding**. It refuses
|
|
263
|
+
quoted identifiers (`#"..."`), record literals, lambda expressions, and
|
|
264
|
+
non-ASCII source.
|
|
265
|
+
- `Retry-After` on the Fabric adapter must be whole seconds; HTTP-date values
|
|
266
|
+
are rejected.
|
|
267
|
+
- **Windows:** two guarantees are weaker there and the code says so rather than pretending.
|
|
268
|
+
A directory `fsync` after the atomic replace is impossible on Windows, so the rename is durable
|
|
269
|
+
only as far as the filesystem makes it; and if the Node subprocess spawns a grandchild that
|
|
270
|
+
inherits its stdout, a reader already blocked in `ReadFile` is not released by closing the pipe,
|
|
271
|
+
so a timed-out call can run until that grandchild exits. Neither affects the bundled bridge,
|
|
272
|
+
which spawns nothing.
|
|
273
|
+
- The parse response is roughly 40x the size of the source, and it is capped at 10 MiB, so `parse`, `check`, `dependencies` and `rename` fail with a typed `NodeError` on sources above roughly 240 KiB. `format` returns only text and is not affected.
|
|
274
|
+
- `eval` walks at most `max_steps` AST nodes (default 1,000,000, an
|
|
275
|
+
`evaluate()` keyword argument, not yet exposed as a CLI flag) before raising
|
|
276
|
+
a typed `EvalError` - a runaway or hostile query cannot hang the caller. A
|
|
277
|
+
`--bind` file goes through the same `--bind`-only read path as everything
|
|
278
|
+
else: 10 MiB cap, no symlinks, no non-regular files.
|
|
279
|
+
|
|
280
|
+
## Working inside .xlsx and .pbix
|
|
281
|
+
|
|
282
|
+
`pqtools` can read the Power Query M source out of the real files it lives
|
|
283
|
+
in - no need to open Excel or Power BI to see or lint a query.
|
|
284
|
+
|
|
285
|
+
**Supported:** `pq check`, `pq parse`, `pq dependencies` and `pq eval` accept
|
|
286
|
+
an `.xlsx`, `.pbix`, `.pbit`, or a `.pbip` project (or its directory) directly.
|
|
287
|
+
Each finds the Power Query section(s) inside the container and runs
|
|
288
|
+
normally; `check` diagnostics and JSON output are labelled
|
|
289
|
+
`container!part` (e.g. `report.pbix!Formulas/Section1.m`) so the output
|
|
290
|
+
stays greppable across a batch of files. `pq eval` needs `--member NAME` to
|
|
291
|
+
pick one `shared` query out of a container that holds more than one.
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
pq check report.pbix
|
|
295
|
+
pq check "Sales.pbip" --json
|
|
296
|
+
pq dependencies workbook.xlsx
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Not supported (yet):** writing back into a container. `pq format`,
|
|
300
|
+
`pq rename` and `pq replace-source` refuse with a clear error on a
|
|
301
|
+
container path. The underlying logic exists
|
|
302
|
+
(`pqtools.containers.write_sections`) and is exercised in this repo's test
|
|
303
|
+
suite against synthesized fixtures and a real Power BI Desktop sample - it
|
|
304
|
+
rebuilds the container with only the M source changed, then re-reads its
|
|
305
|
+
own output and verifies nothing else moved before ever touching disk - but
|
|
306
|
+
it has not been validated against the wide range of real-world files this
|
|
307
|
+
format can take, so it is deliberately kept out of the CLI.
|
|
308
|
+
|
|
309
|
+
`pqtools` is not a Power BI or Excel client: `pq eval` runs a query's own
|
|
310
|
+
transformation chain against data you supply (see [Running M](#running-m)) -
|
|
311
|
+
it never opens a workbook, runs a connector, or writes anything back through
|
|
312
|
+
the CLI.
|
|
313
|
+
|
|
314
|
+
## Optional adapters
|
|
315
|
+
|
|
316
|
+
- **`fabric` extra** (`pip install "pqtools[fabric]"`) - a Fabric
|
|
317
|
+
Execute Query client that takes a caller-provided bearer token and an
|
|
318
|
+
injected HTTP transport. It never manages credentials itself and is fully
|
|
319
|
+
mocked in tests (no network access in the test suite).
|
|
320
|
+
- **`pqtest`** - a bounded wrapper around a user-installed Microsoft PQTest
|
|
321
|
+
executable, Windows-only, pinned to version `2.155.2`. It never downloads a
|
|
322
|
+
binary; it only validates and runs one already on disk.
|
|
323
|
+
|
|
324
|
+
## What it is not
|
|
325
|
+
|
|
326
|
+
- Not the Power Query Mashup Engine. `pq eval` runs a query's transformation
|
|
327
|
+
chain against data you supply (see [Running M](#running-m)); it never runs a
|
|
328
|
+
connector, and anything it does not implement raises a typed error instead
|
|
329
|
+
of approximating one.
|
|
330
|
+
- Not a Power BI or Fabric client, and it does not manage credentials.
|
|
331
|
+
- Not a general-purpose file editor - it only touches files with a supported
|
|
332
|
+
extension and only through the safety model above.
|
|
333
|
+
- Not a replacement for Microsoft's own parser/formatter - it vendors and
|
|
334
|
+
calls them directly rather than reimplementing M syntax.
|
|
335
|
+
|
|
336
|
+
## Development
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
git clone https://github.com/GopalGB/pqtools
|
|
340
|
+
cd pqtools
|
|
341
|
+
python -m venv .venv && source .venv/bin/activate
|
|
342
|
+
pip install -e ".[dev,fabric]"
|
|
343
|
+
npm ci --ignore-scripts
|
|
344
|
+
|
|
345
|
+
pytest -q --cov=pqtools --cov-fail-under=80
|
|
346
|
+
mypy src
|
|
347
|
+
ruff check .
|
|
348
|
+
ruff format --check .
|
|
349
|
+
npm test
|
|
350
|
+
python -m build
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## License
|
|
354
|
+
|
|
355
|
+
MIT - see `LICENSE`. Bundled Microsoft packages
|
|
356
|
+
(`@microsoft/powerquery-parser`, `@microsoft/powerquery-formatter`) and their
|
|
357
|
+
dependencies are also MIT; see `THIRD_PARTY_NOTICES.txt` and `NOTICE`.
|