Source code for osctiny.extensions.projects
"""
Projects extension
------------------
"""
import re
from urllib.parse import urljoin
from warnings import warn
from lxml.etree import tounicode
from lxml.objectify import fromstring, SubElement
from ..utils.base import ExtensionBase
TEMPLATE_CREATE_ATTR = "<attributes><attribute namespace='' name=''></attribute></attributes>"
TEMPLATE_META = "<project name=''><title></title><description></description>" \
"<build><enable/></build><publish><disable/></publish>" \
"<debuginfo><enable/></debuginfo></project>"
[docs]class Project(ExtensionBase):
"""
Osc extension to interact with projects
"""
base_path = "/source"
attribute_pattern = re.compile(r"^((?P<prefix>[^:]+):)?(?P<name>.+)$")
[docs] def get_list(self, deleted=False):
"""
Get list of projects
:param deleted: show deleted projects instead of existing
:type deleted: bool
:return: Objectified XML element
:rtype: lxml.objectify.ObjectifiedElement
"""
response = self.osc.request(
url=urljoin(self.osc.url, self.base_path),
method="GET",
params={'deleted': deleted}
)
return self.osc.get_objectified_xml(response)
[docs] def get_meta(self, project):
"""
Get project metadata
:param project: name of project
:return: Objectified XML element
:rtype: lxml.objectify.ObjectifiedElement
"""
response = self.osc.request(
url=urljoin(
self.osc.url,
"{}/{}/_meta".format(self.base_path, project)
),
method="GET"
)
return self.osc.get_objectified_xml(response)
# pylint: disable=too-many-arguments
[docs] def put_meta(self, project, metafile=None, title=None, description=None,
bugowner=None, maintainer=None):
"""
Edit project meta data or create a new project
If no ``metafile`` is provided, a default template is used.
.. versionadded:: 0.1.5
.. deprecated:: 0.7.2
Use :meth:`set_meta` instead
:param project: name of project
:param metafile: Complete metafile
:type metafile: str or ElementTree
:param title: Title for meta file
:param description: Description for meta file
:param bugowner: Bugowner for meta file
:param maintainer: Maintainer for meta file
:return: ``True``, if successful. Otherwise API response
:rtype: bool or lxml.objectify.ObjectifiedElement
"""
warn("Deprecated. Use projects.set_meta instead")
return self.set_meta(project, metafile, title, description, bugowner,
maintainer)
# pylint: disable=too-many-arguments
[docs] def set_meta(self, project, metafile=None, title=None, description=None,
bugowner=None, maintainer=None, comment=None, force=False):
"""
Edit project meta data or create a new project
If no ``metafile`` is provided, a default template is used.
.. versionadded:: 0.7.2
.. versionchanged:: 0.7.8
Added an optional ``comment`` argument to be used as commit message
.. versionchanged:: 0.7.11
Added an optional ``force`` argument to allow changing the meta even when IBS reports
repository dependency problems
:param project: name of project
:param metafile: Complete metafile
:type metafile: str or ElementTree
:param title: Title for meta file
:param description: Description for meta file
:param bugowner: Bugowner for meta file
:param maintainer: Maintainer for meta file
:param comment: Optional comment to use as commit message
:param force: Whether to force a meta change, even if there are repo dependency errors
:return: ``True``, if successful. Otherwise API response
:rtype: bool or lxml.objectify.ObjectifiedElement
"""
if metafile is None:
metafile = TEMPLATE_META
if isinstance(metafile, (str, bytes)):
metafile = fromstring(metafile)
metafile.set("name", project)
for required_field, text in (('title', title), ('description', description)):
elem = metafile.find(required_field)
if elem is None:
elem = SubElement(metafile, required_field)
if text is not None:
# pylint: disable=protected-access
elem._setText(text)
def add_person(role: str, userid: str) -> None:
person = metafile.xpath(f"person[@role='{role}']")
if not person:
person = [SubElement(metafile, 'person', role=role)]
person[0].set("userid", userid)
if bugowner:
add_person("bugowner", bugowner)
if maintainer:
add_person("maintainer", maintainer)
metafile.insert(0, metafile.title)
metafile.insert(1, metafile.description)
response = self.osc.request(
url=urljoin(self.osc.url,
"/".join((self.base_path, project, "_meta"))),
method="PUT",
data=tounicode(metafile),
params={"comment": comment, "force": force}
)
parsed = self.osc.get_objectified_xml(response)
if response.status_code == 200 and parsed.get("code") == "ok":
return True
return parsed
[docs] def get_files(self, project, directory="", meta=False, rev=None, **kwargs):
"""
List project files
:param project: name of project
:param directory: directory in project
:param meta: switch for _meta files
:type meta: bool
:param rev: revision
:type rev: int
:param kwargs: More keyword arguments for API call
:return: Objectified XML element
:rtype: lxml.objectify.ObjectifiedElement
.. deprecated:: 0.7.4
Use :py:meth:`osctiny.extensions.packages.Package.get_files` or
:py:meth:`osctiny.extensions.packages.Package.get_list` instead.
The API URL used by this method is (depending on the value of ``directory``) identical
to the one used by the above two methods.
"""
if rev:
kwargs["rev"] = str(rev)
if not directory:
return self.osc.packages.get_list(project=project, **kwargs)
kwargs["meta"] = meta
return self.osc.packages.get_files(project=project, package=directory, **kwargs)
[docs] def get_attribute(self, project, attribute=None):
"""
Get one attribute of a project
.. note::
Be aware of namespace prefixes.
When specifying the ``attribute`` argument make sure to include the
namespace prefix and separate both by a colon, e.g.
``OBS:IncidentPriority``.
:param project: name of project
:param attribute: name of attribute
:return: Objectified XML element
:rtype: lxml.objectify.ObjectifiedElement
"""
url = urljoin(
self.osc.url,
"{}/{}/_attribute".format(
self.base_path, project
)
)
if attribute:
url = "{}/{}".format(url, attribute)
response = self.osc.request(url=url, method="GET")
return self.osc.get_objectified_xml(response)
[docs] def set_attribute(self, project, attribute, value):
"""
Set or update an attribute of a project
:param project: project name
:param attribute: attribute name (can include prefix separated by colon)
:param value: attribute value or list of values
:return: ``True``, if successful. Otherwise API response
:rtype: bool or lxml.objectify.ObjectifiedElement
.. versionchanged:: 0.7.0
Support attributes with multiple values
"""
url = urljoin(
self.osc.url,
"{}/{}/_attribute".format(
self.base_path, project
)
)
match = self.attribute_pattern.match(attribute)
if match is None:
raise ValueError("Invalid attribute format: {}".format(attribute))
value = value if isinstance(value, (list, tuple, set)) else [value]
attr_xml = fromstring(TEMPLATE_CREATE_ATTR)
attr_xml.attribute.set('namespace', match.group("prefix"))
attr_xml.attribute.set('name', match.group("name"))
for val in value:
elem = SubElement(attr_xml.attribute, "value")
# pylint: disable=protected-access
elem._setText(str(val))
response = self.osc.request(
url=url,
method="POST",
data=tounicode(attr_xml)
)
parsed = self.osc.get_objectified_xml(response)
if response.status_code == 200 and parsed.get("code") == "ok":
return True
return parsed
[docs] def delete_attribute(self, project, attribute):
"""
Delete an attribute of a project
:param project: name of project
:param attribute: name of attribute
:return: ``True``, if successful. Otherwise API response
:rtype: bool or lxml.objectify.ObjectifiedElement
"""
url = urljoin(
self.osc.url,
"{}/{}/_attribute/{}".format(
self.base_path, project, attribute
)
)
response = self.osc.request(
url=url,
method="DELETE",
)
parsed = self.osc.get_objectified_xml(response)
if response.status_code == 200 and parsed.get("code") == "ok":
return True
return parsed
[docs] def get_comments(self, project):
"""
Get a list of comments for project
.. versionchanged:: 0.1.8
Use internally :py:class:`osctiny.comments.Comment.get`
:param project: name of project
:return: Objectified XML element
:rtype: lxml.objectify.ObjectifiedElement
"""
return self.osc.comments.get(
obj_type="project",
ids=(project,)
)
[docs] def add_comment(self, project, comment, parent_id=None):
"""
Add a comment to a project
.. versionadded: 0.1.2
.. versionchanged:: 0.1.8
Use internally :py:class:`osctiny.comments.Comment.add`
:param project: name of project
:param comment: Comment to be added
:param parent_id: ID of parent comment. Default: ``None``
:return: ``True``, if successful. Otherwise API response
:rtype: bool or lxml.objectify.ObjectifiedElement
"""
return self.osc.comments.add(
obj_type="project",
ids=(project,),
comment=comment,
parent_id=parent_id
)
[docs] def get_history(self, project, meta=True, rev=None, **kwargs):
"""
Get history of project
To get just a particular revision, use the ``rev`` argument.
:param project: name of project
:param meta: Switch between meta and non-meta (normally empty) revision
history
:type meta: bool
:param rev: History revision ID
:return: Objectified XML element
:rtype: lxml.objectify.ObjectifiedElement
"""
if rev:
kwargs["rev"] = rev
kwargs["meta"] = meta
response = self.osc.request(
url=urljoin(self.osc.url, "{}/{}/_project/_history".format(
self.base_path, project
)),
method="GET",
params=kwargs
)
return self.osc.get_objectified_xml(response)
[docs] def get_config(self, project, revision=None):
"""
Get project configuration
.. versionadded:: 0.7.10
:param project: name of project
:param revision: optional revision of the config to get
:return: The project configuration as string
:rtype: str
"""
response = self.osc.request(
url=urljoin(
self.osc.url,
"{}/{}/_config".format(self.base_path, project)
),
params={"rev":revision},
method="GET"
)
return response.text
[docs] def set_config(self, project, config=None, comment=None):
"""
Set project config data
.. versionadded:: 0.7.10
:param project: name of project
:param config: Complete configuration to set
:type config: str
:param comment: Optional comment to use as commit message
:return: ``True``, if successful. Otherwise, API response
:rtype: bool or lxml.objectify.ObjectifiedElement
"""
response = self.osc.request(
url=urljoin(self.osc.url,
"/".join((self.base_path, project, "_config"))),
method="PUT",
data=config,
params={"comment": comment}
)
parsed = self.osc.get_objectified_xml(response)
if response.status_code == 200 and parsed.get("code") == "ok":
return True
return parsed
[docs] def delete(self, project, force=False, comment=None):
"""
Delete project
.. versionadded:: 0.1.2
:param project: Project name
:param force: Delete project even if subprojects, packages or pending
requests for those packages exist
:param comment: Optional comment
:return: ``True``, if successful. Otherwise API response
:rtype: bool or lxml.objectify.ObjectifiedElement
"""
params = {'force': force}
response = self.osc.request(
url=urljoin(
self.osc.url,
"/".join((self.base_path, project))
),
method="DELETE",
params=params,
data=comment
)
parsed = self.osc.get_objectified_xml(response)
if response.status_code == 200 and parsed.get("code") == "ok":
return True
return parsed
[docs] def exists(self, project):
"""
Check whether project exists
.. versionadded:: 0.1.2
:param project: Project name
:return: ``True``, if project exists, otherwise ``False``
"""
response = self.osc.request(
url=urljoin(
self.osc.url,
"/".join((self.base_path, project))
),
method="HEAD",
raise_for_status=False
)
return response.status_code == 200
create = set_meta