From a7589e604c5a6b03b46e0addc1348b21a15c2fdb Mon Sep 17 00:00:00 2001 From: Kumi Date: Mon, 3 Jul 2023 11:24:31 +0200 Subject: [PATCH] Lots of changes, I guess --- .gitignore | 2 +- pyproject.toml | 2 +- reports/__init__.py | 0 src/reportmonster/classes/config.py | 15 +-- src/reportmonster/classes/connection.py | 20 ++-- src/reportmonster/classes/database.py | 43 +++++--- src/reportmonster/classes/user.py | 8 +- src/reportmonster/classes/vessel.py | 133 ++++++++++++++++++------ 8 files changed, 156 insertions(+), 67 deletions(-) create mode 100644 reports/__init__.py diff --git a/.gitignore b/.gitignore index dfda775..e1ae698 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ settings.ini completionmail.ini venv/ output/ -reports/*.py +reports/* !reports/__init__.py .vscode *.old diff --git a/pyproject.toml b/pyproject.toml index 1834b0b..7a84e22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "reportmonster" -version = "0.9.6" +version = "0.9.7" authors = [ { name="Kumi Systems e.U.", email="office@kumi.systems" }, ] diff --git a/reports/__init__.py b/reports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/reportmonster/classes/config.py b/src/reportmonster/classes/config.py index 6415291..9aa707e 100644 --- a/src/reportmonster/classes/config.py +++ b/src/reportmonster/classes/config.py @@ -1,7 +1,7 @@ import configparser from pathlib import Path -from typing import Union +from typing import Union, List from .vessel import Vessel from .user import User @@ -37,16 +37,17 @@ class MonsterConfig: except KeyError: try: import pyadonis + self.pyadonis = Path(pyadonis.__path__[0]) except ImportError: - print(f"PyAdonis is not defined in the MONSTER section of {path}, some features may be missing.") - + print( + f"PyAdonis is not defined in the MONSTER section of {path}, some features may be missing." + ) def __init__(self, path: Union[str, Path]) -> None: - """Initialize a new (empty) MonsterConfig object - """ - self.vessels = [] - self.users = [] + """Initialize a new (empty) MonsterConfig object""" + self.vessels: List[Vessel] = [] + self.users: List[User] = [] self.pyadonis = None if path: diff --git a/src/reportmonster/classes/connection.py b/src/reportmonster/classes/connection.py index 9c945c8..8e27545 100644 --- a/src/reportmonster/classes/connection.py +++ b/src/reportmonster/classes/connection.py @@ -10,8 +10,7 @@ import socket class Connection: - """Class representing an SSH/SFTP connection to a Vessel - """ + """Class representing an SSH/SFTP connection to a Vessel""" def __init__(self, vessel): """Initialize a new Connection to a Vessel @@ -24,9 +23,14 @@ class Connection: self._client = SSHClient() self._client.load_system_host_keys() self._client.set_missing_host_key_policy(WarningPolicy) - self._client.connect(vessel.host, 22, vessel.ssh_username, - vessel.ssh_password, timeout=vessel.ssh_timeout, - passphrase=vessel.ssh_passphrase) + self._client.connect( + vessel.host, + 22, + vessel.ssh_username, + vessel.ssh_password, + timeout=vessel.ssh_timeout, + passphrase=vessel.ssh_passphrase, + ) self._transport = self._client.get_transport() self._transport.set_keepalive(10) self._sftp = self._client.open_sftp() @@ -36,14 +40,14 @@ class Connection: (self._vessel.host, 22), ssh_username=self._vessel.ssh_username, ssh_private_key_password=self._vessel.ssh_passphrase, - remote_bind_address=("127.0.0.1", remote)) + remote_bind_address=("127.0.0.1", remote), + ) self._process.start() return self._process.local_bind_port def __del__(self): - """Close SSH connection when ending Connection - """ + """Close SSH connection when ending Connection""" self._client.close() if self._process: diff --git a/src/reportmonster/classes/database.py b/src/reportmonster/classes/database.py index d1371ef..f423690 100644 --- a/src/reportmonster/classes/database.py +++ b/src/reportmonster/classes/database.py @@ -9,18 +9,23 @@ from .logger import Logger logger = Logger() + class Database: - """Class wrapping MySQL database connection - """ + """Class wrapping MySQL database connection""" def __init__(self, vessel): - """Initialize a new Database object - """ + """Initialize a new Database object""" self.vessel = vessel self._con = None self._ssh = None - def _execute(self, query: str, parameters: Optional[tuple] = None, ctype: Optional[MySQLdb.cursors.BaseCursor] = None, retry: bool = True): + def _execute( + self, + query: str, + parameters: Optional[tuple] = None, + ctype: Optional[MySQLdb.cursors.BaseCursor] = None, + retry: bool = True, + ): """Execute a query on the database Args: @@ -45,15 +50,20 @@ class Database: def _connect(self) -> None: if self.vessel.ssh: - self._ssh = Connection(self.vessel) - port = self._ssh.forward_tcp(3306) - host = "127.0.0.1" + self._ssh = Connection(self.vessel) + port = self._ssh.forward_tcp(3306) + host = "127.0.0.1" else: - port = 3306 - host = self.vessel.host + port = 3306 + host = self.vessel.host - self._con = MySQLdb.connect(host=host, user=self.vessel.username, - passwd=self.vessel.password, db=self.vessel.database, port=port) + self._con = MySQLdb.connect( + host=host, + user=self.vessel.username, + passwd=self.vessel.password, + db=self.vessel.database, + port=port, + ) def commit(self) -> None: """Commit the current database transaction @@ -64,7 +74,9 @@ class Database: """ self._con.commit() - def getCursor(self, ctype: Optional[MySQLdb.cursors.BaseCursor] = None) -> MySQLdb.cursors.BaseCursor: + def getCursor( + self, ctype: Optional[MySQLdb.cursors.BaseCursor] = None + ) -> MySQLdb.cursors.BaseCursor: """Return a cursor to operate on the MySQL database Returns: @@ -73,7 +85,6 @@ class Database: return self._con.cursor(ctype) def __del__(self): - """Close database connection on removal of the Database object - """ + """Close database connection on removal of the Database object""" if self._con: - self._con.close() \ No newline at end of file + self._con.close() diff --git a/src/reportmonster/classes/user.py b/src/reportmonster/classes/user.py index 7b755c0..527e23d 100644 --- a/src/reportmonster/classes/user.py +++ b/src/reportmonster/classes/user.py @@ -10,8 +10,8 @@ from bcrypt import hashpw, gensalt class User: - """Class describing a User - """ + """Class describing a User""" + @classmethod def fromConfig(cls, config: SectionProxy): """Create User object from a User section in the Config file @@ -29,7 +29,6 @@ class User: return cls(config.name.split()[1], config["Password"]) - def __init__(self, username: str, password: str) -> None: """Initialize new Vessel object @@ -39,6 +38,5 @@ class User: self.username = username self.password = password - def validatePasword(self, password) -> bool: - return password == self.password \ No newline at end of file + return password == self.password diff --git a/src/reportmonster/classes/vessel.py b/src/reportmonster/classes/vessel.py index 977c7f2..9875804 100644 --- a/src/reportmonster/classes/vessel.py +++ b/src/reportmonster/classes/vessel.py @@ -1,4 +1,4 @@ -from classes.database import Database +from .database import Database from configparser import SectionProxy from typing import Optional, Union @@ -8,18 +8,18 @@ from MySQLdb.cursors import DictCursor from MySQLdb._exceptions import IntegrityError from bcrypt import hashpw, gensalt -from const import * +from ..const import * class Vessel: - """Class describing a Vessel - """ + """Class describing a Vessel""" + @classmethod def fromConfig(cls, config: SectionProxy): """Create Vessel object from a Vessel section in the Config file Args: - config (configparser.SectionProxy): Vessel section defining a + config (configparser.SectionProxy): Vessel section defining a Vessel Raises: @@ -50,14 +50,37 @@ class Vessel: database = config["Database"] if "SSH" in config.keys(): - if int(config["SSH"]) == 1: + if config.getint("SSH") == 1: ssh = True - return cls(config.name.split()[1], config["Host"], username, password, database, ssh, ssh_username, ssh_password, ssh_timeout, ssh_passphrase) + ssh_username = config.get("SSHUser") - def __init__(self, name: str, host: str, username: Optional[str] = None, - password: Optional[str] = None, database: Optional[str] = None, - ssh = False, ssh_username = None, ssh_password = None, ssh_timeout = None, ssh_passphrase = None) -> None: + return cls( + config.name.split()[1], + config["Host"], + username, + password, + database, + ssh, + ssh_username, + ssh_password, + ssh_timeout, + ssh_passphrase, + ) + + def __init__( + self, + name: str, + host: str, + username: Optional[str] = None, + password: Optional[str] = None, + database: Optional[str] = None, + ssh=False, + ssh_username=None, + ssh_password=None, + ssh_timeout=None, + ssh_passphrase=None, + ) -> None: """Initialize new Vessel object Args: @@ -76,7 +99,7 @@ class Vessel: self.db = self.connect() - def connect(self): + def connect(self) -> Database: return Database(self) def reconnect(self): @@ -98,7 +121,9 @@ class Vessel: results = self.db._execute(QUERY_USER_INFO_FIELD, ctype=DictCursor) return results - def getUserInfoData(self, field: Optional[int] = None, user: Optional[int] = None) -> list: + def getUserInfoData( + self, field: Optional[int] = None, user: Optional[int] = None + ) -> list: query = QUERY_USER_INFO_DATA parameters = [] @@ -119,7 +144,9 @@ class Vessel: return results - def getUsers(self, username: Optional[str] = None, id: Optional[int] = None) -> dict: + def getUsers( + self, username: Optional[str] = None, id: Optional[int] = None + ) -> dict: query = QUERY_USER parameters = tuple() @@ -147,7 +174,9 @@ class Vessel: for value in odata: try: - users[value["userid"]]["custom_fields"][ofield["shortname"]] = value["data"] + users[value["userid"]]["custom_fields"][ + ofield["shortname"] + ] = value["data"] except KeyError: pass @@ -155,7 +184,10 @@ class Vessel: def getHTMLCerts(self, after: int = 0, before: Optional[int] = None): before = before or Vessel.getTimestamp() - results = self.db._execute(f"{QUERY_HTML_CERT_ISSUES} {QUERY_WHERE_TIMESTAMPS % {'column': 'timecreated', 'after': after, 'before': before}}", ctype=DictCursor) + results = self.db._execute( + f"{QUERY_HTML_CERT_ISSUES} {QUERY_WHERE_TIMESTAMPS % {'column': 'timecreated', 'after': after, 'before': before}}", + ctype=DictCursor, + ) ocerts = self.db._execute(QUERY_HTML_CERT, ctype=DictCursor) certs = dict() @@ -175,7 +207,10 @@ class Vessel: def getCustomCerts(self, after: int = 0, before: Optional[int] = None): before = before or Vessel.getTimestamp() - results = self.db._execute(f"{QUERY_CUSTOM_CERT_ISSUES} {QUERY_WHERE_TIMESTAMPS % {'column': 'timecreated', 'after': after, 'before': before}}", ctype=DictCursor) + results = self.db._execute( + f"{QUERY_CUSTOM_CERT_ISSUES} {QUERY_WHERE_TIMESTAMPS % {'column': 'timecreated', 'after': after, 'before': before}}", + ctype=DictCursor, + ) ocerts = self.db._execute(QUERY_CUSTOM_CERT, ctype=DictCursor) certs = dict() @@ -195,7 +230,10 @@ class Vessel: def getCerts(self, after: int = 0, before: Optional[int] = None): before = before or Vessel.getTimestamp() - return sorted(self.getHTMLCerts(after, before) + self.getCustomCerts(after, before), key=lambda d: d["timecreated"]) + return sorted( + self.getHTMLCerts(after, before) + self.getCustomCerts(after, before), + key=lambda d: d["timecreated"], + ) def setPassword(self, username: str, password: str): hashed = hashpw(password.encode(), gensalt(prefix=b"2b")) @@ -218,11 +256,16 @@ class Vessel: if not enrol: self.createEnrol(courseid, enrol) - enrol = list(filter(lambda x: x["courseid"] == courseid , self.getEnrols(enrol))) + enrol = list( + filter(lambda x: x["courseid"] == courseid, self.getEnrols(enrol)) + ) assert enrol - self.db._execute(QUERY_ENROL_USER, (enrol[0]["id"], userid, Vessel.getTimestamp(), Vessel.getTimestamp())) + self.db._execute( + QUERY_ENROL_USER, + (enrol[0]["id"], userid, Vessel.getTimestamp(), Vessel.getTimestamp()), + ) def getEnrolments(self): results = list(self.db._execute(QUERY_ENROLMENTS, ctype=DictCursor)) @@ -230,16 +273,30 @@ class Vessel: def createUser(self, username, password, email, firstname, lastname): email = email or f"{username}@pin.seachefsacademy.com" - self.db._execute(QUERY_USER_CREATE, (username, email, firstname, lastname, Vessel.getTimestamp(), Vessel.getTimestamp())) + self.db._execute( + QUERY_USER_CREATE, + ( + username, + email, + firstname, + lastname, + Vessel.getTimestamp(), + Vessel.getTimestamp(), + ), + ) self.setPassword(username, password) def assignRole(self, userid: int, courseid: int, roleid: int = 5): contextid = self.getCourseContext(courseid)[0]["id"] - self.db._execute(QUERY_ASSIGN_ROLE, (roleid, contextid, userid, Vessel.getTimestamp())) + self.db._execute( + QUERY_ASSIGN_ROLE, (roleid, contextid, userid, Vessel.getTimestamp()) + ) def getRole(self, userid: int, courseid: int) -> Optional[int]: - contextid = self.getCourseContext(courseid)[0]["id"] - results = self.db._execute(QUERY_GET_ROLE, (contextid, userid), ctype=DictCursor) + contextid = self.getCourseContext(courseid)[0]["id"] + results = self.db._execute( + QUERY_GET_ROLE, (contextid, userid), ctype=DictCursor + ) if results: return results[0]["roleid"] @@ -249,7 +306,10 @@ class Vessel: return results[0]["id"] def setEmail(self, userid: int, email: str): - email = email or f"{self.getUsers(id=userid)[userid]['username']}@pin.seachefsacademy.com" + email = ( + email + or f"{self.getUsers(id=userid)[userid]['username']}@pin.seachefsacademy.com" + ) self.db._execute(QUERY_USER_SET_EMAIL, (email, userid)) def setName(self, userid: int, first: str, last: str): @@ -260,23 +320,32 @@ class Vessel: return results def getCourseByContext(self, contextid: int) -> Optional[int]: - results = self.db._execute(QUERY_COURSE_CONTEXT_REVERSE, (contextid,), ctype=DictCursor) + results = self.db._execute( + QUERY_COURSE_CONTEXT_REVERSE, (contextid,), ctype=DictCursor + ) if results: return results[0]["instanceid"] def getCourseModules(self, courseid: int): - results = list(self.db._execute(QUERY_COURSE_MODULES, (courseid,), ctype=DictCursor)) + results = list( + self.db._execute(QUERY_COURSE_MODULES, (courseid,), ctype=DictCursor) + ) return results def getCourseModuleCompletion(self, moduleid: int): - results = list(self.db._execute(QUERY_MODULE_COMPLETION, (moduleid,), ctype=DictCursor)) + results = list( + self.db._execute(QUERY_MODULE_COMPLETION, (moduleid,), ctype=DictCursor) + ) return results def setCourseModuleCompletion(self, moduleid: int, userid: int): try: - self.db._execute(QUERY_INSERT_MODULE_COMPLETION, (moduleid, userid, Vessel.getTimestamp())) + self.db._execute( + QUERY_INSERT_MODULE_COMPLETION, + (moduleid, userid, Vessel.getTimestamp()), + ) except IntegrityError: - pass # Module completion record already exists + pass # Module completion record already exists self.db._execute(QUERY_UPDATE_MODULE_COMPLETION, (moduleid, userid)) def setCourseCompletion(self, courseid: int, userid: int): @@ -287,3 +356,9 @@ class Vessel: def writeLog(self, event, data): self.db._execute(QUERY_LOG_INSERT, (event, data)) + + def getCourseCompletions(self, courseid: int): + results = list( + self.db._execute(QUERY_COURSE_COMPLETION, (courseid,), ctype=DictCursor) + ) + return results