Skip to content

Commit

Permalink
Refacto: move rules context into a structured object (#491)
Browse files Browse the repository at this point in the history
This PR is a following up of #481.

It's mainly a code structure improvement which aims to improve
readability, testing and maintainability:

- create an object
- move it from utils to profiles since it's only related to QDT profiles
management
- add methods to export as dict and JSON
- add unit test
- use it in documentation and GenericJob.filter_on_rules

Next (spoiler alert): a CLI command to generate the rules context to
make the rules writing more handy
  • Loading branch information
Guts authored Apr 23, 2024
2 parents 1b76259 + 241d859 commit c7f7608
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 126 deletions.
21 changes: 6 additions & 15 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"""

# standard
import json
import os
from datetime import datetime
from pathlib import Path
Expand All @@ -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"
Expand Down Expand Up @@ -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(_):
Expand Down
9 changes: 3 additions & 6 deletions qgis_deployment_toolbelt/jobs/generic_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###############
Expand All @@ -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()
Expand Down Expand Up @@ -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}")
Expand All @@ -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)} "
Expand Down
9 changes: 0 additions & 9 deletions qgis_deployment_toolbelt/jobs/job_splash_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
141 changes: 141 additions & 0 deletions qgis_deployment_toolbelt/profiles/rules_context.py
Original file line number Diff line number Diff line change
@@ -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)
96 changes: 0 additions & 96 deletions qgis_deployment_toolbelt/utils/computer_environment.py

This file was deleted.

59 changes: 59 additions & 0 deletions tests/test_rules_context.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit c7f7608

Please sign in to comment.