sqlalchemy-jdbcapi 2.0.0.post2__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.
Files changed (36) hide show
  1. sqlalchemy_jdbcapi/__init__.py +128 -0
  2. sqlalchemy_jdbcapi/_version.py +34 -0
  3. sqlalchemy_jdbcapi/dialects/__init__.py +30 -0
  4. sqlalchemy_jdbcapi/dialects/base.py +879 -0
  5. sqlalchemy_jdbcapi/dialects/db2.py +134 -0
  6. sqlalchemy_jdbcapi/dialects/mssql.py +117 -0
  7. sqlalchemy_jdbcapi/dialects/mysql.py +152 -0
  8. sqlalchemy_jdbcapi/dialects/oceanbase.py +218 -0
  9. sqlalchemy_jdbcapi/dialects/odbc_base.py +389 -0
  10. sqlalchemy_jdbcapi/dialects/odbc_mssql.py +69 -0
  11. sqlalchemy_jdbcapi/dialects/odbc_mysql.py +101 -0
  12. sqlalchemy_jdbcapi/dialects/odbc_oracle.py +80 -0
  13. sqlalchemy_jdbcapi/dialects/odbc_postgresql.py +63 -0
  14. sqlalchemy_jdbcapi/dialects/oracle.py +180 -0
  15. sqlalchemy_jdbcapi/dialects/postgresql.py +110 -0
  16. sqlalchemy_jdbcapi/dialects/sqlite.py +141 -0
  17. sqlalchemy_jdbcapi/jdbc/__init__.py +98 -0
  18. sqlalchemy_jdbcapi/jdbc/connection.py +244 -0
  19. sqlalchemy_jdbcapi/jdbc/cursor.py +329 -0
  20. sqlalchemy_jdbcapi/jdbc/dataframe.py +198 -0
  21. sqlalchemy_jdbcapi/jdbc/driver_manager.py +353 -0
  22. sqlalchemy_jdbcapi/jdbc/exceptions.py +53 -0
  23. sqlalchemy_jdbcapi/jdbc/jvm.py +176 -0
  24. sqlalchemy_jdbcapi/jdbc/type_converter.py +292 -0
  25. sqlalchemy_jdbcapi/jdbc/types.py +72 -0
  26. sqlalchemy_jdbcapi/odbc/__init__.py +46 -0
  27. sqlalchemy_jdbcapi/odbc/connection.py +136 -0
  28. sqlalchemy_jdbcapi/odbc/exceptions.py +48 -0
  29. sqlalchemy_jdbcapi/py.typed +2 -0
  30. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/METADATA +825 -0
  31. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/RECORD +36 -0
  32. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/WHEEL +5 -0
  33. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/entry_points.txt +20 -0
  34. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/licenses/AUTHORS +7 -0
  35. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/licenses/LICENSE +13 -0
  36. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,198 @@
