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 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()