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,176 @@
1
+ """
2
+ JVM management and initialization.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .driver_manager import get_classpath_with_drivers
13
+ from .exceptions import JVMNotStartedError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _jvm_started = False
18
+
19
+
20
+ def get_classpath(
21
+ auto_download: bool = True,
22
+ databases: list[str] | None = None,
23
+ ) -> list[Path]:
24
+ """
25
+ Get JDBC driver classpath from environment, auto-download, or both.
26
+
27
+ Args:
28
+ auto_download: Whether to auto-download recommended JDBC drivers.
29
+ databases: List of database names for auto-download (e.g., ['postgresql', 'mysql']).
30
+ If None and auto_download is True, downloads all recommended drivers.
31
+
32
+ Returns:
33
+ List of paths to JDBC driver JAR files.
34
+ """
35
+ # Get manual classpath from environment
36
+ manual_classpath = []
37
+ classpath_env = os.environ.get("CLASSPATH", "")
38
+ if not classpath_env:
39
+ classpath_env = os.environ.get("JDBC_DRIVER_PATH", "")
40
+
41
+ if classpath_env:
42
+ for path_str in classpath_env.split(os.pathsep):
43
+ path = Path(path_str)
44
+ if path.exists():
45
+ manual_classpath.append(path)
46
+ else:
47
+ logger.warning(f"Classpath entry not found: {path}")
48
+
49
+ # Combine manual and auto-downloaded drivers
50
+ return get_classpath_with_drivers(
51
+ databases=databases,
52
+ auto_download=auto_download,
53
+ manual_classpath=manual_classpath or None,
54
+ )
55
+
56
+
57
+ def start_jvm(
58
+ classpath: list[Path] | None = None,
59
+ jvm_path: Path | None = None,
60
+ jvm_args: list[str] | None = None,
61
+ auto_download: bool = True,
62
+ databases: list[str] | None = None,
63
+ ) -> None:
64
+ """
65
+ Start the JVM with specified classpath and arguments.
66
+
67
+ Args:
68
+ classpath: List of paths to add to classpath. If None, uses environment + auto-download.
69
+ jvm_path: Path to JVM library. If None, JPype will auto-detect.
70
+ jvm_args: Additional JVM arguments (e.g., ['-Xmx512m']).
71
+ auto_download: Whether to auto-download JDBC drivers. Default: True.
72
+ databases: List of database names for auto-download. If None, downloads common drivers.
73
+
74
+ Raises:
75
+ JVMNotStartedError: If JVM fails to start.
76
+ """
77
+ global _jvm_started
78
+
79
+ if _jvm_started:
80
+ logger.debug("JVM already started")
81
+ return
82
+
83
+ try:
84
+ import jpype
85
+ except ImportError as e:
86
+ raise JVMNotStartedError(
87
+ "JPype is not installed. Install with: pip install JPype1"
88
+ ) from e
89
+
90
+ if jpype.isJVMStarted():
91
+ _jvm_started = True
92
+ logger.debug("JVM already started by another process")
93
+ return
94
+
95
+ # Build classpath
96
+ if classpath is None:
97
+ classpath = get_classpath(auto_download=auto_download, databases=databases)
98
+
99
+ if not classpath:
100
+ logger.warning(
101
+ "No JDBC drivers found in classpath. "
102
+ "Set CLASSPATH environment variable or enable auto_download."
103
+ )
104
+
105
+ classpath_str = os.pathsep.join(str(p) for p in classpath)
106
+
107
+ # Build JVM arguments
108
+ args = jvm_args or []
109
+ if classpath_str:
110
+ args.append(f"-Djava.class.path={classpath_str}")
111
+
112
+ # Start JVM
113
+ try:
114
+ if jvm_path:
115
+ jpype.startJVM(str(jvm_path), *args, convertStrings=True)
116
+ else:
117
+ jpype.startJVM(*args, convertStrings=True)
118
+
119
+ _jvm_started = True
120
+ logger.info("JVM started successfully")
121
+ logger.debug(f"Classpath: {classpath_str}")
122
+ logger.debug(f"JVM args: {args}")
123
+ if classpath:
124
+ logger.info(f"Loaded {len(classpath)} JDBC driver(s)")
125
+
126
+ except Exception as e:
127
+ raise JVMNotStartedError(f"Failed to start JVM: {e}") from e
128
+
129
+
130
+ def is_jvm_started() -> bool:
131
+ """Check if JVM is started."""
132
+ try:
133
+ import jpype
134
+
135
+ return jpype.isJVMStarted()
136
+ except ImportError:
137
+ return False
138
+
139
+
140
+ def shutdown_jvm() -> None:
141
+ """Shutdown the JVM (optional, usually not needed)."""
142
+ global _jvm_started
143
+
144
+ try:
145
+ import jpype
146
+
147
+ if jpype.isJVMStarted():
148
+ jpype.shutdownJVM()
149
+ _jvm_started = False
150
+ logger.info("JVM shutdown")
151
+ except ImportError:
152
+ pass
153
+
154
+
155
+ def get_java_class(class_name: str) -> Any:
156
+ """
157
+ Get a Java class by name.
158
+
159
+ Args:
160
+ class_name: Fully qualified Java class name.
161
+
162
+ Returns:
163
+ Java class object.
164
+
165
+ Raises:
166
+ JVMNotStartedError: If JVM is not started.
167
+ """
168
+ if not is_jvm_started():
169
+ raise JVMNotStartedError("JVM is not started. Call start_jvm() first.")
170
+
171
+ try:
172
+ import jpype
173
+
174
+ return jpype.JClass(class_name)
175
+ except Exception as e:
176
+ raise JVMNotStartedError(f"Failed to load Java class {class_name}: {e}") from e
@@ -0,0 +1,292 @@
1
+ """
2
+ Type conversion between JDBC and Python types.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import datetime
8
+ import logging
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class TypeConverter:
15
+ """
16
+ Handles conversion between JDBC SQL types and Python types.
17
+
18
+ Java SQL Types (from java.sql.Types):
19
+ -7 BIT
20
+ -6 TINYINT
21
+ -5 BIGINT
22
+ -4 LONGVARBINARY
23
+ -3 VARBINARY
24
+ -2 BINARY
25
+ -1 LONGVARCHAR
26
+ 1 CHAR
27
+ 2 NUMERIC
28
+ 3 DECIMAL
29
+ 4 INTEGER
30
+ 5 SMALLINT
31
+ 6 FLOAT
32
+ 7 REAL
33
+ 8 DOUBLE
34
+ 12 VARCHAR
35
+ 91 DATE
36
+ 92 TIME
37
+ 93 TIMESTAMP
38
+ 2004 BLOB
39
+ 2005 CLOB
40
+ 2011 NCLOB
41
+ """
42
+
43
+ # SQL Type constants (from java.sql.Types)
44
+ JDBC_TYPES = {
45
+ -7: "BIT",
46
+ -6: "TINYINT",
47
+ -5: "BIGINT",
48
+ -4: "LONGVARBINARY",
49
+ -3: "VARBINARY",
50
+ -2: "BINARY",
51
+ -1: "LONGVARCHAR",
52
+ 1: "CHAR",
53
+ 2: "NUMERIC",
54
+ 3: "DECIMAL",
55
+ 4: "INTEGER",
56
+ 5: "SMALLINT",
57
+ 6: "FLOAT",
58
+ 7: "REAL",
59
+ 8: "DOUBLE",
60
+ 12: "VARCHAR",
61
+ 91: "DATE",
62
+ 92: "TIME",
63
+ 93: "TIMESTAMP",
64
+ 2004: "BLOB",
65
+ 2005: "CLOB",
66
+ 2011: "NCLOB",
67
+ }
68
+
69
+ def convert_from_jdbc( # noqa: C901 - Type conversion requires complexity
70
+ self, resultset: Any, column_index: int, sql_type: int
71
+ ) -> Any:
72
+ """
73
+ Convert JDBC result to Python type.
74
+
75
+ Args:
76
+ resultset: JDBC ResultSet object
77
+ column_index: 1-based column index
78
+ sql_type: JDBC SQL type code
79
+
80
+ Returns:
81
+ Python value
82
+ """
83
+ try:
84
+ # First check if the value is NULL
85
+ # We have to call getObject first to trigger wasNull() properly
86
+ value = resultset.getObject(column_index)
87
+ if value is None or resultset.wasNull():
88
+ return None
89
+
90
+ # String types
91
+ if sql_type in (1, 12, -1): # CHAR, VARCHAR, LONGVARCHAR
92
+ return self._convert_string(resultset, column_index)
93
+
94
+ # Numeric types
95
+ if sql_type in (
96
+ -6,
97
+ 5,
98
+ 4,
99
+ -5,
100
+ ): # TINYINT, SMALLINT, INTEGER, BIGINT
101
+ return self._convert_int(resultset, column_index)
102
+
103
+ if sql_type in (2, 3): # NUMERIC, DECIMAL
104
+ return self._convert_decimal(resultset, column_index)
105
+
106
+ if sql_type in (6, 7, 8): # FLOAT, REAL, DOUBLE
107
+ return self._convert_float(resultset, column_index)
108
+
109
+ # Boolean
110
+ if sql_type == -7: # BIT
111
+ return self._convert_boolean(resultset, column_index)
112
+
113
+ # Date/Time types
114
+ if sql_type == 91: # DATE
115
+ return self._convert_date(resultset, column_index)
116
+
117
+ if sql_type == 92: # TIME
118
+ return self._convert_time(resultset, column_index)
119
+
120
+ if sql_type == 93: # TIMESTAMP
121
+ return self._convert_timestamp(resultset, column_index)
122
+
123
+ # Binary types
124
+ if sql_type in (-4, -3, -2): # LONGVARBINARY, VARBINARY, BINARY
125
+ return self._convert_binary(resultset, column_index)
126
+
127
+ # LOB types
128
+ if sql_type == 2004: # BLOB
129
+ return self._convert_blob(resultset, column_index)
130
+
131
+ if sql_type in (2005, 2011): # CLOB, NCLOB
132
+ return self._convert_clob(resultset, column_index)
133
+
134
+ # Array type (PostgreSQL, Oracle)
135
+ if hasattr(value, "getArray"):
136
+ return self._convert_array(value)
137
+
138
+ # Default: return as-is
139
+ logger.debug(
140
+ f"Unknown SQL type {sql_type} "
141
+ f"({self.JDBC_TYPES.get(sql_type, 'UNKNOWN')}), "
142
+ f"returning as-is"
143
+ )
144
+ return value
145
+
146
+ except Exception as e:
147
+ logger.warning(
148
+ f"Type conversion failed for column {column_index}, "
149
+ f"type {sql_type}: {e}"
150
+ )
151
+ # Fallback to getObject
152
+ return resultset.getObject(column_index)
153
+
154
+ def _convert_string(self, resultset: Any, column_index: int) -> str | None:
155
+ """Convert to Python string."""
156
+ value = resultset.getString(column_index)
157
+ return value if value is not None else None
158
+
159
+ def _convert_int(self, resultset: Any, column_index: int) -> int | None:
160
+ """Convert to Python int."""
161
+ value = resultset.getLong(column_index)
162
+ return value if not resultset.wasNull() else None
163
+
164
+ def _convert_decimal(self, resultset: Any, column_index: int) -> float | None:
165
+ """Convert decimal to Python float or decimal."""
166
+ value = resultset.getBigDecimal(column_index)
167
+ if value is None or resultset.wasNull():
168
+ return None
169
+ # Convert Java BigDecimal to Python float
170
+ # Could use decimal.Decimal for precision, but float is more common
171
+ return float(str(value))
172
+
173
+ def _convert_float(self, resultset: Any, column_index: int) -> float | None:
174
+ """Convert to Python float."""
175
+ value = resultset.getDouble(column_index)
176
+ return value if not resultset.wasNull() else None
177
+
178
+ def _convert_boolean(self, resultset: Any, column_index: int) -> bool | None:
179
+ """Convert to Python bool."""
180
+ value = resultset.getBoolean(column_index)
181
+ return value if not resultset.wasNull() else None
182
+
183
+ def _convert_date(self, resultset: Any, column_index: int) -> datetime.date | None:
184
+ """Convert to Python date."""
185
+ value = resultset.getDate(column_index)
186
+ if value is None or resultset.wasNull():
187
+ return None
188
+
189
+ # Convert java.sql.Date to Python date
190
+ try:
191
+ # Get the timestamp in milliseconds
192
+ timestamp_ms = value.getTime()
193
+ # Convert to seconds
194
+ timestamp_s = timestamp_ms / 1000.0
195
+ # Create Python datetime and extract date
196
+ return datetime.datetime.fromtimestamp(timestamp_s).date()
197
+ except Exception as e:
198
+ logger.warning(f"Date conversion failed: {e}")
199
+ return None
200
+
201
+ def _convert_time(self, resultset: Any, column_index: int) -> datetime.time | None:
202
+ """Convert to Python time."""
203
+ value = resultset.getTime(column_index)
204
+ if value is None or resultset.wasNull():
205
+ return None
206
+
207
+ try:
208
+ timestamp_ms = value.getTime()
209
+ timestamp_s = timestamp_ms / 1000.0
210
+ return datetime.datetime.fromtimestamp(timestamp_s).time()
211
+ except Exception as e:
212
+ logger.warning(f"Time conversion failed: {e}")
213
+ return None
214
+
215
+ def _convert_timestamp(
216
+ self, resultset: Any, column_index: int
217
+ ) -> datetime.datetime | None:
218
+ """Convert to Python datetime."""
219
+ value = resultset.getTimestamp(column_index)
220
+ if value is None or resultset.wasNull():
221
+ return None
222
+
223
+ try:
224
+ timestamp_ms = value.getTime()
225
+ timestamp_s = timestamp_ms / 1000.0
226
+ # Get nanoseconds for microsecond precision
227
+ nanos = value.getNanos()
228
+ micros = nanos // 1000
229
+ dt = datetime.datetime.fromtimestamp(timestamp_s)
230
+ # Replace microseconds
231
+ return dt.replace(microsecond=micros % 1000000)
232
+ except Exception as e:
233
+ logger.warning(f"Timestamp conversion failed: {e}")
234
+ return None
235
+
236
+ def _convert_binary(self, resultset: Any, column_index: int) -> bytes | None:
237
+ """Convert to Python bytes."""
238
+ value = resultset.getBytes(column_index)
239
+ if value is None or resultset.wasNull():
240
+ return None
241
+ return bytes(value)
242
+
243
+ def _convert_blob(self, resultset: Any, column_index: int) -> bytes | None:
244
+ """Convert BLOB to Python bytes."""
245
+ blob = resultset.getBlob(column_index)
246
+ if blob is None or resultset.wasNull():
247
+ return None
248
+
249
+ try:
250
+ length = blob.length()
251
+ return bytes(blob.getBytes(1, length))
252
+ except Exception as e:
253
+ logger.warning(f"BLOB conversion failed: {e}")
254
+ return None
255
+
256
+ def _convert_clob(self, resultset: Any, column_index: int) -> str | None:
257
+ """Convert CLOB to Python string."""
258
+ clob = resultset.getClob(column_index)
259
+ if clob is None or resultset.wasNull():
260
+ return None
261
+
262
+ try:
263
+ length = clob.length()
264
+ return clob.getSubString(1, length)
265
+ except Exception as e:
266
+ # Fallback: try reading as stream
267
+ try:
268
+ reader = clob.getCharacterStream()
269
+ chars = []
270
+ while True:
271
+ char = reader.read()
272
+ if char == -1:
273
+ break
274
+ chars.append(chr(char))
275
+ return "".join(chars)
276
+ except Exception as e2:
277
+ logger.warning(f"CLOB conversion failed: {e}, {e2}")
278
+ return None
279
+
280
+ def _convert_array(self, array: Any) -> list[Any] | None:
281
+ """Convert SQL Array to Python list."""
282
+ try:
283
+ # Call getArray() on the java.sql.Array object
284
+ java_array = array.getArray()
285
+ if java_array is None:
286
+ return None
287
+
288
+ # Convert to Python list
289
+ return list(java_array)
290
+ except Exception as e:
291
+ logger.warning(f"Array conversion failed: {e}")
292
+ return None
@@ -0,0 +1,72 @@
1
+ """
2
+ DB-API 2.0 type objects and constructors.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import datetime
8
+ from typing import Any
9
+
10
+
11
+ # Type Objects for DB-API 2.0 compliance
12
+ class DBAPITypeObject:
13
+ """Base class for DB-API type objects."""
14
+
15
+ def __init__(self, *values: Any) -> None:
16
+ self.values = frozenset(values)
17
+
18
+ def __eq__(self, other: Any) -> bool:
19
+ if isinstance(other, DBAPITypeObject):
20
+ return self.values == other.values
21
+ return other in self.values
22
+
23
+ def __hash__(self) -> int:
24
+ return hash(self.values)
25
+
26
+
27
+ # Type objects
28
+ STRING = DBAPITypeObject(str)
29
+ BINARY = DBAPITypeObject(bytes, bytearray)
30
+ NUMBER = DBAPITypeObject(int, float)
31
+ DATETIME = DBAPITypeObject(datetime.datetime, datetime.date, datetime.time)
32
+ ROWID = DBAPITypeObject(int)
33
+
34
+
35
+ # Date/Time constructors (DB-API 2.0 requires capitalized names)
36
+ def Date(year: int, month: int, day: int) -> datetime.date: # noqa: N802
37
+ """Construct a date object."""
38
+ return datetime.date(year, month, day)
39
+
40
+
41
+ def Time(hour: int, minute: int, second: int) -> datetime.time: # noqa: N802
42
+ """Construct a time object."""
43
+ return datetime.time(hour, minute, second)
44
+
45
+
46
+ def Timestamp( # noqa: N802
47
+ year: int, month: int, day: int, hour: int, minute: int, second: int
48
+ ) -> datetime.datetime:
49
+ """Construct a timestamp object."""
50
+ return datetime.datetime(year, month, day, hour, minute, second) # noqa: DTZ001
51
+
52
+
53
+ def DateFromTicks(ticks: float) -> datetime.date: # noqa: N802
54
+ """Construct a date object from ticks since epoch."""
55
+ return datetime.date.fromtimestamp(ticks) # noqa: DTZ012
56
+
57
+
58
+ def TimeFromTicks(ticks: float) -> datetime.time: # noqa: N802
59
+ """Construct a time object from ticks since epoch."""
60
+ return datetime.datetime.fromtimestamp(ticks).time()
61
+
62
+
63
+ def TimestampFromTicks(ticks: float) -> datetime.datetime: # noqa: N802
64
+ """Construct a timestamp object from ticks since epoch."""
65
+ return datetime.datetime.fromtimestamp(ticks)
66
+
67
+
68
+ def Binary(value: bytes | bytearray | str) -> bytes: # noqa: N802
69
+ """Construct a binary object."""
70
+ if isinstance(value, str):
71
+ return value.encode("utf-8")
72
+ return bytes(value)
@@ -0,0 +1,46 @@
1
+ """
2
+ ODBC Bridge Layer for SQLAlchemy.
3
+
4
+ This module provides a Python DB-API 2.0 compliant interface to ODBC drivers
5
+ using pyodbc. This complements the JDBC bridge and provides an alternative
6
+ connection method for databases with good ODBC driver support.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .connection import Connection, connect
12
+ from .exceptions import (
13
+ DatabaseError,
14
+ DataError,
15
+ Error,
16
+ IntegrityError,
17
+ InterfaceError,
18
+ InternalError,
19
+ NotSupportedError,
20
+ OperationalError,
21
+ ProgrammingError,
22
+ Warning,
23
+ )
24
+
25
+ __all__ = [
26
+ # Core functions
27
+ "connect",
28
+ # Classes
29
+ "Connection",
30
+ # Exceptions
31
+ "Error",
32
+ "Warning",
33
+ "InterfaceError",
34
+ "DatabaseError",
35
+ "InternalError",
36
+ "OperationalError",
37
+ "ProgrammingError",
38
+ "IntegrityError",
39
+ "DataError",
40
+ "NotSupportedError",
41
+ ]
42
+
43
+ # DB-API 2.0 module attributes
44
+ apilevel = "2.0"
45
+ threadsafety = 1 # Threads may share the module, but not connections
46
+ paramstyle = "qmark" # Question mark style, e.g. ...WHERE name=?