orange3-db-connections 0.1.3__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 (26) hide show
  1. orange3_db_connections-0.1.3.dist-info/METADATA +33 -0
  2. orange3_db_connections-0.1.3.dist-info/RECORD +26 -0
  3. orange3_db_connections-0.1.3.dist-info/WHEEL +5 -0
  4. orange3_db_connections-0.1.3.dist-info/entry_points.txt +5 -0
  5. orange3_db_connections-0.1.3.dist-info/licenses/LICENSE +21 -0
  6. orange3_db_connections-0.1.3.dist-info/top_level.txt +1 -0
  7. orangecontrib/__init__.py +2 -0
  8. orangecontrib/dbconnections/__init__.py +10 -0
  9. orangecontrib/dbconnections/utils/__init__.py +3 -0
  10. orangecontrib/dbconnections/utils/driver_installer.py +43 -0
  11. orangecontrib/dbconnections/widgets/__init__.py +16 -0
  12. orangecontrib/dbconnections/widgets/_base_connection.py +240 -0
  13. orangecontrib/dbconnections/widgets/icons/addon_icon.png +0 -0
  14. orangecontrib/dbconnections/widgets/icons/clickhouse.png +0 -0
  15. orangecontrib/dbconnections/widgets/icons/mariadb.png +0 -0
  16. orangecontrib/dbconnections/widgets/icons/msql.png +0 -0
  17. orangecontrib/dbconnections/widgets/icons/mysql.png +0 -0
  18. orangecontrib/dbconnections/widgets/icons/oracle.png +0 -0
  19. orangecontrib/dbconnections/widgets/icons/postgres.png +0 -0
  20. orangecontrib/dbconnections/widgets/icons/sqlite.png +0 -0
  21. orangecontrib/dbconnections/widgets/ow_clickhouse_connection.py +32 -0
  22. orangecontrib/dbconnections/widgets/ow_env_check.py +223 -0
  23. orangecontrib/dbconnections/widgets/ow_mssql_connection.py +76 -0
  24. orangecontrib/dbconnections/widgets/ow_mysql_connection.py +20 -0
  25. orangecontrib/dbconnections/widgets/ow_oracle_connection.py +41 -0
  26. orangecontrib/dbconnections/widgets/ow_pg_connection.py +20 -0
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: orange3-db-connections
3
+ Version: 0.1.3
4
+ Summary: DB Connections – Add-on Orange untuk membuat koneksi database (PostgreSQL, MySQL, SQL Server, SQLite, ClickHouse).
5
+ Author-email: Devi Ardiana <deviardn@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/deviardn/orange3-db-connections
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: orange3>=3.36.0
15
+ Requires-Dist: sqlalchemy>=2.0
16
+ Requires-Dist: AnyQt>=0.0.13
17
+ Requires-Dist: PyQt5>=5.15
18
+ Requires-Dist: PyQtWebEngine>=5.15
19
+ Provides-Extra: postgresql
20
+ Requires-Dist: psycopg2-binary>=2.9; extra == "postgresql"
21
+ Provides-Extra: mysql
22
+ Requires-Dist: pymysql>=1.0; extra == "mysql"
23
+ Provides-Extra: mssql
24
+ Requires-Dist: pyodbc>=4.0; extra == "mssql"
25
+ Provides-Extra: clickhouse
26
+ Requires-Dist: clickhouse-connect>=0.7; extra == "clickhouse"
27
+ Provides-Extra: oracle
28
+ Requires-Dist: cx-Oracle>=8.3; extra == "oracle"
29
+ Dynamic: license-file
30
+
31
+ # orange3-db-connections
32
+
33
+ Add-on Orange untuk membuat koneksi ke berbagai database (PostgreSQL, MySQL, SQL Server, SQLite, ClickHouse)
@@ -0,0 +1,26 @@
1
+ orange3_db_connections-0.1.3.dist-info/licenses/LICENSE,sha256=ogVw-HCawhI-QFk4Rt3-oXImtQPK_OaSNy4jGDwAmm0,1061
2
+ orangecontrib/__init__.py,sha256=mBY-cpLPvimaNLcMQPc9KX8HHH47-Xvd5H_5E2lUbms,91
3
+ orangecontrib/dbconnections/__init__.py,sha256=ZoZIvm7reEp3C-LQS8zxNb3aGls3ioUwR_dhl-OYIEQ,257
4
+ orangecontrib/dbconnections/utils/__init__.py,sha256=GPXY9Ny1mnwse5QiGblfpjNFWqD62y-9T0UxZKpV7co,73
5
+ orangecontrib/dbconnections/utils/driver_installer.py,sha256=ibYCYxJBYnDO9nSeoNrWneiCq52bWQjaY2_2Js7WnBo,1129
6
+ orangecontrib/dbconnections/widgets/__init__.py,sha256=Kp_AnLn2RSUqwS3joRchYovJdZ62NhoQ5iLQBj7N9wc,450
7
+ orangecontrib/dbconnections/widgets/_base_connection.py,sha256=wqR_j0MxfFi4LEjdeRikHCX7rVRGq8RakWqPPxPvnsE,7263
8
+ orangecontrib/dbconnections/widgets/ow_clickhouse_connection.py,sha256=yf7nlTvXfeSrZit54k4CLg0C6tvW9cf6yBOK3HBsrwA,1017
9
+ orangecontrib/dbconnections/widgets/ow_env_check.py,sha256=1msOfLGbm0FOj4ORFN0yLTKFhedyLhzaPC0RKOIQAcU,7042
10
+ orangecontrib/dbconnections/widgets/ow_mssql_connection.py,sha256=Fg4uukupZQFVZiCYWv_oJiAgaglohspNIj6xXOXgxDk,2793
11
+ orangecontrib/dbconnections/widgets/ow_mysql_connection.py,sha256=qQhh_cnXIzujKxFX8w7hMrvXh1CzvQIC6lC6mY-G1OU,659
12
+ orangecontrib/dbconnections/widgets/ow_oracle_connection.py,sha256=ojnb72rTA85A-vwjmA0pOdIBSeW67fsY4569SqRA4Bw,1242
13
+ orangecontrib/dbconnections/widgets/ow_pg_connection.py,sha256=jEQoTo4oDLdAHcu4-uB_g1_TI_RFGi5pfIHKLoJi8Co,698
14
+ orangecontrib/dbconnections/widgets/icons/addon_icon.png,sha256=XWlSBWTzzPSr-fov_S_g-IfzMmYt5f_T8rUEdwNP4k4,63087
15
+ orangecontrib/dbconnections/widgets/icons/clickhouse.png,sha256=qGHZ0e_PJPE_qt2ZWKszvPe1u0zHRZpuLtXoLyCn8D0,1888
16
+ orangecontrib/dbconnections/widgets/icons/mariadb.png,sha256=0O2IrF0oHlFBvdQVpanCNV1gaalgSqeu-wRKbrK9VAA,4194
17
+ orangecontrib/dbconnections/widgets/icons/msql.png,sha256=DrsnbNiorX7pr5BWV5zeqcBmb0fMLT5PGpUgdP7RjDo,10291
18
+ orangecontrib/dbconnections/widgets/icons/mysql.png,sha256=k0p_hQp1e5E6prHLIsp0L49rcfGPUIViQbRdsHzLPZU,4993
19
+ orangecontrib/dbconnections/widgets/icons/oracle.png,sha256=63Zy0KZGTXIGTMyI_2gmxfErUiRJULdsQeLaUru5_rQ,4325
20
+ orangecontrib/dbconnections/widgets/icons/postgres.png,sha256=NZdkF4Ajh7vwnvGtm2nemgKUGrq9Bkqx152sFPhqr4o,8616
21
+ orangecontrib/dbconnections/widgets/icons/sqlite.png,sha256=qKX9CdbSd6gljKcZtZXwq-VRIRmQCFJA0F6oLxCfgNY,3827
22
+ orange3_db_connections-0.1.3.dist-info/METADATA,sha256=V74RrAG0errYTvOqTEZwKCkwASfQIr_CmckH_rg_gpc,1251
23
+ orange3_db_connections-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ orange3_db_connections-0.1.3.dist-info/entry_points.txt,sha256=-Yr3AwKCv-lWfULrkGgfenGzCcywSKmvWUSwlgs_5FI,132
25
+ orange3_db_connections-0.1.3.dist-info/top_level.txt,sha256=Iut-JTfT11SZHHm77_ZeszD7pZDWXcTweCbvrJpqDyQ,14
26
+ orange3_db_connections-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [orange.addons]
2
+ DB Connections = orangecontrib.dbconnections
3
+
4
+ [orange.widgets]
5
+ DB Connections = orangecontrib.dbconnections.widgets
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Devi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ orangecontrib
@@ -0,0 +1,2 @@
1
+ from pkgutil import extend_path # type: ignore
2
+ __path__ = extend_path(__path__, __name__)
@@ -0,0 +1,10 @@
1
+ """orange3-db-connections add-on.
2
+
3
+ Kumpulan widget koneksi database (PostgreSQL, MySQL, SQL Server, SQLite, ClickHouse).
4
+ """
5
+
6
+ NAME = "DB Connections"
7
+ DESCRIPTION = "Widget koneksi ke berbagai database."
8
+ BACKGROUND = "#fdbc73"
9
+ ICON = "icons/addon_icon.png"
10
+
@@ -0,0 +1,3 @@
1
+ from .driver_installer import ensure_driver
2
+
3
+ __all__ = ["ensure_driver"]
@@ -0,0 +1,43 @@
1
+ from importlib import import_module
2
+ from typing import Tuple, Dict, Any
3
+
4
+
5
+ def _check_module(modname: str) -> Tuple[bool, str]:
6
+ try:
7
+ import_module(modname)
8
+ return True, f"Module {modname} tersedia."
9
+ except Exception as e:
10
+ return False, f"Module {modname} belum terpasang: {e}"
11
+
12
+
13
+ def ensure_driver(kind: str) -> Tuple[bool, str, Dict[str, Any]]:
14
+ """Pastikan driver Python untuk DB terkait sudah tersedia.
15
+
16
+ Parameters
17
+ ----------
18
+ kind:
19
+ Nama jenis DB, misal: "PostgreSQL", "MySQL", "SQLite",
20
+ "SQL Server", "ClickHouse", "Oracle".
21
+
22
+ Returns
23
+ -------
24
+ ok, log, extra
25
+ """
26
+ k = kind.lower()
27
+
28
+ if "postgres" in k:
29
+ return (*_check_module("psycopg2"), {})
30
+
31
+ if "mysql" in k:
32
+ return (*_check_module("pymysql"), {})
33
+
34
+ if "sql server" in k or "mssql" in k or "pyodbc" in k:
35
+ return (*_check_module("pyodbc"), {})
36
+
37
+ if "clickhouse" in k:
38
+ return (*_check_module("clickhouse_connect"), {})
39
+
40
+ if "oracle" in k:
41
+ return (*_check_module("cx_Oracle"), {})
42
+
43
+ return False, f"Jenis DB tidak dikenali: {kind}", {}
@@ -0,0 +1,16 @@
1
+ from .ow_pg_connection import OWPostgresConnection
2
+ from .ow_mysql_connection import OWMySQLConnection
3
+ from .ow_mssql_connection import OWMSSQLConnection
4
+ from .ow_clickhouse_connection import OWClickHouseConnection
5
+ from .ow_env_check import OWDbEnvCheck
6
+
7
+ __all__ = [
8
+ "OWPostgresConnection",
9
+ "OWMySQLConnection",
10
+ "OWMSSQLConnection",
11
+ "OWClickHouseConnection",
12
+ "OWDbEnvCheck",
13
+ ]
14
+
15
+ BACKGROUND = "#fdbc73"
16
+ ICON = "icons/addon_icon.png"
@@ -0,0 +1,240 @@
1
+ from AnyQt import QtWidgets, QtCore
2
+ from AnyQt.QtCore import QThread, pyqtSignal
3
+ from Orange.widgets.widget import OWWidget, Output, Msg
4
+ from orangewidget.settings import Setting
5
+ from orangewidget import gui
6
+
7
+ import sqlalchemy as sa
8
+ from typing import Dict, Any, Optional, Callable
9
+
10
+
11
+ class ConnectWorker(QThread):
12
+ finished_ok = pyqtSignal(object) # engine
13
+ failed = pyqtSignal(str)
14
+
15
+ def __init__(
16
+ self,
17
+ driver_kind: str,
18
+ params: Dict[str, Any],
19
+ build_url: Callable[[Dict[str, Any]], str],
20
+ parent=None,
21
+ ):
22
+ super().__init__(parent)
23
+ from orangecontrib.dbconnections.utils import ensure_driver
24
+
25
+ self.driver_kind = driver_kind
26
+ self.params = params
27
+ self.build_url = build_url
28
+ self._ensure_driver = ensure_driver
29
+
30
+ def run(self):
31
+ ok, log, _ = self._ensure_driver(self.driver_kind)
32
+ if not ok:
33
+ self.failed.emit(
34
+ f"Driver Python belum tersedia untuk {self.driver_kind}.\n{log}\n"
35
+ "Catatan: beberapa DB juga butuh ODBC/Client di OS."
36
+ )
37
+ return
38
+
39
+ try:
40
+ url = self.build_url(self.params)
41
+ engine = sa.create_engine(url, pool_pre_ping=True)
42
+
43
+ with engine.connect() as con:
44
+ # pilih test query sesuai dialect
45
+ try:
46
+ dname = engine.dialect.name.lower()
47
+ except Exception:
48
+ dname = ""
49
+
50
+ test_sql = "SELECT 1"
51
+ if "oracle" in dname:
52
+ test_sql = "SELECT 1 FROM DUAL"
53
+
54
+ con.exec_driver_sql(test_sql)
55
+
56
+ self.finished_ok.emit(engine)
57
+ except Exception as e:
58
+ self.failed.emit(str(e))
59
+
60
+ class BaseDBConnectionWidget(OWWidget, openclass=True):
61
+ """Base class untuk semua widget koneksi database.
62
+
63
+ Subclass wajib set:
64
+ - DB_KIND
65
+ - DEFAULT_PORT
66
+ - name, id, description, icon
67
+ - override :meth:`_build_url`
68
+ - optional override :meth:`_extra_controls`, :meth:`_on_connected_extra`
69
+ """
70
+
71
+ DB_KIND: str = "Generic DB"
72
+ DEFAULT_PORT: int = 0
73
+
74
+ icon = "icons/db_connection.svg"
75
+ priority = 10
76
+ want_main_area = False
77
+
78
+ class Outputs:
79
+ Connection = Output("Connection", object, auto_summary=False)
80
+
81
+ # settings umum
82
+ host: str = Setting("localhost")
83
+ port: int = Setting(0)
84
+ database: str = Setting("")
85
+ user: str = Setting("")
86
+ remember_password: bool = Setting(False)
87
+
88
+ _password_mem: str = "" # tidak disimpan sebagai Setting
89
+
90
+ class Error(OWWidget.Error):
91
+ connect_error = Msg("Gagal konek: {}")
92
+
93
+ class Info(OWWidget.Information):
94
+ connected = Msg("Terkoneksi ke database.")
95
+ # hint = Msg("Password tidak disimpan kecuali centang Remember.")
96
+ hint = Msg("")
97
+
98
+ class Warning(OWWidget.Warning):
99
+ generic_warn = Msg("{}")
100
+
101
+ def __init__(self):
102
+ super().__init__()
103
+
104
+ # --- form umum ---
105
+ box = gui.widgetBox(self.controlArea, "Koneksi")
106
+
107
+ gui.lineEdit(box, self, "host", label="Host:")
108
+ gui.spin(box, self, "port", 0, 65535, label="Port:", step=1)
109
+ gui.lineEdit(box, self, "database", label="Database/Schema/Path:")
110
+ gui.lineEdit(box, self, "user", label="Username:")
111
+ gui.lineEdit(
112
+ box, self, "_password_mem",
113
+ label="Password:",
114
+ echoMode=QtWidgets.QLineEdit.Password,
115
+ )
116
+ # gui.checkBox(box, self, "remember_password", "Remember password (plaintext)")
117
+
118
+ # area ekstra untuk subclass
119
+ self._extra_controls(box)
120
+
121
+ btns = gui.widgetBox(box, orientation=QtCore.Qt.Horizontal)
122
+ self.btn_connect = gui.button(btns, self, "Connect", callback=self._connect)
123
+
124
+ self.Info.hint()
125
+ self._worker: Optional[ConnectWorker] = None
126
+
127
+ if not self.port:
128
+ self._apply_default_port()
129
+
130
+ # ===== hooks untuk subclass =====
131
+ def _extra_controls(self, box: QtWidgets.QGroupBox) -> None:
132
+ """Override di subclass kalau butuh field tambahan."""
133
+ return None
134
+
135
+ def _apply_default_port(self):
136
+ self.port = getattr(self, "DEFAULT_PORT", 0)
137
+
138
+ def _params(self) -> Dict[str, Any]:
139
+ return {
140
+ "host": self.host.strip(),
141
+ "port": int(self.port),
142
+ "database": self.database.strip(),
143
+ "user": self.user.strip(),
144
+ "password": self._password_mem or "",
145
+ }
146
+
147
+ def _build_url(self, params: Dict[str, Any]) -> str:
148
+ """Override di subclass, return SQLAlchemy URL."""
149
+ raise NotImplementedError
150
+
151
+ def _driver_kind(self) -> str:
152
+ return getattr(self, "DB_KIND", "Generic DB")
153
+
154
+ # ===== lifecycle =====
155
+ def onDeleteWidget(self):
156
+ self._safe_kill_worker()
157
+ super().onDeleteWidget()
158
+
159
+ # ===== worker handling =====
160
+ def _toggle_busy(self, busy: bool):
161
+ self.btn_connect.setDisabled(busy)
162
+
163
+ def _safe_kill_worker(self):
164
+ w = getattr(self, "_worker", None)
165
+ if not w:
166
+ return
167
+ try:
168
+ try:
169
+ w.finished_ok.disconnect(self._on_connected)
170
+ except Exception:
171
+ pass
172
+ try:
173
+ w.failed.disconnect(self._on_failed)
174
+ except Exception:
175
+ pass
176
+ try:
177
+ w.finished.disconnect(self._on_worker_finished)
178
+ except Exception:
179
+ pass
180
+
181
+ if hasattr(w, "isRunning"):
182
+ try:
183
+ if w.isRunning():
184
+ getattr(w, "requestInterruption", lambda: None)()
185
+ w.quit()
186
+ w.wait(2000)
187
+ except RuntimeError:
188
+ pass
189
+
190
+ try:
191
+ w.setParent(None)
192
+ except Exception:
193
+ pass
194
+ try:
195
+ w.deleteLater()
196
+ except Exception:
197
+ pass
198
+ finally:
199
+ self._worker = None
200
+
201
+ # ===== actions =====
202
+ def _connect(self):
203
+ self._safe_kill_worker()
204
+ self.Error.clear()
205
+ self.Info.clear()
206
+ self.Warning.clear()
207
+ self._toggle_busy(True)
208
+
209
+ params = self._params()
210
+ self._worker = ConnectWorker(
211
+ driver_kind=self._driver_kind(),
212
+ params=params,
213
+ build_url=self._build_url,
214
+ parent=self,
215
+ )
216
+ self._worker.finished_ok.connect(self._on_connected)
217
+ self._worker.failed.connect(self._on_failed)
218
+ self._worker.finished.connect(self._on_worker_finished)
219
+ self._worker.start()
220
+
221
+ def _on_worker_finished(self):
222
+ self._toggle_busy(False)
223
+ self._safe_kill_worker()
224
+
225
+ def _on_connected_extra(self, engine) -> None:
226
+ """Subclass boleh override untuk cek tambahan setelah connect."""
227
+ return None
228
+
229
+ def _on_connected(self, engine):
230
+ self._on_connected_extra(engine)
231
+
232
+ if not self.remember_password:
233
+ self._password_mem = ""
234
+
235
+ self.Info.connected()
236
+ self.Outputs.Connection.send(engine)
237
+
238
+ def _on_failed(self, err: str):
239
+ self.Outputs.Connection.send(None)
240
+ self.Error.connect_error(err)
@@ -0,0 +1,32 @@
1
+ from typing import Dict, Any
2
+ from ._base_connection import BaseDBConnectionWidget
3
+
4
+
5
+ class OWClickHouseConnection(BaseDBConnectionWidget):
6
+ name = "ClickHouse"
7
+ id = "dbconnections-clickhouse-connection"
8
+ description = "Koneksi ke ClickHouse (driver native)."
9
+ icon = "icons/clickhouse.png"
10
+
11
+ DB_KIND = "ClickHouse"
12
+ DEFAULT_PORT = 8123 # HTTP port (clickhouse-connect)
13
+
14
+ def _build_url(self, params: Dict[str, Any]) -> str:
15
+ user = params.get("user") or ""
16
+ pwd = params.get("password") or ""
17
+ host = params.get("host") or "localhost"
18
+ port = params.get("port") or 8123
19
+ db = params.get("database") or ""
20
+
21
+ auth = ""
22
+ if user:
23
+ if pwd:
24
+ auth = f"{user}:{pwd}@"
25
+ else:
26
+ auth = f"{user}@"
27
+
28
+ # Dialect milik clickhouse-connect
29
+ return f"clickhousedb://{auth}{host}:{port}/{db}"
30
+ # atau kalau mau eksplisit:
31
+ # return f"clickhousedb+connect://{auth}{host}:{port}/{db}"
32
+
@@ -0,0 +1,223 @@
1
+ import sys
2
+ import platform
3
+ from importlib import import_module
4
+
5
+ from AnyQt.QtCore import Qt
6
+ from AnyQt.QtWidgets import (
7
+ QWidget,
8
+ QVBoxLayout,
9
+ QPushButton,
10
+ QTextEdit,
11
+ QTableWidget,
12
+ QTableWidgetItem,
13
+ QHeaderView,
14
+ QLabel,
15
+ )
16
+
17
+ from Orange.widgets.widget import OWWidget
18
+
19
+
20
+ # Daftar “tipe koneksi” yang ada di add-on DB Connections
21
+ # Fokus: modul Python & catatan OS/driver
22
+ CONNECTION_SPECS = [
23
+ {
24
+ "id": "mssql",
25
+ "label": "SQL Server (pyodbc + ODBC)",
26
+ "module": "pyodbc",
27
+ "pip": "pyodbc",
28
+ "note": "Butuh ODBC Driver 17/18 for SQL Server di Windows.",
29
+ },
30
+ {
31
+ "id": "postgresql",
32
+ "label": "PostgreSQL (psycopg2-binary)",
33
+ "module": "psycopg2",
34
+ "pip": "psycopg2-binary",
35
+ "note": "Driver pure Python, tidak perlu client tambahan.",
36
+ },
37
+ {
38
+ "id": "mysql",
39
+ "label": "MySQL (pymysql)",
40
+ "module": "pymysql",
41
+ "pip": "pymysql",
42
+ "note": "Driver pure Python, tidak perlu client tambahan.",
43
+ },
44
+ {
45
+ "id": "sqlite",
46
+ "label": "SQLite (builtin sqlite3)",
47
+ "module": "sqlite3",
48
+ "pip": None,
49
+ "note": "Sudah bawaan Python, seharusnya selalu tersedia.",
50
+ },
51
+ {
52
+ "id": "clickhouse",
53
+ "label": "ClickHouse (clickhouse-connect)",
54
+ "module": "clickhouse_connect",
55
+ "pip": "clickhouse-connect",
56
+ "note": "Tidak perlu client tambahan.",
57
+ },
58
+ {
59
+ "id": "oracle",
60
+ "label": "Oracle (cx-Oracle)",
61
+ "module": "cx_Oracle",
62
+ "pip": "cx-Oracle",
63
+ "note": "Butuh Oracle Instant Client + konfigurasi PATH/ORACLE_HOME.",
64
+ },
65
+ ]
66
+
67
+
68
+ def _check_module(mod_name: str):
69
+ """
70
+ Coba import modul. Return (status, detail).
71
+ status: 'OK' | 'Missing'
72
+ """
73
+ try:
74
+ mod = import_module(mod_name)
75
+ version = getattr(mod, "__version__", "?")
76
+ return "OK", f"Terpasang (versi {version})"
77
+ except Exception as e:
78
+ return "Missing", f"Tidak ditemukan: {e.__class__.__name__}: {e}"
79
+
80
+
81
+ def _check_odbc():
82
+ """
83
+ Cek daftar ODBC driver (kalau pyodbc tersedia).
84
+ """
85
+ try:
86
+ import pyodbc
87
+ except ImportError:
88
+ return "Missing", "pyodbc belum terpasang, tidak bisa cek ODBC driver.", []
89
+
90
+ drivers = list(pyodbc.drivers())
91
+ if not drivers:
92
+ return (
93
+ "Warning",
94
+ "pyodbc ada, tapi tidak ada ODBC driver terdeteksi. "
95
+ "Pastikan ODBC Driver 17 atau 18 for SQL Server sudah terinstal.",
96
+ [],
97
+ )
98
+
99
+ return "OK", f"Ditemukan {len(drivers)} driver ODBC.", drivers
100
+
101
+
102
+ class OWDbEnvCheck(OWWidget):
103
+ """
104
+ Widget khusus DB Connections:
105
+ - Menampilkan info Python/OS yang dipakai Orange
106
+ - Mengecek modul driver DB (pyodbc, psycopg2, dll.)
107
+ - Mengecek ODBC driver (kalau pyodbc tersedia)
108
+ """
109
+
110
+ name = "Env Check"
111
+ description = "Cek environment untuk koneksi database (driver Python & ODBC)."
112
+ icon = "icons/db_env_check.svg" # ganti sementara ke icon yang ada kalau perlu
113
+ priority = 100 # supaya muncul agak ke kanan di palette
114
+
115
+ want_main_area = True
116
+ want_control_area = False
117
+
118
+ def __init__(self):
119
+ super().__init__()
120
+
121
+ # ---- Widgets UI ----
122
+ self.btn_scan = QPushButton("🔍 Scan DB Environment")
123
+ self.btn_scan.clicked.connect(self.scan_environment)
124
+
125
+ self.table = QTableWidget(0, 5)
126
+ self.table.setHorizontalHeaderLabels(
127
+ ["Connection", "Python Module", "Status", "Detail", "Cara Perbaikan"]
128
+ )
129
+ self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
130
+ self.table.setMinimumHeight(240)
131
+
132
+ self.txt_summary = QTextEdit()
133
+ self.txt_summary.setReadOnly(True)
134
+ self.txt_summary.setMinimumHeight(160)
135
+
136
+ main = QVBoxLayout()
137
+ main.addWidget(
138
+ QLabel(
139
+ "<b>Bidics DB Connections – Environment Check</b><br/>"
140
+ "Scan modul Python & driver ODBC yang dibutuhkan oleh widget koneksi database."
141
+ )
142
+ )
143
+ main.addWidget(self.btn_scan)
144
+ main.addWidget(self.table)
145
+ main.addWidget(QLabel("<b>Ringkasan</b>"))
146
+ main.addWidget(self.txt_summary)
147
+
148
+ w = QWidget()
149
+ w.setLayout(main)
150
+ self.mainArea.layout().addWidget(w)
151
+
152
+ # --------------------------------------------------
153
+ # HELPER
154
+ # --------------------------------------------------
155
+ def _build_fix_command(self, pip_name: str | None) -> str:
156
+ """
157
+ Perintah yang disarankan untuk memperbaiki.
158
+ pip_name bisa None (untuk modul builtin seperti sqlite3).
159
+ """
160
+ python_exe = sys.executable
161
+ if pip_name:
162
+ return f'"{python_exe}" -m pip install {pip_name}'
163
+ return "(builtin; kalau error, cek instalasi Python/Orange)."
164
+
165
+ # --------------------------------------------------
166
+ # MAIN ACTION
167
+ # --------------------------------------------------
168
+ def scan_environment(self):
169
+ """
170
+ Scan lengkap dan isi tabel + summary.
171
+ """
172
+ self.table.setRowCount(0)
173
+
174
+ # Info Python & OS
175
+ py_info_lines = [
176
+ f"Python Executable : {sys.executable}",
177
+ f"Python Version : {sys.version.split()[0]}",
178
+ f"Platform : {platform.platform()}",
179
+ f"Arch : {platform.architecture()[0]}",
180
+ "",
181
+ "=== Status Driver Python ===",
182
+ ]
183
+
184
+ # Cek modul untuk tiap jenis koneksi
185
+ rows = []
186
+ for spec in CONNECTION_SPECS:
187
+ status, detail = _check_module(spec["module"])
188
+ fix_cmd = self._build_fix_command(spec["pip"])
189
+ rows.append(
190
+ (
191
+ spec["label"],
192
+ spec["module"],
193
+ status,
194
+ detail,
195
+ fix_cmd,
196
+ )
197
+ )
198
+ py_info_lines.append(f"- {spec['label']}: {status} ({detail})")
199
+
200
+ # Tambah info ODBC (khusus SQL Server)
201
+ py_info_lines.append("")
202
+ py_info_lines.append("=== Status ODBC (SQL Server) ===")
203
+ odbc_status, odbc_detail, odbc_drivers = _check_odbc()
204
+ py_info_lines.append(f"ODBC Drivers: {odbc_status} ({odbc_detail})")
205
+ if odbc_drivers:
206
+ py_info_lines.append("Daftar driver:")
207
+ for drv in odbc_drivers:
208
+ py_info_lines.append(f" - {drv}")
209
+
210
+ # Isi tabel
211
+ for row_data in rows:
212
+ r = self.table.rowCount()
213
+ self.table.insertRow(r)
214
+ for c, value in enumerate(row_data):
215
+ item = QTableWidgetItem(value)
216
+ if c == 2: # kolom Status
217
+ if value == "Missing":
218
+ item.setForeground(Qt.red)
219
+ elif value == "Warning":
220
+ item.setForeground(Qt.darkYellow)
221
+ self.table.setItem(r, c, item)
222
+
223
+ self.txt_summary.setText("\n".join(py_info_lines))
@@ -0,0 +1,76 @@
1
+ from typing import Dict, Any
2
+
3
+ from AnyQt import QtWidgets
4
+ from Orange.widgets.widget import Msg
5
+ from orangewidget import gui
6
+ from orangewidget.settings import Setting
7
+
8
+ from ._base_connection import BaseDBConnectionWidget
9
+
10
+
11
+ class OWMSSQLConnection(BaseDBConnectionWidget):
12
+ name = "SQL Server"
13
+ id = "dbconnections-mssql-connection"
14
+ description = "Koneksi ke Microsoft SQL Server via pyodbc."
15
+ icon = "icons/msql.png"
16
+
17
+ DB_KIND = "SQL Server (pyodbc)"
18
+ DEFAULT_PORT = 1433
19
+
20
+ integrated_auth: bool = Setting(False)
21
+ odbc_driver: str = Setting("ODBC Driver 18 for SQL Server")
22
+ trust_server_cert: bool = Setting(True)
23
+
24
+ class Warning(BaseDBConnectionWidget.Warning):
25
+ mssql_odbc_missing = Msg(
26
+ "pyodbc terpasang, tetapi ODBC driver OS untuk SQL Server tidak ditemukan. "
27
+ "Pasang 'Microsoft ODBC Driver 18 for SQL Server' di OS Anda."
28
+ )
29
+
30
+ def _extra_controls(self, box: QtWidgets.QGroupBox) -> None:
31
+ # gui.checkBox(box, self, "integrated_auth", "Login dengan AD / Integrated")
32
+ gui.checkBox(box, self, "trust_server_cert", "Trust Server Certificate")
33
+ gui.lineEdit(box, self, "odbc_driver", label="ODBC Driver:")
34
+
35
+ def _params(self) -> Dict[str, Any]:
36
+ p = super()._params()
37
+ p.update({
38
+ "integrated_auth": bool(self.integrated_auth),
39
+ "odbc_driver": self.odbc_driver.strip(),
40
+ "trust_server_cert": bool(self.trust_server_cert),
41
+ })
42
+ return p
43
+
44
+ def _build_url(self, params: Dict[str, Any]) -> str:
45
+ host = params.get("host") or ""
46
+ port = params.get("port") or 1433
47
+ db = params.get("database") or ""
48
+ user = params.get("user") or ""
49
+ pwd = params.get("password") or ""
50
+ integrated = params.get("integrated_auth", False)
51
+ odbc_driver = params.get("odbc_driver", "ODBC Driver 18 for SQL Server")
52
+ extra = f"TrustServerCertificate={'yes' if params.get('trust_server_cert', True) else 'no'}"
53
+
54
+ if not integrated:
55
+ return (
56
+ "mssql+pyodbc://"
57
+ f"{user}:{pwd}@{host}:{port}/{db}"
58
+ f"?driver={odbc_driver.replace(' ', '+')}&{extra}"
59
+ )
60
+
61
+ return (
62
+ "mssql+pyodbc://@"
63
+ f"{host}:{port}/{db}"
64
+ f"?driver={odbc_driver.replace(' ', '+')}&Trusted_Connection=yes&{extra}"
65
+ )
66
+
67
+ def _on_connected_extra(self, engine) -> None:
68
+ # cek ODBC driver OS
69
+ try:
70
+ import pyodbc # type: ignore
71
+ drivers = {d.lower() for d in pyodbc.drivers()}
72
+ expected = self.odbc_driver.lower()
73
+ if not any(expected in d for d in drivers):
74
+ self.Warning.mssql_odbc_missing()
75
+ except Exception:
76
+ pass
@@ -0,0 +1,20 @@
1
+ from typing import Dict, Any
2
+ from ._base_connection import BaseDBConnectionWidget
3
+
4
+
5
+ class OWMySQLConnection(BaseDBConnectionWidget):
6
+ name = "MySQL"
7
+ id = "dbconnections-mysql-connection"
8
+ description = "Koneksi ke MySQL/MariaDB via PyMySQL."
9
+ icon = "icons/mysql.png"
10
+
11
+ DB_KIND = "MySQL"
12
+ DEFAULT_PORT = 3306
13
+
14
+ def _build_url(self, params: Dict[str, Any]) -> str:
15
+ user = params.get("user") or ""
16
+ pwd = params.get("password") or ""
17
+ host = params.get("host") or ""
18
+ port = params.get("port") or 3306
19
+ db = params.get("database") or ""
20
+ return f"mysql+pymysql://{user}:{pwd}@{host}:{port}/{db}"
@@ -0,0 +1,41 @@
1
+ from typing import Dict, Any
2
+ from ._base_connection import BaseDBConnectionWidget
3
+
4
+
5
+ class OWOracleConnection(BaseDBConnectionWidget):
6
+ name = "Oracle"
7
+ id = "dbconnections-oracle-connection"
8
+ description = "Koneksi ke Oracle Database. Output: SQLAlchemy Engine."
9
+ icon = "icons/oracle.png"
10
+
11
+ DB_KIND = "Oracle"
12
+ DEFAULT_PORT = 1521 # port default listener Oracle
13
+
14
+ def _build_url(self, params: Dict[str, Any]) -> str:
15
+ """
16
+ Bangun SQLAlchemy URL untuk Oracle (cx_Oracle).
17
+
18
+ Field "Database/Schema/Path" di form diisi dengan SERVICE_NAME,
19
+ misalnya: ORCLPDB1
20
+ """
21
+ user = params.get("user") or ""
22
+ pwd = params.get("password") or ""
23
+ host = params.get("host") or "localhost"
24
+ port = params.get("port") or 1521
25
+ service = params.get("database") or "" # SERVICE_NAME
26
+
27
+ # auth
28
+ auth = ""
29
+ if user:
30
+ if pwd:
31
+ auth = f"{user}:{pwd}@"
32
+ else:
33
+ auth = f"{user}@"
34
+
35
+ # easy connect dengan service_name
36
+ if service:
37
+ dsn = f"{host}:{port}/?service_name={service}"
38
+ else:
39
+ dsn = f"{host}:{port}"
40
+
41
+ return f"oracle+cx_oracle://{auth}{dsn}"
@@ -0,0 +1,20 @@
1
+ from typing import Dict, Any
2
+ from ._base_connection import BaseDBConnectionWidget
3
+
4
+
5
+ class OWPostgresConnection(BaseDBConnectionWidget):
6
+ name = "PostgreSQL"
7
+ id = "dbconnections-postgresql-connection"
8
+ description = "Koneksi ke PostgreSQL. Output: SQLAlchemy Engine."
9
+ icon = "icons/postgres.png"
10
+
11
+ DB_KIND = "PostgreSQL"
12
+ DEFAULT_PORT = 5432
13
+
14
+ def _build_url(self, params: Dict[str, Any]) -> str:
15
+ user = params.get("user") or ""
16
+ pwd = params.get("password") or ""
17
+ host = params.get("host") or ""
18
+ port = params.get("port") or 5432
19
+ db = params.get("database") or ""
20
+ return f"postgresql+psycopg2://{user}:{pwd}@{host}:{port}/{db}"