mquery-toolkit 0.1.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.
@@ -0,0 +1,213 @@
1
+ """Mockable, caller-authenticated Fabric Execute Query state machine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import re
7
+ from collections.abc import Callable, Mapping
8
+ from dataclasses import dataclass
9
+ from time import monotonic, sleep
10
+ from typing import Any
11
+ from urllib.parse import quote, urljoin, urlsplit, urlunsplit
12
+
13
+ from .core import MAX_BYTES, AdapterError
14
+
15
+ Response = Mapping[str, Any]
16
+ Transport = Callable[
17
+ [str, Mapping[str, str], Mapping[str, Any] | None, float], Response
18
+ ]
19
+ ArrowValidator = Callable[[bytes], None]
20
+ _OPERATION_ID = re.compile(r"^[A-Za-z0-9-]{1,128}$")
21
+
22
+
23
+ def redact(value: str) -> str:
24
+ return "<redacted>" if value else ""
25
+
26
+
27
+ def _same_origin(start: str, candidate: str) -> bool:
28
+ left, right = urlsplit(start), urlsplit(candidate)
29
+ return (left.scheme, left.netloc) == (right.scheme, right.netloc)
30
+
31
+
32
+ def _validate_arrow(body: bytes) -> None:
33
+ if len(body) > MAX_BYTES:
34
+ raise AdapterError("Fabric Arrow stream exceeds 10 MiB")
35
+ try:
36
+ import pyarrow as pa # type: ignore[import-untyped]
37
+ except ImportError as error:
38
+ raise AdapterError(
39
+ "Arrow validation requires the mquery-toolkit[fabric] extra"
40
+ ) from error
41
+ stream = io.BytesIO(body)
42
+ streams = 0
43
+ total_bytes = 0
44
+ try:
45
+ while stream.tell() < len(body):
46
+ start = stream.tell()
47
+ reader = pa.ipc.open_stream(stream)
48
+ metadata = {
49
+ key.decode("utf-8", "replace"): value.decode("utf-8", "replace")
50
+ for key, value in (reader.schema.metadata or {}).items()
51
+ }
52
+ if metadata.get("IsError", "").lower() == "true":
53
+ code = metadata.get("FaultCode", "unknown")
54
+ message = metadata.get("FaultString", "Fabric query failed")
55
+ raise AdapterError(f"Fabric Arrow error [{code}]: {message}")
56
+ while True:
57
+ try:
58
+ batch = reader.read_next_batch()
59
+ except StopIteration:
60
+ break
61
+ total_bytes += batch.nbytes
62
+ if total_bytes > MAX_BYTES:
63
+ raise AdapterError("Fabric Arrow stream exceeds 10 MiB")
64
+ streams += 1
65
+ if stream.tell() <= start:
66
+ raise AdapterError("Fabric Arrow decoder made no progress")
67
+ except AdapterError:
68
+ raise
69
+ except Exception as error:
70
+ raise AdapterError("Fabric returned an invalid Arrow stream") from error
71
+ if streams == 0:
72
+ raise AdapterError("Fabric returned an empty Arrow stream")
73
+
74
+
75
+ def _headers(response: Response) -> Mapping[str, Any]:
76
+ return {
77
+ str(key).lower(): value for key, value in response.get("headers", {}).items()
78
+ }
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class FabricClient:
83
+ transport: Transport
84
+ timeout_seconds: int = 90
85
+ sleeper: Callable[[float], None] = sleep
86
+ clock: Callable[[], float] = monotonic
87
+ arrow_validator: ArrowValidator = _validate_arrow
88
+
89
+ def _request(
90
+ self,
91
+ url: str,
92
+ headers: Mapping[str, str],
93
+ payload: Mapping[str, Any] | None,
94
+ deadline: float,
95
+ ) -> Response:
96
+ remaining = deadline - self.clock()
97
+ if remaining <= 0:
98
+ raise AdapterError("Fabric operation exceeded caller timeout")
99
+ try:
100
+ return self.transport(url, headers, payload, remaining)
101
+ except TimeoutError as error:
102
+ raise AdapterError("Fabric transport timed out") from error
103
+
104
+ @staticmethod
105
+ def _require_https_same_origin(start: str, candidate: str) -> None:
106
+ if urlsplit(candidate).scheme != "https" or not _same_origin(start, candidate):
107
+ raise AdapterError(
108
+ "Fabric operation Location must remain HTTPS and same-origin"
109
+ )
110
+
111
+ @staticmethod
112
+ def _poll_url(start: str, location: str, operation_id: str) -> str:
113
+ candidate = urljoin(start, location) if location else ""
114
+ if not candidate:
115
+ if not _OPERATION_ID.fullmatch(operation_id):
116
+ raise AdapterError("Fabric operation id is missing or invalid")
117
+ origin = urlsplit(start)
118
+ candidate = (
119
+ f"{origin.scheme}://{origin.netloc}/v1/operations/"
120
+ f"{quote(operation_id, safe='')}"
121
+ )
122
+ FabricClient._require_https_same_origin(start, candidate)
123
+ return candidate
124
+
125
+ def execute(self, url: str, token: str, payload: Mapping[str, Any]) -> Response:
126
+ if not token:
127
+ raise AdapterError("Fabric caller token is required")
128
+ if urlsplit(url).scheme != "https":
129
+ raise AdapterError("Fabric URL must use HTTPS")
130
+ headers = {
131
+ "Authorization": f"Bearer {token}",
132
+ "Content-Type": "application/json",
133
+ }
134
+ deadline = self.clock() + self.timeout_seconds
135
+ request_url: str = url
136
+ request_payload: Mapping[str, Any] | None = payload
137
+ operation_id: str | None = None
138
+ result_requested = False
139
+ while True:
140
+ if self.clock() > deadline:
141
+ raise AdapterError("Fabric operation exceeded caller timeout")
142
+ response = self._request(request_url, headers, request_payload, deadline)
143
+ status = response.get("status")
144
+ response_headers = _headers(response)
145
+ if status == 200:
146
+ content_type = str(response_headers.get("content-type", "")).lower()
147
+ body = response.get("body")
148
+ if "json" in content_type and isinstance(body, Mapping):
149
+ state = str(body.get("status", ""))
150
+ if state in {"Running", "NotStarted"}:
151
+ request_payload = None
152
+ self.sleeper(1)
153
+ continue
154
+ if state == "Failed":
155
+ raise AdapterError("Fabric operation reported Failed")
156
+ if state == "Succeeded":
157
+ if request_url == url:
158
+ raise AdapterError(
159
+ "Fabric returned a JSON status instead of an "
160
+ "Arrow stream"
161
+ )
162
+ if result_requested:
163
+ raise AdapterError(
164
+ "Fabric result endpoint returned JSON instead of Arrow"
165
+ )
166
+ parts = urlsplit(request_url)
167
+ result_url = urlunsplit(
168
+ parts._replace(path=parts.path.rstrip("/") + "/result")
169
+ )
170
+ self._require_https_same_origin(url, result_url)
171
+ request_url = result_url
172
+ request_payload = None
173
+ result_requested = True
174
+ continue
175
+ if (
176
+ "arrow" not in content_type
177
+ or not isinstance(body, bytes)
178
+ or not body
179
+ ):
180
+ raise AdapterError(
181
+ "Fabric 200 response is not a non-empty Arrow stream"
182
+ )
183
+ self.arrow_validator(body)
184
+ return response
185
+ retry_raw = response_headers.get("retry-after", 1)
186
+ try:
187
+ retry = max(1, int(str(retry_raw)))
188
+ except ValueError as error:
189
+ raise AdapterError(
190
+ "Fabric Retry-After must be whole seconds"
191
+ ) from error
192
+ if status == 429:
193
+ self.sleeper(min(retry, max(0, deadline - self.clock())))
194
+ continue
195
+ if status != 202:
196
+ shown = status if status is not None else "unknown"
197
+ raise AdapterError(f"Fabric returned HTTP {shown}")
198
+ location = str(response_headers.get("location", ""))
199
+ next_operation = str(response_headers.get("x-ms-operation-id", ""))
200
+ if not location and not next_operation:
201
+ raise AdapterError(
202
+ "Fabric 202 response requires Location or x-ms-operation-id"
203
+ )
204
+ if (
205
+ operation_id is not None
206
+ and next_operation
207
+ and operation_id != next_operation
208
+ ):
209
+ raise AdapterError("Fabric operation id changed during polling")
210
+ operation_id = next_operation or operation_id
211
+ request_url = self._poll_url(url, location, operation_id or "")
212
+ request_payload = None
213
+ self.sleeper(min(retry, max(0, deadline - self.clock())))
@@ -0,0 +1,93 @@
1
+ """Bounded wrapper for a user-installed PQTest executable."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ import re
7
+ import subprocess
8
+ from collections.abc import Callable, Sequence
9
+ from pathlib import Path
10
+
11
+ from .core import (
12
+ MAX_BYTES,
13
+ AdapterError,
14
+ _ProcessOutputLimit,
15
+ _run_process_bounded,
16
+ )
17
+
18
+ PQTEST_VERSION = "2.155.2"
19
+ PQTEST_TIMEOUT_SECONDS = 300
20
+ Runner = Callable[..., subprocess.CompletedProcess[str]]
21
+
22
+
23
+ def _run(
24
+ runner: Runner, command: Sequence[str], timeout: int = PQTEST_TIMEOUT_SECONDS
25
+ ) -> subprocess.CompletedProcess[str]:
26
+ if runner is subprocess.run:
27
+ return _run_bounded(command, timeout)
28
+ try:
29
+ result = runner(
30
+ command,
31
+ capture_output=True,
32
+ text=True,
33
+ encoding="utf-8",
34
+ errors="strict",
35
+ check=False,
36
+ timeout=timeout,
37
+ )
38
+ except (OSError, subprocess.SubprocessError, UnicodeError) as error:
39
+ raise AdapterError("PQTest process failed") from error
40
+ if (
41
+ len(result.stdout.encode()) > MAX_BYTES
42
+ or len(result.stderr.encode()) > MAX_BYTES
43
+ ):
44
+ raise AdapterError("PQTest output exceeds 10 MiB")
45
+ if result.returncode:
46
+ raise AdapterError(f"PQTest exited {result.returncode}")
47
+ return result
48
+
49
+
50
+ def _run_bounded(
51
+ command: Sequence[str], timeout: int = PQTEST_TIMEOUT_SECONDS
52
+ ) -> subprocess.CompletedProcess[str]:
53
+ try:
54
+ result = _run_process_bounded(list(command), None, timeout)
55
+ except subprocess.TimeoutExpired as error:
56
+ raise AdapterError(f"PQTest timed out after {timeout} seconds") from error
57
+ except _ProcessOutputLimit as error:
58
+ raise AdapterError("PQTest output exceeds 10 MiB") from error
59
+ except OSError as error:
60
+ raise AdapterError("PQTest process failed") from error
61
+ try:
62
+ output = result.stdout.decode("utf-8", "strict")
63
+ error_output = result.stderr.decode("utf-8", "strict")
64
+ except UnicodeDecodeError as error:
65
+ raise AdapterError("PQTest output is not valid UTF-8") from error
66
+ if result.returncode:
67
+ raise AdapterError(f"PQTest exited {result.returncode}")
68
+ return subprocess.CompletedProcess(command, result.returncode, output, error_output)
69
+
70
+
71
+ def validate_pqtest(
72
+ path: Path,
73
+ runner: Runner = subprocess.run,
74
+ timeout: int = PQTEST_TIMEOUT_SECONDS,
75
+ ) -> Path:
76
+ if platform.system() != "Windows":
77
+ raise AdapterError("PQTest adapter is supported on Windows only")
78
+ if not path.is_file() or path.is_symlink() or path.suffix.lower() != ".exe":
79
+ raise AdapterError("PQTest path must name a user-installed regular .exe")
80
+ version = _run(runner, [str(path), "version"], timeout).stdout
81
+ if not re.search(rf"(?<![0-9.]){re.escape(PQTEST_VERSION)}(?![0-9.])", version):
82
+ raise AdapterError(f"PQTest {PQTEST_VERSION} is required")
83
+ return path
84
+
85
+
86
+ def run_pqtest(
87
+ path: Path,
88
+ args: Sequence[str],
89
+ runner: Runner = subprocess.run,
90
+ timeout: int = PQTEST_TIMEOUT_SECONDS,
91
+ ) -> str:
92
+ validate_pqtest(path, runner, timeout)
93
+ return _run(runner, [str(path), *args], timeout).stdout
File without changes
@@ -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,15 @@
1
+ mquery_toolkit/THIRD_PARTY_NOTICES.txt,sha256=D77F5aSJndizys2F9xVBof9KLyJlXZ-dbJmUC7XKvSU,1438
2
+ mquery_toolkit/__init__.py,sha256=kqj3em44u-DjdVMhTo0kszbKeeU6SBnUbcY7aIeqzrw,578
3
+ mquery_toolkit/_bridge.cjs,sha256=obBJjhiN2LdYF_cTgyaV_O0gy9UcPiXzbZi0YkLXMXM,2601383
4
+ mquery_toolkit/cli.py,sha256=tnK8-UMsmAlQvW46f97NKCRK1rUaD8hsgKsU-iqnVm4,3627
5
+ mquery_toolkit/core.py,sha256=1w1ocGduLXjLwgRsMRs-wg5sRYc8a7OvdHQ5sdJhCPU,20363
6
+ mquery_toolkit/fabric.py,sha256=-8HfRfVxEhrw-xuYzo0l-zXuftROkGkMQCfSqvv-v7c,8703
7
+ mquery_toolkit/pqtest.py,sha256=5Fh5l6X8updrezAgkm-5pB4uBuzBPw1xK9mrfjVenIw,3152
8
+ mquery_toolkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ mquery_toolkit-0.1.0.dist-info/licenses/LICENSE,sha256=gszjyPUNaVUz78eaIo48Yi0qo0GvBv9lLMerAT-C5lk,1071
10
+ mquery_toolkit-0.1.0.dist-info/licenses/NOTICE,sha256=XchoRITIzvbsT8KGQc3RLpb-Tbuz0a_76Skq3J_ZwCg,509
11
+ mquery_toolkit-0.1.0.dist-info/METADATA,sha256=aehnhePuQYzibBaRzPMJp90lkwB5R8-WUexVxibPMjw,7645
12
+ mquery_toolkit-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ mquery_toolkit-0.1.0.dist-info/entry_points.txt,sha256=uT91guSn55M3YHnhCxV6Vw6bo2VuqXcovQwrT9ATl_s,51
14
+ mquery_toolkit-0.1.0.dist-info/top_level.txt,sha256=tlfo50yrjuNCMRVyi2HM_yIEWB24nhp7Px2hyKxJ3Rc,15
15
+ mquery_toolkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mquery = mquery_toolkit.cli:main
@@ -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 @@
1
+ mquery_toolkit