From 09ad87c865af0fc7382fddb641a729da430fbbbc Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 10 Feb 2023 11:18:57 +0100 Subject: [PATCH 1/8] core.Request: Store apiurl the object was initialized from --- osc/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/osc/core.py b/osc/core.py index d83363cf..62f401bc 100644 --- a/osc/core.py +++ b/osc/core.py @@ -4501,6 +4501,7 @@ def get_request(apiurl: str, reqid): r = Request() r.read(root) + r.apiurl = apiurl return r From 17e25776a2f63a5caaadef14511b64eafafc1fc6 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 10 Feb 2023 11:04:34 +0100 Subject: [PATCH 2/8] core.Request: Add from_api() class method --- osc/core.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osc/core.py b/osc/core.py index 62f401bc..da5ee20a 100644 --- a/osc/core.py +++ b/osc/core.py @@ -2967,6 +2967,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() From 27c3159fdb28736b970cc33d5a16c6258cb61096 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 10 Feb 2023 11:05:40 +0100 Subject: [PATCH 3/8] core.Request: Add id property that is a shortcut to the reqid attribute --- osc/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osc/core.py b/osc/core.py index da5ee20a..bdd87046 100644 --- a/osc/core.py +++ b/osc/core.py @@ -2995,6 +2995,10 @@ class Request: def __lt__(self, other): return int(self.reqid) < int(other.reqid) + @property + def id(self): + return self.reqid + def read(self, root): """read in a request""" self._init_attributes() From 770217bb8d2148eb412b555531db9370fcef3bcd Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 10 Feb 2023 11:09:19 +0100 Subject: [PATCH 4/8] core.Request: Add issues property that loads issues on demand --- osc/core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osc/core.py b/osc/core.py index bdd87046..b73aeb59 100644 --- a/osc/core.py +++ b/osc/core.py @@ -2988,6 +2988,7 @@ class Request: self.actions = [] self.statehistory = [] self.reviews = [] + self._issues = None def __eq__(self, other): return int(self.reqid) == int(other.reqid) @@ -2999,6 +3000,12 @@ class Request: 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): """read in a request""" self._init_attributes() From f5da27ad2471e2387b3e1e48d891d1e26eac8da6 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 10 Feb 2023 15:27:46 +0100 Subject: [PATCH 5/8] core.Action: Remember apiurl --- osc/core.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osc/core.py b/osc/core.py index b73aeb59..5174f2c9 100644 --- a/osc/core.py +++ b/osc/core.py @@ -2879,6 +2879,7 @@ class Action: prefix_to_elm = {'src': 'source', 'tgt': 'target', 'opt': 'options'} def __init__(self, type, **kwargs): + self.apiurl = kwargs.pop("apiurl", None) if type not in Action.type_args.keys(): raise oscerr.WrongArgs('invalid action type: \'%s\'' % type) self.type = type @@ -2935,7 +2936,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 +2961,7 @@ class Action: l.append(v) else: kwargs[k] = v + kwargs["apiurl"] = apiurl return Action(action_node.get('type'), **kwargs) @@ -3006,9 +3008,10 @@ class Request: self._issues = get_request_issues(self.apiurl, self.id) return self._issues - def read(self, root): + 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') @@ -3025,7 +3028,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'): @@ -4517,8 +4520,7 @@ def get_request(apiurl: str, reqid): root = ET.parse(f).getroot() r = Request() - r.read(root) - r.apiurl = apiurl + r.read(root, apiurl=apiurl) return r From 95be11130ef8789db8c5bbb73bb4fdf9128b0320 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Mon, 13 Feb 2023 13:52:00 +0100 Subject: [PATCH 6/8] core.Action: Add src_pkg_object and tgt_pkg_object properties providing object wrappers to file lists --- osc/_private/__init__.py | 1 + osc/_private/package.py | 23 +++++++++++++++++++++++ osc/core.py | 36 ++++++++++++++++++++++++++++++++++++ osc/store.py | 10 ++++++++++ tests/test_store.py | 23 +++++++++++++++++++++++ 5 files changed, 93 insertions(+) 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 5174f2c9..ab1bc305 100644 --- a/osc/core.py +++ b/osc/core.py @@ -2880,6 +2880,8 @@ class Action: 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 @@ -2890,6 +2892,40 @@ class Action: for i in Action.type_args[type]: setattr(self, i, kwargs.get(i)) + @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. 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() From e132d093e7c63d040ce36cb290972bc72dbc15a6 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Mon, 13 Feb 2023 13:59:32 +0100 Subject: [PATCH 7/8] core.Action: Add __repr__() method --- osc/core.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osc/core.py b/osc/core.py index ab1bc305..bad98ad8 100644 --- a/osc/core.py +++ b/osc/core.py @@ -2892,6 +2892,26 @@ 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): From 815009f647f4ae034f0b209a3b33c28ef9341de1 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Mon, 13 Feb 2023 14:15:15 +0100 Subject: [PATCH 8/8] core.ReviewState: Add __repr__() method --- osc/core.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osc/core.py b/osc/core.py index bad98ad8..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')