mirror of
https://github.com/openSUSE/osc.git
synced 2024-11-12 23:56:13 +01:00
Merge pull request #1502 from dmach/xmlmodel-request
Add Request and PackageSources XML models
This commit is contained in:
commit
2129908dd6
74
osc/core.py
74
osc/core.py
@ -4634,53 +4634,37 @@ def create_submit_request(
|
||||
src_package: Optional[str] = None,
|
||||
dst_project: Optional[str] = None,
|
||||
dst_package: Optional[str] = None,
|
||||
message="",
|
||||
orev=None,
|
||||
src_update=None,
|
||||
dst_updatelink=None,
|
||||
message: str = "",
|
||||
orev: Optional[str] = None,
|
||||
src_update: Optional[str] = None,
|
||||
dst_updatelink: Optional[bool] = None,
|
||||
):
|
||||
options_block = ""
|
||||
package = ""
|
||||
if src_package:
|
||||
package = f"""package="{src_package}" """
|
||||
options_block = "<options>"
|
||||
if src_update:
|
||||
options_block += f"""<sourceupdate>{src_update}</sourceupdate>"""
|
||||
if dst_updatelink:
|
||||
options_block += """<updatelink>true</updatelink>"""
|
||||
options_block += "</options>"
|
||||
from . import obs_api
|
||||
|
||||
# Yes, this kind of xml construction is horrible
|
||||
targetxml = ""
|
||||
if dst_project:
|
||||
packagexml = ""
|
||||
if dst_package:
|
||||
packagexml = f"""package="{dst_package}" """
|
||||
targetxml = f"""<target project="{dst_project}" {packagexml} /> """
|
||||
# XXX: keep the old template for now in order to work with old obs instances
|
||||
xml = """\
|
||||
<request>
|
||||
<action type="submit">
|
||||
<source project="%s" %s rev="%s"/>
|
||||
%s
|
||||
%s
|
||||
</action>
|
||||
<state name="new"/>
|
||||
<description>%s</description>
|
||||
</request>
|
||||
""" % (src_project,
|
||||
package,
|
||||
orev or show_upstream_rev(apiurl, src_project, src_package),
|
||||
targetxml,
|
||||
options_block,
|
||||
_html_escape(message))
|
||||
req = obs_api.Request(
|
||||
action_list=[
|
||||
{
|
||||
"type": "submit",
|
||||
"source": {
|
||||
"project": src_project,
|
||||
"package": src_package,
|
||||
"rev": orev or show_upstream_rev(apiurl, src_project, src_package),
|
||||
},
|
||||
"target": {
|
||||
"project": dst_project,
|
||||
"package": dst_package,
|
||||
},
|
||||
"options": {
|
||||
"sourceupdate": src_update,
|
||||
"updatelink": "true" if dst_updatelink else None,
|
||||
}
|
||||
},
|
||||
],
|
||||
description=message,
|
||||
)
|
||||
|
||||
u = makeurl(apiurl, ["request"], query={"cmd": "create"})
|
||||
r = None
|
||||
try:
|
||||
f = http_POST(u, data=xml)
|
||||
root = ET.parse(f).getroot()
|
||||
r = root.get('id')
|
||||
new_req = req.cmd_create(apiurl)
|
||||
except HTTPError as e:
|
||||
if e.hdrs.get('X-Opensuse-Errorcode') == "submit_request_rejected":
|
||||
print('WARNING: As the project is in maintenance, a maintenance incident request is')
|
||||
@ -4700,11 +4684,11 @@ def create_submit_request(
|
||||
raise oscerr.APIError("Server did not define a default maintenance project, can't submit.")
|
||||
tproject = project.get('name')
|
||||
r = create_maintenance_request(apiurl, src_project, [src_package], tproject, dst_project, src_update, message, rev=orev)
|
||||
r = r.reqid
|
||||
return r.reqid
|
||||
else:
|
||||
raise
|
||||
|
||||
return r
|
||||
return new_req.id
|
||||
|
||||
|
||||
def get_request(apiurl: str, reqid):
|
||||
|
@ -1,2 +1,4 @@
|
||||
from .package import Package
|
||||
from .package_sources import PackageSources
|
||||
from .project import Project
|
||||
from .request import Request
|
||||
|
@ -67,6 +67,13 @@ class LocalRole(str, Enum):
|
||||
READER = "reader"
|
||||
|
||||
|
||||
class ObsRatings(str, Enum):
|
||||
LOW = "low"
|
||||
MODERATE = "moderate"
|
||||
IMPORTANT = "important"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class RebuildModes(str, Enum):
|
||||
TRANSITIVE = "transitive"
|
||||
DIRECT = "direct"
|
||||
@ -77,3 +84,13 @@ class ReleaseTriggers(str, Enum):
|
||||
MANUAL = "manual"
|
||||
MAINTENANCE = "maintenance"
|
||||
OBSGENDIFF = "obsgendiff"
|
||||
|
||||
|
||||
class RequestStates(str, Enum):
|
||||
REVIEW = "review"
|
||||
NEW = "new"
|
||||
ACCEPTED = "accepted"
|
||||
DECLINED = "declined"
|
||||
REVOKED = "revoked"
|
||||
SUPERSEDED = "superseded"
|
||||
DELETED = "deleted"
|
||||
|
41
osc/obs_api/linkinfo.py
Normal file
41
osc/obs_api/linkinfo.py
Normal file
@ -0,0 +1,41 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class Linkinfo(XmlModel):
|
||||
XML_TAG = "linkinfo"
|
||||
|
||||
project: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
package: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
lsrcmd5: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
xsrcmd5: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
baserev: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
rev: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
srcmd5: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
error: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
missingok: Optional[bool] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
@ -69,12 +69,13 @@ class Package(XmlModel):
|
||||
|
||||
@classmethod
|
||||
def from_api(cls, apiurl, project, package, *, rev=None):
|
||||
# ``rev`` is metadata revision, not revision of the source code
|
||||
url_path = ["source", project, package, "_meta"]
|
||||
url_query = {
|
||||
"rev": rev,
|
||||
}
|
||||
response = cls.xml_request("GET", apiurl, url_path, url_query)
|
||||
return cls.from_file(response)
|
||||
return cls.from_file(response, apiurl=apiurl)
|
||||
|
||||
def to_api(self, apiurl, *, project=None, package=None):
|
||||
project = project or self.project
|
||||
@ -82,7 +83,7 @@ class Package(XmlModel):
|
||||
url_path = ["source", project, package, "_meta"]
|
||||
url_query = {}
|
||||
response = self.xml_request("PUT", apiurl, url_path, url_query, data=self.to_string())
|
||||
return Status.from_file(response)
|
||||
return Status.from_file(response, apiurl=apiurl)
|
||||
|
||||
@classmethod
|
||||
def cmd_release(
|
||||
@ -124,4 +125,4 @@ class Package(XmlModel):
|
||||
"nodelay": nodelay,
|
||||
}
|
||||
response = cls.xml_request("POST", apiurl, url_path, url_query)
|
||||
return Status.from_string(response.read())
|
||||
return Status.from_file(response, apiurl=apiurl)
|
||||
|
68
osc/obs_api/package_sources.py
Normal file
68
osc/obs_api/package_sources.py
Normal file
@ -0,0 +1,68 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from .linkinfo import Linkinfo
|
||||
from .package_sources_file import PackageSourcesFile
|
||||
from .serviceinfo import Serviceinfo
|
||||
|
||||
|
||||
class PackageSources(XmlModel):
|
||||
XML_TAG = "directory"
|
||||
|
||||
name: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
rev: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
vrev: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
srcmd5: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
linkinfo: Optional[Linkinfo] = Field(
|
||||
)
|
||||
|
||||
serviceinfo: Optional[Serviceinfo] = Field(
|
||||
)
|
||||
|
||||
file_list: Optional[List[PackageSourcesFile]] = Field(
|
||||
xml_name="entry",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api(
|
||||
cls,
|
||||
apiurl: str,
|
||||
project: str,
|
||||
package: str,
|
||||
*,
|
||||
deleted: Optional[bool] = None,
|
||||
expand: Optional[bool] = None,
|
||||
meta: Optional[bool] = None,
|
||||
rev: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
:param deleted: Set to ``True`` to list source files of a deleted package.
|
||||
Throws 400: Bad Request if such package exists.
|
||||
:param expand: Expand links.
|
||||
:param meta: Set to ``True`` to list metadata file (``_meta``) instead of the sources.
|
||||
:param rev: Show sources of the specified revision.
|
||||
"""
|
||||
from ..core import revision_is_empty
|
||||
|
||||
if revision_is_empty(rev):
|
||||
rev = None
|
||||
|
||||
url_path = ["source", project, package]
|
||||
url_query = {
|
||||
"deleted": deleted,
|
||||
"expand": expand,
|
||||
"meta": meta,
|
||||
"rev": rev,
|
||||
}
|
||||
response = cls.xml_request("GET", apiurl, url_path, url_query)
|
||||
return cls.from_file(response, apiurl=apiurl)
|
28
osc/obs_api/package_sources_file.py
Normal file
28
osc/obs_api/package_sources_file.py
Normal file
@ -0,0 +1,28 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class PackageSourcesFile(XmlModel):
|
||||
XML_TAG = "entry"
|
||||
|
||||
name: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
md5: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
mtime: int = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
size: int = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
skipped: Optional[bool] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
def _get_cmp_data(self):
|
||||
return (self.name, self.mtime, self.size, self.md5, self.skipped or False)
|
@ -112,14 +112,14 @@ class Project(XmlModel):
|
||||
url_path = ["source", project, "_meta"]
|
||||
url_query = {}
|
||||
response = cls.xml_request("GET", apiurl, url_path, url_query)
|
||||
return cls.from_file(response)
|
||||
return cls.from_file(response, apiurl=apiurl)
|
||||
|
||||
def to_api(self, apiurl, *, project=None):
|
||||
project = project or self.name
|
||||
url_path = ["source", project, "_meta"]
|
||||
url_query = {}
|
||||
response = self.xml_request("PUT", apiurl, url_path, url_query, data=self.to_string())
|
||||
return Status.from_file(response)
|
||||
return Status.from_file(response, apiurl=apiurl)
|
||||
|
||||
def resolve_repository_flags(self, package_obj=None):
|
||||
"""
|
||||
|
140
osc/obs_api/request.py
Normal file
140
osc/obs_api/request.py
Normal file
@ -0,0 +1,140 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from .enums import ObsRatings
|
||||
from .request_action import RequestAction
|
||||
from .request_history import RequestHistory
|
||||
from .request_review import RequestReview
|
||||
from .request_state import RequestState
|
||||
|
||||
|
||||
class Request(XmlModel):
|
||||
XML_TAG = "request"
|
||||
|
||||
id: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
actions: Optional[int] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
creator: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
action_list: List[RequestAction] = Field(
|
||||
xml_name="action",
|
||||
)
|
||||
|
||||
state: Optional[RequestState] = Field(
|
||||
)
|
||||
|
||||
description: Optional[str] = Field(
|
||||
)
|
||||
|
||||
priority: Optional[ObsRatings] = Field(
|
||||
)
|
||||
|
||||
review_list: Optional[List[RequestReview]] = Field(
|
||||
xml_name="review",
|
||||
)
|
||||
|
||||
history_list: Optional[List[RequestHistory]] = Field(
|
||||
xml_name="history",
|
||||
)
|
||||
|
||||
title: Optional[str] = Field(
|
||||
)
|
||||
|
||||
accept_at: Optional[str] = Field(
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api(
|
||||
cls,
|
||||
apiurl: str,
|
||||
request_id: int,
|
||||
*,
|
||||
with_history: Optional[bool] = None,
|
||||
with_full_history: Optional[bool] = None
|
||||
) -> "Request":
|
||||
"""
|
||||
Return the specified request.
|
||||
|
||||
:param request_id: Id of the request.
|
||||
:param withhistory: Include the request history in the results.
|
||||
:param withfullhistory: Includes both, request and review history in the results.
|
||||
"""
|
||||
url_path = ["request", request_id]
|
||||
url_query = {
|
||||
"withhistory": with_history,
|
||||
"withfullhistory": with_full_history,
|
||||
}
|
||||
response = cls.xml_request("GET", apiurl, url_path, url_query)
|
||||
return cls.from_file(response, apiurl=apiurl)
|
||||
|
||||
@classmethod
|
||||
def cmd_diff(
|
||||
cls,
|
||||
apiurl: str,
|
||||
request_id: int,
|
||||
*,
|
||||
with_issues: Optional[bool] = None,
|
||||
with_description_issues: Optional[bool] = None,
|
||||
diff_to_superseded: Optional[int] = None
|
||||
) -> "Request":
|
||||
"""
|
||||
Return the specified request including a diff of all packages in the request.
|
||||
|
||||
:param request_id: Id of the request.
|
||||
:param with_issues: Include parsed issues from referenced sources in the change files.
|
||||
:param with_description_issues: Include parsed issues from request description.
|
||||
:param diff_to_superseded: Diff relatively to the given superseded request.
|
||||
"""
|
||||
url_path = ["request", str(request_id)]
|
||||
url_query = {
|
||||
"cmd": "diff",
|
||||
"view": "xml",
|
||||
"withissues": with_issues,
|
||||
"withdescriptionissues": with_description_issues,
|
||||
"diff_to_superseded": diff_to_superseded,
|
||||
}
|
||||
response = cls.xml_request("POST", apiurl, url_path, url_query)
|
||||
return cls.from_file(response, apiurl=apiurl)
|
||||
|
||||
def get_issues(self):
|
||||
"""
|
||||
Aggregate issues from action/sourcediff into a single list.
|
||||
The list may contain duplicates.
|
||||
|
||||
To get any issues returned, it is crucial to load the request with the issues
|
||||
by calling ``cmd_diff()`` with appropriate arguments first.
|
||||
"""
|
||||
result = []
|
||||
for action in self.action_list or []:
|
||||
if action.sourcediff is None:
|
||||
continue
|
||||
for issue in action.sourcediff.issue_list or []:
|
||||
result.append(issue)
|
||||
return result
|
||||
|
||||
def cmd_create(self,
|
||||
apiurl: str,
|
||||
*,
|
||||
add_revision: Optional[bool] = None,
|
||||
enforce_branching: Optional[bool] = None,
|
||||
ignore_build_state: Optional[bool] = None,
|
||||
ignore_delegate: Optional[bool] = None,
|
||||
):
|
||||
"""
|
||||
:param add_revision: Ask the server to add revisions of the current sources to the request.
|
||||
:param ignore_build_state: Skip the build state check.
|
||||
:param ignore_delegate: Enforce a new package instance in a project which has OBS:DelegateRequestTarget set.
|
||||
"""
|
||||
url_path = ["request"]
|
||||
url_query = {
|
||||
"cmd": "create",
|
||||
"addrevision": add_revision,
|
||||
"ignore_delegate": ignore_delegate,
|
||||
}
|
||||
response = self.xml_request("POST", apiurl, url_path, url_query, data=self.to_string())
|
||||
return Request.from_file(response, apiurl=apiurl)
|
236
osc/obs_api/request_action.py
Normal file
236
osc/obs_api/request_action.py
Normal file
@ -0,0 +1,236 @@
|
||||
import urllib.error
|
||||
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from .package import Package
|
||||
from .package_sources import PackageSources
|
||||
from .request_action_acceptinfo import RequestActionAcceptinfo
|
||||
from .request_action_group import RequestActionGroup
|
||||
from .request_action_grouped import RequestActionGrouped
|
||||
from .request_action_options import RequestActionOptions
|
||||
from .request_action_person import RequestActionPerson
|
||||
from .request_action_source import RequestActionSource
|
||||
from .request_action_target import RequestActionTarget
|
||||
from .request_sourcediff import RequestSourcediff
|
||||
|
||||
|
||||
class RequestAction(XmlModel):
|
||||
XML_TAG = "action"
|
||||
|
||||
class TypeEnum(str, Enum):
|
||||
SUBMIT = "submit"
|
||||
DELETE = "delete"
|
||||
CHANGE_DEVEL = "change_devel"
|
||||
ADD_ROLE = "add_role"
|
||||
SET_BUGOWNER = "set_bugowner"
|
||||
MAINTENANCE_INCIDENT = "maintenance_incident"
|
||||
MAINTENANCE_RELEASE = "maintenance_release"
|
||||
RELEASE = "release"
|
||||
GROUP = "group"
|
||||
|
||||
type: TypeEnum = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
source: Optional[RequestActionSource] = Field(
|
||||
)
|
||||
|
||||
target: Optional[RequestActionTarget] = Field(
|
||||
)
|
||||
|
||||
person: Optional[RequestActionPerson] = Field(
|
||||
)
|
||||
|
||||
group: Optional[RequestActionGroup] = Field(
|
||||
)
|
||||
|
||||
grouped_list: Optional[List[RequestActionGrouped]] = Field(
|
||||
xml_name="grouped",
|
||||
)
|
||||
|
||||
options: Optional[RequestActionOptions] = Field(
|
||||
)
|
||||
|
||||
sourcediff: Optional[RequestSourcediff] = Field(
|
||||
)
|
||||
|
||||
acceptinfo: Optional[RequestActionAcceptinfo] = Field(
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._allow_new_attributes = True
|
||||
# source and target always come from ``self._apiurl`` while devel and factory projects may live elsewhere
|
||||
self._devel_apiurl = self._apiurl
|
||||
self._factory_apiurl = self._apiurl
|
||||
self._factory_project = "openSUSE:Factory"
|
||||
self._props = {}
|
||||
self._allow_new_attributes = False
|
||||
|
||||
def _get_package(self, package_type):
|
||||
key = f"{package_type}_package"
|
||||
if key not in self._props:
|
||||
func = getattr(self, f"_get_{package_type}_apiurl_project_package")
|
||||
apiurl, project, package = func()
|
||||
if apiurl is None:
|
||||
self._props[key] = None
|
||||
else:
|
||||
try:
|
||||
self._props[key] = Package.from_api(apiurl, project, package)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 404:
|
||||
raise
|
||||
self._props[key] = None
|
||||
return self._props[key]
|
||||
|
||||
def _get_package_sources(self, package_type, *, rev=None):
|
||||
key = f"{package_type}_package_sources"
|
||||
if key not in self._props:
|
||||
func = getattr(self, f"_get_{package_type}_apiurl_project_package")
|
||||
apiurl, project, package = func()
|
||||
if apiurl is None:
|
||||
self._props[key] = None
|
||||
else:
|
||||
try:
|
||||
self._props[key] = PackageSources.from_api(apiurl, project, package, rev=rev)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 404:
|
||||
raise
|
||||
self._props[key] = None
|
||||
return self._props[key]
|
||||
|
||||
def _get_source_apiurl_project_package(self):
|
||||
return self._apiurl, self.source.project, self.source.package
|
||||
|
||||
@property
|
||||
def source_package(self) -> Optional[Package]:
|
||||
"""
|
||||
Return a ``Package`` object that encapsulates metadata of the source package.
|
||||
"""
|
||||
return self._get_package("source")
|
||||
|
||||
@property
|
||||
def source_package_sources(self) -> Optional[PackageSources]:
|
||||
"""
|
||||
Return a ``PackageSources`` object that contains information about the ``source.rev`` revision of the source package sources in OBS SCM.
|
||||
"""
|
||||
if self.source is None:
|
||||
return None
|
||||
return self._get_package_sources("source", rev=self.source.rev)
|
||||
|
||||
def _get_target_apiurl_project_package(self):
|
||||
if self.target is None:
|
||||
return None, None, None
|
||||
target_project, target_package = self.get_actual_target_project_package()
|
||||
return self._apiurl, target_project, target_package
|
||||
|
||||
@property
|
||||
def target_package(self) -> Optional[Package]:
|
||||
"""
|
||||
Return a ``Package`` object that encapsulates metadata of the target package.
|
||||
"""
|
||||
return self._get_package("target")
|
||||
|
||||
@property
|
||||
def target_package_sources(self) -> Optional[PackageSources]:
|
||||
"""
|
||||
Return a ``PackageSources`` object that contains information about the current revision of the target package sources in OBS SCM.
|
||||
"""
|
||||
return self._get_package_sources("target")
|
||||
|
||||
def _get_factory_apiurl_project_package(self):
|
||||
if self.target is None:
|
||||
# a new package was submitted, it doesn't exist on target; let's read the package name from the source
|
||||
target_project, target_package = None, self.source.package
|
||||
else:
|
||||
target_project, target_package = self.get_actual_target_project_package()
|
||||
|
||||
if (self._apiurl, target_project) == (self._factory_apiurl, self._factory_project):
|
||||
# factory package equals the target package
|
||||
return None, None, None
|
||||
|
||||
return self._factory_apiurl, self._factory_project, target_package
|
||||
|
||||
@property
|
||||
def factory_package(self) -> Optional[Package]:
|
||||
"""
|
||||
Return a ``Package`` object that encapsulates metadata of the package in the factory project.
|
||||
The name of the package equals the target package name.
|
||||
"""
|
||||
return self._get_package("factory")
|
||||
|
||||
@property
|
||||
def factory_package_sources(self) -> Optional[PackageSources]:
|
||||
"""
|
||||
Return a ``PackageSources`` object that contains information about the current revision of the factory package sources in OBS SCM.
|
||||
"""
|
||||
return self._get_package_sources("factory")
|
||||
|
||||
def _get_devel_apiurl_project_package(self):
|
||||
if self.factory_package is None:
|
||||
return None, None, None
|
||||
|
||||
devel = self.factory_package.devel
|
||||
if devel is None:
|
||||
return None, None, None
|
||||
|
||||
return (
|
||||
self._devel_apiurl,
|
||||
devel.project,
|
||||
devel.package or self.factory_package.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def devel_package(self) -> Optional[Package]:
|
||||
"""
|
||||
Return a ``Package`` object that encapsulates metadata of the package in the devel project.
|
||||
The devel project name and package name come from ``self.factory_package.devel``.
|
||||
If the devel package name is not set, target package name is used.
|
||||
"""
|
||||
return self._get_package("devel")
|
||||
|
||||
@property
|
||||
def devel_package_sources(self) -> Optional[PackageSources]:
|
||||
"""
|
||||
Return a ``PackageSources`` object that contains information about the current revision of the devel package sources in OBS SCM.
|
||||
"""
|
||||
return self._get_package_sources("devel")
|
||||
|
||||
def get_actual_target_project_package(self) -> Tuple[str, str]:
|
||||
"""
|
||||
Return the target project and package names because maintenance incidents require special handling.
|
||||
|
||||
The target project for maintenance incidents is virtual and cannot be queried.
|
||||
The actual target project is specified in target's ``releaseproject`` field.
|
||||
|
||||
Also the target package for maintenance incidents is not set explicitly.
|
||||
It is extracted from ``releasename`` field from the source metadata.
|
||||
If ``releasename`` is not defined, source package name is used.
|
||||
"""
|
||||
|
||||
if self.type == "maintenance_incident":
|
||||
# dmach's note on security:
|
||||
# The ``releaseproject`` is baked into the target information in the request and that's perfectly ok.
|
||||
# The ``releasename`` is part of the source package metadata and *may change* after the request is created.
|
||||
# After consulting this with OBS developers, I believe this doesn't represent any security issue
|
||||
# because the project is fixed and tampering with ``releasename`` might only lead to inconsistent naming,
|
||||
# the package would still end up it the same project.
|
||||
|
||||
# target.releaseproject is always set for a maintenance_incident
|
||||
assert self.target
|
||||
assert self.target.releaseproject
|
||||
project = self.target.releaseproject
|
||||
|
||||
# the target package is not specified
|
||||
# we need to extract it from source package's metadata or use source package name as a fallback
|
||||
assert self.source_package
|
||||
if self.source_package.releasename:
|
||||
package = self.source_package.releasename.split(".")[0]
|
||||
else:
|
||||
package = self.source_package.name
|
||||
|
||||
return project, package
|
||||
|
||||
assert self.target
|
||||
assert self.target.project
|
||||
assert self.target.package
|
||||
return self.target.project, self.target.package
|
33
osc/obs_api/request_action_acceptinfo.py
Normal file
33
osc/obs_api/request_action_acceptinfo.py
Normal file
@ -0,0 +1,33 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestActionAcceptinfo(XmlModel):
|
||||
XML_TAG = "acceptinfo"
|
||||
|
||||
rev: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
srcmd5: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
osrcmd5: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
oproject: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
opackage: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
xsrcmd5: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
oxsrcmd5: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
13
osc/obs_api/request_action_group.py
Normal file
13
osc/obs_api/request_action_group.py
Normal file
@ -0,0 +1,13 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestActionGroup(XmlModel):
|
||||
XML_TAG = "group"
|
||||
|
||||
name: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
role: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
9
osc/obs_api/request_action_grouped.py
Normal file
9
osc/obs_api/request_action_grouped.py
Normal file
@ -0,0 +1,9 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestActionGrouped(XmlModel):
|
||||
XML_TAG = "grouped"
|
||||
|
||||
id: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
27
osc/obs_api/request_action_options.py
Normal file
27
osc/obs_api/request_action_options.py
Normal file
@ -0,0 +1,27 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestActionOptions(XmlModel):
|
||||
XML_TAG = "options"
|
||||
|
||||
class SourceupdateEnum(str, Enum):
|
||||
UPDATE = "update"
|
||||
NOUPDATE = "noupdate"
|
||||
CLEANUP = "cleanup"
|
||||
|
||||
sourceupdate: Optional[SourceupdateEnum] = Field(
|
||||
)
|
||||
|
||||
class UpdatelinkEnum(str, Enum):
|
||||
TRUE = "true"
|
||||
FALSE = "false"
|
||||
|
||||
updatelink: Optional[UpdatelinkEnum] = Field(
|
||||
)
|
||||
|
||||
class MakeoriginolderEnum(str, Enum):
|
||||
TRUE = "true"
|
||||
FALSE = "false"
|
||||
|
||||
makeoriginolder: Optional[MakeoriginolderEnum] = Field(
|
||||
)
|
13
osc/obs_api/request_action_person.py
Normal file
13
osc/obs_api/request_action_person.py
Normal file
@ -0,0 +1,13 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestActionPerson(XmlModel):
|
||||
XML_TAG = "person"
|
||||
|
||||
name: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
role: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
17
osc/obs_api/request_action_source.py
Normal file
17
osc/obs_api/request_action_source.py
Normal file
@ -0,0 +1,17 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestActionSource(XmlModel):
|
||||
XML_TAG = "source"
|
||||
|
||||
project: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
package: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
rev: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
21
osc/obs_api/request_action_target.py
Normal file
21
osc/obs_api/request_action_target.py
Normal file
@ -0,0 +1,21 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestActionTarget(XmlModel):
|
||||
XML_TAG = "target"
|
||||
|
||||
project: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
package: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
releaseproject: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
repository: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
19
osc/obs_api/request_history.py
Normal file
19
osc/obs_api/request_history.py
Normal file
@ -0,0 +1,19 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestHistory(XmlModel):
|
||||
XML_TAG = "history"
|
||||
|
||||
who: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
when: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
description: str = Field(
|
||||
)
|
||||
|
||||
comment: Optional[str] = Field(
|
||||
)
|
57
osc/obs_api/request_review.py
Normal file
57
osc/obs_api/request_review.py
Normal file
@ -0,0 +1,57 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from .enums import RequestStates
|
||||
from .request_review_history import RequestReviewHistory
|
||||
|
||||
|
||||
class RequestReview(XmlModel):
|
||||
XML_TAG = "review"
|
||||
|
||||
state: RequestStates = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
created: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
by_user: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
by_group: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
by_project: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
by_package: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
who: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
when: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
comment: Optional[str] = Field(
|
||||
)
|
||||
|
||||
history_list: Optional[List[RequestReviewHistory]] = Field(
|
||||
xml_name="history",
|
||||
)
|
||||
|
||||
def get_user_and_type(self):
|
||||
if self.by_user:
|
||||
return (self.by_user, "user")
|
||||
if self.by_group:
|
||||
return (self.by_group, "group")
|
||||
if self.by_package:
|
||||
return (f"{self.by_project}/{self.by_package}", "package")
|
||||
if self.by_project:
|
||||
return (self.by_project, "project")
|
||||
raise RuntimeError("Unable to determine user and its type")
|
19
osc/obs_api/request_review_history.py
Normal file
19
osc/obs_api/request_review_history.py
Normal file
@ -0,0 +1,19 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestReviewHistory(XmlModel):
|
||||
XML_TAG = "history"
|
||||
|
||||
who: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
when: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
description: str = Field(
|
||||
)
|
||||
|
||||
comment: Optional[str] = Field(
|
||||
)
|
30
osc/obs_api/request_sourcediff.py
Normal file
30
osc/obs_api/request_sourcediff.py
Normal file
@ -0,0 +1,30 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from .request_sourcediff_files_file import RequestSourcediffFilesFile
|
||||
from .request_sourcediff_issue import RequestSourcediffIssue
|
||||
from .request_sourcediff_new import RequestSourcediffNew
|
||||
from .request_sourcediff_old import RequestSourcediffOld
|
||||
|
||||
|
||||
class RequestSourcediff(XmlModel):
|
||||
XML_TAG = "sourcediff"
|
||||
|
||||
key: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
old: Optional[RequestSourcediffOld] = Field(
|
||||
)
|
||||
|
||||
new: Optional[RequestSourcediffNew] = Field(
|
||||
)
|
||||
|
||||
files_list: List[RequestSourcediffFilesFile] = Field(
|
||||
xml_name="files",
|
||||
xml_wrapped=True,
|
||||
)
|
||||
|
||||
issue_list: Optional[List[RequestSourcediffIssue]] = Field(
|
||||
xml_name="issues",
|
||||
xml_wrapped=True,
|
||||
xml_item_name="issue",
|
||||
)
|
13
osc/obs_api/request_sourcediff_file_diff.py
Normal file
13
osc/obs_api/request_sourcediff_file_diff.py
Normal file
@ -0,0 +1,13 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestSourcediffFileDiff(XmlModel):
|
||||
XML_TAG = "diff"
|
||||
|
||||
lines: int = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
text: str = Field(
|
||||
xml_set_text=True,
|
||||
)
|
17
osc/obs_api/request_sourcediff_file_new.py
Normal file
17
osc/obs_api/request_sourcediff_file_new.py
Normal file
@ -0,0 +1,17 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestSourcediffFileNew(XmlModel):
|
||||
XML_TAG = "new"
|
||||
|
||||
name: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
md5: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
size: int = Field(
|
||||
xml_attribute=True,
|
||||
)
|
17
osc/obs_api/request_sourcediff_file_old.py
Normal file
17
osc/obs_api/request_sourcediff_file_old.py
Normal file
@ -0,0 +1,17 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestSourcediffFileOld(XmlModel):
|
||||
XML_TAG = "old"
|
||||
|
||||
name: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
md5: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
size: int = Field(
|
||||
xml_attribute=True,
|
||||
)
|
21
osc/obs_api/request_sourcediff_files_file.py
Normal file
21
osc/obs_api/request_sourcediff_files_file.py
Normal file
@ -0,0 +1,21 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from .request_sourcediff_file_diff import RequestSourcediffFileDiff
|
||||
from .request_sourcediff_file_new import RequestSourcediffFileNew
|
||||
from .request_sourcediff_file_old import RequestSourcediffFileOld
|
||||
|
||||
|
||||
class RequestSourcediffFilesFile(XmlModel):
|
||||
XML_TAG = "file"
|
||||
|
||||
state: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
old: Optional[RequestSourcediffFileOld] = Field(
|
||||
)
|
||||
|
||||
new: Optional[RequestSourcediffFileNew] = Field(
|
||||
)
|
||||
|
||||
diff: RequestSourcediffFileDiff = Field(
|
||||
)
|
25
osc/obs_api/request_sourcediff_issue.py
Normal file
25
osc/obs_api/request_sourcediff_issue.py
Normal file
@ -0,0 +1,25 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestSourcediffIssue(XmlModel):
|
||||
XML_TAG = "issue"
|
||||
|
||||
state: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
tracker: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
name: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
label: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
url: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
21
osc/obs_api/request_sourcediff_new.py
Normal file
21
osc/obs_api/request_sourcediff_new.py
Normal file
@ -0,0 +1,21 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestSourcediffNew(XmlModel):
|
||||
XML_TAG = "new"
|
||||
|
||||
project: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
package: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
rev: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
srcmd5: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
21
osc/obs_api/request_sourcediff_old.py
Normal file
21
osc/obs_api/request_sourcediff_old.py
Normal file
@ -0,0 +1,21 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class RequestSourcediffOld(XmlModel):
|
||||
XML_TAG = "old"
|
||||
|
||||
project: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
package: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
rev: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
srcmd5: str = Field(
|
||||
xml_attribute=True,
|
||||
)
|
33
osc/obs_api/request_state.py
Normal file
33
osc/obs_api/request_state.py
Normal file
@ -0,0 +1,33 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from .enums import RequestStates
|
||||
|
||||
|
||||
class RequestState(XmlModel):
|
||||
XML_TAG = "state"
|
||||
|
||||
name: RequestStates = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
who: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
when: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
created: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
superseded_by: Optional[int] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
approver: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
comment: Optional[str] = Field(
|
||||
)
|
21
osc/obs_api/serviceinfo.py
Normal file
21
osc/obs_api/serviceinfo.py
Normal file
@ -0,0 +1,21 @@
|
||||
from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
class Serviceinfo(XmlModel):
|
||||
XML_TAG = "serviceinfo"
|
||||
|
||||
xsrcmd5: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
lsrcmd5: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
error: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
||||
|
||||
code: Optional[str] = Field(
|
||||
xml_attribute=True,
|
||||
)
|
@ -7,6 +7,7 @@ This module IS NOT a supported API, it is meant for osc internal use only.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
import tempfile
|
||||
@ -71,8 +72,9 @@ NotSet = NotSetClass()
|
||||
|
||||
|
||||
class FromParent(NotSetClass):
|
||||
def __init__(self, field_name):
|
||||
def __init__(self, field_name, *, fallback=NotSet):
|
||||
self.field_name = field_name
|
||||
self.fallback = fallback
|
||||
|
||||
def __repr__(self):
|
||||
return f"FromParent(field_name={self.field_name})"
|
||||
@ -256,7 +258,7 @@ class Field(property):
|
||||
for num, i in enumerate(result):
|
||||
if isinstance(i, dict):
|
||||
klass = self.inner_type
|
||||
result[num] = klass(**i)
|
||||
result[num] = klass(**i, _parent=obj)
|
||||
|
||||
if self.get_callback is not None:
|
||||
result = self.get_callback(obj, result)
|
||||
@ -279,7 +281,10 @@ class Field(property):
|
||||
|
||||
if isinstance(self.default, FromParent):
|
||||
if obj._parent is None:
|
||||
raise RuntimeError(f"The field '{self.name}' has default {self.default} but the model has no parent set")
|
||||
if self.default.fallback is not NotSet:
|
||||
return self.default.fallback
|
||||
else:
|
||||
raise RuntimeError(f"The field '{self.name}' has default {self.default} but the model has no parent set")
|
||||
return getattr(obj._parent, self.default.field_name or self.name)
|
||||
|
||||
if self.default is NotSet:
|
||||
@ -308,20 +313,23 @@ class Field(property):
|
||||
if self.is_model and isinstance(value, dict):
|
||||
# initialize a model instance from a dictionary
|
||||
klass = self.origin_type
|
||||
value = klass(**value) # pylint: disable=not-callable
|
||||
value = klass(**value, _parent=obj) # pylint: disable=not-callable
|
||||
elif self.is_model_list and isinstance(value, list):
|
||||
new_value = []
|
||||
for i in value:
|
||||
if isinstance(i, dict):
|
||||
klass = self.inner_type
|
||||
new_value.append(klass(**i))
|
||||
new_value.append(klass(**i, _parent=obj))
|
||||
else:
|
||||
i._parent = obj
|
||||
new_value.append(i)
|
||||
value = new_value
|
||||
elif self.is_model and isinstance(value, str) and hasattr(self.origin_type, "XML_TAG_FIELD"):
|
||||
klass = self.origin_type
|
||||
key = getattr(self.origin_type, "XML_TAG_FIELD")
|
||||
value = klass(**{key: value})
|
||||
value = klass(**{key: value}, _parent=obj)
|
||||
elif self.is_model and value is not None:
|
||||
value._parent = obj
|
||||
|
||||
self.validate_type(value)
|
||||
obj._values[self.name] = value
|
||||
@ -361,6 +369,7 @@ class ModelMeta(type):
|
||||
return new_cls
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class BaseModel(metaclass=ModelMeta):
|
||||
__fields__: Dict[str, Field]
|
||||
|
||||
@ -372,6 +381,7 @@ class BaseModel(metaclass=ModelMeta):
|
||||
raise AttributeError(f"Setting attribute '{self.__class__.__name__}.{name}' is not allowed")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._allow_new_attributes = True
|
||||
self._defaults = {} # field defaults cached in field.get()
|
||||
self._values = {} # field values explicitly set after initializing the model
|
||||
self._parent = kwargs.pop("_parent", None)
|
||||
@ -404,10 +414,28 @@ class BaseModel(metaclass=ModelMeta):
|
||||
|
||||
self._allow_new_attributes = False
|
||||
|
||||
def _get_cmp_data(self):
|
||||
result = []
|
||||
for name, field in self.__fields__.items():
|
||||
if field.exclude:
|
||||
continue
|
||||
value = getattr(self, name)
|
||||
if isinstance(value, dict):
|
||||
value = sorted(list(value.items()))
|
||||
result.append((name, value))
|
||||
return result
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(self) != type(other):
|
||||
return False
|
||||
return self.dict() == other.dict()
|
||||
if self._get_cmp_data() != other._get_cmp_data():
|
||||
print(self._get_cmp_data(), other._get_cmp_data())
|
||||
return self._get_cmp_data() == other._get_cmp_data()
|
||||
|
||||
def __lt__(self, other):
|
||||
if type(self) != type(other):
|
||||
return False
|
||||
return self._get_cmp_data() < other._get_cmp_data()
|
||||
|
||||
def dict(self):
|
||||
result = {}
|
||||
@ -440,6 +468,11 @@ class BaseModel(metaclass=ModelMeta):
|
||||
class XmlModel(BaseModel):
|
||||
XML_TAG = None
|
||||
|
||||
_apiurl: Optional[str] = Field(
|
||||
exclude=True,
|
||||
default=FromParent("_apiurl", fallback=None),
|
||||
)
|
||||
|
||||
def to_xml(self) -> ET.Element:
|
||||
xml_tag = None
|
||||
|
||||
@ -459,6 +492,8 @@ class XmlModel(BaseModel):
|
||||
root = ET.Element(xml_tag)
|
||||
|
||||
for field_name, field in self.__fields__.items():
|
||||
if field.exclude:
|
||||
continue
|
||||
xml_attribute = field.extra.get("xml_attribute", False)
|
||||
xml_set_tag = field.extra.get("xml_set_tag", False)
|
||||
xml_set_text = field.extra.get("xml_set_text", False)
|
||||
@ -510,20 +545,20 @@ class XmlModel(BaseModel):
|
||||
return root
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string: str) -> "XmlModel":
|
||||
def from_string(cls, string: str, *, apiurl: Optional[str] = None) -> "XmlModel":
|
||||
"""
|
||||
Instantiate model from string.
|
||||
"""
|
||||
root = ET.fromstring(string)
|
||||
return cls.from_xml(root)
|
||||
return cls.from_xml(root, apiurl=apiurl)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, file: Union[str, typing.IO]) -> "XmlModel":
|
||||
def from_file(cls, file: Union[str, typing.IO], *, apiurl: Optional[str] = None) -> "XmlModel":
|
||||
"""
|
||||
Instantiate model from file.
|
||||
"""
|
||||
root = ET.parse(file).getroot()
|
||||
return cls.from_xml(root)
|
||||
return cls.from_xml(root, apiurl=apiurl)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""
|
||||
@ -581,7 +616,7 @@ class XmlModel(BaseModel):
|
||||
parent.remove(node)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, root: ET.Element):
|
||||
def from_xml(cls, root: ET.Element, *, apiurl: Optional[str] = None):
|
||||
"""
|
||||
Instantiate model from a XML root.
|
||||
"""
|
||||
@ -661,7 +696,7 @@ class XmlModel(BaseModel):
|
||||
for node in nodes:
|
||||
if field.is_model_list:
|
||||
klass = field.inner_type
|
||||
entry = klass.from_xml(node)
|
||||
entry = klass.from_xml(node, apiurl=apiurl)
|
||||
|
||||
# clear node as it was checked in from_xml() already
|
||||
node.text = None
|
||||
@ -691,7 +726,7 @@ class XmlModel(BaseModel):
|
||||
if node is None:
|
||||
continue
|
||||
klass = field.origin_type
|
||||
kwargs[field_name] = klass.from_xml(node)
|
||||
kwargs[field_name] = klass.from_xml(node, apiurl=apiurl)
|
||||
|
||||
# clear node as it was checked in from_xml() already
|
||||
node.text = None
|
||||
@ -707,16 +742,16 @@ class XmlModel(BaseModel):
|
||||
continue
|
||||
value = cls.value_from_string(field, node.text)
|
||||
node.text = None
|
||||
cls._remove_processed_node(root, node)
|
||||
if value is None:
|
||||
if field.is_optional:
|
||||
continue
|
||||
value = ""
|
||||
kwargs[field_name] = value
|
||||
cls._remove_processed_node(root, node)
|
||||
|
||||
cls._remove_processed_node(None, root)
|
||||
|
||||
obj = cls(**kwargs)
|
||||
obj = cls(**kwargs, _apiurl=apiurl)
|
||||
obj.__dict__["_root"] = orig_root
|
||||
return obj
|
||||
|
||||
@ -760,7 +795,7 @@ class XmlModel(BaseModel):
|
||||
while True:
|
||||
run_editor(f.name)
|
||||
try:
|
||||
edited_obj = self.__class__.from_file(f.name)
|
||||
edited_obj = self.__class__.from_file(f.name, apiurl=self._apiurl)
|
||||
f.seek(0)
|
||||
edited_data = f.read()
|
||||
break
|
||||
|
@ -291,6 +291,49 @@ class Test(unittest.TestCase):
|
||||
self.assertEqual(c.field, "new-text")
|
||||
self.assertEqual(c.field2, "text")
|
||||
|
||||
def test_parent_fallback(self):
|
||||
class SubModel(BaseModel):
|
||||
field: str = Field(default=FromParent("field", fallback="submodel-fallback"))
|
||||
|
||||
class Model(BaseModel):
|
||||
field: str = Field(default=FromParent("field", fallback="model-fallback"))
|
||||
sub: Optional[SubModel] = Field()
|
||||
sub_list: Optional[List[SubModel]] = Field()
|
||||
|
||||
m = Model()
|
||||
s = SubModel(_parent=m)
|
||||
m.sub = s
|
||||
self.assertEqual(m.field, "model-fallback")
|
||||
self.assertEqual(m.sub.field, "model-fallback")
|
||||
|
||||
m = Model(sub={})
|
||||
self.assertEqual(m.field, "model-fallback")
|
||||
self.assertEqual(m.sub.field, "model-fallback")
|
||||
|
||||
m = Model(sub=SubModel())
|
||||
self.assertEqual(m.field, "model-fallback")
|
||||
self.assertEqual(m.sub.field, "model-fallback")
|
||||
|
||||
m = Model()
|
||||
s = SubModel(_parent=m)
|
||||
m.sub_list = [s]
|
||||
self.assertEqual(m.field, "model-fallback")
|
||||
self.assertEqual(m.sub_list[0].field, "model-fallback")
|
||||
|
||||
m = Model(sub_list=[{}])
|
||||
self.assertEqual(m.field, "model-fallback")
|
||||
self.assertEqual(m.sub_list[0].field, "model-fallback")
|
||||
|
||||
m = Model(sub_list=[SubModel()])
|
||||
self.assertEqual(m.field, "model-fallback")
|
||||
self.assertEqual(m.sub_list[0].field, "model-fallback")
|
||||
|
||||
m = Model()
|
||||
m.sub_list = []
|
||||
m.sub_list.append({})
|
||||
self.assertEqual(m.field, "model-fallback")
|
||||
self.assertEqual(m.sub_list[0].field, "model-fallback")
|
||||
|
||||
def test_get_callback(self):
|
||||
class Model(BaseModel):
|
||||
quiet: bool = Field(
|
||||
@ -352,6 +395,57 @@ class Test(unittest.TestCase):
|
||||
self.assertIsInstance(m.field[0], BaseModel)
|
||||
self.assertEqual(m.field[0].text, "value")
|
||||
|
||||
def test_ordering(self):
|
||||
class TestSubmodel(BaseModel):
|
||||
txt: Optional[str] = Field()
|
||||
|
||||
class TestModel(BaseModel):
|
||||
num: Optional[int] = Field()
|
||||
txt: Optional[str] = Field()
|
||||
sub: Optional[TestSubmodel] = Field()
|
||||
dct: Optional[Dict[str, TestSubmodel]] = Field()
|
||||
|
||||
m1 = TestModel()
|
||||
m2 = TestModel()
|
||||
self.assertEqual(m1, m2)
|
||||
|
||||
m1 = TestModel(num=1)
|
||||
m2 = TestModel(num=2)
|
||||
self.assertNotEqual(m1, m2)
|
||||
self.assertLess(m1, m2)
|
||||
self.assertGreater(m2, m1)
|
||||
|
||||
m1 = TestModel(txt="a")
|
||||
m2 = TestModel(txt="b")
|
||||
self.assertNotEqual(m1, m2)
|
||||
self.assertLess(m1, m2)
|
||||
self.assertGreater(m2, m1)
|
||||
|
||||
m1 = TestModel(sub={})
|
||||
m2 = TestModel(sub={})
|
||||
self.assertEqual(m1, m2)
|
||||
|
||||
m1 = TestModel(sub={"txt": "a"})
|
||||
m2 = TestModel(sub={"txt": "b"})
|
||||
self.assertNotEqual(m1, m2)
|
||||
self.assertLess(m1, m2)
|
||||
self.assertGreater(m2, m1)
|
||||
|
||||
m1 = TestModel(dct={})
|
||||
m2 = TestModel(dct={})
|
||||
self.assertEqual(m1, m2)
|
||||
|
||||
m1 = TestModel(dct={"a": TestSubmodel()})
|
||||
m2 = TestModel(dct={"b": TestSubmodel()})
|
||||
self.assertNotEqual(m1, m2)
|
||||
self.assertLess(m1, m2)
|
||||
self.assertGreater(m2, m1)
|
||||
|
||||
# dict ordering doesn't matter
|
||||
m1 = TestModel(dct={"a": TestSubmodel(), "b": TestSubmodel()})
|
||||
m2 = TestModel(dct={"b": TestSubmodel(), "a": TestSubmodel()})
|
||||
self.assertEqual(m1, m2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
@ -1,3 +1,4 @@
|
||||
import io
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
@ -149,6 +150,41 @@ class TestXmlModel(unittest.TestCase):
|
||||
m = ParentModel.from_string(expected)
|
||||
self.assertEqual(m.to_string(), expected)
|
||||
|
||||
def test_apiurl(self):
|
||||
class ChildModel(XmlModel):
|
||||
XML_TAG = "child"
|
||||
value: str = Field()
|
||||
|
||||
class ParentModel(XmlModel):
|
||||
XML_TAG = "parent"
|
||||
text: str = Field()
|
||||
child: List[ChildModel] = Field(xml_wrapped=True, xml_name="children")
|
||||
|
||||
# serialize the model and load it with apiurl set
|
||||
m = ParentModel(text="TEXT", child=[{"value": "FOO"}, {"value": "BAR"}])
|
||||
xml = m.to_string()
|
||||
|
||||
apiurl = "https://api.example.com"
|
||||
|
||||
m = ParentModel.from_string(xml, apiurl=apiurl)
|
||||
m.child.append({"value": "BAZ"})
|
||||
|
||||
self.assertEqual(m._apiurl, apiurl)
|
||||
self.assertEqual(m.child[0]._apiurl, apiurl)
|
||||
self.assertEqual(m.child[1]._apiurl, apiurl)
|
||||
self.assertEqual(m.child[2]._apiurl, apiurl)
|
||||
|
||||
# test the same as above but with a file
|
||||
f = io.StringIO(xml)
|
||||
|
||||
m = ParentModel.from_file(f, apiurl=apiurl)
|
||||
m.child.append({"value": "BAZ"})
|
||||
|
||||
self.assertEqual(m._apiurl, apiurl)
|
||||
self.assertEqual(m.child[0]._apiurl, apiurl)
|
||||
self.assertEqual(m.child[1]._apiurl, apiurl)
|
||||
self.assertEqual(m.child[2]._apiurl, apiurl)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
Loading…
Reference in New Issue
Block a user