From 683e55ea0d60670d9d50fbc0f1db4b20a955aae3 Mon Sep 17 00:00:00 2001 From: TheOnePath Date: Fri, 11 Aug 2023 23:46:22 +0100 Subject: [PATCH] Added bottler.py Parse bottle files and constructure a package class which can be used in transactions. Set of valid packages keys is defined. func@_parse_bottle_file: - Module protected function called internally to parse bottle files. - Extract the Description field (this is a little different from other fields given it can be multiline). - Remove any comments which may have been left by a user. - Process the bottle file and extract all key-value pairs. This will also ensure that mandatory fields exist, and that any fields which are repeated are flagged (parser will cause app to crash). - Return a dictionary of key-value pairs. class@_Soybottle: - Class (struct) which represents a bottle file in a structured format. The dictionary is unwrapped after parsing and populates this class using the `.loadPackage()` classmethod. - Defined repr dunder which will return a string representation of the class as a dictionary (note class uses slots). - method@loadPackage(): - Defines a set of mandatory fields required in the bottle file. - Reports and exits if mandatory fields are missing. Warn if unrecognised fields exist. - Separate optional fields from mandatory ones. - Construct new class instance and return the object. class@PackageItem: - Class which stores information regarding a package, including whether it is installed, and the package information from _Soybottle. Class is early development. func@loadPackage: - This function is akin to _Soybottle@loadPackage, but *should* be called instead of the classmethod. To be called when a bottle file is given as argv, not by bottle file in the database. - Given a filename (`item=`), will attempt to open and read the contents of a bottle file. If the file is readable, it will then begin parsing and constructing a new class@PackageItem object. - Function will attempt to permanently store a bottle file to database. If this fails, the app will sys-exit. - Either the class@PackageItem or Exception is returned. This is to allow for exception messages to be given back to the install command. --- soypak/deps/bottler.py | 120 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 soypak/deps/bottler.py diff --git a/soypak/deps/bottler.py b/soypak/deps/bottler.py new file mode 100644 index 0000000..081688b --- /dev/null +++ b/soypak/deps/bottler.py @@ -0,0 +1,120 @@ +import re +import sys +import clog +import soypak.util as util +from io import TextIOWrapper + + +_log = clog.Logger.get("runtime_logger") + + +_PACKAGE_KEYS = {"Package", "Version", "Maintainer", "Filename", "Package-Repo", "Description", "Section", + "Priority", "Essential", "Architecture", "Origin", "Bugs", "Homepage", "Tag", "Depends", + "Pre-Depends", "Recommends", "Suggests", "Breaks", "Conflicts", "Replaces", "Provides"} + + +def _parse_bottle_file(item: TextIOWrapper) -> dict[str, str]: + # read the contents directly into memory + contents = item.read() + + # first grab the description (if exists), then remove it + desc = list(re.finditer(util.re_desc_tag, contents)) + contents = re.sub(util.re_desc_tag, "", contents) + # clean out the comments, strip leading/trailing whitespace, and split the lines + contents = re.sub(util.re_comments, "", contents).strip().splitlines() + parameters = {} + + # handle for description + if not desc: + _log.error("Missing field from bottle file, 'Description'.").withConsole() + sys.exit(-1) + # format, clean and add to parameters dictionary + desc = str(desc[0].group()).split("\n") + parameters['Description'] = (desc[0].lstrip("Description:").strip(), "\n".join(desc[1:])) + + for param in contents: + # handle for when newlines were split (empty string) + if not param: + continue + + # split the key-value and strip it + try: + key, val = map(lambda _: _.strip(), param.split(':')) + except ValueError: + # we should stop processing the file + _log.error("The bottle file contains broken fields.").withConsole() + sys.exit(-1) + + if key in parameters: + # The bottle file has a parameter repeated multiple times. This is parsing error + _log.error(f"Parameter {key!r} occurs more than once in bottle file.").withConsole() + sys.exit(-1) + + if key not in _PACKAGE_KEYS: + _log.error(f"Parameter {key!r} is not a valid field for bottle files.").withConsole() + sys.exit(-1) + + # now we can safely add the key to the dictionary + parameters[key] = val + + return parameters + + +class _Soybottle: + __slots__ = "package", "version", "maintainer", "desc", "package_repo", "filename", "optional" + def __init__(self, *, pkg, version, maintainer, desc, pkg_repo, filename, optionals={}) -> None: + self.package = pkg + self.version = version + self.maintainer = maintainer + self.desc = desc + self.package_repo = pkg_repo + self.filename = filename + self.optional = optionals + + + def __repr__(self) -> str: + return '{' + ", ".join("{0!r}: {1!r}".format(k, getattr(self, k)) for k in type(self).__slots__) + '}' + + + @classmethod + def loadPackage(cls, *, fields: dict[str, str]): + REQUIRED = {"Package", "Version", "Maintainer", "Description", "Package-Repo", "Filename"} + try: + for k in REQUIRED: + fields[k] + except KeyError as err: + _log.error("Missing mandatory key from bottle file %s" % str(err)).withConsole() + sys.exit(-1) + + # grab all the optional fields from the bottle file + optionals = dict(map(lambda k: (k, fields.pop(k)), REQUIRED ^ _PACKAGE_KEYS)) + if len(fields) > 6: + # just log a warning that the bottle file as some extra values unrecognised (not really critical) + _log.warn("Identified additional keys in bottle file that are unrecognised.") + + return cls(pkg=fields['Package'], version=fields['Version'], maintainer=['Maintainer'], desc=['Description'], + pkg_repo=fields['Package-Repo'], filename=fields['Filename'], optionals=optionals) + + +class PackageItem: + def __init__(self, info: _Soybottle) -> None: + self.installed = False + self.pkg_info = info + + +def loadPackage(item: str) -> PackageItem | Exception: + try: + fp = open(item, 'r') + except (IOError, FileNotFoundError) as err: + return err + + _log.debug(f"Obtained file point {fp!r}") + res = _Soybottle.loadPackage(fields=_parse_bottle_file(fp)) + + # permanently store the bottle file to the database + result = util.rack_bottle(fp, "") + if not result: + _log.error("The bottle file could not be permanently saved to soypak.").withConsole() + sys.exit(-1) + + return PackageItem(res)