forked from CEN38/plugin_gn_tools
init plugin
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
#! python3 # noqa: E265
|
||||
from .log_handler import PlgLogger # noqa: F401
|
||||
from .preferences import PlgOptionsManager # noqa: F401
|
||||
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
from typing import Type, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class EnvVarParser:
|
||||
"""Utility class to retrieve and convert environment variables."""
|
||||
|
||||
@staticmethod
|
||||
def get_env_var(name: str, default: T) -> T:
|
||||
"""Retrieves an environment variable and converts it based on the default value type.
|
||||
|
||||
Args:
|
||||
name (str): The environment variable name.
|
||||
default (T): The default value, used to infer the expected type.
|
||||
|
||||
Returns:
|
||||
T: The converted value, matching the type of `default`.
|
||||
"""
|
||||
value = os.getenv(name)
|
||||
if value is None:
|
||||
return (
|
||||
default # Return the default value if the environment variable is not
|
||||
)
|
||||
|
||||
# Otherwise, treat it as a single value
|
||||
return EnvVarParser._convert_single(value, type(default), default)
|
||||
|
||||
@staticmethod
|
||||
def _convert_single(value: str, expected_type: Type[T], default: T) -> T:
|
||||
"""Converts a string into a single value of the expected type."""
|
||||
try:
|
||||
if expected_type is int:
|
||||
return int(value)
|
||||
elif expected_type is float:
|
||||
return float(value)
|
||||
elif expected_type is bool:
|
||||
return EnvVarParser._convert_bool(value, default)
|
||||
elif expected_type is str:
|
||||
return value # String value
|
||||
except ValueError:
|
||||
return default # Return default value in case of conversion failure
|
||||
|
||||
raise TypeError(
|
||||
f"Unsupported type: {expected_type}. Value definition from environment variable is not possible."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _convert_bool(value: str, default: bool) -> bool:
|
||||
"""Converts a string into a boolean, handling explicit True/False values."""
|
||||
true_values = {"1", "true", "yes", "on"}
|
||||
false_values = {"0", "false", "no", "off"}
|
||||
value_lower = value.lower()
|
||||
|
||||
if value_lower in true_values:
|
||||
return True
|
||||
elif value_lower in false_values:
|
||||
return False
|
||||
return default # Return default value if conversion fails
|
||||
Executable
+193
@@ -0,0 +1,193 @@
|
||||
#! python3 # noqa: E265
|
||||
|
||||
# standard library
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Callable, Literal, Optional, Union
|
||||
|
||||
# PyQGIS
|
||||
from qgis.core import Qgis, QgsMessageLog, QgsMessageOutput
|
||||
from qgis.gui import QgsMessageBar
|
||||
from qgis.PyQt.QtWidgets import QPushButton, QWidget
|
||||
from qgis.utils import iface
|
||||
|
||||
# project package
|
||||
import gn_tools.toolbelt.preferences as plg_prefs_hdlr
|
||||
from gn_tools.__about__ import __title__
|
||||
|
||||
# ############################################################################
|
||||
# ########## Classes ###############
|
||||
# ##################################
|
||||
|
||||
|
||||
class PlgLogger(logging.Handler):
|
||||
"""Python logging handler supercharged with QGIS useful methods."""
|
||||
|
||||
@staticmethod
|
||||
def log(
|
||||
message: str,
|
||||
application: str = __title__,
|
||||
log_level: Union[
|
||||
Qgis.MessageLevel, Literal[0, 1, 2, 3, 4]
|
||||
] = Qgis.MessageLevel.Info,
|
||||
push: bool = False,
|
||||
duration: Optional[int] = None,
|
||||
# widget
|
||||
button: bool = False,
|
||||
button_text: Optional[str] = None,
|
||||
button_more_text: Optional[str] = None,
|
||||
button_connect: Optional[Callable] = None,
|
||||
# parent
|
||||
parent_location: Optional[QWidget] = None,
|
||||
):
|
||||
"""Send messages to QGIS messages windows and to the user as a message bar. \
|
||||
Plugin name is used as title. If debug mode is disabled, only warnings (1) and \
|
||||
errors (2) or with push are sent.
|
||||
|
||||
:param message: message to display
|
||||
:type message: str
|
||||
:param application: name of the application sending the message. \
|
||||
Defaults to __about__.__title__
|
||||
:type application: str, optional
|
||||
:param log_level: message level. Possible values: any values of enum \
|
||||
`Qgis.MessageLevel`. For legacy purposes, it's also possible to pass \
|
||||
corresponding integers but it's not recommended anymore. Legacy values: \
|
||||
0 (info), 1 (warning), 2 (critical), 3 (success), 4 (none - grey). Defaults \
|
||||
to Qgis.MessageLevel(0) (info)
|
||||
:type log_level: Union[Qgis.MessageLevel, Literal[0, 1, 2, 3, 4]], optional
|
||||
:param push: also display the message in the QGIS message bar in addition to \
|
||||
the log, defaults to False
|
||||
:type push: bool, optional
|
||||
:param duration: duration of the message in seconds. If not set, the \
|
||||
duration is calculated from the log level: `(log_level + 1) * 3`. seconds. \
|
||||
If set to 0, then the message must be manually dismissed by the user. \
|
||||
Defaults to None.
|
||||
:type duration: int, optional
|
||||
:param button: display a button in the message bar. Defaults to False.
|
||||
:type button: bool, optional
|
||||
:param button_text: text label of the button. Defaults to None.
|
||||
:type button_text: str, optional
|
||||
:param button_more_text: text to display within the QgsMessageOutput
|
||||
:type button_more_text: str, optional
|
||||
:param button_connect: function to be called when the button is pressed. \
|
||||
If not set, a simple dialog (QgsMessageOutput) is used to dislay the message. \
|
||||
Defaults to None.
|
||||
:type button_connect: Callable, optional
|
||||
:param parent_location: parent location widget. \
|
||||
If not set, QGIS canvas message bar is used to push message, \
|
||||
otherwise if a QgsMessageBar is available in parent_location it is used instead. \
|
||||
Defaults to None.
|
||||
:type parent_location: Widget, optional
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# using enums from Qgis:
|
||||
# Qgis.Info, Qgis.MessageLevel.Warning, Qgis.MessageLevel.Critical, Qgis.MessageLevel.Success, Qgis.MessageLevel.NoLevel
|
||||
from qgis.core import Qgis
|
||||
|
||||
log(message="Plugin loaded - INFO", log_level=Qgis.MessageLevel.Info, push=False)
|
||||
log(
|
||||
message="Something went wrong but it's not blocking",
|
||||
log_level=Qgis.MessageLevel.Warning
|
||||
)
|
||||
log(
|
||||
message="Plugin failed to load - CRITICAL",
|
||||
log_level=Qgis.MessageLevel(2),
|
||||
push=True
|
||||
)
|
||||
|
||||
# LEGACY - using integers:
|
||||
log(message="Plugin loaded - INFO", log_level=Qgis.MessageLevel.Info, push=False)
|
||||
log(message="Plugin loaded - WARNING", log_level=Qgis.MessageLevel.Warning, push=1, duration=5)
|
||||
log(message="Plugin loaded - ERROR", log_level=Qgis.MessageLevel.Critical, push=1, duration=0)
|
||||
log(
|
||||
message="Plugin loaded - SUCCESS",
|
||||
log_level=Qgis.MessageLevel.Success,
|
||||
push=1,
|
||||
duration=10,
|
||||
button=True
|
||||
)
|
||||
log(
|
||||
message="Plugin loaded",
|
||||
log_level=Qgis.MessageLevel.Critical,
|
||||
push=1,
|
||||
duration=0
|
||||
button=True,
|
||||
button_label=self.tr("See details"),
|
||||
button_more_text=detailed_error_message
|
||||
)
|
||||
log(message="Plugin loaded - TEST", log_level=Qgis.MessageLevel.NoLevel, push=0)
|
||||
"""
|
||||
# if not debug mode and not push, let's ignore INFO, SUCCESS and TEST
|
||||
debug_mode = plg_prefs_hdlr.PlgOptionsManager.get_plg_settings().debug_mode
|
||||
if not debug_mode and not push and (log_level < 1 or log_level > 2):
|
||||
return
|
||||
|
||||
# if log_level is an int, convert it to Qgis.MessageLevel
|
||||
if isinstance(log_level, int):
|
||||
log_level = Qgis.MessageLevel(log_level)
|
||||
|
||||
# ensure message is a string
|
||||
if not isinstance(message, str):
|
||||
try:
|
||||
message = str(message)
|
||||
except Exception as err:
|
||||
err_msg = "Log message must be a string, not: {}. Trace: {}".format(
|
||||
type(message), err
|
||||
)
|
||||
logging.error(err_msg)
|
||||
message = err_msg
|
||||
|
||||
# send it to QGIS messages panel
|
||||
QgsMessageLog.logMessage(
|
||||
message=message, tag=application, notifyUser=push, level=log_level
|
||||
)
|
||||
|
||||
# optionally, display message on QGIS Message bar (above the map canvas)
|
||||
if push and iface is not None:
|
||||
msg_bar = None
|
||||
|
||||
# QGIS or custom dialog
|
||||
if parent_location and isinstance(parent_location, QWidget):
|
||||
msg_bar = parent_location.findChild(QgsMessageBar)
|
||||
|
||||
if not msg_bar:
|
||||
msg_bar = iface.messageBar()
|
||||
|
||||
# calc duration
|
||||
if duration is None:
|
||||
duration = (log_level + 1) * 3
|
||||
|
||||
# create message with/out a widget
|
||||
if button:
|
||||
# create output message
|
||||
notification = iface.messageBar().createMessage(
|
||||
title=application, text=message
|
||||
)
|
||||
widget_button = QPushButton(button_text or "More...")
|
||||
if button_connect:
|
||||
widget_button.clicked.connect(button_connect)
|
||||
else:
|
||||
mini_dlg: QgsMessageOutput = QgsMessageOutput.createMessageOutput()
|
||||
mini_dlg.setTitle(application)
|
||||
mini_dlg.setMessage(
|
||||
f"{message}\n{button_more_text}",
|
||||
QgsMessageOutput.MessageType.MessageText,
|
||||
)
|
||||
widget_button.clicked.connect(partial(mini_dlg.showMessage, False))
|
||||
|
||||
notification.layout().addWidget(widget_button)
|
||||
msg_bar.pushWidget(
|
||||
widget=notification, level=log_level, duration=duration
|
||||
)
|
||||
|
||||
else:
|
||||
# send simple message
|
||||
msg_bar.pushMessage(
|
||||
title=application,
|
||||
text=message,
|
||||
level=log_level,
|
||||
duration=duration,
|
||||
)
|
||||
Executable
+184
@@ -0,0 +1,184 @@
|
||||
#! python3 # noqa: E265
|
||||
|
||||
"""
|
||||
Plugin settings.
|
||||
"""
|
||||
|
||||
# standard
|
||||
from dataclasses import asdict, dataclass, fields
|
||||
|
||||
# PyQGIS
|
||||
from qgis.core import Qgis, QgsSettings
|
||||
|
||||
# package
|
||||
import gn_tools.toolbelt.log_handler as log_hdlr
|
||||
from gn_tools.__about__ import (
|
||||
__domain_url__,
|
||||
__title__,
|
||||
__version__
|
||||
)
|
||||
from gn_tools.toolbelt.env_var_parser import EnvVarParser
|
||||
|
||||
# ############################################################################
|
||||
# ########## Classes ###############
|
||||
# ##################################
|
||||
|
||||
PREFIX_ENV_VARIABLE = "QGIS_GN_TOOLS_"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlgEnvVariableSettings:
|
||||
"""Plugin settings from environnement variable"""
|
||||
|
||||
def env_variable_used(self, attribute: str, default_from_name: bool = True) -> str:
|
||||
"""Get environnement variable used for environnement variable settings
|
||||
|
||||
:param attribute: attribute to check
|
||||
:type attribute: str
|
||||
:param default_from_name: define default environnement value from attribute name PREFIX_ENV_VARIABLE_<upper case attribute>
|
||||
:type default_from_name: bool
|
||||
:return: environnement variable used
|
||||
:rtype: str
|
||||
"""
|
||||
settings_env_variable = asdict(self)
|
||||
env_variable = settings_env_variable.get(attribute, "")
|
||||
if not env_variable and default_from_name:
|
||||
env_variable = f"{PREFIX_ENV_VARIABLE}{attribute}".upper()
|
||||
return env_variable
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlgSettingsStructure:
|
||||
"""Plugin settings structure and defaults values."""
|
||||
|
||||
# global
|
||||
debug_mode: bool = False
|
||||
version: str = __version__
|
||||
|
||||
# param
|
||||
domain_url: str = __domain_url__
|
||||
|
||||
class PlgOptionsManager:
|
||||
@staticmethod
|
||||
def get_plg_settings() -> PlgSettingsStructure:
|
||||
"""Load and return plugin settings as a dictionary. \
|
||||
Useful to get user preferences across plugin logic.
|
||||
|
||||
:return: plugin settings
|
||||
:rtype: PlgSettingsStructure
|
||||
"""
|
||||
# get dataclass fields definition
|
||||
settings_fields = fields(PlgSettingsStructure)
|
||||
env_variable_settings = PlgEnvVariableSettings()
|
||||
|
||||
# retrieve settings from QGIS/Qt
|
||||
settings = QgsSettings()
|
||||
settings.beginGroup(__title__)
|
||||
|
||||
# map settings values to preferences object
|
||||
li_settings_values = []
|
||||
for i in settings_fields:
|
||||
try:
|
||||
value = settings.value(key=i.name, defaultValue=i.default, type=i.type)
|
||||
# If environnement variable used, get value from environnement variable
|
||||
env_variable = env_variable_settings.env_variable_used(i.name)
|
||||
if env_variable:
|
||||
value = EnvVarParser.get_env_var(env_variable, value)
|
||||
li_settings_values.append(value)
|
||||
except TypeError:
|
||||
li_settings_values.append(
|
||||
settings.value(key=i.name, defaultValue=i.default)
|
||||
)
|
||||
|
||||
# instanciate new settings object
|
||||
options = PlgSettingsStructure(*li_settings_values)
|
||||
|
||||
settings.endGroup()
|
||||
|
||||
return options
|
||||
|
||||
@staticmethod
|
||||
def get_value_from_key(key: str, default=None, exp_type=None):
|
||||
"""Load and return plugin settings as a dictionary. \
|
||||
Useful to get user preferences across plugin logic.
|
||||
|
||||
:return: plugin settings value matching key
|
||||
"""
|
||||
if not hasattr(PlgSettingsStructure, key):
|
||||
log_hdlr.PlgLogger.log(
|
||||
message="Bad settings key. Must be one of: {}".format(
|
||||
",".join(PlgSettingsStructure._fields)
|
||||
),
|
||||
log_level=Qgis.MessageLevel.Warning,
|
||||
)
|
||||
return None
|
||||
|
||||
settings = QgsSettings()
|
||||
settings.beginGroup(__title__)
|
||||
|
||||
try:
|
||||
out_value = settings.value(key=key, defaultValue=default, type=exp_type)
|
||||
except Exception as err:
|
||||
log_hdlr.PlgLogger.log(
|
||||
message="Error occurred trying to get settings: {}.Trace: {}".format(
|
||||
key, err
|
||||
)
|
||||
)
|
||||
out_value = None
|
||||
|
||||
settings.endGroup()
|
||||
|
||||
return out_value
|
||||
|
||||
@classmethod
|
||||
def set_value_from_key(cls, key: str, value) -> bool:
|
||||
"""Set plugin QSettings value using the key.
|
||||
|
||||
:param key: QSettings key
|
||||
:type key: str
|
||||
:param value: value to set
|
||||
:type value: depending on the settings
|
||||
:return: operation status
|
||||
:rtype: bool
|
||||
"""
|
||||
if not hasattr(PlgSettingsStructure, key):
|
||||
log_hdlr.PlgLogger.log(
|
||||
message="Bad settings key. Must be one of: {}".format(
|
||||
",".join(PlgSettingsStructure._fields)
|
||||
),
|
||||
log_level=Qgis.MessageLevel.Critical,
|
||||
)
|
||||
return False
|
||||
|
||||
settings = QgsSettings()
|
||||
settings.beginGroup(__title__)
|
||||
|
||||
try:
|
||||
settings.setValue(key, value)
|
||||
out_value = True
|
||||
except Exception as err:
|
||||
log_hdlr.PlgLogger.log(
|
||||
message="Error occurred trying to set settings: {}.Trace: {}".format(
|
||||
key, err
|
||||
)
|
||||
)
|
||||
out_value = False
|
||||
|
||||
settings.endGroup()
|
||||
|
||||
return out_value
|
||||
|
||||
@classmethod
|
||||
def save_from_object(cls, plugin_settings_obj: PlgSettingsStructure):
|
||||
"""Load and return plugin settings as a dictionary. \
|
||||
Useful to get user preferences across plugin logic.
|
||||
|
||||
:return: plugin settings value matching key
|
||||
"""
|
||||
settings = QgsSettings()
|
||||
settings.beginGroup(__title__)
|
||||
|
||||
for k, v in asdict(plugin_settings_obj).items():
|
||||
cls.set_value_from_key(k, v)
|
||||
|
||||
settings.endGroup()
|
||||
Reference in New Issue
Block a user