From b6b4d9aa32f3f3874297eee8472ea64f9114b174 Mon Sep 17 00:00:00 2001 From: Julien Moura Date: Tue, 23 Apr 2024 11:56:40 +0200 Subject: [PATCH 1/2] refacto(rules): make rules context an object --- .../profiles/rules_context.py | 141 ++++++++++++++++++ .../utils/computer_environment.py | 96 ------------ 2 files changed, 141 insertions(+), 96 deletions(-) create mode 100644 qgis_deployment_toolbelt/profiles/rules_context.py delete mode 100644 qgis_deployment_toolbelt/utils/computer_environment.py diff --git a/qgis_deployment_toolbelt/profiles/rules_context.py b/qgis_deployment_toolbelt/profiles/rules_context.py new file mode 100644 index 00000000..2c34bcb4 --- /dev/null +++ b/qgis_deployment_toolbelt/profiles/rules_context.py @@ -0,0 +1,141 @@ +#! python3 # noqa: E265 + +""" + Rules context. + + Author: Julien Moura (https://github.com/guts) +""" + + +# ############################################################################# +# ########## Libraries ############# +# ################################## + +# Standard library +import json +import logging +import platform +from datetime import date +from getpass import getuser +from sys import platform as opersys + +# package +from qgis_deployment_toolbelt.utils.user_groups import ( + get_user_domain_groups, + get_user_local_groups, +) + +# ############################################################################# +# ########## Globals ############### +# ################################## + +# logs +logger = logging.getLogger(__name__) + + +# ############################################################################# +# ########## Functions ############# +# ################################## + + +class QdtRulesContext: + pass + + @property + def _context_date(self) -> dict: + """Returns a context dictionary with date informations that can be used in QDT + various places: rules... + + Returns: + dict: dict with current date informations + """ + today = date.today() + return { + "current_day": today.day, + "current_weekday": today.weekday(), # monday = 0, sunday = 6 + "current_month": today.month, + "current_year": today.year, + } + + @property + def _context_environment(self) -> dict: + """Returns a dictionary containing some environment information (computer, network, + platform) that can be used in QDT various places: rules... + + Returns: + dict: dict with some environment metadata to use in rules. + """ + try: + linux_distribution_name = f"{platform.freedesktop_os_release().get('NAME')}" + linux_distribution_version = ( + f"{platform.freedesktop_os_release().get('VERSION_ID')}" + ) + except OSError as err: + logger.debug( + f"Unable to determine current Linux distribution. Trace: {err}." + ) + linux_distribution_name = None + linux_distribution_version = None + + return { + "computer_network_name": platform.node(), + "operating_system_code": opersys, + "processor_architecture": platform.machine(), + # custom Linux + "linux_distribution_name": linux_distribution_name, + "linux_distribution_version": linux_distribution_version, + # custom Windows + "windows_edition": platform.win32_edition(), + } + + @property + def _context_user(self) -> dict: + """Returns a dictionary containing user informations that can be used in QDT Rules + context. + + Returns: + dict: dict user information. + """ + return { + "name": getuser(), + "groups_local": get_user_local_groups(), + "groups_domain": get_user_domain_groups(), + } + + # -- EXPORT + def to_dict(self) -> dict: + """Convert object into dictionary. + + Returns: + dict: object as dictionary + """ + result = {} + for attr in dir(self): + if isinstance( + getattr(self.__class__, attr, None), property + ) and attr.startswith("_context_"): + result[attr.removeprefix("_context_")] = getattr(self, attr) + return result + + def to_json(self, **kwargs) -> str: + """Supersedes json.dumps using the dictionary returned by to_dict(). + kwargs are passed to json.dumps. + + Returns: + str: object serialized as JSON string + + Example: + + .. code-block:: python + + from pathlib import Path + + rules_context = QdtRulesContext() + + # write into the file passing extra parameters to json.dumps + with Path("qdt_rules_context.json").open("w", encoding="UTF8") as wf: + wf.write(rules_context.to_json(indent=4, sort_keys=True)) + """ + obj_as_dict = self.to_dict() + + return json.dumps(obj_as_dict, **kwargs) diff --git a/qgis_deployment_toolbelt/utils/computer_environment.py b/qgis_deployment_toolbelt/utils/computer_environment.py deleted file mode 100644 index 4a770789..00000000 --- a/qgis_deployment_toolbelt/utils/computer_environment.py +++ /dev/null @@ -1,96 +0,0 @@ -#! python3 # noqa: E265 - -""" - Rules context. - - Author: Julien Moura (https://github.com/guts) -""" - - -# ############################################################################# -# ########## Libraries ############# -# ################################## - -# Standard library -import logging -import platform -from datetime import date -from getpass import getuser -from sys import platform as opersys - -# package -from qgis_deployment_toolbelt.utils.user_groups import ( - get_user_domain_groups, - get_user_local_groups, -) - -# ############################################################################# -# ########## Globals ############### -# ################################## - -# logs -logger = logging.getLogger(__name__) - - -# ############################################################################# -# ########## Functions ############# -# ################################## - - -def date_dict() -> dict: - """Returns a context dictionary with date informations that can be used in QDT - various places: rules... - - Returns: - dict: dict with current date informations - """ - today = date.today() - return { - "current_day": today.day, - "current_weekday": today.weekday(), # monday = 0, sunday = 6 - "current_month": today.month, - "current_year": today.year, - } - - -def environment_dict() -> dict: - """Returns a dictionary containing some environment information (computer, network, - platform) that can be used in QDT various places: rules... - - Returns: - dict: dict with some environment metadata to use in rules. - """ - try: - linux_distribution_name = f"{platform.freedesktop_os_release().get('NAME')}" - linux_distribution_version = ( - f"{platform.freedesktop_os_release().get('VERSION_ID')}" - ) - except OSError as err: - logger.debug(f"Trace: {err}.") - linux_distribution_name = None - linux_distribution_version = None - - return { - "computer_network_name": platform.node(), - "operating_system_code": opersys, - "processor_architecture": platform.machine(), - # custom Linux - "linux_distribution_name": linux_distribution_name, - "linux_distribution_version": linux_distribution_version, - # custom Windows - "windows_edition": platform.win32_edition(), - } - - -def user_dict() -> dict: - """Returns a dictionary containing user informations that can be used in QDT Rules - context. - - Returns: - dict: dict user information. - """ - return { - "name": getuser(), - "groups_local": get_user_local_groups(), - "groups_domain": get_user_domain_groups(), - } From 241d859a4d368e7f0f2e2baf4cd4db70fda33fb6 Mon Sep 17 00:00:00 2001 From: Julien Moura Date: Tue, 23 Apr 2024 11:57:07 +0200 Subject: [PATCH 2/2] refacto(rules): adapt to and test new QdtRulesContext --- docs/conf.py | 21 ++----- qgis_deployment_toolbelt/jobs/generic_job.py | 9 +-- .../jobs/job_splash_screen.py | 9 --- tests/test_rules_context.py | 59 +++++++++++++++++++ 4 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 tests/test_rules_context.py diff --git a/docs/conf.py b/docs/conf.py index 466ba366..bacad652 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,7 +5,6 @@ """ # standard -import json import os from datetime import datetime from pathlib import Path @@ -17,11 +16,7 @@ get_latest_release, replace_domain, ) -from qgis_deployment_toolbelt.utils.computer_environment import ( - date_dict, - environment_dict, - user_dict, -) +from qgis_deployment_toolbelt.profiles.rules_context import QdtRulesContext # -- Build environment ----------------------------------------------------- on_rtd = os.environ.get("READTHEDOCS", None) == "True" @@ -221,15 +216,11 @@ def generate_rules_context(_): """Generate context object as JSON that it passed to rules engine to check profiles conditions.""" - context_object = { - "date": date_dict(), - "environment": environment_dict(), - "user": user_dict(), - } - with Path("./docs/reference/rules_context.json").open( - mode="w", encoding="utf-8" - ) as out_json: - json.dump(context_object, out_json, sort_keys=True, indent=4) + rules_context = QdtRulesContext() + + # write into the file passing extra parameters to json.dumps + with Path("./docs/reference/rules_context.json").open("w", encoding="UTF8") as wf: + wf.write(rules_context.to_json(indent=4, sort_keys=True)) def populate_download_page(_): diff --git a/qgis_deployment_toolbelt/jobs/generic_job.py b/qgis_deployment_toolbelt/jobs/generic_job.py index 79075ba1..d243536a 100644 --- a/qgis_deployment_toolbelt/jobs/generic_job.py +++ b/qgis_deployment_toolbelt/jobs/generic_job.py @@ -31,10 +31,7 @@ JobOptionBadValueType, ) from qgis_deployment_toolbelt.profiles.qdt_profile import QdtProfile -from qgis_deployment_toolbelt.utils.computer_environment import ( - date_dict, - environment_dict, -) +from qgis_deployment_toolbelt.profiles.rules_context import QdtRulesContext # ############################################################################# # ########## Globals ############### @@ -58,6 +55,7 @@ def __init__(self) -> None: """Object instanciation.""" # operating system configuration self.os_config = OSConfiguration.from_opersys() + self.qdt_rules_context = QdtRulesContext() # local QDT folders self.qdt_working_folder = get_qdt_working_directory() @@ -168,7 +166,6 @@ def filter_profiles_on_rules( li_profiles_matched = [] li_profiles_unmatched = [] - context_object = {"date": date_dict(), "environment": environment_dict()} for profile in tup_qdt_profiles: if profile.rules is None: logger.debug(f"No rules to apply to {profile.name}") @@ -181,7 +178,7 @@ def filter_profiles_on_rules( ) try: engine = RuleEngine(rules=profile.rules) - results = engine.evaluate(obj=context_object) + results = engine.evaluate(obj=self.qdt_rules_context.to_dict()) if len(results) == len(profile.rules): logger.debug( f"Profile '{profile.name}' matches {len(profile.rules)} " diff --git a/qgis_deployment_toolbelt/jobs/job_splash_screen.py b/qgis_deployment_toolbelt/jobs/job_splash_screen.py index 226ee76d..47bdfe63 100644 --- a/qgis_deployment_toolbelt/jobs/job_splash_screen.py +++ b/qgis_deployment_toolbelt/jobs/job_splash_screen.py @@ -189,12 +189,3 @@ def run(self) -> None: raise NotImplementedError logger.debug(f"Job {self.ID} ran successfully.") - - -# ############################################################################# -# ##### Stand alone program ######## -# ################################## - -if __name__ == "__main__": - """Standalone execution.""" - pass diff --git a/tests/test_rules_context.py b/tests/test_rules_context.py new file mode 100644 index 00000000..e2911700 --- /dev/null +++ b/tests/test_rules_context.py @@ -0,0 +1,59 @@ +#! python3 # noqa E265 + +""" + Usage from the repo root folder: + + .. code-block:: bash + # for whole tests + python -m unittest tests.test_rules_context + # for specific test + python -m unittest tests.test_rules_context.testQdtRulesContext.test_rules_export_to_json +""" + + +# standard library +import json +import tempfile +import unittest +from pathlib import Path + +# project +from qgis_deployment_toolbelt.profiles.rules_context import QdtRulesContext + +# ############################################################################ +# ########## Classes ############# +# ################################ + + +class TestQdtRulesContext(unittest.TestCase): + """Test QDT rules context.""" + + def test_rules_export_to_json(self): + """Test export to JSON.""" + + rules_context = QdtRulesContext() + + with tempfile.TemporaryDirectory( + prefix="qdt_test_rules_context" + ) as tmp_dir_name: + context_json_path = Path(tmp_dir_name).joinpath("qdt_rules_context.json") + + # write into the file passing extra parameters to json.dumps + with context_json_path.open("w", encoding="UTF8") as wf: + wf.write(rules_context.to_json(indent=4, sort_keys=True)) + + # test reading + with context_json_path.open(mode="r", encoding="utf8") as in_json: + context_data = json.load(in_json) + + self.assertIsInstance(context_data, dict) + self.assertIn("date", context_data) + self.assertIn("environment", context_data) + self.assertIn("user", context_data) + + +# ############################################################################ +# ####### Stand-alone run ######## +# ################################ +if __name__ == "__main__": + unittest.main()