1
+ """
2
+ DataFrame integration for pandas, polars, and Apache Arrow.
3
+
4
+ This module provides utilities to convert JDBC query results directly
5
+ into DataFrames for data science and ML workflows.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from .cursor import Cursor
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def cursor_to_pandas(cursor: Cursor) -> Any:
20
+ """
21
+ Convert cursor results to pandas DataFrame.
22
+
23
+ Args:
24
+ cursor: Cursor with executed query
25
+
26
+ Returns:
27
+ pandas.DataFrame
28
+
29
+ Raises:
30
+ ImportError: If pandas is not installed
31
+ ValueError: If cursor has no results
32
+
33
+ Example:
34
+ >>> cursor.execute("SELECT * FROM users")
35
+ >>> df = cursor_to_pandas(cursor)
36
+ >>> print(df.head())
37
+ """
38
+ try:
39
+ import pandas as pd
40
+ except ImportError as e:
41
+ raise ImportError(
42
+ "pandas is not installed. Install with: pip install pandas"
43
+ ) from e
44
+
45
+ if cursor.description is None:
46
+ raise ValueError("Cursor has no result set")
47
+
48
+ # Get column names
49
+ columns = [desc[0] for desc in cursor.description]
50
+
51
+ # Fetch all rows
52
+ rows = cursor.fetchall()
53
+
54
+ # Create DataFrame
55
+ df = pd.DataFrame(rows, columns=columns)
56
+ logger.debug(f"Created pandas DataFrame with shape {df.shape}")
57
+
58
+ return df
59
+
60
+
61
+ def cursor_to_polars(cursor: Cursor) -> Any:
62
+ """
63
+ Convert cursor results to polars DataFrame.
64
+
65
+ Args:
66
+ cursor: Cursor with executed query
67
+
68
+ Returns:
69
+ polars.DataFrame
70
+
71
+ Raises:
72
+ ImportError: If polars is not installed
73
+ ValueError: If cursor has no results
74
+
75
+ Example:
76
+ >>> cursor.execute("SELECT * FROM users")
77
+ >>> df = cursor_to_polars(cursor)
78
+ >>> print(df.head())
79
+ """
80
+ try:
81
+ import polars as pl
82
+ except ImportError as e:
83
+ raise ImportError(
84
+ "polars is not installed. Install with: pip install polars"
85
+ ) from e
86
+
87
+ if cursor.description is None:
88
+ raise ValueError("Cursor has no result set")
89
+
90
+ # Get column names
91
+ columns = [desc[0] for desc in cursor.description]
92
+
93
+ # Fetch all rows
94
+ rows = cursor.fetchall()
95
+
96
+ # Create DataFrame from dict of lists
97
+ data = {col: [row[i] for row in rows] for i, col in enumerate(columns)}
98
+ df = pl.DataFrame(data)
99
+ logger.debug(f"Created polars DataFrame with shape {df.shape}")
100
+
101
+ return df
102
+
103
+
104
+ def cursor_to_arrow(cursor: Cursor) -> Any:
105
+ """
106
+ Convert cursor results to Apache Arrow Table.
107
+
108
+ Args:
109
+ cursor: Cursor with executed query
110
+
111
+ Returns:
112
+ pyarrow.Table
113
+
114
+ Raises:
115
+ ImportError: If pyarrow is not installed
116
+ ValueError: If cursor has no results
117
+
118
+ Example:
119
+ >>> cursor.execute("SELECT * FROM users")
120
+ >>> table = cursor_to_arrow(cursor)
121
+ >>> print(table.schema)
122
+ """
123
+ try:
124
+ import pyarrow as pa
125
+ except ImportError as e:
126
+ raise ImportError(
127
+ "pyarrow is not installed. Install with: pip install pyarrow"
128
+ ) from e
129
+
130
+ if cursor.description is None:
131
+ raise ValueError("Cursor has no result set")
132
+
133
+ # Get column names
134
+ columns = [desc[0] for desc in cursor.description]
135
+
136
+ # Fetch all rows
137
+ rows = cursor.fetchall()
138
+
139
+ # Convert to Arrow Table
140
+ # Build column arrays
141
+ if not rows:
142
+ # Empty result
143
+ arrays = [pa.array([]) for _ in columns]
144
+ else:
145
+ # Transpose rows to columns
146
+ col_data = [[row[i] for row in rows] for i in range(len(columns))]
147
+ arrays = [pa.array(col) for col in col_data]
148
+
149
+ table = pa.Table.from_arrays(arrays, names=columns)
150
+ logger.debug(f"Created Arrow Table with {table.num_rows} rows")
151
+
152
+ return table
153
+
154
+
155
+ def cursor_to_dict(cursor: Cursor) -> list[dict[str, Any]]:
156
+ """
157
+ Convert cursor results to list of dictionaries.
158
+
159
+ Args:
160
+ cursor: Cursor with executed query
161
+
162
+ Returns:
163
+ List of row dictionaries
164
+
165
+ Example:
166
+ >>> cursor.execute("SELECT * FROM users")
167
+ >>> rows = cursor_to_dict(cursor)
168
+ >>> print(rows[0])
169
+ {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
170
+ """
171
+ if cursor.description is None:
172
+ raise ValueError("Cursor has no result set")
173
+
174
+ columns = [desc[0] for desc in cursor.description]
175
+ rows = cursor.fetchall()
176
+
177
+ return [dict(zip(columns, row)) for row in rows]
178
+
179
+
180
+ # Add convenience methods to Cursor class
181
+ def _add_dataframe_methods() -> None:
182
+ """Add DataFrame methods to Cursor class."""
183
+ from .cursor import Cursor
184
+
185
+ # Add methods
186
+ Cursor.to_pandas = lambda self: cursor_to_pandas(self) # type: ignore
187
+ Cursor.to_polars = lambda self: cursor_to_polars(self) # type: ignore
188
+ Cursor.to_arrow = lambda self: cursor_to_arrow(self) # type: ignore
189
+ Cursor.to_dict = lambda self: cursor_to_dict(self) # type: ignore
190
+
191
+ logger.debug("Added DataFrame methods to Cursor class")
192
+
193
+
194
+ # Auto-register methods on import
195
+ try:
196
+ _add_dataframe_methods()
197
+ except Exception as e:
198
+ logger.debug(f"Could not add DataFrame methods: {e}")
@@ -0,0 +1,353 @@
1
+ """
2
+ JDBC driver auto-download and management.
3
+
4
+ This module handles automatic downloading of JDBC drivers from Maven Central
5
+ and provides fallback to manual driver configuration via CLASSPATH.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ import shutil
13
+ import urllib.request
14
+ from pathlib import Path
15
+ from typing import NamedTuple
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Default driver cache directory
20
+ DEFAULT_DRIVER_CACHE = Path.home() / ".sqlalchemy-jdbcapi" / "drivers"
21
+
22
+
23
+ class JDBCDriver(NamedTuple):
24
+ """JDBC driver metadata for automatic download."""
25
+
26
+ group_id: str
27
+ artifact_id: str
28
+ version: str
29
+ classifier: str | None = None
30
+
31
+ @property
32
+ def filename(self) -> str:
33
+ """Get the JAR filename."""
34
+ if self.classifier:
35
+ return f"{self.artifact_id}-{self.version}-{self.classifier}.jar"
36
+ return f"{self.artifact_id}-{self.version}.jar"
37
+
38
+ @property
39
+ def maven_url(self) -> str:
40
+ """Get the Maven Central download URL."""
41
+ base_url = "https://repo1.maven.org/maven2"
42
+ group_path = self.group_id.replace(".", "/")
43
+ return (
44
+ f"{base_url}/{group_path}/{self.artifact_id}/{self.version}/{self.filename}"
45
+ )
46
+
47
+
48
+ # Recommended JDBC drivers for auto-download from Maven Central
49
+ # These versions are tested and known to work well
50
+ # TODO: Consider checking for newer versions periodically
51
+ RECOMMENDED_JDBC_DRIVERS = {
52
+ "postgresql": JDBCDriver(
53
+ group_id="org.postgresql",
54
+ artifact_id="postgresql",
55
+ version="42.7.1", # Latest stable as of 2024
56
+ ),
57
+ "mysql": JDBCDriver(
58
+ group_id="com.mysql",
59
+ artifact_id="mysql-connector-j",
60
+ version="8.3.0", # Note: Oracle renamed this from mysql-connector-java
61
+ ),
62
+ "mariadb": JDBCDriver(
63
+ group_id="org.mariadb.jdbc",
64
+ artifact_id="mariadb-java-client",
65
+ version="3.3.2",
66
+ ),
67
+ "mssql": JDBCDriver(
68
+ group_id="com.microsoft.sqlserver",
69
+ artifact_id="mssql-jdbc",
70
+ version="12.6.0.jre11", # JRE11 version for Java 11+ compatibility
71
+ ),
72
+ "oracle": JDBCDriver(
73
+ group_id="com.oracle.database.jdbc",
74
+ artifact_id="ojdbc11",
75
+ version="23.3.0.23.09",
76
+ ),
77
+ "db2": JDBCDriver(
78
+ group_id="com.ibm.db2",
79
+ artifact_id="jcc",
80
+ version="11.5.9.0",
81
+ ),
82
+ "sqlite": JDBCDriver(
83
+ group_id="org.xerial",
84
+ artifact_id="sqlite-jdbc",
85
+ version="3.45.0.0",
86
+ ),
87
+ "oceanbase": JDBCDriver(
88
+ group_id="com.oceanbase",
89
+ artifact_id="oceanbase-client",
90
+ version="2.4.9",
91
+ ),
92
+ }
93
+
94
+
95
+ def get_driver_cache_dir() -> Path:
96
+ """
97
+ Get the driver cache directory.
98
+
99
+ Returns:
100
+ Path to the driver cache directory.
101
+ """
102
+ cache_dir = os.environ.get("SQLALCHEMY_JDBCAPI_DRIVER_CACHE")
103
+ if cache_dir:
104
+ return Path(cache_dir)
105
+ return DEFAULT_DRIVER_CACHE
106
+
107
+
108
+ def download_driver(
109
+ driver: JDBCDriver,
110
+ cache_dir: Path | None = None,
111
+ force: bool = False,
112
+ ) -> Path:
113
+ """
114
+ Download a JDBC driver from Maven Central.
115
+
116
+ Args:
117
+ driver: JDBC driver metadata.
118
+ cache_dir: Directory to cache downloaded drivers. If None, uses default.
119
+ force: Force re-download even if driver exists.
120
+
121
+ Returns:
122
+ Path to the downloaded driver JAR file.
123
+
124
+ Raises:
125
+ RuntimeError: If download fails.
126
+ """
127
+ if cache_dir is None:
128
+ cache_dir = get_driver_cache_dir()
129
+
130
+ # Create cache directory if it doesn't exist
131
+ cache_dir.mkdir(parents=True, exist_ok=True)
132
+
133
+ # Target file path
134
+ target_path = cache_dir / driver.filename
135
+
136
+ # Check if driver already exists
137
+ if target_path.exists() and not force:
138
+ logger.debug(f"Driver already cached: {target_path}")
139
+ return target_path
140
+
141
+ # Download driver
142
+ logger.info(f"Downloading JDBC driver: {driver.filename}")
143
+ logger.debug(f"URL: {driver.maven_url}")
144
+
145
+ try:
146
+ with urllib.request.urlopen(driver.maven_url) as response:
147
+ # Download to temporary file first
148
+ temp_path = target_path.with_suffix(".tmp")
149
+ with open(temp_path, "wb") as f:
150
+ shutil.copyfileobj(response, f)
151
+
152
+ # Move to final location
153
+ temp_path.replace(target_path)
154
+
155
+ logger.info(f"Driver downloaded successfully: {target_path}")
156
+ return target_path
157
+
158
+ except Exception as e:
159
+ error_msg = f"Failed to download driver from {driver.maven_url}: {e}"
160
+ logger.error(error_msg)
161
+ raise RuntimeError(error_msg) from e
162
+
163
+
164
+ def get_driver_path(
165
+ database: str,
166
+ driver: JDBCDriver | None = None,
167
+ auto_download: bool = True,
168
+ cache_dir: Path | None = None,
169
+ ) -> Path:
170
+ """
171
+ Get the path to a JDBC driver, downloading if necessary.
172
+
173
+ Args:
174
+ database: Database name (e.g., 'postgresql', 'mysql').
175
+ driver: Custom driver metadata. If None, uses recommended driver.
176
+ auto_download: Whether to auto-download driver if not found.
177
+ cache_dir: Directory to cache downloaded drivers.
178
+
179
+ Returns:
180
+ Path to the JDBC driver JAR file.
181
+
182
+ Raises:
183
+ RuntimeError: If driver not found and auto-download disabled.
184
+ """
185
+ if driver is None:
186
+ driver = RECOMMENDED_JDBC_DRIVERS.get(database.lower())
187
+ if driver is None:
188
+ raise ValueError(f"No recommended driver for database: {database}")
189
+
190
+ if cache_dir is None:
191
+ cache_dir = get_driver_cache_dir()
192
+
193
+ target_path = cache_dir / driver.filename
194
+
195
+ # Check if driver exists in cache
196
+ if target_path.exists():
197
+ return target_path
198
+
199
+ # Try to auto-download
200
+ if auto_download:
201
+ return download_driver(driver, cache_dir)
202
+
203
+ raise RuntimeError(
204
+ f"JDBC driver not found: {target_path}. "
205
+ f"Enable auto_download or set CLASSPATH environment variable."
206
+ )
207
+
208
+
209
+ def get_all_driver_paths(
210
+ databases: list[str] | None = None,
211
+ auto_download: bool = True,
212
+ cache_dir: Path | None = None,
213
+ ) -> list[Path]:
214
+ """
215
+ Get paths to multiple JDBC drivers.
216
+
217
+ Args:
218
+ databases: List of database names. If None, downloads all recommended drivers.
219
+ auto_download: Whether to auto-download drivers if not found.
220
+ cache_dir: Directory to cache downloaded drivers.
221
+
222
+ Returns:
223
+ List of paths to JDBC driver JAR files.
224
+ """
225
+ if databases is None:
226
+ databases = list(RECOMMENDED_JDBC_DRIVERS.keys())
227
+
228
+ paths = []
229
+ for database in databases:
230
+ try:
231
+ path = get_driver_path(
232
+ database, auto_download=auto_download, cache_dir=cache_dir
233
+ )
234
+ paths.append(path)
235
+ except Exception as e:
236
+ logger.warning(f"Failed to get driver for {database}: {e}")
237
+
238
+ return paths
239
+
240
+
241
+ def get_classpath_with_drivers(
242
+ databases: list[str] | None = None,
243
+ auto_download: bool = True,
244
+ manual_classpath: list[Path] | None = None,
245
+ ) -> list[Path]:
246
+ """
247
+ Get comprehensive classpath including auto-downloaded and manual drivers.
248
+
249
+ Args:
250
+ databases: List of database names for auto-download. If None, downloads all recommended.
251
+ auto_download: Whether to auto-download drivers.
252
+ manual_classpath: Additional manual classpath entries.
253
+
254
+ Returns:
255
+ List of all classpath entries.
256
+ """
257
+ classpath = []
258
+
259
+ # Add manual classpath entries first (higher priority)
260
+ if manual_classpath:
261
+ classpath.extend(manual_classpath)
262
+
263
+ # Add auto-downloaded drivers
264
+ if auto_download:
265
+ try:
266
+ auto_paths = get_all_driver_paths(databases, auto_download=True)
267
+ classpath.extend(auto_paths)
268
+ except Exception as e:
269
+ logger.warning(f"Failed to auto-download some drivers: {e}")
270
+
271
+ # Remove duplicates while preserving order
272
+ seen = set()
273
+ unique_classpath = []
274
+ for path in classpath:
275
+ if path not in seen:
276
+ seen.add(path)
277
+ unique_classpath.append(path)
278
+
279
+ return unique_classpath
280
+
281
+
282
+ def verify_driver(driver_path: Path) -> bool:
283
+ """
284
+ Verify that a JDBC driver JAR file is valid.
285
+
286
+ Args:
287
+ driver_path: Path to the driver JAR file.
288
+
289
+ Returns:
290
+ True if driver appears valid, False otherwise.
291
+ """
292
+ if not driver_path.exists():
293
+ return False
294
+
295
+ if not driver_path.is_file():
296
+ return False
297
+
298
+ if not driver_path.suffix == ".jar":
299
+ return False
300
+
301
+ # Check if file is not empty
302
+ if driver_path.stat().st_size == 0:
303
+ return False
304
+
305
+ # Could add more validation (e.g., ZIP file structure)
306
+ return True
307
+
308
+
309
+ def list_cached_drivers(cache_dir: Path | None = None) -> list[Path]:
310
+ """
311
+ List all cached JDBC drivers.
312
+
313
+ Args:
314
+ cache_dir: Directory to check. If None, uses default.
315
+
316
+ Returns:
317
+ List of paths to cached driver JAR files.
318
+ """
319
+ if cache_dir is None:
320
+ cache_dir = get_driver_cache_dir()
321
+
322
+ if not cache_dir.exists():
323
+ return []
324
+
325
+ return [path for path in cache_dir.glob("*.jar") if verify_driver(path)]
326
+
327
+
328
+ def clear_driver_cache(cache_dir: Path | None = None) -> int:
329
+ """
330
+ Clear the driver cache directory.
331
+
332
+ Args:
333
+ cache_dir: Directory to clear. If None, uses default.
334
+
335
+ Returns:
336
+ Number of files deleted.
337
+ """
338
+ if cache_dir is None:
339
+ cache_dir = get_driver_cache_dir()
340
+
341
+ if not cache_dir.exists():
342
+ return 0
343
+
344
+ count = 0
345
+ for path in cache_dir.glob("*.jar"):
346
+ try:
347
+ path.unlink()
348
+ count += 1
349
+ logger.debug(f"Deleted cached driver: {path}")
350
+ except Exception as e:
351
+ logger.warning(f"Failed to delete {path}: {e}")
352
+
353
+ return count
@@ -0,0 +1,53 @@
1
+ """
2
+ JDBC Exception hierarchy following DB-API 2.0 specification.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+
8
+ class Error(Exception):
9
+ """Base class for all JDBC-related errors."""
10
+
11
+
12
+ class Warning(Exception): # noqa: A001 - DB-API 2.0 requires Warning exception class
13
+ """Exception raised for important warnings."""
14
+
15
+
16
+ class InterfaceError(Error):
17
+ """Exception raised for errors related to the database interface."""
18
+
19
+
20
+ class DatabaseError(Error):
21
+ """Exception raised for errors related to the database."""
22
+
23
+
24
+ class InternalError(DatabaseError):
25
+ """Exception raised when the database encounters an internal error."""
26
+
27
+
28
+ class OperationalError(DatabaseError):
29
+ """Exception raised for operational database errors."""
30
+
31
+
32
+ class ProgrammingError(DatabaseError):
33
+ """Exception raised for programming errors."""
34
+
35
+
36
+ class IntegrityError(DatabaseError):
37
+ """Exception raised when database integrity is violated."""
38
+
39
+
40
+ class DataError(DatabaseError):
41
+ """Exception raised for errors related to processed data."""
42
+
43
+
44
+ class NotSupportedError(DatabaseError):
45
+ """Exception raised when a method or database API is not supported."""
46
+
47
+
48
+ class JDBCDriverNotFoundError(InterfaceError):
49
+ """Exception raised when JDBC driver cannot be found."""
50
+
51
+
52
+ class JVMNotStartedError(InterfaceError):
53
+ """Exception raised when JVM is not started."""