oqtopus 0.1.14__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.
- oqtopus/__init__.py +4 -0
- oqtopus/core/module.py +115 -0
- oqtopus/core/module_asset.py +16 -0
- oqtopus/core/module_package.py +118 -0
- oqtopus/core/modules_config.py +11 -0
- oqtopus/core/package_prepare_task.py +148 -0
- oqtopus/gui/__init__.py +0 -0
- oqtopus/gui/about_dialog.py +61 -0
- oqtopus/gui/database_connection_widget.py +154 -0
- oqtopus/gui/database_create_dialog.py +210 -0
- oqtopus/gui/database_duplicate_dialog.py +100 -0
- oqtopus/gui/logs_widget.py +177 -0
- oqtopus/gui/main_dialog.py +168 -0
- oqtopus/gui/module_selection_widget.py +350 -0
- oqtopus/gui/module_widget.py +199 -0
- oqtopus/gui/parameters_groupbox.py +75 -0
- oqtopus/gui/project_widget.py +170 -0
- oqtopus/gui/settings_dialog.py +37 -0
- oqtopus/oqtopus.py +67 -0
- oqtopus/oqtopus_plugin.py +184 -0
- oqtopus/ui/__init__.py +0 -0
- oqtopus/utils/__init__.py +0 -0
- oqtopus/utils/plugin_utils.py +172 -0
- oqtopus/utils/qt_utils.py +94 -0
- oqtopus/utils/tmmtlogging.py +50 -0
- oqtopus/utils/translation.py +84 -0
- oqtopus-0.1.14.dist-info/METADATA +363 -0
- oqtopus-0.1.14.dist-info/RECORD +33 -0
- oqtopus-0.1.14.dist-info/WHEEL +5 -0
- oqtopus-0.1.14.dist-info/licenses/LICENSE +339 -0
- oqtopus-0.1.14.dist-info/top_level.txt +2 -0
- tests/__init__.py +12 -0
- tests/test_plugin_load.py +22 -0
oqtopus/__init__.py
ADDED
oqtopus/core/module.py
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from qgis.PyQt.QtCore import QByteArray, QObject, QUrl, pyqtSignal
|
4
|
+
from qgis.PyQt.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
|
5
|
+
|
6
|
+
from ..utils.plugin_utils import PluginUtils
|
7
|
+
from .module_package import ModulePackage
|
8
|
+
|
9
|
+
|
10
|
+
class Module(QObject):
|
11
|
+
signal_versionsLoaded = pyqtSignal(str)
|
12
|
+
signal_developmentVersionsLoaded = pyqtSignal(str)
|
13
|
+
|
14
|
+
def __init__(self, name: str, organisation: str, repository: str, parent=None):
|
15
|
+
super().__init__(parent)
|
16
|
+
self.name = name
|
17
|
+
self.organisation = organisation
|
18
|
+
self.repository = repository
|
19
|
+
self.versions = []
|
20
|
+
self.development_versions = []
|
21
|
+
self.latest_version = None
|
22
|
+
self.network_manager = QNetworkAccessManager(self)
|
23
|
+
|
24
|
+
def __repr__(self):
|
25
|
+
return f"Module(name={self.name}, organisation={self.organisation}, repository={self.repository})"
|
26
|
+
|
27
|
+
def start_load_versions(self):
|
28
|
+
url = QUrl(f"https://api.github.com/repos/{self.organisation}/{self.repository}/releases")
|
29
|
+
request = QNetworkRequest(url)
|
30
|
+
headers = PluginUtils.get_github_headers()
|
31
|
+
for key, value in headers.items():
|
32
|
+
request.setRawHeader(QByteArray(key.encode()), QByteArray(value.encode()))
|
33
|
+
reply = self.network_manager.get(request)
|
34
|
+
reply.finished.connect(lambda: self._on_versions_reply(reply))
|
35
|
+
|
36
|
+
def _on_versions_reply(self, reply):
|
37
|
+
if reply.error() != QNetworkReply.NetworkError.NoError:
|
38
|
+
self.signal_versionsLoaded.emit(reply.errorString())
|
39
|
+
reply.deleteLater()
|
40
|
+
return
|
41
|
+
try:
|
42
|
+
data = reply.readAll().data()
|
43
|
+
json_versions = json.loads(data.decode())
|
44
|
+
self.versions = []
|
45
|
+
self.latest_version = None
|
46
|
+
for json_version in json_versions:
|
47
|
+
module_package = ModulePackage(
|
48
|
+
module=self,
|
49
|
+
organisation=self.organisation,
|
50
|
+
repository=self.repository,
|
51
|
+
json_payload=json_version,
|
52
|
+
type=ModulePackage.Type.RELEASE,
|
53
|
+
)
|
54
|
+
self.versions.append(module_package)
|
55
|
+
|
56
|
+
# Latest version -> most recent commit date for non prerelease
|
57
|
+
if module_package.prerelease is True:
|
58
|
+
continue
|
59
|
+
|
60
|
+
if self.latest_version is None:
|
61
|
+
self.latest_version = module_package
|
62
|
+
continue
|
63
|
+
|
64
|
+
if module_package.created_at > self.latest_version.created_at:
|
65
|
+
self.latest_version = module_package
|
66
|
+
self.signal_versionsLoaded.emit("")
|
67
|
+
except Exception as e:
|
68
|
+
self.signal_versionsLoaded.emit(str(e))
|
69
|
+
reply.deleteLater()
|
70
|
+
|
71
|
+
def start_load_development_versions(self):
|
72
|
+
self.development_versions = []
|
73
|
+
|
74
|
+
# Create version for the main branch
|
75
|
+
mainVersion = ModulePackage(
|
76
|
+
module=self,
|
77
|
+
organisation=self.organisation,
|
78
|
+
repository=self.repository,
|
79
|
+
json_payload="",
|
80
|
+
type=ModulePackage.Type.BRANCH,
|
81
|
+
name="main",
|
82
|
+
branch="main",
|
83
|
+
)
|
84
|
+
self.development_versions.append(mainVersion)
|
85
|
+
|
86
|
+
url = QUrl(f"https://api.github.com/repos/{self.organisation}/{self.repository}/pulls")
|
87
|
+
request = QNetworkRequest(url)
|
88
|
+
headers = PluginUtils.get_github_headers()
|
89
|
+
for key, value in headers.items():
|
90
|
+
request.setRawHeader(QByteArray(key.encode()), QByteArray(value.encode()))
|
91
|
+
reply = self.network_manager.get(request)
|
92
|
+
reply.finished.connect(lambda: self._on_development_versions_reply(reply))
|
93
|
+
|
94
|
+
def _on_development_versions_reply(self, reply):
|
95
|
+
if reply.error() != QNetworkReply.NetworkError.NoError:
|
96
|
+
self.signal_developmentVersionsLoaded.emit(reply.errorString())
|
97
|
+
reply.deleteLater()
|
98
|
+
return
|
99
|
+
|
100
|
+
try:
|
101
|
+
data = reply.readAll().data()
|
102
|
+
json_versions = json.loads(data.decode())
|
103
|
+
for json_version in json_versions:
|
104
|
+
module_package = ModulePackage(
|
105
|
+
module=self,
|
106
|
+
organisation=self.organisation,
|
107
|
+
repository=self.repository,
|
108
|
+
json_payload=json_version,
|
109
|
+
type=ModulePackage.Type.PULL_REQUEST,
|
110
|
+
)
|
111
|
+
self.development_versions.append(module_package)
|
112
|
+
self.signal_developmentVersionsLoaded.emit("")
|
113
|
+
except Exception as e:
|
114
|
+
self.signal_developmentVersionsLoaded.emit(str(e))
|
115
|
+
reply.deleteLater()
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
|
4
|
+
class ModuleAsset:
|
5
|
+
class Type(Enum):
|
6
|
+
PLUGIN = "oqtopus.plugin"
|
7
|
+
PROJECT = "oqtopus.project"
|
8
|
+
|
9
|
+
def __init__(
|
10
|
+
self, name: str, label: str, download_url: str, size: int, type: "ModuleAsset.Type" = None
|
11
|
+
):
|
12
|
+
self.name = name
|
13
|
+
self.label = label
|
14
|
+
self.download_url = download_url
|
15
|
+
self.size = size
|
16
|
+
self.type = type
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import requests
|
2
|
+
from qgis.PyQt.QtCore import QDateTime, Qt
|
3
|
+
|
4
|
+
from ..utils.plugin_utils import PluginUtils
|
5
|
+
from .module_asset import ModuleAsset
|
6
|
+
|
7
|
+
|
8
|
+
class ModulePackage:
|
9
|
+
|
10
|
+
# enum for package type
|
11
|
+
class Type:
|
12
|
+
RELEASE = "release"
|
13
|
+
BRANCH = "branch"
|
14
|
+
PULL_REQUEST = "pull_request"
|
15
|
+
FROM_ZIP = "from_zip"
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
module,
|
20
|
+
organisation,
|
21
|
+
repository,
|
22
|
+
json_payload: dict,
|
23
|
+
type=Type.RELEASE,
|
24
|
+
name=None,
|
25
|
+
branch=None,
|
26
|
+
):
|
27
|
+
self.module = module
|
28
|
+
self.type = type
|
29
|
+
self.name = name
|
30
|
+
self.branch = branch
|
31
|
+
self.created_at = None
|
32
|
+
self.prerelease = False
|
33
|
+
self.html_url = None
|
34
|
+
|
35
|
+
self.asset_project = None
|
36
|
+
self.asset_plugin = None
|
37
|
+
|
38
|
+
if self.type == ModulePackage.Type.RELEASE:
|
39
|
+
self.__parse_release(json_payload)
|
40
|
+
elif self.type == ModulePackage.Type.BRANCH:
|
41
|
+
pass
|
42
|
+
elif self.type == ModulePackage.Type.PULL_REQUEST:
|
43
|
+
self.__parse_pull_request(json_payload)
|
44
|
+
elif self.type == ModulePackage.Type.FROM_ZIP:
|
45
|
+
return
|
46
|
+
else:
|
47
|
+
raise ValueError(f"Unknown type '{type}'")
|
48
|
+
|
49
|
+
type = "heads"
|
50
|
+
if self.type == ModulePackage.Type.RELEASE:
|
51
|
+
type = "tags"
|
52
|
+
|
53
|
+
self.download_url = (
|
54
|
+
f"https://github.com/{organisation}/{repository}/archive/refs/{type}/{self.branch}.zip"
|
55
|
+
)
|
56
|
+
|
57
|
+
self.zip_file = None
|
58
|
+
self.package_dir = None
|
59
|
+
|
60
|
+
def display_name(self):
|
61
|
+
if self.prerelease:
|
62
|
+
return f"{self.name} (prerelease)"
|
63
|
+
|
64
|
+
return self.name
|
65
|
+
|
66
|
+
def __parse_release(self, json_payload: dict):
|
67
|
+
if self.name is None:
|
68
|
+
self.name = json_payload["name"]
|
69
|
+
|
70
|
+
if self.name is None or self.name == "":
|
71
|
+
self.name = json_payload["tag_name"]
|
72
|
+
|
73
|
+
self.branch = self.name
|
74
|
+
self.created_at = QDateTime.fromString(json_payload["created_at"], Qt.DateFormat.ISODate)
|
75
|
+
self.prerelease = json_payload["prerelease"]
|
76
|
+
self.html_url = json_payload["html_url"]
|
77
|
+
|
78
|
+
self.__parse_release_assets(json_payload["assets_url"])
|
79
|
+
|
80
|
+
def __parse_release_assets(self, assets_url: str):
|
81
|
+
|
82
|
+
# Load assets
|
83
|
+
r = requests.get(assets_url, headers=PluginUtils.get_github_headers())
|
84
|
+
|
85
|
+
# Raise an exception in case of http errors
|
86
|
+
r.raise_for_status()
|
87
|
+
|
88
|
+
json_assets = r.json()
|
89
|
+
for json_asset in json_assets:
|
90
|
+
asset = ModuleAsset(
|
91
|
+
name=json_asset["name"],
|
92
|
+
label=json_asset["label"],
|
93
|
+
download_url=json_asset["browser_download_url"],
|
94
|
+
size=json_asset["size"],
|
95
|
+
type=None,
|
96
|
+
)
|
97
|
+
|
98
|
+
if asset.label == ModuleAsset.Type.PROJECT.value:
|
99
|
+
asset.type = ModuleAsset.Type.PROJECT
|
100
|
+
self.asset_project = asset
|
101
|
+
continue
|
102
|
+
|
103
|
+
if asset.label == ModuleAsset.Type.PLUGIN.value:
|
104
|
+
asset.type = ModuleAsset.Type.PLUGIN
|
105
|
+
self.asset_plugin = asset
|
106
|
+
continue
|
107
|
+
|
108
|
+
if self.asset_project and self.asset_plugin:
|
109
|
+
# We already have all assets we need
|
110
|
+
break
|
111
|
+
|
112
|
+
def __parse_pull_request(self, json_payload: dict):
|
113
|
+
if self.name is None:
|
114
|
+
self.name = f"#{json_payload['number']} {json_payload['title']}"
|
115
|
+
self.branch = json_payload["head"]["ref"]
|
116
|
+
self.created_at = QDateTime.fromString(json_payload["created_at"], Qt.DateFormat.ISODate)
|
117
|
+
self.prerelease = False
|
118
|
+
self.html_url = json_payload["html_url"]
|
@@ -0,0 +1,148 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
import zipfile
|
4
|
+
|
5
|
+
import requests
|
6
|
+
from qgis.PyQt.QtCore import QThread, pyqtSignal
|
7
|
+
|
8
|
+
from ..utils.plugin_utils import PluginUtils, logger
|
9
|
+
|
10
|
+
|
11
|
+
class PackagePrepareTaskCanceled(Exception):
|
12
|
+
pass
|
13
|
+
|
14
|
+
|
15
|
+
class PackagePrepareTask(QThread):
|
16
|
+
"""
|
17
|
+
This class is responsible for preparing the package for the Oqtopus module management tool.
|
18
|
+
It inherits from QThread to run the preparation process in a separate thread.
|
19
|
+
"""
|
20
|
+
|
21
|
+
signalPackagingProgress = pyqtSignal(float)
|
22
|
+
|
23
|
+
def __init__(self, parent=None):
|
24
|
+
super().__init__(parent)
|
25
|
+
|
26
|
+
self.module_package = None
|
27
|
+
|
28
|
+
self.__canceled = False
|
29
|
+
self.lastError = None
|
30
|
+
|
31
|
+
def startFromZip(self, zip_file: str):
|
32
|
+
|
33
|
+
self.module_package = None
|
34
|
+
|
35
|
+
self.__canceled = False
|
36
|
+
self.start()
|
37
|
+
|
38
|
+
def startFromModulePackage(self, module_package):
|
39
|
+
self.module_package = module_package
|
40
|
+
|
41
|
+
self.__canceled = False
|
42
|
+
self.start()
|
43
|
+
|
44
|
+
def cancel(self):
|
45
|
+
self.__canceled = True
|
46
|
+
|
47
|
+
def run(self):
|
48
|
+
"""
|
49
|
+
The main method that runs when the thread starts.
|
50
|
+
"""
|
51
|
+
|
52
|
+
try:
|
53
|
+
if self.module_package is None:
|
54
|
+
raise Exception(self.tr("No module version provided."))
|
55
|
+
|
56
|
+
self.__download_module_assets(self.module_package)
|
57
|
+
self.lastError = None
|
58
|
+
|
59
|
+
except Exception as e:
|
60
|
+
# Handle any exceptions that occur during processing
|
61
|
+
logger.critical(f"Package prepare task error: {e}")
|
62
|
+
self.lastError = e
|
63
|
+
|
64
|
+
def __download_module_assets(self, module_package):
|
65
|
+
|
66
|
+
# Download the source
|
67
|
+
zip_file = self.__download_module_asset(module_package.download_url, "source.zip")
|
68
|
+
module_package.zip_file = zip_file
|
69
|
+
package_dir = self.__extract_zip_file(zip_file)
|
70
|
+
module_package.package_dir = package_dir
|
71
|
+
|
72
|
+
# Download the release assets
|
73
|
+
self.__checkForCanceled()
|
74
|
+
if module_package.asset_project is not None:
|
75
|
+
zip_file = self.__download_module_asset(
|
76
|
+
module_package.asset_project.download_url,
|
77
|
+
module_package.asset_project.type.value + ".zip",
|
78
|
+
)
|
79
|
+
package_dir = self.__extract_zip_file(zip_file)
|
80
|
+
module_package.asset_project.package_dir = package_dir
|
81
|
+
|
82
|
+
self.__checkForCanceled()
|
83
|
+
if module_package.asset_plugin is not None:
|
84
|
+
zip_file = self.__download_module_asset(
|
85
|
+
module_package.asset_plugin.download_url,
|
86
|
+
module_package.asset_plugin.type.value + ".zip",
|
87
|
+
)
|
88
|
+
package_dir = self.__extract_zip_file(zip_file)
|
89
|
+
module_package.asset_plugin.package_dir = package_dir
|
90
|
+
|
91
|
+
def __download_module_asset(self, url: str, filename: str):
|
92
|
+
|
93
|
+
temp_dir = PluginUtils.plugin_temp_path()
|
94
|
+
destination_directory = os.path.join(temp_dir, "Downloads")
|
95
|
+
os.makedirs(destination_directory, exist_ok=True)
|
96
|
+
|
97
|
+
zip_file = os.path.join(destination_directory, filename)
|
98
|
+
|
99
|
+
# Streaming, so we can iterate over the response.
|
100
|
+
response = requests.get(url, allow_redirects=True, stream=True)
|
101
|
+
|
102
|
+
# Raise an exception in case of http errors
|
103
|
+
response.raise_for_status()
|
104
|
+
|
105
|
+
self.__checkForCanceled()
|
106
|
+
|
107
|
+
logger.info(f"Downloading from '{url}' to '{zip_file}'")
|
108
|
+
data_size = 0
|
109
|
+
with open(zip_file, "wb") as file:
|
110
|
+
next_emit_threshold = 10 * 1024 * 1024 # 10MB threshold
|
111
|
+
for data in response.iter_content(chunk_size=None):
|
112
|
+
file.write(data)
|
113
|
+
|
114
|
+
self.__checkForCanceled()
|
115
|
+
|
116
|
+
data_size += len(data)
|
117
|
+
if data_size >= next_emit_threshold: # Emit signal when threshold is exceeded
|
118
|
+
self.signalPackagingProgress.emit(data_size)
|
119
|
+
next_emit_threshold += 10 * 1024 * 1024 # Update to the next threshold
|
120
|
+
|
121
|
+
return zip_file
|
122
|
+
|
123
|
+
def __extract_zip_file(self, zip_file):
|
124
|
+
temp_dir = PluginUtils.plugin_temp_path()
|
125
|
+
|
126
|
+
# Unzip the file to plugin temp dir
|
127
|
+
try:
|
128
|
+
with zipfile.ZipFile(zip_file, "r") as zip_ref:
|
129
|
+
# Find the top-level directory
|
130
|
+
zip_dirname = zip_ref.namelist()[0].split("/")[0]
|
131
|
+
package_dir = os.path.join(temp_dir, zip_dirname)
|
132
|
+
|
133
|
+
if os.path.exists(package_dir):
|
134
|
+
shutil.rmtree(package_dir)
|
135
|
+
|
136
|
+
zip_ref.extractall(temp_dir)
|
137
|
+
|
138
|
+
except zipfile.BadZipFile:
|
139
|
+
raise Exception(self.tr(f"The selected file '{zip_file}' is not a valid zip archive."))
|
140
|
+
|
141
|
+
return package_dir
|
142
|
+
|
143
|
+
def __checkForCanceled(self):
|
144
|
+
"""
|
145
|
+
Check if the task has been canceled.
|
146
|
+
"""
|
147
|
+
if self.__canceled:
|
148
|
+
raise PackagePrepareTaskCanceled(self.tr("The task has been canceled."))
|
oqtopus/gui/__init__.py
ADDED
File without changes
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# -----------------------------------------------------------
|
2
|
+
#
|
3
|
+
# Profile
|
4
|
+
# Copyright (C) 2012 Patrice Verchere
|
5
|
+
# -----------------------------------------------------------
|
6
|
+
#
|
7
|
+
# licensed under the terms of GNU GPL 2
|
8
|
+
#
|
9
|
+
# This program is free software; you can redistribute it and/or modify
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
11
|
+
# the Free Software Foundation; either version 2 of the License, or
|
12
|
+
# (at your option) any later version.
|
13
|
+
#
|
14
|
+
# This program is distributed in the hope that it will be useful,
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
17
|
+
# GNU General Public License for more details.
|
18
|
+
#
|
19
|
+
# You should have received a copy of the GNU General Public License along
|
20
|
+
# with this program; if not, print to the Free Software Foundation, Inc.,
|
21
|
+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
22
|
+
#
|
23
|
+
# ---------------------------------------------------------------------
|
24
|
+
|
25
|
+
|
26
|
+
from qgis.PyQt.QtCore import QSettings, Qt
|
27
|
+
from qgis.PyQt.QtGui import QPixmap
|
28
|
+
from qgis.PyQt.QtWidgets import QDialog
|
29
|
+
|
30
|
+
from ..utils.plugin_utils import PluginUtils
|
31
|
+
|
32
|
+
DIALOG_UI = PluginUtils.get_ui_class("about_dialog.ui")
|
33
|
+
|
34
|
+
|
35
|
+
class AboutDialog(QDialog, DIALOG_UI):
|
36
|
+
def __init__(self, parent=None):
|
37
|
+
QDialog.__init__(self, parent)
|
38
|
+
self.setupUi(self)
|
39
|
+
|
40
|
+
metadata_file_path = PluginUtils.get_metadata_file_path()
|
41
|
+
|
42
|
+
ini_text = QSettings(metadata_file_path, QSettings.Format.IniFormat)
|
43
|
+
version = ini_text.value("version")
|
44
|
+
name = ini_text.value("name")
|
45
|
+
description = "".join(ini_text.value("description"))
|
46
|
+
about = " ".join(ini_text.value("about"))
|
47
|
+
qgisMinimumVersion = ini_text.value("qgisMinimumVersion")
|
48
|
+
|
49
|
+
self.setWindowTitle(f"{name} - {version}")
|
50
|
+
self.titleLabel.setText(self.windowTitle())
|
51
|
+
self.descriptionLabel.setText(description)
|
52
|
+
self.aboutLabel.setText(about)
|
53
|
+
self.qgisMinimumVersionLabel.setText(qgisMinimumVersion)
|
54
|
+
|
55
|
+
scaled_logo = QPixmap(PluginUtils.get_plugin_icon_path("oqtopus-logo.png")).scaled(
|
56
|
+
254,
|
57
|
+
254,
|
58
|
+
aspectRatioMode=Qt.AspectRatioMode.KeepAspectRatio,
|
59
|
+
transformMode=Qt.TransformationMode.SmoothTransformation,
|
60
|
+
)
|
61
|
+
self.iconLabel.setPixmap(scaled_logo)
|
@@ -0,0 +1,154 @@
|
|
1
|
+
import psycopg
|
2
|
+
from pgserviceparser import conf_path as pgserviceparser_conf_path
|
3
|
+
from pgserviceparser import service_config as pgserviceparser_service_config
|
4
|
+
from pgserviceparser import service_names as pgserviceparser_service_names
|
5
|
+
from qgis.PyQt.QtCore import pyqtSignal
|
6
|
+
from qgis.PyQt.QtGui import QAction
|
7
|
+
from qgis.PyQt.QtWidgets import QDialog, QMenu, QWidget
|
8
|
+
|
9
|
+
from ..utils.plugin_utils import PluginUtils, logger
|
10
|
+
from ..utils.qt_utils import CriticalMessageBox, QtUtils
|
11
|
+
from .database_create_dialog import DatabaseCreateDialog
|
12
|
+
from .database_duplicate_dialog import DatabaseDuplicateDialog
|
13
|
+
|
14
|
+
DIALOG_UI = PluginUtils.get_ui_class("database_connection_widget.ui")
|
15
|
+
|
16
|
+
|
17
|
+
class DatabaseConnectionWidget(QWidget, DIALOG_UI):
|
18
|
+
|
19
|
+
signal_connectionChanged = pyqtSignal()
|
20
|
+
|
21
|
+
def __init__(self, parent=None):
|
22
|
+
QWidget.__init__(self, parent)
|
23
|
+
self.setupUi(self)
|
24
|
+
|
25
|
+
self.db_database_label.setText(self.tr("No database"))
|
26
|
+
QtUtils.setForegroundColor(self.db_database_label, PluginUtils.COLOR_WARNING)
|
27
|
+
QtUtils.setFontItalic(self.db_database_label, True)
|
28
|
+
|
29
|
+
self.__loadDatabaseInformations()
|
30
|
+
self.db_services_comboBox.currentIndexChanged.connect(self.__serviceChanged)
|
31
|
+
|
32
|
+
db_operations_menu = QMenu(self.db_operations_toolButton)
|
33
|
+
|
34
|
+
actionCreateDb = QAction(self.tr("Create database"), db_operations_menu)
|
35
|
+
self.__actionDuplicateDb = QAction(self.tr("Duplicate database"), db_operations_menu)
|
36
|
+
actionReloadPgServices = QAction(self.tr("Reload PG Service config"), db_operations_menu)
|
37
|
+
|
38
|
+
actionCreateDb.triggered.connect(self.__createDatabaseClicked)
|
39
|
+
self.__actionDuplicateDb.triggered.connect(self.__duplicateDatabaseClicked)
|
40
|
+
actionReloadPgServices.triggered.connect(self.__loadDatabaseInformations)
|
41
|
+
|
42
|
+
db_operations_menu.addAction(actionCreateDb)
|
43
|
+
db_operations_menu.addAction(self.__actionDuplicateDb)
|
44
|
+
db_operations_menu.addAction(actionReloadPgServices)
|
45
|
+
|
46
|
+
self.db_operations_toolButton.setMenu(db_operations_menu)
|
47
|
+
|
48
|
+
self.__database_connection = None
|
49
|
+
|
50
|
+
try:
|
51
|
+
self.__serviceChanged()
|
52
|
+
except Exception:
|
53
|
+
# Silence errors during widget initialization
|
54
|
+
pass
|
55
|
+
|
56
|
+
def getConnection(self):
|
57
|
+
"""
|
58
|
+
Returns the current database connection.
|
59
|
+
If no connection is established, returns None.
|
60
|
+
"""
|
61
|
+
return self.__database_connection
|
62
|
+
|
63
|
+
def __loadDatabaseInformations(self):
|
64
|
+
pg_service_conf_path = pgserviceparser_conf_path()
|
65
|
+
self.db_servicesConfigFilePath_label.setText(
|
66
|
+
f"<a href='file://{pg_service_conf_path.resolve()}'>{pg_service_conf_path.as_posix()}</a>"
|
67
|
+
)
|
68
|
+
|
69
|
+
self.db_services_comboBox.clear()
|
70
|
+
|
71
|
+
try:
|
72
|
+
for service_name in pgserviceparser_service_names():
|
73
|
+
self.db_services_comboBox.addItem(service_name)
|
74
|
+
except Exception as exception:
|
75
|
+
CriticalMessageBox(
|
76
|
+
self.tr("Error"), self.tr("Can't load database services:"), exception, self
|
77
|
+
).exec()
|
78
|
+
return
|
79
|
+
|
80
|
+
def __serviceChanged(self, index=None):
|
81
|
+
if self.db_services_comboBox.currentText() == "":
|
82
|
+
self.db_database_label.setText(self.tr("No database"))
|
83
|
+
QtUtils.setForegroundColor(self.db_database_label, PluginUtils.COLOR_WARNING)
|
84
|
+
QtUtils.setFontItalic(self.db_database_label, True)
|
85
|
+
|
86
|
+
self.__actionDuplicateDb.setDisabled(True)
|
87
|
+
|
88
|
+
self.__set_connection(None)
|
89
|
+
return
|
90
|
+
|
91
|
+
service_name = self.db_services_comboBox.currentText()
|
92
|
+
service_config = pgserviceparser_service_config(service_name)
|
93
|
+
|
94
|
+
service_database = service_config.get("dbname", None)
|
95
|
+
|
96
|
+
if service_database is None:
|
97
|
+
self.db_database_label.setText(self.tr("No database provided by the service"))
|
98
|
+
QtUtils.setForegroundColor(self.db_database_label, PluginUtils.COLOR_WARNING)
|
99
|
+
QtUtils.setFontItalic(self.db_database_label, True)
|
100
|
+
|
101
|
+
self.__actionDuplicateDb.setDisabled(True)
|
102
|
+
return
|
103
|
+
|
104
|
+
self.db_database_label.setText(service_database)
|
105
|
+
QtUtils.resetForegroundColor(self.db_database_label)
|
106
|
+
QtUtils.setFontItalic(self.db_database_label, False)
|
107
|
+
|
108
|
+
self.__actionDuplicateDb.setEnabled(True)
|
109
|
+
|
110
|
+
# Try connection
|
111
|
+
try:
|
112
|
+
database_connection = psycopg.connect(service=service_name)
|
113
|
+
self.__set_connection(database_connection)
|
114
|
+
|
115
|
+
except Exception as exception:
|
116
|
+
self.__set_connection(None)
|
117
|
+
|
118
|
+
self.db_moduleInfo_label.setText("Can't connect to service.")
|
119
|
+
QtUtils.setForegroundColor(self.db_moduleInfo_label, PluginUtils.COLOR_WARNING)
|
120
|
+
errorText = self.tr(f"Can't connect to service '{service_name}':\n{exception}.")
|
121
|
+
logger.error(errorText)
|
122
|
+
return
|
123
|
+
|
124
|
+
self.db_moduleInfo_label.setText("Connected.")
|
125
|
+
logger.info(f"Connected to service '{service_name}'.")
|
126
|
+
QtUtils.resetForegroundColor(self.db_moduleInfo_label)
|
127
|
+
|
128
|
+
def __createDatabaseClicked(self):
|
129
|
+
databaseCreateDialog = DatabaseCreateDialog(
|
130
|
+
selected_service=self.db_services_comboBox.currentText(), parent=self
|
131
|
+
)
|
132
|
+
|
133
|
+
if databaseCreateDialog.exec() == QDialog.DialogCode.Rejected:
|
134
|
+
return
|
135
|
+
|
136
|
+
self.__loadDatabaseInformations()
|
137
|
+
|
138
|
+
# Select the created service
|
139
|
+
created_service_name = databaseCreateDialog.created_service_name()
|
140
|
+
self.db_services_comboBox.setCurrentText(created_service_name)
|
141
|
+
|
142
|
+
def __duplicateDatabaseClicked(self):
|
143
|
+
databaseDuplicateDialog = DatabaseDuplicateDialog(
|
144
|
+
selected_service=self.db_services_comboBox.currentText(), parent=self
|
145
|
+
)
|
146
|
+
if databaseDuplicateDialog.exec() == QDialog.DialogCode.Rejected:
|
147
|
+
return
|
148
|
+
|
149
|
+
def __set_connection(self, connection):
|
150
|
+
"""
|
151
|
+
Set the current database connection and emit the signal_connectionChanged signal.
|
152
|
+
"""
|
153
|
+
self.__database_connection = connection
|
154
|
+
self.signal_connectionChanged.emit()
|