duckle 0.5.7__py3-none-win_amd64.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.
- duckle/__init__.py +40 -0
- duckle/__main__.py +95 -0
- duckle/_components.py +1925 -0
- duckle/_ns.py +183 -0
- duckle/api.py +349 -0
- duckle/duckle-runner.exe +0 -0
- duckle/expr.py +195 -0
- duckle-0.5.7.dist-info/METADATA +134 -0
- duckle-0.5.7.dist-info/RECORD +12 -0
- duckle-0.5.7.dist-info/WHEEL +5 -0
- duckle-0.5.7.dist-info/entry_points.txt +2 -0
- duckle-0.5.7.dist-info/top_level.txt +1 -0
duckle/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Duckle: local-first ETL/ELT pipelines that run on DuckDB.
|
|
2
|
+
|
|
3
|
+
import duckle
|
|
4
|
+
|
|
5
|
+
(duckle.read_csv("orders.csv")
|
|
6
|
+
.where("amount >= 20")
|
|
7
|
+
.derive(total="round(amount * 1.2, 2)")
|
|
8
|
+
.write_parquet("out.parquet")
|
|
9
|
+
.run())
|
|
10
|
+
|
|
11
|
+
Python builds the plan; DuckDB moves the rows. Nothing here pulls data into
|
|
12
|
+
the interpreter, and the Python expressions above are compiled to vectorized
|
|
13
|
+
SQL before the run starts.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .expr import Expr, col, lit, when # noqa: F401
|
|
17
|
+
from ._ns import component_ids, describe, root_namespaces # noqa: F401
|
|
18
|
+
|
|
19
|
+
# Every component id becomes an attribute path: duckle.src.salesforce(...),
|
|
20
|
+
# duckle.xf.geo.reproject(...), duckle.snk.salesforce.bulk(...).
|
|
21
|
+
globals().update(root_namespaces())
|
|
22
|
+
from .api import ( # noqa: F401
|
|
23
|
+
DuckleError,
|
|
24
|
+
Pipeline,
|
|
25
|
+
from_json,
|
|
26
|
+
read_csv,
|
|
27
|
+
read_duckdb,
|
|
28
|
+
read_json,
|
|
29
|
+
read_parquet,
|
|
30
|
+
read_postgres,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__version__ = "0.5.7"
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Pipeline", "DuckleError", "from_json",
|
|
36
|
+
"col", "lit", "when", "Expr",
|
|
37
|
+
"component_ids", "describe",
|
|
38
|
+
*sorted(__import__("duckle._ns", fromlist=["root_namespaces"]).root_namespaces()),
|
|
39
|
+
"read_csv", "read_parquet", "read_json", "read_postgres", "read_duckdb",
|
|
40
|
+
]
|
duckle/__main__.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Console entry point: hand off to the bundled duckle-runner binary.
|
|
2
|
+
|
|
3
|
+
The wheel ships a real compiled executable next to this module. This shim
|
|
4
|
+
exists only so the command lands on PATH as `duckle` rather than under the
|
|
5
|
+
Cargo target name, and so the DuckDB CLI that `duckdb-cli` installs is found
|
|
6
|
+
without the user configuring anything.
|
|
7
|
+
|
|
8
|
+
On POSIX it execs, replacing this process, so signals, exit codes and stdio
|
|
9
|
+
behave exactly as if the binary had been invoked directly. Windows has no
|
|
10
|
+
exec that preserves the process, so there it subprocesses and forwards the
|
|
11
|
+
return code.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
_BIN_NAME = "duckle-runner.exe" if os.name == "nt" else "duckle-runner"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _binary_path():
|
|
23
|
+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), _BIN_NAME)
|
|
24
|
+
if not os.path.exists(path):
|
|
25
|
+
sys.stderr.write(
|
|
26
|
+
"duckle: bundled runner not found at {}\n"
|
|
27
|
+
"This wheel appears to be built for a different platform.\n"
|
|
28
|
+
"Report at https://github.com/slothflowlabs/duckle/issues\n".format(path)
|
|
29
|
+
)
|
|
30
|
+
raise SystemExit(2)
|
|
31
|
+
return path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _find_duckdb():
|
|
35
|
+
"""Locate the DuckDB CLI that `duckdb-cli` installed alongside us.
|
|
36
|
+
|
|
37
|
+
PATH alone is not enough. `duckle` is frequently invoked by absolute path
|
|
38
|
+
rather than through an activated environment - CI steps, cron entries,
|
|
39
|
+
pipx and uvx all do this - and then the venv's scripts directory is not on
|
|
40
|
+
PATH and `duckdb` is invisible even though pip installed it right next to
|
|
41
|
+
this entry point. So look in our own environment first and treat PATH as
|
|
42
|
+
the fallback, not the primary.
|
|
43
|
+
"""
|
|
44
|
+
exe = "duckdb.exe" if os.name == "nt" else "duckdb"
|
|
45
|
+
candidates = [
|
|
46
|
+
# The scripts/bin dir of the interpreter running this shim.
|
|
47
|
+
os.path.join(os.path.dirname(os.path.abspath(sys.executable)), exe),
|
|
48
|
+
# sys.argv[0] is the generated `duckle` launcher, so its directory is
|
|
49
|
+
# the scripts dir even when a different interpreter is in play.
|
|
50
|
+
os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), exe),
|
|
51
|
+
]
|
|
52
|
+
for path in candidates:
|
|
53
|
+
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
54
|
+
return path
|
|
55
|
+
return shutil.which("duckdb")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _engine_env():
|
|
59
|
+
"""Point the runner at the DuckDB CLI without the user configuring it.
|
|
60
|
+
|
|
61
|
+
An explicit DUCKLE_DUCKDB_BIN always wins, so a user pinning their own
|
|
62
|
+
build is never overridden.
|
|
63
|
+
"""
|
|
64
|
+
env = os.environ.copy()
|
|
65
|
+
if env.get("DUCKLE_DUCKDB_BIN"):
|
|
66
|
+
return env
|
|
67
|
+
found = _find_duckdb()
|
|
68
|
+
if found:
|
|
69
|
+
env["DUCKLE_DUCKDB_BIN"] = found
|
|
70
|
+
return env
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
binary = _binary_path()
|
|
75
|
+
# argv[0] is what the runner sees as its own name, and it renders help
|
|
76
|
+
# under that name. Passing the bundled binary's path would make a pip user
|
|
77
|
+
# read "duckle-runner --pipeline ..." for a command they do not have, so
|
|
78
|
+
# pass the launcher's name (normally "duckle") instead.
|
|
79
|
+
invoked = os.path.splitext(os.path.basename(sys.argv[0]))[0] or "duckle"
|
|
80
|
+
argv = [invoked] + sys.argv[1:]
|
|
81
|
+
env = _engine_env()
|
|
82
|
+
if os.name == "nt":
|
|
83
|
+
# No exec that preserves the process on Windows: subprocess and
|
|
84
|
+
# forward the child's exit code so CI still sees 0 / 1 / 2.
|
|
85
|
+
# `executable` says what to launch while argv[0] says what the child
|
|
86
|
+
# believes it is called. Without it, subprocess would try to launch
|
|
87
|
+
# argv[0] ("duckle") as a path and fail with WinError 2.
|
|
88
|
+
raise SystemExit(subprocess.call(argv, executable=binary, env=env))
|
|
89
|
+
# execve already takes the binary separately from argv, so argv[0] is
|
|
90
|
+
# free to be the friendly name.
|
|
91
|
+
os.execve(binary, argv, env)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
main()
|