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:
Ethan Smith-Coss 2023-08-11 23:46:22 +01:00
parent 851e37538c
commit 683e55ea0d
Signed by: TheOnePath
GPG Key ID: 4E7D436CE1A0BAF1

120
soypak/deps/bottler.py Normal file
View 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)