duckdb-kql 0.0.1.dev1__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.
duckdb_kql/__init__.py ADDED
@@ -0,0 +1,205 @@
1
+ """duckdb-kql — run Kusto KQL queries on DuckDB, from Python.
2
+
3
+ **Status: pre-alpha.** Translation is being built wave by wave
4
+ (``docs/implementation-plan.md``). Anything outside the covered surface raises
5
+ ``KqlUnsupportedError`` — deliberately, rather than returning something
6
+ plausible and wrong.
7
+
8
+ The API comes in three layers, each adding one dependency. Import only the layer
9
+ you need and you pay only for that layer::
10
+
11
+ Layer 0 duckdb_kql KQL text in, DuckDB SQL out. antlr4 only.
12
+ Layer 1 duckdb_kql.engine Run it. + duckdb
13
+ Layer 2 duckdb_kql.kusto KustoClient drop-in. + pandas
14
+
15
+ Layer 0 — translate and inspect, no database anywhere::
16
+
17
+ >>> import duckdb_kql
18
+ >>> duckdb_kql.to_sql("print x = 1 + 1")
19
+ 'SELECT (CAST(1 AS BIGINT) + CAST(1 AS BIGINT)) AS "x"'
20
+ >>> duckdb_kql.parse('Logs | where Level == "Error" | take 10').ok
21
+ True
22
+ >>> duckdb_kql.validate("Logs | wherex") # doctest: +ELLIPSIS
23
+ [...]
24
+
25
+ Layer 1 — execute against a DuckDB connection::
26
+
27
+ import duckdb, duckdb_kql
28
+ con = duckdb_kql.connect() # or duckdb.connect(), TimeZone=UTC
29
+ con.sql("CREATE TABLE Logs AS SELECT 'Error' AS Level")
30
+ duckdb_kql.sql(con, "Logs | where Level == 'Error'").fetchall()
31
+
32
+ Layer 2 — the ``azure-kusto-data`` shape, for code already written against it::
33
+
34
+ from duckdb_kql.kusto import KustoClient, ClientRequestProperties
35
+ from duckdb_kql.kusto.helpers import dataframe_from_result_table
36
+
37
+ client = KustoClient("mydata.duckdb")
38
+ response = client.execute("db", "Logs | take 10")
39
+ df = dataframe_from_result_table(response.primary_results[0])
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from typing import TYPE_CHECKING, Any
45
+
46
+ from .errors import (
47
+ Diagnostic,
48
+ KqlError,
49
+ KqlSchemaError,
50
+ KqlSyntaxError,
51
+ KqlUnsupportedError,
52
+ SourceSpan,
53
+ )
54
+ from .params import ParameterDeclaration
55
+ from .parser import ParseResult, parse, validate
56
+
57
+ if TYPE_CHECKING:
58
+ # Layer 1 is resolved lazily at runtime (see ``__getattr__``), which leaves
59
+ # a type checker with nothing to go on — every re-export would be ``Any``,
60
+ # and a caller would be told their code is fine when it is not. Importing
61
+ # the real names here, for the checker only, keeps the laziness without
62
+ # paying for it in the signatures.
63
+ from .engine import Parameters, arrow, connect, df, execute, sql
64
+ from .schema import Schema
65
+ from .translate import TranslationResult
66
+
67
+ try:
68
+ # Written by hatch-vcs at build time from the git tag (pyproject.toml).
69
+ from ._version import __version__
70
+ except ImportError: # pragma: no cover - a source tree that was never built
71
+ # Running straight out of `src/` with nothing installed — several of the
72
+ # dev tools do exactly that. Ask the installed distribution, and if there
73
+ # isn't one, say so with a version that could not be mistaken for a release
74
+ # rather than inventing a plausible number.
75
+ from importlib.metadata import PackageNotFoundError
76
+ from importlib.metadata import version as _installed_version
77
+
78
+ try:
79
+ __version__ = _installed_version("duckdb-kql")
80
+ except PackageNotFoundError:
81
+ __version__ = "0.0.0.dev0+unbuilt"
82
+
83
+ __all__ = [
84
+ # Layer 0 — KQL text, no database
85
+ "parse",
86
+ "validate",
87
+ "to_sql",
88
+ "query_parameters",
89
+ "ParseResult",
90
+ "ParameterDeclaration",
91
+ "Diagnostic",
92
+ "SourceSpan",
93
+ # errors (Layer 0)
94
+ "KqlError",
95
+ "KqlSyntaxError",
96
+ "KqlUnsupportedError",
97
+ "KqlSchemaError",
98
+ # Layer 1 — requires duckdb
99
+ "connect",
100
+ "sql",
101
+ "execute",
102
+ "df",
103
+ "arrow",
104
+ # type aliases used in the public signatures above, so callers can annotate
105
+ "TranslationResult",
106
+ "Schema",
107
+ "Parameters",
108
+ "__version__",
109
+ ]
110
+
111
+ #: Layer 1 names, re-exported here for convenience but defined in ``engine``.
112
+ #: Resolved on first access so that importing this package never imports duckdb.
113
+ _LAYER1 = frozenset({"connect", "sql", "execute", "df", "arrow"})
114
+
115
+
116
+ #: Types named in the public signatures. Callers need them to annotate their own
117
+ #: code, and a name that only exists under TYPE_CHECKING cannot be imported.
118
+ #: Resolved from the Layer 0 modules that define them, so this stays duckdb-free.
119
+ _LAYER0_TYPES = {"TranslationResult": "translate", "Parameters": "translate",
120
+ "Schema": "schema"}
121
+
122
+
123
+ def __getattr__(name: str) -> Any:
124
+ if name in _LAYER1:
125
+ from . import engine
126
+
127
+ return getattr(engine, name)
128
+ if name in _LAYER0_TYPES:
129
+ import importlib
130
+
131
+ module = importlib.import_module(f".{_LAYER0_TYPES[name]}", __name__)
132
+ return getattr(module, name)
133
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
134
+
135
+
136
+ def __dir__() -> list[str]:
137
+ return sorted(__all__)
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Layer 0
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def to_sql(
146
+ kql: str,
147
+ schema: Schema | None = None,
148
+ parameters: Parameters | None = None,
149
+ ) -> TranslationResult:
150
+ """Translate *kql* to DuckDB SQL. Requires no connection and no database.
151
+
152
+ Returns a ``str`` subclass that also carries ``.parameters`` — the values
153
+ for the placeholders a ``declare query_parameters`` query renders. Pass
154
+ those to DuckDB alongside the SQL; :func:`duckdb_kql.sql` does it for you.
155
+
156
+ Args:
157
+ kql: the query text.
158
+ schema: table name -> column names. Only ``join`` consults it.
159
+ parameters: values for the query's declared parameters, by KQL name.
160
+ They are bound as values, never spliced into the SQL, so a value may
161
+ contain any text at all without changing what the query does.
162
+
163
+ .. important::
164
+ KQL datetimes are UTC (``docs/TRANSLATION.md`` R8), and DuckDB reads the
165
+ **session** ``TimeZone`` when casting a string that carries no offset. Run
166
+ the returned SQL on a connection with ``SET TimeZone='UTC'`` or datetime
167
+ values will be silently shifted. :func:`duckdb_kql.sql` and
168
+ :func:`duckdb_kql.connect` do this for you; the requirement only falls to
169
+ callers who execute the SQL themselves.
170
+
171
+ Raises:
172
+ KqlSyntaxError: the query does not parse.
173
+ KqlUnsupportedError: it parses but uses a construct outside this wave.
174
+ KqlSchemaError: a supplied value does not match its declared type, or
175
+ names a parameter the query does not declare.
176
+
177
+ Translating without supplying every declared value is allowed — the SQL is
178
+ worth reading on its own. The names still missing are listed in
179
+ ``.unbound``, and executing is what turns them into an error.
180
+ """
181
+ from .lower import lower
182
+ from .params import bind
183
+ from .translate import to_sql as _emit
184
+
185
+ query = lower(kql)
186
+ result = _emit(query, schema)
187
+ if query.parameters or parameters:
188
+ bound, unbound = bind(query.parameters, parameters)
189
+ return result.with_parameters(bound, unbound, tuple(query.parameters))
190
+ return result
191
+
192
+
193
+ def query_parameters(kql: str) -> list[ParameterDeclaration]:
194
+ """The parameters *kql* declares, in declaration order.
195
+
196
+ Lets a caller discover what a query expects before deciding what to supply::
197
+
198
+ >>> [(p.name, p.type) for p in query_parameters(
199
+ ... "declare query_parameters(state:string);"
200
+ ... " StormEvents | where State == state")]
201
+ [('state', 'string')]
202
+ """
203
+ from .lower import query_parameters as _declared
204
+
205
+ return _declared(kql)
duckdb_kql/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ """``python -m duckdb_kql`` — the same command as the ``duckdb-kql`` script.
2
+
3
+ Present so the CLI is reachable when the console script is not on PATH, which
4
+ is the normal situation inside a virtualenv a CI job did not activate.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .cli import main
10
+
11
+ raise SystemExit(main())