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.
This commit is contained in:
parent
851e37538c
commit
683e55ea0d
120
soypak/deps/bottler.py
Normal file
120
soypak/deps/bottler.py
Normal file
|
|
@ -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)
|
||||
Loading…
Reference in New Issue
Block a user