mquery-toolkit 0.1.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.
@@ -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.
@@ -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
+ `mquery_toolkit/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.
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: mquery-toolkit
3
+ Version: 0.1.0
4
+ Summary: Unofficial offline tooling for Power Query M source (parse, format, check, rename) via Microsoft's official parser and formatter
5
+ Author: Gopal Bagaswar
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/GopalGB/mquery-toolkit
8
+ Project-URL: Repository, https://github.com/GopalGB/mquery-toolkit
9
+ Project-URL: Issues, https://github.com/GopalGB/mquery-toolkit/issues
10
+ Keywords: power-query,m-language,power-bi,fabric,linter,formatter
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
+ # mquery-toolkit
38
+
39
+ Offline command-line and Python tooling for Power Query M source: parse, format,
40
+ lint (`check`), and safely rename a `let` binding.
41
+
42
+ > **Unofficial.** Not affiliated with or endorsed by Microsoft. Not an M runtime -
43
+ > it parses and formats M source text; it does not evaluate queries.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install mquery-toolkit
49
+ ```
50
+
51
+ Requires **Node.js 22 or newer** on `PATH`, or point `MQUERY_NODE` at a Node
52
+ binary. The Microsoft parser and formatter packages are bundled inside the
53
+ wheel (`_bridge.cjs`) - no `npm install` needed.
54
+
55
+ ## Quick start
56
+
57
+ ```bash
58
+ # Parse to deterministic JSON (tokens, root kind, bindings/references)
59
+ mquery parse query.pq
60
+
61
+ # Format - dry run prints a unified diff, nothing is written
62
+ mquery format query.pq
63
+
64
+ # Format and write in place (atomic replace, preserves mode/newline/encoding)
65
+ mquery format query.pq --write
66
+
67
+ # Lint, machine-readable output; exit code 2 if any diagnostic is severity=error
68
+ mquery check query.pq --json
69
+
70
+ # Rename one top-level let binding - dry run first
71
+ mquery rename query.pq --old OldName --new NewName
72
+ ```
73
+
74
+ ## Python API
75
+
76
+ ```python
77
+ from mquery_toolkit import check, format_source, parse, rename, update_file
78
+
79
+ parsed = parse(source_text) # dict: tokens, rootKind, analysis
80
+ formatted = format_source(source_text) # formatted M source, same encoding
81
+ diagnostics = check(source_text, "query.pq") # list[Diagnostic]
82
+ renamed = rename(source_text, "OldName", "NewName")
83
+
84
+ # File-level edit with the same dry-run/--write safety model as the CLI
85
+ diff = update_file(path, format_source) # dry run: unified diff
86
+ diff = update_file(path, format_source, write=True) # atomic write
87
+ ```
88
+
89
+ ## Diagnostics
90
+
91
+ | Code | Severity | Meaning |
92
+ |---|---|---|
93
+ | `M_PARSE_ERROR` | error | source does not parse |
94
+ | `M001` | error | duplicate `let` binding name |
95
+ | `M002` | warning | `Web.Contents` called with a non-literal (dynamic) URL |
96
+ | `M003` | warning | credential-like literal (`password`/`token`/`secret` = `"..."`) |
97
+ | `M004` | warning | `let` binding unreachable from the result |
98
+ | `M005` | warning | unresolved unqualified reference |
99
+ | `M006` | info | source-function inventory (`*.Contents` dependency) |
100
+
101
+ `M002` and `M003` are conservative text-pattern checks on the raw source, not
102
+ AST checks; expect occasional false positives inside comments or strings.
103
+ Every matching occurrence is reported, one diagnostic per call site or
104
+ literal.
105
+
106
+ `check --json` emits stable objects; `check` without `--json` prints
107
+ `file:line:column: severity code: message` per diagnostic. The CLI exits `2`
108
+ when any diagnostic has severity `error`, `0` otherwise.
109
+
110
+ ## Safety model
111
+
112
+ - **Dry-run by default.** Every edit command (`format`, `rename`,
113
+ `replace-source`) prints a unified diff and touches nothing unless `--write`
114
+ is passed.
115
+ - **`--write` is an atomic replace**: the file is written to a sibling temp
116
+ file, `fsync`'d, `chmod`'d to match the original, then moved into place with
117
+ `os.replace`.
118
+ - **Layout is preserved**: UTF-8 encoding, a leading BOM (present in every
119
+ Power Query SDK connector file), newline convention (`\n` vs `\r\n`),
120
+ final-newline state, and file mode all round-trip unchanged.
121
+ - **Refuses symlinks and hardlinks** - writes require a regular, single-link
122
+ file.
123
+ - **Detects concurrent change**: the source is snapshotted before the
124
+ transform and re-checked immediately before the atomic replace - this final
125
+ snapshot check, not the lock, is the guarantee against lost updates; a
126
+ change in that microsecond window raises `SafeWriteError`.
127
+ - **Advisory lock while writing only** - a `--write` call takes a
128
+ cross-process advisory lock (`fcntl`/`msvcrt`) for the duration of the
129
+ write and removes the lock file afterward, best-effort. It only serialises
130
+ cooperating `mquery` processes and is not a correctness guarantee: because
131
+ the lock file is removed after use, a waiting process and a freshly
132
+ started one can end up locking different inodes. Dry-run calls take no
133
+ lock and create no lock file.
134
+ - This is **not mandatory locking** - no OS provides a portable mandatory
135
+ lock, and the advisory lock is not itself the correctness guard. Use
136
+ source control or external exclusive ownership for concurrent editors.
137
+
138
+ ## Limits
139
+
140
+ - Input and output are capped at **10 MiB**.
141
+ - The Node subprocess is bounded to a **30 second** timeout.
142
+ - Supported extensions: `.pq`, `.m`, `.pqm`, and any `*.query.pq` file.
143
+ - `rename` scope: exactly **one unquoted top-level `let` binding**. It refuses
144
+ quoted identifiers (`#"..."`), record literals, lambda expressions, and
145
+ non-ASCII source.
146
+ - `Retry-After` on the Fabric adapter must be whole seconds; HTTP-date values
147
+ are rejected.
148
+
149
+ ## Optional adapters
150
+
151
+ - **`fabric` extra** (`pip install "mquery-toolkit[fabric]"`) - a Fabric
152
+ Execute Query client that takes a caller-provided bearer token and an
153
+ injected HTTP transport. It never manages credentials itself and is fully
154
+ mocked in tests (no network access in the test suite).
155
+ - **`pqtest`** - a bounded wrapper around a user-installed Microsoft PQTest
156
+ executable, Windows-only, pinned to version `2.155.2`. It never downloads a
157
+ binary; it only validates and runs one already on disk.
158
+
159
+ ## What it is not
160
+
161
+ - Not an M language runtime or evaluator.
162
+ - Not a Power BI or Fabric client, and it does not manage credentials.
163
+ - Not a general-purpose file editor - it only touches files with a supported
164
+ extension and only through the safety model above.
165
+ - Not a replacement for Microsoft's own parser/formatter - it vendors and
166
+ calls them directly rather than reimplementing M syntax.
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ git clone https://github.com/GopalGB/mquery-toolkit
172
+ cd mquery-toolkit
173
+ python -m venv .venv && source .venv/bin/activate
174
+ pip install -e ".[dev,fabric]"
175
+ npm ci --ignore-scripts
176
+
177
+ pytest -q --cov=mquery_toolkit --cov-fail-under=80
178
+ mypy src
179
+ ruff check .
180
+ ruff format --check .
181
+ npm test
182
+ python -m build
183
+ ```
184
+
185
+ ## License
186
+
187
+ MIT - see `LICENSE`. Bundled Microsoft packages
188
+ (`@microsoft/powerquery-parser`, `@microsoft/powerquery-formatter`) and their
189
+ dependencies are also MIT; see `THIRD_PARTY_NOTICES.txt` and `NOTICE`.
@@ -0,0 +1,153 @@
1
+ # mquery-toolkit
2
+
3
+ Offline command-line and Python tooling for Power Query M source: parse, format,
4
+ lint (`check`), and safely rename a `let` binding.
5
+
6
+ > **Unofficial.** Not affiliated with or endorsed by Microsoft. Not an M runtime -
7
+ > it parses and formats M source text; it does not evaluate queries.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install mquery-toolkit
13
+ ```
14
+
15
+ Requires **Node.js 22 or newer** on `PATH`, or point `MQUERY_NODE` at a Node
16
+ binary. The Microsoft parser and formatter packages are bundled inside the
17
+ wheel (`_bridge.cjs`) - no `npm install` needed.
18
+
19
+ ## Quick start
20
+
21
+ ```bash
22
+ # Parse to deterministic JSON (tokens, root kind, bindings/references)
23
+ mquery parse query.pq
24
+
25
+ # Format - dry run prints a unified diff, nothing is written
26
+ mquery format query.pq
27
+
28
+ # Format and write in place (atomic replace, preserves mode/newline/encoding)
29
+ mquery format query.pq --write
30
+
31
+ # Lint, machine-readable output; exit code 2 if any diagnostic is severity=error
32
+ mquery check query.pq --json
33
+
34
+ # Rename one top-level let binding - dry run first
35
+ mquery rename query.pq --old OldName --new NewName
36
+ ```
37
+
38
+ ## Python API
39
+
40
+ ```python
41
+ from mquery_toolkit import check, format_source, parse, rename, update_file
42
+
43
+ parsed = parse(source_text) # dict: tokens, rootKind, analysis
44
+ formatted = format_source(source_text) # formatted M source, same encoding
45
+ diagnostics = check(source_text, "query.pq") # list[Diagnostic]
46
+ renamed = rename(source_text, "OldName", "NewName")
47
+
48
+ # File-level edit with the same dry-run/--write safety model as the CLI
49
+ diff = update_file(path, format_source) # dry run: unified diff
50
+ diff = update_file(path, format_source, write=True) # atomic write
51
+ ```
52
+
53
+ ## Diagnostics
54
+
55
+ | Code | Severity | Meaning |
56
+ |---|---|---|
57
+ | `M_PARSE_ERROR` | error | source does not parse |
58
+ | `M001` | error | duplicate `let` binding name |
59
+ | `M002` | warning | `Web.Contents` called with a non-literal (dynamic) URL |
60
+ | `M003` | warning | credential-like literal (`password`/`token`/`secret` = `"..."`) |
61
+ | `M004` | warning | `let` binding unreachable from the result |
62
+ | `M005` | warning | unresolved unqualified reference |
63
+ | `M006` | info | source-function inventory (`*.Contents` dependency) |
64
+
65
+ `M002` and `M003` are conservative text-pattern checks on the raw source, not
66
+ AST checks; expect occasional false positives inside comments or strings.
67
+ Every matching occurrence is reported, one diagnostic per call site or
68
+ literal.
69
+
70
+ `check --json` emits stable objects; `check` without `--json` prints
71
+ `file:line:column: severity code: message` per diagnostic. The CLI exits `2`
72
+ when any diagnostic has severity `error`, `0` otherwise.
73
+
74
+ ## Safety model
75
+
76
+ - **Dry-run by default.** Every edit command (`format`, `rename`,
77
+ `replace-source`) prints a unified diff and touches nothing unless `--write`
78
+ is passed.
79
+ - **`--write` is an atomic replace**: the file is written to a sibling temp
80
+ file, `fsync`'d, `chmod`'d to match the original, then moved into place with
81
+ `os.replace`.
82
+ - **Layout is preserved**: UTF-8 encoding, a leading BOM (present in every
83
+ Power Query SDK connector file), newline convention (`\n` vs `\r\n`),
84
+ final-newline state, and file mode all round-trip unchanged.
85
+ - **Refuses symlinks and hardlinks** - writes require a regular, single-link
86
+ file.
87
+ - **Detects concurrent change**: the source is snapshotted before the
88
+ transform and re-checked immediately before the atomic replace - this final
89
+ snapshot check, not the lock, is the guarantee against lost updates; a
90
+ change in that microsecond window raises `SafeWriteError`.
91
+ - **Advisory lock while writing only** - a `--write` call takes a
92
+ cross-process advisory lock (`fcntl`/`msvcrt`) for the duration of the
93
+ write and removes the lock file afterward, best-effort. It only serialises
94
+ cooperating `mquery` processes and is not a correctness guarantee: because
95
+ the lock file is removed after use, a waiting process and a freshly
96
+ started one can end up locking different inodes. Dry-run calls take no
97
+ lock and create no lock file.
98
+ - This is **not mandatory locking** - no OS provides a portable mandatory
99
+ lock, and the advisory lock is not itself the correctness guard. Use
100
+ source control or external exclusive ownership for concurrent editors.
101
+
102
+ ## Limits
103
+
104
+ - Input and output are capped at **10 MiB**.
105
+ - The Node subprocess is bounded to a **30 second** timeout.
106
+ - Supported extensions: `.pq`, `.m`, `.pqm`, and any `*.query.pq` file.
107
+ - `rename` scope: exactly **one unquoted top-level `let` binding**. It refuses
108
+ quoted identifiers (`#"..."`), record literals, lambda expressions, and
109
+ non-ASCII source.
110
+ - `Retry-After` on the Fabric adapter must be whole seconds; HTTP-date values
111
+ are rejected.
112
+
113
+ ## Optional adapters
114
+
115
+ - **`fabric` extra** (`pip install "mquery-toolkit[fabric]"`) - a Fabric
116
+ Execute Query client that takes a caller-provided bearer token and an
117
+ injected HTTP transport. It never manages credentials itself and is fully
118
+ mocked in tests (no network access in the test suite).
119
+ - **`pqtest`** - a bounded wrapper around a user-installed Microsoft PQTest
120
+ executable, Windows-only, pinned to version `2.155.2`. It never downloads a
121
+ binary; it only validates and runs one already on disk.
122
+
123
+ ## What it is not
124
+
125
+ - Not an M language runtime or evaluator.
126
+ - Not a Power BI or Fabric client, and it does not manage credentials.
127
+ - Not a general-purpose file editor - it only touches files with a supported
128
+ extension and only through the safety model above.
129
+ - Not a replacement for Microsoft's own parser/formatter - it vendors and
130
+ calls them directly rather than reimplementing M syntax.
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ git clone https://github.com/GopalGB/mquery-toolkit
136
+ cd mquery-toolkit
137
+ python -m venv .venv && source .venv/bin/activate
138
+ pip install -e ".[dev,fabric]"
139
+ npm ci --ignore-scripts
140
+
141
+ pytest -q --cov=mquery_toolkit --cov-fail-under=80
142
+ mypy src
143
+ ruff check .
144
+ ruff format --check .
145
+ npm test
146
+ python -m build
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT - see `LICENSE`. Bundled Microsoft packages
152
+ (`@microsoft/powerquery-parser`, `@microsoft/powerquery-formatter`) and their
153
+ dependencies are also MIT; see `THIRD_PARTY_NOTICES.txt` and `NOTICE`.
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mquery-toolkit"
7
+ version = "0.1.0"
8
+ description = "Unofficial offline tooling for Power Query M source (parse, format, check, rename) via Microsoft's official parser and formatter"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE", "NOTICE"]
13
+ authors = [{ name = "Gopal Bagaswar" }]
14
+ keywords = ["power-query", "m-language", "power-bi", "fabric", "linter", "formatter"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Libraries",
25
+ "Topic :: Software Development :: Quality Assurance",
26
+ ]
27
+ dependencies = []
28
+
29
+ [project.optional-dependencies]
30
+ fabric = ["pyarrow>=14"]
31
+ dev = [
32
+ "pytest>=8",
33
+ "pytest-cov>=5",
34
+ "mypy>=1.10",
35
+ "ruff>=0.5",
36
+ "build>=1.2",
37
+ "twine>=6.1",
38
+ "pip-audit>=2.7",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/GopalGB/mquery-toolkit"
43
+ Repository = "https://github.com/GopalGB/mquery-toolkit"
44
+ Issues = "https://github.com/GopalGB/mquery-toolkit/issues"
45
+
46
+ [project.scripts]
47
+ mquery = "mquery_toolkit.cli:main"
48
+
49
+ [tool.setuptools]
50
+ package-dir = { "" = "src" }
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.setuptools.package-data]
56
+ mquery_toolkit = ["_bridge.cjs", "THIRD_PARTY_NOTICES.txt", "py.typed"]
57
+
58
+ [tool.ruff]
59
+ line-length = 88
60
+ target-version = "py311"
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "I", "UP", "B"]
64
+
65
+ [tool.mypy]
66
+ python_version = "3.11"
67
+ strict = true
68
+ files = ["src"]
69
+
70
+ [tool.pytest.ini_options]
71
+ testpaths = ["tests"]
72
+
73
+ [tool.coverage.run]
74
+ source = ["mquery_toolkit"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,34 @@
1
+ Bundled JavaScript dependencies
2
+ ===============================
3
+
4
+ @microsoft/powerquery-parser 2.0.0 and 0.19.0
5
+ Copyright (c) Microsoft Corporation. All rights reserved.
6
+
7
+ @microsoft/powerquery-formatter 1.0.0
8
+ Copyright (c) Microsoft Corporation.
9
+
10
+ grapheme-splitter 1.0.4
11
+ Copyright (c) 2015 Orlin Georgiev
12
+
13
+ performance-now 2.1.0
14
+ Copyright (c) 2013 Braveg1rl
15
+
16
+ All components above are licensed under the MIT License:
17
+
18
+ Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ of this software and associated documentation files (the "Software"), to deal
20
+ in the Software without restriction, including without limitation the rights
21
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+ copies of the Software, and to permit persons to whom the Software is
23
+ furnished to do so, subject to the following conditions:
24
+
25
+ The above copyright notice and this permission notice shall be included in all
26
+ copies or substantial portions of the Software.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34
+ SOFTWARE.
@@ -0,0 +1,35 @@
1
+ """Typed offline tooling for Power Query M source."""
2
+
3
+ from .core import (
4
+ AdapterError,
5
+ Diagnostic,
6
+ MQueryError,
7
+ NodeError,
8
+ ParseError,
9
+ RenameRefusal,
10
+ SafeWriteError,
11
+ check,
12
+ dependencies,
13
+ format_source,
14
+ parse,
15
+ rename,
16
+ replace_source,
17
+ update_file,
18
+ )
19
+
20
+ __all__ = [
21
+ "AdapterError",
22
+ "Diagnostic",
23
+ "MQueryError",
24
+ "NodeError",
25
+ "ParseError",
26
+ "RenameRefusal",
27
+ "SafeWriteError",
28
+ "check",
29
+ "dependencies",
30
+ "format_source",
31
+ "parse",
32
+ "rename",
33
+ "replace_source",
34
+ "update_file",
35
+ ]