adbcbridge 0.1.0__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.
- adbcbridge/__init__.py +157 -0
- adbcbridge/__main__.py +24 -0
- adbcbridge/_cli.py +134 -0
- adbcbridge/_locate.py +491 -0
- adbcbridge/_preload.py +147 -0
- adbcbridge/libadbc_driver_odbc.dll +0 -0
- adbcbridge/py.typed +0 -0
- adbcbridge-0.1.0.dist-info/METADATA +137 -0
- adbcbridge-0.1.0.dist-info/RECORD +14 -0
- adbcbridge-0.1.0.dist-info/WHEEL +5 -0
- adbcbridge-0.1.0.dist-info/entry_points.txt +2 -0
- adbcbridge-0.1.0.dist-info/licenses/LICENSE +323 -0
- adbcbridge-0.1.0.dist-info/licenses/NOTICE +8 -0
- adbcbridge-0.1.0.dist-info/top_level.txt +1 -0
adbcbridge/__init__.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright 2026 the adbcbridge authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""adbcbridge: use any ODBC data source as an ADBC (Arrow) database.
|
|
18
|
+
|
|
19
|
+
import adbcbridge
|
|
20
|
+
|
|
21
|
+
with adbcbridge.connect(uri="Driver=SQLite3;Database=my.db;") as conn:
|
|
22
|
+
with conn.cursor() as cur:
|
|
23
|
+
cur.execute("SELECT * FROM t")
|
|
24
|
+
table = cur.fetch_arrow_table()
|
|
25
|
+
|
|
26
|
+
This package is a thin convenience layer: it finds the adbcbridge shared
|
|
27
|
+
library (:func:`driver_path`) and hands it to ``adbc_driver_manager``. The
|
|
28
|
+
object it returns is a plain ``adbc_driver_manager.dbapi.Connection``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
34
|
+
|
|
35
|
+
from . import _preload
|
|
36
|
+
from ._locate import (
|
|
37
|
+
DriverNotFoundError,
|
|
38
|
+
OdbcDriver,
|
|
39
|
+
driver_path,
|
|
40
|
+
odbc_driver_library,
|
|
41
|
+
odbc_drivers,
|
|
42
|
+
odbcinst_ini,
|
|
43
|
+
)
|
|
44
|
+
from ._preload import preload_odbc_driver
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
47
|
+
from adbc_driver_manager import dbapi as _dbapi
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"DriverNotFoundError",
|
|
51
|
+
"OdbcDriver",
|
|
52
|
+
"__version__",
|
|
53
|
+
"connect",
|
|
54
|
+
"driver_path",
|
|
55
|
+
"odbc_driver_library",
|
|
56
|
+
"odbc_drivers",
|
|
57
|
+
"odbcinst_ini",
|
|
58
|
+
"preload_odbc_driver",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
__version__ = "0.1.0"
|
|
62
|
+
|
|
63
|
+
# Local alias: the `driver_path` parameter of connect() shadows the function.
|
|
64
|
+
_find_driver = driver_path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _dbapi_module():
|
|
68
|
+
"""Import ``adbc_driver_manager.dbapi`` -- and, with it, pyarrow.
|
|
69
|
+
|
|
70
|
+
Deferred rather than done at import time so that ``import adbcbridge`` does
|
|
71
|
+
not pull pyarrow into the process: an ODBC driver that needs static
|
|
72
|
+
thread-local storage can then still be loaded first (see ``_preload``).
|
|
73
|
+
"""
|
|
74
|
+
from adbc_driver_manager import dbapi
|
|
75
|
+
|
|
76
|
+
return dbapi
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
#: Prefix for this driver's own options, e.g. ``adbc.odbc.batch_size``.
|
|
80
|
+
OPTION_PREFIX = "adbc.odbc."
|
|
81
|
+
|
|
82
|
+
# Options the driver takes verbatim (they are ADBC-standard names, not ours).
|
|
83
|
+
_PLAIN_OPTIONS = frozenset({"uri", "dsn", "username", "password"})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _option_key(name: str) -> str:
|
|
87
|
+
"""``batch_size`` -> ``adbc.odbc.batch_size``; dotted names pass through."""
|
|
88
|
+
if "." in name or name in _PLAIN_OPTIONS:
|
|
89
|
+
return name
|
|
90
|
+
return OPTION_PREFIX + name
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _option_value(value: Any) -> str:
|
|
94
|
+
if isinstance(value, bool): # the driver parses "true"/"false"
|
|
95
|
+
return "true" if value else "false"
|
|
96
|
+
return str(value)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def connect(
|
|
100
|
+
uri: Optional[str] = None,
|
|
101
|
+
dsn: Optional[str] = None,
|
|
102
|
+
username: Optional[str] = None,
|
|
103
|
+
password: Optional[str] = None,
|
|
104
|
+
driver_path: Optional[str] = None, # noqa: A002 - matches the documented API
|
|
105
|
+
*,
|
|
106
|
+
autocommit: bool = False,
|
|
107
|
+
conn_kwargs: Optional[Dict[str, str]] = None,
|
|
108
|
+
**options: Any,
|
|
109
|
+
) -> _dbapi.Connection:
|
|
110
|
+
"""Connect to an ODBC data source and return a DBAPI 2.0 connection.
|
|
111
|
+
|
|
112
|
+
:param uri: full ODBC connection string, e.g.
|
|
113
|
+
``"Driver=/usr/lib/x86_64-linux-gnu/odbc/libsqlite3odbc.so;Database=my.db;"``.
|
|
114
|
+
:param dsn: DSN name from ``odbc.ini`` (used instead of, or alongside, *uri*).
|
|
115
|
+
:param username: sent as ``UID=``.
|
|
116
|
+
:param password: sent as ``PWD=``.
|
|
117
|
+
:param driver_path: path to ``libadbc_driver_odbc.so``; defaults to
|
|
118
|
+
:func:`driver_path`. An ADBC driver manifest name such as ``"odbc"``
|
|
119
|
+
also works, since the value is passed straight to the driver manager.
|
|
120
|
+
:param autocommit: passed to the driver manager (default: transactional).
|
|
121
|
+
:param conn_kwargs: extra connection-level ADBC options.
|
|
122
|
+
:param options: further database options. A bare name is prefixed with
|
|
123
|
+
``adbc.odbc.`` (``batch_size=4096`` sets ``adbc.odbc.batch_size``);
|
|
124
|
+
a dotted name is used as given. ``bool`` becomes ``"true"``/``"false"``.
|
|
125
|
+
|
|
126
|
+
The return value is an ``adbc_driver_manager.dbapi.Connection``, so
|
|
127
|
+
everything ADBC's DBAPI layer offers (``cursor()``, ``fetch_arrow_table()``,
|
|
128
|
+
``adbc_ingest()``, ``adbc_get_objects()``, ...) is available.
|
|
129
|
+
"""
|
|
130
|
+
if uri is None and dsn is None:
|
|
131
|
+
raise ValueError("connect() needs at least one of uri= or dsn=")
|
|
132
|
+
|
|
133
|
+
db_kwargs: Dict[str, str] = {}
|
|
134
|
+
if uri is not None:
|
|
135
|
+
db_kwargs["uri"] = uri
|
|
136
|
+
if dsn is not None:
|
|
137
|
+
db_kwargs["dsn"] = dsn
|
|
138
|
+
if username is not None:
|
|
139
|
+
db_kwargs["username"] = username
|
|
140
|
+
if password is not None:
|
|
141
|
+
db_kwargs["password"] = password
|
|
142
|
+
for key, value in options.items():
|
|
143
|
+
if value is None:
|
|
144
|
+
continue
|
|
145
|
+
db_kwargs[_option_key(key)] = _option_value(value)
|
|
146
|
+
|
|
147
|
+
driver = driver_path if driver_path is not None else _find_driver()
|
|
148
|
+
if _preload.preload_enabled():
|
|
149
|
+
# Before pyarrow comes in below. Best effort: a driver that will not
|
|
150
|
+
# load here still gets its real failure reported by the connection.
|
|
151
|
+
preload_odbc_driver(uri=uri, dsn=dsn)
|
|
152
|
+
return _dbapi_module().connect(
|
|
153
|
+
driver=driver,
|
|
154
|
+
db_kwargs=db_kwargs,
|
|
155
|
+
conn_kwargs=dict(conn_kwargs or {}),
|
|
156
|
+
autocommit=autocommit,
|
|
157
|
+
)
|
adbcbridge/__main__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copyright 2026 the adbcbridge authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""``python -m adbcbridge`` / the ``adbcbridge`` console script."""
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from ._cli import main
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
sys.exit(main())
|
adbcbridge/_cli.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright 2026 the adbcbridge authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""The ``adbcbridge`` command line tool."""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import pathlib
|
|
23
|
+
import sys
|
|
24
|
+
from typing import List, Optional, Sequence
|
|
25
|
+
|
|
26
|
+
from . import __version__, connect
|
|
27
|
+
from ._locate import DriverNotFoundError, driver_path, odbc_drivers, odbcinst_ini
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cmd_query(args: argparse.Namespace) -> int:
|
|
31
|
+
params: Optional[List[str]] = args.param or None
|
|
32
|
+
with connect(uri=args.connection_string, driver_path=args.driver_path,
|
|
33
|
+
autocommit=True) as conn:
|
|
34
|
+
with conn.cursor() as cur:
|
|
35
|
+
cur.execute(args.sql, parameters=params)
|
|
36
|
+
if not cur.description: # DDL/DML: no result set
|
|
37
|
+
affected = cur.rowcount
|
|
38
|
+
# ODBC reports -1 when a statement has no meaningful row count.
|
|
39
|
+
print("OK" if affected is None or affected < 0
|
|
40
|
+
else "rows affected: %d" % affected)
|
|
41
|
+
return 0
|
|
42
|
+
table = cur.fetch_arrow_table()
|
|
43
|
+
if args.limit is not None and table.num_rows > args.limit:
|
|
44
|
+
table = table.slice(0, args.limit)
|
|
45
|
+
if args.format == "csv":
|
|
46
|
+
import pyarrow.csv
|
|
47
|
+
|
|
48
|
+
pyarrow.csv.write_csv(table, sys.stdout.buffer)
|
|
49
|
+
elif args.format == "schema":
|
|
50
|
+
print(table.schema)
|
|
51
|
+
else:
|
|
52
|
+
print(table)
|
|
53
|
+
print("%d rows x %d columns" % (table.num_rows, table.num_columns))
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _cmd_drivers(args: argparse.Namespace) -> int:
|
|
58
|
+
ini = odbcinst_ini()
|
|
59
|
+
drivers = odbc_drivers()
|
|
60
|
+
if not drivers:
|
|
61
|
+
where = str(ini) if ini else "odbcinst -j / odbcinst.ini"
|
|
62
|
+
print("no ODBC drivers registered (%s)" % where, file=sys.stderr)
|
|
63
|
+
return 1
|
|
64
|
+
print("# %s" % ini)
|
|
65
|
+
width = max(len(d.name) for d in drivers)
|
|
66
|
+
for driver in drivers:
|
|
67
|
+
note = ""
|
|
68
|
+
if driver.path and not pathlib.Path(driver.path).is_file():
|
|
69
|
+
note = " (missing)"
|
|
70
|
+
print("%-*s %s%s" % (width, driver.name, driver.path or "-", note))
|
|
71
|
+
if args.verbose and driver.description:
|
|
72
|
+
print("%-*s %s" % (width, "", driver.description))
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _cmd_driver_path(args: argparse.Namespace) -> int:
|
|
77
|
+
print(driver_path())
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
82
|
+
parser = argparse.ArgumentParser(
|
|
83
|
+
prog="adbcbridge",
|
|
84
|
+
description="Query any ODBC data source through adbcbridge (ADBC/Arrow).",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument("--version", action="version", version="adbcbridge " + __version__)
|
|
87
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
88
|
+
|
|
89
|
+
query = sub.add_parser(
|
|
90
|
+
"query",
|
|
91
|
+
help="run a SQL statement and print the resulting Arrow table",
|
|
92
|
+
description="Run a SQL statement against an ODBC connection string.",
|
|
93
|
+
epilog='example: adbcbridge query "Driver=SQLite3;Database=my.db;" '
|
|
94
|
+
'"SELECT * FROM t"',
|
|
95
|
+
)
|
|
96
|
+
query.add_argument("connection_string", help="ODBC connection string (Driver=...;)")
|
|
97
|
+
query.add_argument("sql", help="SQL to execute")
|
|
98
|
+
query.add_argument("-p", "--param", action="append", metavar="VALUE",
|
|
99
|
+
help="value for a '?' parameter (repeat, in order); "
|
|
100
|
+
"always sent as a string")
|
|
101
|
+
query.add_argument("--driver-path", metavar="PATH",
|
|
102
|
+
help="path to libadbc_driver_odbc.so (default: auto-detect)")
|
|
103
|
+
query.add_argument("--limit", type=int, metavar="N", help="print at most N rows")
|
|
104
|
+
query.add_argument("--format", choices=("table", "csv", "schema"), default="table",
|
|
105
|
+
help="output format (default: table)")
|
|
106
|
+
query.set_defaults(func=_cmd_query)
|
|
107
|
+
|
|
108
|
+
drivers = sub.add_parser(
|
|
109
|
+
"drivers",
|
|
110
|
+
help="list the ODBC drivers registered on this machine",
|
|
111
|
+
description="List the ODBC drivers from odbcinst -j / odbcinst.ini.",
|
|
112
|
+
)
|
|
113
|
+
drivers.add_argument("-v", "--verbose", action="store_true",
|
|
114
|
+
help="also print each driver's description")
|
|
115
|
+
drivers.set_defaults(func=_cmd_drivers)
|
|
116
|
+
|
|
117
|
+
where = sub.add_parser(
|
|
118
|
+
"driver-path",
|
|
119
|
+
help="print the path of the adbcbridge shared library that would be used",
|
|
120
|
+
)
|
|
121
|
+
where.set_defaults(func=_cmd_driver_path)
|
|
122
|
+
return parser
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
126
|
+
args = build_parser().parse_args(argv)
|
|
127
|
+
try:
|
|
128
|
+
return args.func(args)
|
|
129
|
+
except DriverNotFoundError as exc:
|
|
130
|
+
print("adbcbridge: %s" % exc, file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
except Exception as exc: # database errors: a traceback helps nobody here
|
|
133
|
+
print("adbcbridge: %s: %s" % (type(exc).__name__, exc), file=sys.stderr)
|
|
134
|
+
return 1
|