duckdb-kql 0.0.1.dev3__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.
Files changed (111) hide show
  1. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/PKG-INFO +3 -2
  2. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/README.md +1 -1
  3. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/demo/demo.ipynb +1 -1
  4. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/api.md +1 -1
  5. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/cli.md +24 -19
  6. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/getting-started.md +1 -1
  7. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/kusto-server.md +7 -0
  8. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/pyproject.toml +2 -1
  9. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_version.py +2 -2
  10. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/cli.py +114 -84
  11. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/server.py +35 -7
  12. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_cli.py +92 -40
  13. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_server.py +110 -8
  14. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_workflows.py +4 -3
  15. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/.gitignore +0 -0
  16. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/CODE_OF_CONDUCT.md +0 -0
  17. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/CONTRIBUTING.md +0 -0
  18. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/LICENSE +0 -0
  19. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/SECURITY.md +0 -0
  20. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/THIRD-PARTY-NOTICES.md +0 -0
  21. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/demo/demo.kql +0 -0
  22. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/TRANSLATION.md +0 -0
  23. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/ai-cost-strategy.md +0 -0
  24. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/azure-monitor-profile.md +0 -0
  25. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/README.md +0 -0
  26. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/kusto-client-compat.md +0 -0
  27. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/public-api-and-typing.md +0 -0
  28. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/review-2026-08-04.md +0 -0
  29. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/security-and-injection.md +0 -0
  30. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/testing-oracle-and-fixtures.md +0 -0
  31. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/tooling-packaging-ci-docs.md +0 -0
  32. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/translation-correctness.md +0 -0
  33. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/frequency-scan-results.md +0 -0
  34. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/implementation-options.md +0 -0
  35. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/implementation-plan.md +0 -0
  36. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/kql-on-duckdb-landscape.md +0 -0
  37. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/kql-support.md +0 -0
  38. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/kusto-client.md +0 -0
  39. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/lessons-from-bun-rewrite.md +0 -0
  40. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/licensing.md +0 -0
  41. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/m0-grammar-spike.md +0 -0
  42. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/oracle-harness.md +0 -0
  43. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/test-plan.md +0 -0
  44. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/grammar/Kql.g4 +0 -0
  45. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/grammar/KqlTokens.g4 +0 -0
  46. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/grammar/UPSTREAM.md +0 -0
  47. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/__init__.py +0 -0
  48. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/__main__.py +0 -0
  49. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/Kql.interp +0 -0
  50. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/Kql.tokens +0 -0
  51. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.interp +0 -0
  52. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.py +0 -0
  53. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.tokens +0 -0
  54. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlListener.py +0 -0
  55. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlParser.py +0 -0
  56. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlVisitor.py +0 -0
  57. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/__init__.py +0 -0
  58. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/comparison.py +0 -0
  59. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/control.py +0 -0
  60. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/engine.py +0 -0
  61. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/errors.py +0 -0
  62. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/fixtures.py +0 -0
  63. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/ir.py +0 -0
  64. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/__init__.py +0 -0
  65. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/_models.py +0 -0
  66. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/client.py +0 -0
  67. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/client_request_properties.py +0 -0
  68. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/exceptions.py +0 -0
  69. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/helpers.py +0 -0
  70. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/response.py +0 -0
  71. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/lower.py +0 -0
  72. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/oracle.py +0 -0
  73. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/params.py +0 -0
  74. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/parser.py +0 -0
  75. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/py.typed +0 -0
  76. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/schema.py +0 -0
  77. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/translate/__init__.py +0 -0
  78. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/translate/functions.py +0 -0
  79. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/types.py +0 -0
  80. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/conftest.py +0 -0
  81. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/profiles/azure-monitor.json +0 -0
  82. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_behavior.py +0 -0
  83. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_column_order_and_null_sort.py +0 -0
  84. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_comparison.py +0 -0
  85. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_control_commands.py +0 -0
  86. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_corpus.py +0 -0
  87. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_datetime_traps.py +0 -0
  88. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_demo_notebook.py +0 -0
  89. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_devcontainers.py +0 -0
  90. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_docs.py +0 -0
  91. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_dynamic.py +0 -0
  92. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_fixtures.py +0 -0
  93. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_getschema.py +0 -0
  94. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_join.py +0 -0
  95. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_kusto_client.py +0 -0
  96. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_let.py +0 -0
  97. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_null_semantics.py +0 -0
  98. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_parse.py +0 -0
  99. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_profile_azure_monitor.py +0 -0
  100. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_query_parameters.py +0 -0
  101. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_range_in_render.py +0 -0
  102. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_summarize.py +0 -0
  103. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_support_matrix.py +0 -0
  104. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_typing.py +0 -0
  105. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/check_profile.py +0 -0
  106. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/frequency_scan.py +0 -0
  107. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/gen_support_matrix.py +0 -0
  108. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/harvest_docs.py +0 -0
  109. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/make_fixtures.py +0 -0
  110. {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/regen_expectations.py +0 -0
  111. {duckdb_kql-0.0.1.dev3 → 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.dev3
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
@@ -133,7 +134,7 @@ Translate at build time and the output has no dependency on this package at all
133
134
  — not even Python. Only your CI machine installs it.
134
135
 
135
136
  ```bash
136
- duckdb-kql queries/ -o build/sql/ --check # fails the build if a .sql is stale
137
+ duckdb-kql translate queries/ -o build/sql/ --check # fails the build if a .sql is stale
137
138
  ```
138
139
 
139
140
  See [Build-time translation](https://github.com/mmaitre314/duckdb-kql/blob/main/docs/cli.md).
@@ -52,7 +52,7 @@ Translate at build time and the output has no dependency on this package at all
52
52
  — not even Python. Only your CI machine installs it.
53
53
 
54
54
  ```bash
55
- duckdb-kql queries/ -o build/sql/ --check # fails the build if a .sql is stale
55
+ duckdb-kql translate queries/ -o build/sql/ --check # fails the build if a .sql is stale
56
56
  ```
57
57
 
58
58
  See [Build-time translation](https://github.com/mmaitre314/duckdb-kql/blob/main/docs/cli.md).
@@ -457,7 +457,7 @@
457
457
  }
458
458
  ],
459
459
  "source": [
460
- "!duckdb-kql demo.kql"
460
+ "!duckdb-kql translate demo.kql"
461
461
  ]
462
462
  },
463
463
  {
@@ -277,7 +277,7 @@ translates `.kql` files to `.sql` so the output can be run without this package
277
277
  installed at all. `--check` makes a stale generated file fail CI.
278
278
 
279
279
  ```bash
280
- duckdb-kql queries/ -o build/sql/ --check
280
+ duckdb-kql translate queries/ -o build/sql/ --check
281
281
  ```
282
282
 
283
283
  Full reference, including the generated header and how to bind the placeholders
@@ -1,19 +1,18 @@
1
- # Build-time translation (`duckdb-kql`)
1
+ # Build-time translation (`duckdb-kql translate`)
2
2
 
3
3
  Translate `.kql` to `.sql` once, in CI. What ships is plain SQL, and **nothing
4
4
  that runs it needs this package** — not the transpiler, not Python.
5
5
 
6
- > There is one subcommand, `duckdb-kql serve`, which does something else
7
- > entirely: it runs a [local Kusto REST endpoint](kusto-server.md) over a DuckDB
8
- > database. Everything that is not the literal word `serve` is a file to
9
- > translate, so nothing on this page changes.
10
-
11
6
  ```bash
12
- pip install duckdb-kql # antlr4 only; no database
13
- duckdb-kql queries/ -o build/sql/ # translate
14
- duckdb-kql queries/ -o build/sql/ --check # fail the build if stale
7
+ pip install duckdb-kql # antlr4 only; no database
8
+ duckdb-kql translate queries/ -o build/sql/ # translate
9
+ duckdb-kql translate queries/ -o build/sql/ --check # fail the build if stale
15
10
  ```
16
11
 
12
+ > `translate` is one of two subcommands. The other,
13
+ > [`duckdb-kql serve`](kusto-server.md), runs a local Kusto REST endpoint over a
14
+ > DuckDB database — a different job with different dependencies.
15
+
17
16
  - [Why](#why)
18
17
  - [Usage](#usage)
19
18
  - [The generated header](#the-generated-header)
@@ -41,7 +40,7 @@ build time means:
41
40
  ## Usage
42
41
 
43
42
  ```
44
- duckdb-kql FILE... [-o PATH] [--check] [--schema FILE] [--no-header] [-v]
43
+ duckdb-kql translate FILE... [-o PATH] [--check] [--schema FILE] [--no-header] [-v]
45
44
  ```
46
45
 
47
46
  | | |
@@ -54,14 +53,20 @@ duckdb-kql FILE... [-o PATH] [--check] [--schema FILE] [--no-header] [-v]
54
53
  | `-v, --verbose` | Report each file written, on stderr. |
55
54
 
56
55
  ```bash
57
- duckdb-kql queries/errors.kql # to stdout
58
- duckdb-kql queries/errors.kql -o build/errors.sql # to one file
59
- duckdb-kql queries/ -o build/ # a directory of each
60
- echo 'print x = 1' | duckdb-kql - # from stdin
56
+ duckdb-kql translate queries/errors.kql # to stdout
57
+ duckdb-kql translate queries/errors.kql -o build/errors.sql # to one file
58
+ duckdb-kql translate queries/ -o build/ # a directory of each
59
+ echo 'print x = 1' | duckdb-kql translate - # from stdin
61
60
  ```
62
61
 
63
- The command is also `python -m duckdb_kql`, for CI jobs where the console script
64
- is not on `PATH`.
62
+ The command is also `python -m duckdb_kql translate`, for CI jobs where the
63
+ console script is not on `PATH`.
64
+
65
+ The verb is required: a bare `duckdb-kql queries/` is a usage error, not a
66
+ translation. It used to be the latter, and reading well was the whole argument
67
+ for it — but a filename in the verb slot is exactly the ambiguity a subcommand
68
+ removes, and the alternative is a second verb that silently collides with a file
69
+ of the same name.
65
70
 
66
71
  ## The generated header
67
72
 
@@ -128,7 +133,7 @@ Commit the `.sql` and let `--check` prove it matches:
128
133
 
129
134
  ```yaml
130
135
  - run: pip install duckdb-kql
131
- - run: duckdb-kql queries/ -o sql/ --check
136
+ - run: duckdb-kql translate queries/ -o sql/ --check
132
137
  ```
133
138
 
134
139
  An edited `.kql` whose `.sql` was not regenerated fails with exit 3 and a list
@@ -140,7 +145,7 @@ If you would rather not commit the output, translate into the build directory
140
145
  and skip `--check`:
141
146
 
142
147
  ```yaml
143
- - run: pip install duckdb-kql && duckdb-kql queries/ -o build/sql/
148
+ - run: pip install duckdb-kql && duckdb-kql translate queries/ -o build/sql/
144
149
  ```
145
150
 
146
151
  Errors are reported as `file:line:column: error: …`, which GitHub Actions and
@@ -160,7 +165,7 @@ time there is no connection, so pass a JSON file:
160
165
  ```
161
166
 
162
167
  ```bash
163
- duckdb-kql queries/ -o build/sql/ --schema schema.json
168
+ duckdb-kql translate queries/ -o build/sql/ --schema schema.json
164
169
  ```
165
170
 
166
171
  Everything else translates schema-free. A `join` without a schema fails with a
@@ -169,7 +169,7 @@ There is a command for exactly this. Translate your `.kql` files to `.sql` in
169
169
  CI, ship the SQL, and nothing at runtime needs this package:
170
170
 
171
171
  ```bash
172
- duckdb-kql queries/ -o build/sql/ --check
172
+ duckdb-kql translate queries/ -o build/sql/ --check
173
173
  ```
174
174
 
175
175
  See [Build-time translation](cli.md).
@@ -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 3.13 one.
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.dev3'
22
- __version_tuple__ = version_tuple = (0, 0, 1, 'dev3')
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
@@ -1,30 +1,31 @@
1
- """``duckdb-kql`` — translate ``.kql`` files to ``.sql`` at build time.
2
-
3
- The point of this command is that the *output* has no dependencies. Translate
4
- your queries in CI, commit or ship the ``.sql``, and the thing that runs them
5
- needs nothing from this package — not even DuckDB's Python bindings. A Go
6
- service, a dbt model, a psql script and a notebook can all read the same file.
7
-
8
- Translation is Layer 0 only: no database is opened and ``duckdb`` is never
9
- imported, so ``pip install duckdb-kql`` alone is enough to run it.
1
+ """``duckdb-kql`` — the command line, one subcommand per job.
10
2
 
11
3
  ::
12
4
 
13
- duckdb-kql queries/*.kql -o build/sql/ # translate
14
- duckdb-kql queries/*.kql -o build/sql/ --check # ... and fail if stale
15
-
16
- The ``--check`` mode is the one that belongs in CI: it regenerates in memory and
17
- compares, so a ``.kql`` edited without regenerating its ``.sql`` fails the build
18
- instead of shipping a stale query.
19
-
20
- There is one subcommand, ``serve``, which is a different job entirely — it runs
21
- a local Kusto-compatible HTTP endpoint over a DuckDB database so that Kusto
22
- tools, including the Azure Data Explorer web UI, can query it::
23
-
24
- duckdb-kql serve logs.duckdb
25
-
26
- It needs the ``duckdb`` extra. Everything that is not the literal word ``serve``
27
- is a file to translate, so the interface above is unchanged.
5
+ duckdb-kql translate queries/ -o build/sql/ # KQL files -> SQL files
6
+ duckdb-kql translate queries/ -o build/sql/ --check # ... and fail if stale
7
+ duckdb-kql serve logs.duckdb # a local Kusto endpoint
8
+
9
+ **translate** is the build-time path, and the point of it is that the *output*
10
+ has no dependencies. Translate your queries in CI, commit or ship the ``.sql``,
11
+ and the thing that runs them needs nothing from this package — not even DuckDB's
12
+ Python bindings. A Go service, a dbt model, a psql script and a notebook can all
13
+ read the same file. ``--check`` is the mode that belongs in CI: it regenerates in
14
+ memory and compares, so a ``.kql`` edited without regenerating its ``.sql`` fails
15
+ the build instead of shipping a stale query.
16
+
17
+ It is Layer 0 only — no database is opened and ``duckdb`` is never imported, so
18
+ ``pip install duckdb-kql`` alone is enough to run it.
19
+
20
+ **serve** is a different job entirely: a local Kusto-compatible HTTP endpoint
21
+ over a DuckDB database, so Kusto tools — including the Azure Data Explorer web
22
+ UI — can query it. It needs the ``duckdb`` extra.
23
+
24
+ Every subcommand is explicit. An earlier version took a bare list of files
25
+ (``duckdb-kql queries/ -o build/``), which read well while translation was the
26
+ only thing this command did, but leaves no room for a second verb: any new one
27
+ would be ambiguous with a file of the same name, and the ambiguity would be
28
+ silent. Naming the verb costs one word and keeps the space open.
28
29
  """
29
30
 
30
31
  from __future__ import annotations
@@ -33,9 +34,9 @@ import argparse
33
34
  import datetime as dt
34
35
  import json
35
36
  import sys
36
- from collections.abc import Sequence
37
+ from collections.abc import Callable, Sequence
37
38
  from pathlib import Path
38
- from typing import Any
39
+ from typing import Any, cast
39
40
 
40
41
  from . import __version__, to_sql
41
42
  from .errors import KqlError, KqlSyntaxError
@@ -56,15 +57,17 @@ EXIT_STALE = 3
56
57
 
57
58
  def main(argv: Sequence[str] | None = None) -> int:
58
59
  """Entry point. Returns a process exit code rather than raising."""
59
- argv = list(sys.argv[1:] if argv is None else argv)
60
- # Dispatched by hand rather than with argparse subparsers, because
61
- # subparsers would make the existing form — a bare list of files — a
62
- # subcommand too, and every documented invocation would have to change.
63
- if argv and argv[0] == "serve":
64
- return _serve(argv[1:])
65
-
66
60
  args = _parser().parse_args(argv)
61
+ return cast("Callable[[argparse.Namespace], int]", args.run)(args)
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # translate
66
+ # ---------------------------------------------------------------------------
67
+
67
68
 
69
+ def _translate_command(args: argparse.Namespace) -> int:
70
+ """``duckdb-kql translate`` — KQL files in, SQL files out."""
68
71
  try:
69
72
  schema = _load_schema(args.schema)
70
73
  except (OSError, ValueError) as exc:
@@ -129,9 +132,10 @@ def main(argv: Sequence[str] | None = None) -> int:
129
132
  # ---------------------------------------------------------------------------
130
133
 
131
134
 
132
- def _serve(argv: Sequence[str]) -> int:
135
+ def _serve_command(args: argparse.Namespace) -> int:
133
136
  """``duckdb-kql serve`` — a local Kusto endpoint over a DuckDB database."""
134
- args = _serve_parser().parse_args(argv)
137
+ # Imported here, not at module scope: this is the only subcommand that needs
138
+ # a database, and `translate` is documented to run without one installed.
135
139
  from .server import serve # noqa: PLC0415
136
140
 
137
141
  origins = tuple(args.allow_origin) if args.allow_origin else ADX_ORIGINS
@@ -148,48 +152,6 @@ def _serve(argv: Sequence[str]) -> int:
148
152
  return EXIT_OK
149
153
 
150
154
 
151
- def _serve_parser() -> argparse.ArgumentParser:
152
- parser = argparse.ArgumentParser(
153
- prog="duckdb-kql serve",
154
- description=(
155
- "Serve a DuckDB database over the Kusto REST API, so Kusto tools "
156
- "can query it. Open https://dataexplorer.azure.com, choose Add "
157
- "connection, and give it the URL this prints."
158
- ),
159
- epilog=(
160
- "Listens on 127.0.0.1 only and cannot be made to listen anywhere "
161
- "else: it answers unauthenticated queries, so reaching it has to "
162
- "mean already being on this machine."
163
- ),
164
- formatter_class=argparse.RawDescriptionHelpFormatter,
165
- )
166
- parser.add_argument(
167
- "database",
168
- nargs="?",
169
- default=":memory:",
170
- metavar="DATABASE",
171
- help="DuckDB database file to serve. Omit for an empty in-memory one.",
172
- )
173
- parser.add_argument(
174
- "-p",
175
- "--port",
176
- type=int,
177
- default=DEFAULT_PORT,
178
- metavar="PORT",
179
- help=f"TCP port to listen on (default: {DEFAULT_PORT})",
180
- )
181
- parser.add_argument(
182
- "--allow-origin",
183
- action="append",
184
- metavar="ORIGIN",
185
- help=(
186
- "additionally allow a browser origin to make cross-origin requests. "
187
- "Repeatable. Replaces the Azure Data Explorer default list, and is a "
188
- "decision about who may read this database from another browser tab."
189
- ),
190
- )
191
- parser.add_argument("--version", action="version", version=f"duckdb-kql {__version__}")
192
- return parser
193
155
 
194
156
 
195
157
  # ---------------------------------------------------------------------------
@@ -360,8 +322,34 @@ def _load_schema(path: str | None) -> dict[str, list[str]] | None:
360
322
 
361
323
 
362
324
  def _parser() -> argparse.ArgumentParser:
325
+ """The whole command line. Each subparser stores its handler in ``run``.
326
+
327
+ Dispatching through ``set_defaults(run=...)`` rather than a chain of
328
+ ``if args.command == ...`` means a new subcommand is added in exactly one
329
+ place, and cannot be registered without being wired up.
330
+ """
363
331
  parser = argparse.ArgumentParser(
364
332
  prog="duckdb-kql",
333
+ description=(
334
+ "Run KQL on DuckDB. `translate` turns .kql files into .sql at build "
335
+ "time; `serve` puts a local Kusto REST endpoint in front of a DuckDB "
336
+ "database."
337
+ ),
338
+ epilog=(
339
+ "exit codes: 0 ok; 1 a query failed to translate, or the server "
340
+ "could not start; 2 bad usage; 3 --check found a missing or stale "
341
+ "output"
342
+ ),
343
+ formatter_class=argparse.RawDescriptionHelpFormatter,
344
+ )
345
+ parser.add_argument("--version", action="version", version=f"duckdb-kql {__version__}")
346
+ # `required` so a bare `duckdb-kql` prints usage rather than a traceback
347
+ # about a missing `run` attribute.
348
+ subcommands = parser.add_subparsers(dest="command", metavar="COMMAND", required=True)
349
+
350
+ translate = subcommands.add_parser(
351
+ "translate",
352
+ help="translate .kql files to .sql",
365
353
  description=(
366
354
  "Translate KQL files to DuckDB SQL. The generated SQL has no "
367
355
  "dependency on this package, so queries can be translated once at "
@@ -373,13 +361,14 @@ def _parser() -> argparse.ArgumentParser:
373
361
  ),
374
362
  formatter_class=argparse.RawDescriptionHelpFormatter,
375
363
  )
376
- parser.add_argument(
364
+ translate.set_defaults(run=_translate_command)
365
+ translate.add_argument(
377
366
  "files",
378
367
  nargs="+",
379
368
  metavar="FILE",
380
369
  help="KQL files or directories to translate; '-' reads stdin",
381
370
  )
382
- parser.add_argument(
371
+ translate.add_argument(
383
372
  "-o",
384
373
  "--output",
385
374
  metavar="PATH",
@@ -388,7 +377,7 @@ def _parser() -> argparse.ArgumentParser:
388
377
  "stdout."
389
378
  ),
390
379
  )
391
- parser.add_argument(
380
+ translate.add_argument(
392
381
  "--check",
393
382
  action="store_true",
394
383
  help=(
@@ -396,7 +385,7 @@ def _parser() -> argparse.ArgumentParser:
396
385
  "in CI so an edited .kql cannot ship with a stale .sql."
397
386
  ),
398
387
  )
399
- parser.add_argument(
388
+ translate.add_argument(
400
389
  "--schema",
401
390
  metavar="FILE",
402
391
  help=(
@@ -404,18 +393,59 @@ def _parser() -> argparse.ArgumentParser:
404
393
  "Only `join` needs it."
405
394
  ),
406
395
  )
407
- parser.add_argument(
396
+ translate.add_argument(
408
397
  "--no-header",
409
398
  action="store_true",
410
399
  help="omit the generated-file comment block",
411
400
  )
412
- parser.add_argument(
401
+ translate.add_argument(
413
402
  "-v",
414
403
  "--verbose",
415
404
  action="store_true",
416
405
  help="report each file written, on stderr",
417
406
  )
418
- parser.add_argument("--version", action="version", version=f"duckdb-kql {__version__}")
407
+
408
+ serve = subcommands.add_parser(
409
+ "serve",
410
+ help="serve a DuckDB database over the Kusto REST API",
411
+ description=(
412
+ "Serve a DuckDB database over the Kusto REST API, so Kusto tools "
413
+ "can query it. Open https://dataexplorer.azure.com, choose Add "
414
+ "connection, and give it the URL this prints."
415
+ ),
416
+ epilog=(
417
+ "Listens on 127.0.0.1 only and cannot be made to listen anywhere "
418
+ "else: it answers unauthenticated queries, so reaching it has to "
419
+ "mean already being on this machine."
420
+ ),
421
+ formatter_class=argparse.RawDescriptionHelpFormatter,
422
+ )
423
+ serve.set_defaults(run=_serve_command)
424
+ serve.add_argument(
425
+ "database",
426
+ nargs="?",
427
+ default=":memory:",
428
+ metavar="DATABASE",
429
+ help="DuckDB database file to serve. Omit for an empty in-memory one.",
430
+ )
431
+ serve.add_argument(
432
+ "-p",
433
+ "--port",
434
+ type=int,
435
+ default=DEFAULT_PORT,
436
+ metavar="PORT",
437
+ help=f"TCP port to listen on (default: {DEFAULT_PORT})",
438
+ )
439
+ serve.add_argument(
440
+ "--allow-origin",
441
+ action="append",
442
+ metavar="ORIGIN",
443
+ help=(
444
+ "additionally allow a browser origin to make cross-origin requests. "
445
+ "Repeatable. Replaces the Azure Data Explorer default list, and is a "
446
+ "decision about who may read this database from another browser tab."
447
+ ),
448
+ )
419
449
  return parser
420
450
 
421
451
 
@@ -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
- server_version = f"duckdb-kql/{CLUSTER_NAME}"
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
- "Access-Control-Allow-Headers",
411
- "Authorization, Content-Type, x-ms-client-request-id, "
412
- "x-ms-app, x-ms-user, x-ms-client-version, Accept, Connection",
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():