duckdb-kql 0.0.1.dev4__tar.gz → 0.0.1.dev5__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.
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/PKG-INFO +2 -1
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/kusto-server.md +7 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/pyproject.toml +2 -1
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_version.py +2 -2
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/server.py +35 -7
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_server.py +100 -6
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_workflows.py +4 -3
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/.gitignore +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/CODE_OF_CONDUCT.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/CONTRIBUTING.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/LICENSE +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/README.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/SECURITY.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/THIRD-PARTY-NOTICES.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/demo/demo.ipynb +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/demo/demo.kql +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/TRANSLATION.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/ai-cost-strategy.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/api.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/azure-monitor-profile.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/cli.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/README.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/kusto-client-compat.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/public-api-and-typing.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/review-2026-08-04.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/security-and-injection.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/testing-oracle-and-fixtures.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/tooling-packaging-ci-docs.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/translation-correctness.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/frequency-scan-results.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/getting-started.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/implementation-options.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/implementation-plan.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/kql-on-duckdb-landscape.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/kql-support.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/kusto-client.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/lessons-from-bun-rewrite.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/licensing.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/m0-grammar-spike.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/oracle-harness.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/test-plan.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/grammar/Kql.g4 +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/grammar/KqlTokens.g4 +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/grammar/UPSTREAM.md +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/__main__.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/Kql.interp +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/Kql.tokens +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.interp +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.tokens +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlListener.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlParser.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlVisitor.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/cli.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/comparison.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/control.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/engine.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/errors.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/ir.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/_models.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/client.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/client_request_properties.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/exceptions.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/helpers.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/response.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/lower.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/oracle.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/params.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/parser.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/py.typed +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/schema.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/translate/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/translate/functions.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/types.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/conftest.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/profiles/azure-monitor.json +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_behavior.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_cli.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_column_order_and_null_sort.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_comparison.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_control_commands.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_corpus.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_datetime_traps.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_demo_notebook.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_devcontainers.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_docs.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_dynamic.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_getschema.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_join.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_kusto_client.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_let.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_null_semantics.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_parse.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_profile_azure_monitor.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_query_parameters.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_range_in_render.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_summarize.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_support_matrix.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tests/test_typing.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tools/check_profile.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tools/frequency_scan.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tools/gen_support_matrix.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tools/harvest_docs.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tools/make_fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tools/regen_expectations.py +0 -0
- {duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/tools/regen_parser.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: duckdb-kql
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev5
|
|
4
4
|
Summary: Run Kusto KQL queries on DuckDB, from Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/mmaitre314/duckdb-kql
|
|
6
6
|
Project-URL: Documentation, https://github.com/mmaitre314/duckdb-kql/blob/main/docs/getting-started.md
|
|
@@ -44,6 +44,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
44
44
|
Classifier: Programming Language :: Python :: 3.11
|
|
45
45
|
Classifier: Programming Language :: Python :: 3.12
|
|
46
46
|
Classifier: Programming Language :: Python :: 3.13
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
47
48
|
Classifier: Topic :: Database
|
|
48
49
|
Classifier: Topic :: Database :: Front-Ends
|
|
49
50
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
@@ -66,6 +66,13 @@ https://dataexplorer.azure.us
|
|
|
66
66
|
`--allow-origin` replaces that list. Widening it is a decision about who may read
|
|
67
67
|
this database from another browser tab, not a formatting preference.
|
|
68
68
|
|
|
69
|
+
The **origin** is the whole check. Request *headers* are not: a preflight echoes
|
|
70
|
+
back whatever `Access-Control-Request-Headers` asked for, because naming a header
|
|
71
|
+
authorises nothing and a hand-written list is only a second place to be wrong
|
|
72
|
+
about someone else's client. It was wrong once — the list said `x-ms-user` where
|
|
73
|
+
the web UI sends `x-ms-user-id`, and the browser answered by failing the real
|
|
74
|
+
POST with a bare `net::ERR_FAILED` after a preflight that returned 204.
|
|
75
|
+
|
|
69
76
|
**Nothing it serves can write.** The translated surface is read-only: no
|
|
70
77
|
supported KQL operator produces a statement that modifies the database, and
|
|
71
78
|
control commands that would administer a cluster are refused rather than
|
|
@@ -31,6 +31,7 @@ classifiers = [
|
|
|
31
31
|
"Programming Language :: Python :: 3.11",
|
|
32
32
|
"Programming Language :: Python :: 3.12",
|
|
33
33
|
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Programming Language :: Python :: 3.14",
|
|
34
35
|
"Topic :: Database",
|
|
35
36
|
"Topic :: Database :: Front-Ends",
|
|
36
37
|
"Topic :: Software Development :: Compilers",
|
|
@@ -167,7 +168,7 @@ files = ["src/duckdb_kql"]
|
|
|
167
168
|
# way, and the next dependency to adopt the syntax would do it again. Letting
|
|
168
169
|
# each interpreter check against itself is more robust *and* better coverage:
|
|
169
170
|
# CI runs the floor and the ceiling, so 3.10-incompatible code fails on the
|
|
170
|
-
# 3.10 lane while the newest semantics are exercised on the
|
|
171
|
+
# 3.10 lane while the newest semantics are exercised on the newest one.
|
|
171
172
|
disallow_untyped_defs = true
|
|
172
173
|
disallow_incomplete_defs = true
|
|
173
174
|
disallow_untyped_calls = true
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0, 1, '
|
|
21
|
+
__version__ = version = '0.0.1.dev5'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 1, 'dev5')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -41,6 +41,7 @@ import uuid
|
|
|
41
41
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
42
42
|
from typing import TYPE_CHECKING, Any, NamedTuple, cast
|
|
43
43
|
|
|
44
|
+
from . import __version__
|
|
44
45
|
from .control import SCHEMA, CommandColumn, is_control_command, split_command
|
|
45
46
|
from .errors import KqlError
|
|
46
47
|
from .types import kusto_type, rest_datatype
|
|
@@ -347,11 +348,18 @@ def error_response(message: str, *, code: str = "General_BadRequest") -> dict[st
|
|
|
347
348
|
|
|
348
349
|
_LOOPBACK = re.compile(r"^(127\.\d+\.\d+\.\d+|::1)$")
|
|
349
350
|
|
|
351
|
+
#: Only for a preflight that names no headers, which no browser sends — a
|
|
352
|
+
#: preflight exists *because* there is something non-simple to declare. Kept so
|
|
353
|
+
#: the header is never emitted empty.
|
|
354
|
+
_DEFAULT_ALLOW_HEADERS = "Authorization, Content-Type, Accept"
|
|
355
|
+
|
|
350
356
|
|
|
351
357
|
class _Handler(BaseHTTPRequestHandler):
|
|
352
358
|
"""One request. The connection and the allow-list come from the server."""
|
|
353
359
|
|
|
354
|
-
|
|
360
|
+
# `duckdb-kql/0.1.2`, not `duckdb-kql/duckdb-kql`: the slot after the slash
|
|
361
|
+
# is a version, and this used to interpolate the cluster name into it.
|
|
362
|
+
server_version = f"duckdb-kql/{__version__}"
|
|
355
363
|
# Announce HTTP/1.1 so the web UI's keep-alive works; every response below
|
|
356
364
|
# sets Content-Length, which is what makes that safe.
|
|
357
365
|
protocol_version = "HTTP/1.1"
|
|
@@ -406,17 +414,37 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
406
414
|
self.send_response(204)
|
|
407
415
|
self.send_header("Access-Control-Allow-Origin", origin)
|
|
408
416
|
self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
|
409
|
-
self.send_header(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
)
|
|
417
|
+
self.send_header("Access-Control-Allow-Headers", self._allow_headers())
|
|
418
|
+
# Only when asked. Chrome gates requests from a public page to a private
|
|
419
|
+
# address behind this; answering unprompted would claim a policy the
|
|
420
|
+
# browser did not ask about.
|
|
421
|
+
if self.headers.get("Access-Control-Request-Private-Network") == "true":
|
|
422
|
+
self.send_header("Access-Control-Allow-Private-Network", "true")
|
|
414
423
|
self.send_header("Access-Control-Allow-Credentials", "true")
|
|
415
424
|
self.send_header("Access-Control-Max-Age", "600")
|
|
416
|
-
self.send_header("Vary", "Origin")
|
|
425
|
+
self.send_header("Vary", "Origin, Access-Control-Request-Headers")
|
|
417
426
|
self.send_header("Content-Length", "0")
|
|
418
427
|
self.end_headers()
|
|
419
428
|
|
|
429
|
+
def _allow_headers(self) -> str:
|
|
430
|
+
"""The request headers the preflight permits: whatever was asked for.
|
|
431
|
+
|
|
432
|
+
Echoed rather than listed, because a hand-written list is a second place
|
|
433
|
+
to be wrong about someone else's client. It *was* wrong: the list said
|
|
434
|
+
`x-ms-user` where the Azure Data Explorer UI sends `x-ms-user-id`, CORS
|
|
435
|
+
matches header names exactly, and the browser answered by failing the
|
|
436
|
+
real POST with a bare `net::ERR_FAILED` — a preflight that returned 204
|
|
437
|
+
and still blocked the request. Every header the UI adds in future would
|
|
438
|
+
break it the same way.
|
|
439
|
+
|
|
440
|
+
This gives up nothing. Naming a header does not authorise anything; the
|
|
441
|
+
origin allow-list and the loopback bind are what decide who may talk to
|
|
442
|
+
this endpoint, and both are checked before we get here. `*` would be the
|
|
443
|
+
lazy version of this and is not equivalent — it is invalid alongside
|
|
444
|
+
`Allow-Credentials: true`, which is why the exact list is echoed back.
|
|
445
|
+
"""
|
|
446
|
+
return self.headers.get("Access-Control-Request-Headers", _DEFAULT_ALLOW_HEADERS)
|
|
447
|
+
|
|
420
448
|
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's interface
|
|
421
449
|
"""A human landing page. Anything else 404s."""
|
|
422
450
|
if not self._is_local():
|
|
@@ -146,16 +146,110 @@ def test_an_unknown_route_is_a_404_not_a_crash(server) -> None:
|
|
|
146
146
|
# ---------------------------------------------------------------------------
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
#: What Chrome asks for on behalf of the Azure Data Explorer UI, captured
|
|
150
|
+
#: verbatim from a `dataexplorer.azure.com` HAR against this server.
|
|
151
|
+
ADX_REQUEST_HEADERS = "authorization,content-type,x-ms-app,x-ms-client-request-id,x-ms-user-id"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def preflight(server: Any, requested: str = ADX_REQUEST_HEADERS, *, origin: str = ALLOWED_ORIGIN):
|
|
155
|
+
request = urllib.request.Request(server.url + "/v1/rest/mgmt", method="OPTIONS")
|
|
156
|
+
request.add_header("Origin", origin)
|
|
157
|
+
request.add_header("Access-Control-Request-Method", "POST")
|
|
158
|
+
request.add_header("Access-Control-Request-Headers", requested)
|
|
159
|
+
try:
|
|
160
|
+
with urllib.request.urlopen(request, timeout=10) as response:
|
|
161
|
+
return response.status, dict(response.headers)
|
|
162
|
+
except urllib.error.HTTPError as exc:
|
|
163
|
+
with exc:
|
|
164
|
+
return exc.status, dict(exc.headers)
|
|
165
|
+
|
|
166
|
+
|
|
149
167
|
def test_a_preflight_from_the_web_ui_is_allowed(server) -> None:
|
|
150
|
-
status, headers
|
|
168
|
+
status, headers = preflight(server)
|
|
151
169
|
assert status == 204
|
|
152
170
|
assert headers["Access-Control-Allow-Origin"] == ALLOWED_ORIGIN
|
|
153
171
|
assert "POST" in headers["Access-Control-Allow-Methods"]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_the_preflight_covers_every_header_the_web_ui_asks_for(server) -> None:
|
|
175
|
+
"""The check a browser actually performs, done the way it performs it.
|
|
176
|
+
|
|
177
|
+
A preflight can return 204 and still block the request: the browser compares
|
|
178
|
+
`Access-Control-Request-Headers` against `Access-Control-Allow-Headers` and
|
|
179
|
+
fails the *real* call — as a bare `net::ERR_FAILED`, with nothing in the
|
|
180
|
+
response to point at. That happened, because the allow-list said `x-ms-user`
|
|
181
|
+
where the UI sends `x-ms-user-id`, and asserting "authorization is in the
|
|
182
|
+
string" did not catch it. Set coverage is the assertion that would have.
|
|
183
|
+
"""
|
|
184
|
+
_, headers = preflight(server)
|
|
185
|
+
allowed = {h.strip().lower() for h in headers["Access-Control-Allow-Headers"].split(",")}
|
|
186
|
+
asked = [h.strip().lower() for h in ADX_REQUEST_HEADERS.split(",")]
|
|
187
|
+
assert not [h for h in asked if h not in allowed], (
|
|
188
|
+
f"preflight would block the request: {headers['Access-Control-Allow-Headers']}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_a_header_the_ui_adds_later_does_not_break_the_preflight(server) -> None:
|
|
193
|
+
"""Echoing is the point: the previous list broke the moment the UI changed."""
|
|
194
|
+
_, headers = preflight(server, "content-type,x-ms-invented-tomorrow")
|
|
195
|
+
allowed = {h.strip().lower() for h in headers["Access-Control-Allow-Headers"].split(",")}
|
|
196
|
+
assert "x-ms-invented-tomorrow" in allowed
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_the_preflight_never_answers_with_a_wildcard(server) -> None:
|
|
200
|
+
"""`*` is invalid next to `Allow-Credentials: true`, so it is not a shortcut.
|
|
201
|
+
|
|
202
|
+
Echoing the requested list is not a wildcard in disguise either — what
|
|
203
|
+
decides who may reach this endpoint is the origin allow-list and the
|
|
204
|
+
loopback bind, both checked before a header name is ever read.
|
|
205
|
+
"""
|
|
206
|
+
_, headers = preflight(server)
|
|
207
|
+
assert headers["Access-Control-Allow-Headers"] != "*"
|
|
208
|
+
assert headers["Access-Control-Allow-Origin"] == ALLOWED_ORIGIN
|
|
209
|
+
assert "Access-Control-Request-Headers" in headers.get("Vary", "")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_the_preflight_answers_the_private_network_question_only_when_asked(server) -> None:
|
|
213
|
+
"""Chrome gates a public page reaching a private address behind this."""
|
|
214
|
+
request = urllib.request.Request(server.url + "/v1/rest/mgmt", method="OPTIONS")
|
|
215
|
+
request.add_header("Origin", ALLOWED_ORIGIN)
|
|
216
|
+
request.add_header("Access-Control-Request-Method", "POST")
|
|
217
|
+
request.add_header("Access-Control-Request-Private-Network", "true")
|
|
218
|
+
with urllib.request.urlopen(request, timeout=10) as response:
|
|
219
|
+
assert response.headers["Access-Control-Allow-Private-Network"] == "true"
|
|
220
|
+
|
|
221
|
+
_, headers = preflight(server)
|
|
222
|
+
assert "Access-Control-Allow-Private-Network" not in headers
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_the_post_the_web_ui_makes_after_the_preflight_succeeds(server) -> None:
|
|
226
|
+
"""The request the browser blocked. End to end, with the UI's own headers."""
|
|
227
|
+
request = urllib.request.Request(
|
|
228
|
+
server.url + "/v1/rest/mgmt",
|
|
229
|
+
data=json.dumps({"csl": ".show version", "properties": None}).encode(),
|
|
230
|
+
headers={
|
|
231
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
232
|
+
"Accept": "application/json",
|
|
233
|
+
"Origin": ALLOWED_ORIGIN,
|
|
234
|
+
"x-ms-app": "Kusto.Web.KWE:2.259.0-5|embeddedIn:dataexplorer.azure.com",
|
|
235
|
+
"x-ms-client-request-id": "Kusto.Web.KWE.Query;29e3ba3d;740cd2cb",
|
|
236
|
+
"x-ms-user-id": "someone",
|
|
237
|
+
"Authorization": "Bearer ignored",
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
with urllib.request.urlopen(request, timeout=10) as response:
|
|
241
|
+
assert response.status == 200
|
|
242
|
+
assert response.headers["Access-Control-Allow-Origin"] == ALLOWED_ORIGIN
|
|
243
|
+
payload = json.loads(response.read())
|
|
244
|
+
assert payload["Tables"][0]["Columns"][0]["ColumnName"] == "BuildVersion"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_the_server_header_reports_a_version(server) -> None:
|
|
248
|
+
"""It read `duckdb-kql/duckdb-kql` — the cluster name in the version slot."""
|
|
249
|
+
from duckdb_kql import __version__ # noqa: PLC0415
|
|
250
|
+
|
|
251
|
+
_, headers, _ = call(server, "/", method="GET")
|
|
252
|
+
assert headers["Server"].startswith(f"duckdb-kql/{__version__} ")
|
|
159
253
|
|
|
160
254
|
|
|
161
255
|
def test_a_preflight_from_anywhere_else_is_refused(server) -> None:
|
|
@@ -52,9 +52,10 @@ def test_ci_runs_on_pushes_to_main() -> None:
|
|
|
52
52
|
def test_the_ci_matrix_spans_the_supported_range() -> None:
|
|
53
53
|
"""The lanes must be the floor and the ceiling of what we claim to support.
|
|
54
54
|
|
|
55
|
-
Claiming
|
|
56
|
-
``main``: numpy 2.5 ships stubs using PEP 695 syntax
|
|
57
|
-
the newer interpreters,
|
|
55
|
+
Claiming a version and never running it is how a version-specific failure
|
|
56
|
+
reaches ``main``: numpy 2.5 ships stubs using PEP 695 syntax that only
|
|
57
|
+
resolves on the newer interpreters, and it broke a lane nobody had run
|
|
58
|
+
locally. So adding a classifier without adding the lane fails here.
|
|
58
59
|
"""
|
|
59
60
|
from conftest import read_pyproject # noqa: PLC0415
|
|
60
61
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/testing-oracle-and-fixtures.md
RENAMED
|
File without changes
|
{duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/docs/code-review/tooling-packaging-ci-docs.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{duckdb_kql-0.0.1.dev4 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/client_request_properties.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|