diff --git a/osc/_private/__init__.py b/osc/_private/__init__.py index 868c741e..e26b045c 100644 --- a/osc/_private/__init__.py +++ b/osc/_private/__init__.py @@ -13,4 +13,5 @@ from .api_source import release from .common import print_msg from .common import format_msg_project_package_options from .package import ApiPackage +from .package import LocalPackage from .request import forward_request diff --git a/osc/_private/package.py b/osc/_private/package.py index c1fca72e..3f767155 100644 --- a/osc/_private/package.py +++ b/osc/_private/package.py @@ -1,5 +1,6 @@ import functools +from .. import oscerr from . import api @@ -18,6 +19,7 @@ class PackageBase: self.files = [] directory_node = self._get_directory_node() self._load_from_directory_node(directory_node) + self._meta_node = None def __str__(self): return f"{self.project}/{self.name}" @@ -60,6 +62,19 @@ class PackageBase: # the name is omitted and we want it present for overall sanity self.linkinfo.package = self.name + def _get_meta_node(self): + raise NotImplementedError() + + def get_meta_value(self, option): + if not self._meta_node: + self._meta_node = self._get_meta_node() + if not self._meta_node: + return None + node = api.find_node(self._meta_node, "package", option) + if node is None or not node.text: + raise oscerr.APIError(f"Couldn't get '{option}' from package _meta") + return node.text + class ApiPackage(PackageBase): def __init__(self, apiurl, project, package, rev=None): @@ -75,6 +90,11 @@ class ApiPackage(PackageBase): url_query["rev"] = self.__rev return api.get(self.apiurl, url_path, url_query) + def _get_meta_node(self): + url_path = ["source", self.project, self.name, "_meta"] + url_query = {} + return api.get(self.apiurl, url_path, url_query) + class LocalPackage(PackageBase): def __init__(self, path): @@ -86,3 +106,6 @@ class LocalPackage(PackageBase): def _get_directory_node(self): return self.store.read_xml_node("_files", "directory").getroot() + + def _get_meta_node(self): + return self.store._meta_node diff --git a/osc/core.py b/osc/core.py index d83363cf..0991bd93 100644 --- a/osc/core.py +++ b/osc/core.py @@ -2749,6 +2749,23 @@ class ReviewState(AbstractState): review_node.find('comment').text: self.comment = review_node.find('comment').text.strip() + def __repr__(self): + result = super().__repr__() + result += "(" + result += f"{self.state}" + + if self.who: + result += f" by {self.who}" + + for by in ("user", "group", "project", "package"): + by_value = getattr(self, f"by_{by}", None) + if by_value: + result += f" [{by} {by_value}])" + break + + result += ")" + return result + def get_node_attrs(self): return ('state', 'by_user', 'by_group', 'by_project', 'by_package', 'who', 'when') @@ -2879,6 +2896,9 @@ class Action: prefix_to_elm = {'src': 'source', 'tgt': 'target', 'opt': 'options'} def __init__(self, type, **kwargs): + self.apiurl = kwargs.pop("apiurl", None) + self._src_pkg_object = None + self._tgt_pkg_object = None if type not in Action.type_args.keys(): raise oscerr.WrongArgs('invalid action type: \'%s\'' % type) self.type = type @@ -2889,6 +2909,60 @@ class Action: for i in Action.type_args[type]: setattr(self, i, kwargs.get(i)) + def __repr__(self): + result = super().__repr__() + result += "(" + result += f"type={self.type}" + + src_pkg = self.src_pkg_object + if src_pkg: + result += f" source={src_pkg.project}/{src_pkg.name}" + elif getattr(self, "src_project", None): + result += f" source={self.src_project}" + + tgt_pkg = self.tgt_pkg_object + if tgt_pkg: + result += f" target={tgt_pkg.project}/{tgt_pkg.name}" + elif getattr(self, "tgt_project", None): + result += f" target={self.tgt_project}" + + result += ")" + return result + + @property + def src_pkg_object(self): + if not getattr(self, "src_project", None) or not getattr(self, "src_package", None): + return None + if not self._src_pkg_object: + src_rev = getattr(self, "src_rev", None) + self._src_pkg_object = _private.ApiPackage(self.apiurl, self.src_project, self.src_package, src_rev) + return self._src_pkg_object + + @property + def tgt_pkg_object(self): + if not self._tgt_pkg_object: + if self.type == "maintenance_incident": + # the target project for maintenance incidents is virtual and cannot be queried + # the actual target project is in the "releaseproject" attribute + # + # tgt_releaseproject is always set for a maintenance_incident + # pylint: disable=no-member + tgt_project = self.tgt_releaseproject + + # the target package is not specified + # we need to extract it from source package's _meta + src_package_meta_releasename = self.src_pkg_object.get_meta_value("releasename") + tgt_package = src_package_meta_releasename.split(".")[0] + else: + if not getattr(self, "tgt_project", None) or not getattr(self, "tgt_package", None): + return None + # tgt_project and tgt_package are checked above + # pylint: disable=no-member + tgt_project = self.tgt_project + tgt_package = self.tgt_package + self._tgt_pkg_object = _private.ApiPackage(self.apiurl, tgt_project, tgt_package) + return self._tgt_pkg_object + def to_xml(self): """ Serialize object to XML. @@ -2935,7 +3009,7 @@ class Action: return ET.tostring(root, encoding=ET_ENCODING) @staticmethod - def from_xml(action_node): + def from_xml(action_node, apiurl=None): """create action from XML""" if action_node is None or \ action_node.get('type') not in Action.type_args.keys() or \ @@ -2960,6 +3034,7 @@ class Action: l.append(v) else: kwargs[k] = v + kwargs["apiurl"] = apiurl return Action(action_node.get('type'), **kwargs) @@ -2967,6 +3042,12 @@ class Action: class Request: """Represents a request (````)""" + @classmethod + def from_api(cls, apiurl: str, req_id: int): + # TODO: deprecate get_request() or move its content here + req_id = str(req_id) + return get_request(apiurl, req_id) + def __init__(self): self._init_attributes() @@ -2982,6 +3063,7 @@ class Request: self.actions = [] self.statehistory = [] self.reviews = [] + self._issues = None def __eq__(self, other): return int(self.reqid) == int(other.reqid) @@ -2989,9 +3071,20 @@ class Request: def __lt__(self, other): return int(self.reqid) < int(other.reqid) - def read(self, root): + @property + def id(self): + return self.reqid + + @property + def issues(self): + if self._issues is None: + self._issues = get_request_issues(self.apiurl, self.id) + return self._issues + + def read(self, root, apiurl=None): """read in a request""" self._init_attributes() + self.apiurl = apiurl if not root.get('id'): raise oscerr.APIError('invalid request: %s\n' % ET.tostring(root, encoding=ET_ENCODING)) self.reqid = root.get('id') @@ -3008,7 +3101,7 @@ class Request: i.set('type', 'submit') action_nodes.append(i) for action in action_nodes: - self.actions.append(Action.from_xml(action)) + self.actions.append(Action.from_xml(action, self.apiurl)) for review in root.findall('review'): self.reviews.append(ReviewState(review)) for history_element in root.findall('history'): @@ -4500,7 +4593,7 @@ def get_request(apiurl: str, reqid): root = ET.parse(f).getroot() r = Request() - r.read(root) + r.read(root, apiurl=apiurl) return r diff --git a/osc/store.py b/osc/store.py index fd6d306f..a4f8f4fa 100644 --- a/osc/store.py +++ b/osc/store.py @@ -299,3 +299,13 @@ class Store: if len(value) != 3: raise ValueError("A list with exactly 3 items is expected: [repo, arch, vm_type]") self.write_list("_last_buildroot", value) + + @property + def _meta_node(self): + if not self.exists("_meta"): + return None + if self.is_package: + root = self.read_xml_node("_meta", "package").getroot() + else: + root = self.read_xml_node("_meta", "project").getroot() + return root diff --git a/tests/test_store.py b/tests/test_store.py index 58a1e751..f77cf613 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -203,6 +203,29 @@ class TestStore(unittest.TestCase): store2 = Store(self.tmpdir) self.assertEqual(store2.last_buildroot, ["repo", "arch", "vm_type"]) + def test_meta_node(self): + self.store.write_string( + "_meta", + """ + title + desc + name + + + + +""", + ) + node = self.store._meta_node + self.assertNotEqual(node, None) + + # try to read the _meta via a package class + from osc._private import LocalPackage + + self.store.files = [] + pkg = LocalPackage(self.tmpdir) + self.assertEqual(pkg.get_meta_value("releasename"), "name") + if __name__ == "__main__": unittest.main()