mirror of
https://github.com/openSUSE/osc.git
synced 2025-03-01 13:42:12 +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,
|
src_package: Optional[str] = None,
|
||||||
dst_project: Optional[str] = None,
|
dst_project: Optional[str] = None,
|
||||||
dst_package: Optional[str] = None,
|
dst_package: Optional[str] = None,
|
||||||
message="",
|
message: str = "",
|
||||||
orev=None,
|
orev: Optional[str] = None,
|
||||||
src_update=None,
|
src_update: Optional[str] = None,
|
||||||
dst_updatelink=None,
|
dst_updatelink: Optional[bool] = None,
|
||||||
):
|
):
|
||||||
options_block = ""
|
from . import obs_api
|
||||||
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>"
|
|
||||||
|
|
||||||
# Yes, this kind of xml construction is horrible
|
req = obs_api.Request(
|
||||||
targetxml = ""
|
action_list=[
|
||||||
if dst_project:
|
{
|
||||||
packagexml = ""
|
"type": "submit",
|
||||||
if dst_package:
|
"source": {
|
||||||
packagexml = f"""package="{dst_package}" """
|
"project": src_project,
|
||||||
targetxml = f"""<target project="{dst_project}" {packagexml} /> """
|
"package": src_package,
|
||||||
# XXX: keep the old template for now in order to work with old obs instances
|
"rev": orev or show_upstream_rev(apiurl, src_project, src_package),
|
||||||
xml = """\
|
},
|
||||||
<request>
|
"target": {
|
||||||
<action type="submit">
|
"project": dst_project,
|
||||||
<source project="%s" %s rev="%s"/>
|
"package": dst_package,
|
||||||
%s
|
},
|
||||||
%s
|
"options": {
|
||||||
</action>
|
"sourceupdate": src_update,
|
||||||
<state name="new"/>
|
"updatelink": "true" if dst_updatelink else None,
|
||||||
<description>%s</description>
|
}
|
||||||
</request>
|
},
|
||||||
""" % (src_project,
|
],
|
||||||
package,
|
description=message,
|
||||||
orev or show_upstream_rev(apiurl, src_project, src_package),
|
)
|
||||||
targetxml,
|
|
||||||
options_block,
|
|
||||||
_html_escape(message))
|
|
||||||
|
|
||||||
u = makeurl(apiurl, ["request"], query={"cmd": "create"})
|
|
||||||
r = None
|
|
||||||
try:
|
try:
|
||||||
f = http_POST(u, data=xml)
|
new_req = req.cmd_create(apiurl)
|
||||||
root = ET.parse(f).getroot()
|
|
||||||
r = root.get('id')
|
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
if e.hdrs.get('X-Opensuse-Errorcode') == "submit_request_rejected":
|
if e.hdrs.get('X-Opensuse-Errorcode') == "submit_request_rejected":
|
||||||
print('WARNING: As the project is in maintenance, a maintenance incident request is')
|
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.")
|
raise oscerr.APIError("Server did not define a default maintenance project, can't submit.")
|
||||||
tproject = project.get('name')
|
tproject = project.get('name')
|
||||||
r = create_maintenance_request(apiurl, src_project, [src_package], tproject, dst_project, src_update, message, rev=orev)
|
r = create_maintenance_request(apiurl, src_project, [src_package], tproject, dst_project, src_update, message, rev=orev)
|
||||||
r = r.reqid
|
return r.reqid
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return r
|
return new_req.id
|
||||||
|
|
||||||
|
|
||||||
def get_request(apiurl: str, reqid):
|
def get_request(apiurl: str, reqid):
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
from .package import Package
|
from .package import Package
|
||||||
|
from .package_sources import PackageSources
|
||||||
from .project import Project
|
from .project import Project
|
||||||
|
from .request import Request
|
||||||
|
@ -67,6 +67,13 @@ class LocalRole(str, Enum):
|
|||||||
READER = "reader"
|
READER = "reader"
|
||||||
|
|
||||||
|
|
||||||
|
class ObsRatings(str, Enum):
|
||||||
|
LOW = "low"
|
||||||
|
MODERATE = "moderate"
|
||||||
|
IMPORTANT = "important"
|
||||||
|
CRITICAL = "critical"
|
||||||
|
|
||||||
|
|
||||||
class RebuildModes(str, Enum):
|
class RebuildModes(str, Enum):
|
||||||
TRANSITIVE = "transitive"
|
TRANSITIVE = "transitive"
|
||||||
DIRECT = "direct"
|
DIRECT = "direct"
|
||||||
@ -77,3 +84,13 @@ class ReleaseTriggers(str, Enum):
|
|||||||
MANUAL = "manual"
|
MANUAL = "manual"
|
||||||
MAINTENANCE = "maintenance"
|
MAINTENANCE = "maintenance"
|
||||||
OBSGENDIFF = "obsgendiff"
|
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
|
@classmethod
|
||||||
def from_api(cls, apiurl, project, package, *, rev=None):
|
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_path = ["source", project, package, "_meta"]
|
||||||
url_query = {
|
url_query = {
|
||||||
"rev": rev,
|
"rev": rev,
|
||||||
}
|
}
|
||||||
response = cls.xml_request("GET", apiurl, url_path, 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, package=None):
|
def to_api(self, apiurl, *, project=None, package=None):
|
||||||
project = project or self.project
|
project = project or self.project
|
||||||
@ -82,7 +83,7 @@ class Package(XmlModel):
|
|||||||
url_path = ["source", project, package, "_meta"]
|
url_path = ["source", project, package, "_meta"]
|
||||||
url_query = {}
|
url_query = {}
|
||||||
response = self.xml_request("PUT", apiurl, url_path, url_query, data=self.to_string())
|
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
|
@classmethod
|
||||||
def cmd_release(
|
def cmd_release(
|
||||||
@ -124,4 +125,4 @@ class Package(XmlModel):
|
|||||||
"nodelay": nodelay,
|
"nodelay": nodelay,
|
||||||
}
|
}
|
||||||
response = cls.xml_request("POST", apiurl, url_path, url_query)
|
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_path = ["source", project, "_meta"]
|
||||||
url_query = {}
|
url_query = {}
|
||||||
response = cls.xml_request("GET", apiurl, url_path, 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):
|
def to_api(self, apiurl, *, project=None):
|
||||||
project = project or self.name
|
project = project or self.name
|
||||||
url_path = ["source", project, "_meta"]
|
url_path = ["source", project, "_meta"]
|
||||||
url_query = {}
|
url_query = {}
|
||||||
response = self.xml_request("PUT", apiurl, url_path, url_query, data=self.to_string())
|
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):
|
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 copy
|
||||||
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -71,8 +72,9 @@ NotSet = NotSetClass()
|
|||||||
|
|
||||||
|
|
||||||
class FromParent(NotSetClass):
|
class FromParent(NotSetClass):
|
||||||
def __init__(self, field_name):
|
def __init__(self, field_name, *, fallback=NotSet):
|
||||||
self.field_name = field_name
|
self.field_name = field_name
|
||||||
|
self.fallback = fallback
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"FromParent(field_name={self.field_name})"
|
return f"FromParent(field_name={self.field_name})"
|
||||||
@ -256,7 +258,7 @@ class Field(property):
|
|||||||
for num, i in enumerate(result):
|
for num, i in enumerate(result):
|
||||||
if isinstance(i, dict):
|
if isinstance(i, dict):
|
||||||
klass = self.inner_type
|
klass = self.inner_type
|
||||||
result[num] = klass(**i)
|
result[num] = klass(**i, _parent=obj)
|
||||||
|
|
||||||
if self.get_callback is not None:
|
if self.get_callback is not None:
|
||||||
result = self.get_callback(obj, result)
|
result = self.get_callback(obj, result)
|
||||||
@ -279,7 +281,10 @@ class Field(property):
|
|||||||
|
|
||||||
if isinstance(self.default, FromParent):
|
if isinstance(self.default, FromParent):
|
||||||
if obj._parent is None:
|
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)
|
return getattr(obj._parent, self.default.field_name or self.name)
|
||||||
|
|
||||||
if self.default is NotSet:
|
if self.default is NotSet:
|
||||||
@ -308,20 +313,23 @@ class Field(property):
|
|||||||
if self.is_model and isinstance(value, dict):
|
if self.is_model and isinstance(value, dict):
|
||||||
# initialize a model instance from a dictionary
|
# initialize a model instance from a dictionary
|
||||||
klass = self.origin_type
|
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):
|
elif self.is_model_list and isinstance(value, list):
|
||||||
new_value = []
|
new_value = []
|
||||||
for i in value:
|
for i in value:
|
||||||
if isinstance(i, dict):
|
if isinstance(i, dict):
|
||||||
klass = self.inner_type
|
klass = self.inner_type
|
||||||
new_value.append(klass(**i))
|
new_value.append(klass(**i, _parent=obj))
|
||||||
else:
|
else:
|
||||||
|
i._parent = obj
|
||||||
new_value.append(i)
|
new_value.append(i)
|
||||||
value = new_value
|
value = new_value
|
||||||
elif self.is_model and isinstance(value, str) and hasattr(self.origin_type, "XML_TAG_FIELD"):
|
elif self.is_model and isinstance(value, str) and hasattr(self.origin_type, "XML_TAG_FIELD"):
|
||||||
klass = self.origin_type
|
klass = self.origin_type
|
||||||
key = getattr(self.origin_type, "XML_TAG_FIELD")
|
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)
|
self.validate_type(value)
|
||||||
obj._values[self.name] = value
|
obj._values[self.name] = value
|
||||||
@ -361,6 +369,7 @@ class ModelMeta(type):
|
|||||||
return new_cls
|
return new_cls
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
class BaseModel(metaclass=ModelMeta):
|
class BaseModel(metaclass=ModelMeta):
|
||||||
__fields__: Dict[str, Field]
|
__fields__: Dict[str, Field]
|
||||||
|
|
||||||
@ -372,6 +381,7 @@ class BaseModel(metaclass=ModelMeta):
|
|||||||
raise AttributeError(f"Setting attribute '{self.__class__.__name__}.{name}' is not allowed")
|
raise AttributeError(f"Setting attribute '{self.__class__.__name__}.{name}' is not allowed")
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self._allow_new_attributes = True
|
||||||
self._defaults = {} # field defaults cached in field.get()
|
self._defaults = {} # field defaults cached in field.get()
|
||||||
self._values = {} # field values explicitly set after initializing the model
|
self._values = {} # field values explicitly set after initializing the model
|
||||||
self._parent = kwargs.pop("_parent", None)
|
self._parent = kwargs.pop("_parent", None)
|
||||||
@ -404,10 +414,28 @@ class BaseModel(metaclass=ModelMeta):
|
|||||||
|
|
||||||
self._allow_new_attributes = False
|
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):
|
def __eq__(self, other):
|
||||||
if type(self) != type(other):
|
if type(self) != type(other):
|
||||||
return False
|
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):
|
def dict(self):
|
||||||
result = {}
|
result = {}
|
||||||
@ -440,6 +468,11 @@ class BaseModel(metaclass=ModelMeta):
|
|||||||
class XmlModel(BaseModel):
|
class XmlModel(BaseModel):
|
||||||
XML_TAG = None
|
XML_TAG = None
|
||||||
|
|
||||||
|
_apiurl: Optional[str] = Field(
|
||||||
|
exclude=True,
|
||||||
|
default=FromParent("_apiurl", fallback=None),
|
||||||
|
)
|
||||||
|
|
||||||
def to_xml(self) -> ET.Element:
|
def to_xml(self) -> ET.Element:
|
||||||
xml_tag = None
|
xml_tag = None
|
||||||
|
|
||||||
@ -459,6 +492,8 @@ class XmlModel(BaseModel):
|
|||||||
root = ET.Element(xml_tag)
|
root = ET.Element(xml_tag)
|
||||||
|
|
||||||
for field_name, field in self.__fields__.items():
|
for field_name, field in self.__fields__.items():
|
||||||
|
if field.exclude:
|
||||||
|
continue
|
||||||
xml_attribute = field.extra.get("xml_attribute", False)
|
xml_attribute = field.extra.get("xml_attribute", False)
|
||||||
xml_set_tag = field.extra.get("xml_set_tag", False)
|
xml_set_tag = field.extra.get("xml_set_tag", False)
|
||||||
xml_set_text = field.extra.get("xml_set_text", False)
|
xml_set_text = field.extra.get("xml_set_text", False)
|
||||||
@ -510,20 +545,20 @@ class XmlModel(BaseModel):
|
|||||||
return root
|
return root
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_string(cls, string: str) -> "XmlModel":
|
def from_string(cls, string: str, *, apiurl: Optional[str] = None) -> "XmlModel":
|
||||||
"""
|
"""
|
||||||
Instantiate model from string.
|
Instantiate model from string.
|
||||||
"""
|
"""
|
||||||
root = ET.fromstring(string)
|
root = ET.fromstring(string)
|
||||||
return cls.from_xml(root)
|
return cls.from_xml(root, apiurl=apiurl)
|
||||||
|
|
||||||
@classmethod
|
@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.
|
Instantiate model from file.
|
||||||
"""
|
"""
|
||||||
root = ET.parse(file).getroot()
|
root = ET.parse(file).getroot()
|
||||||
return cls.from_xml(root)
|
return cls.from_xml(root, apiurl=apiurl)
|
||||||
|
|
||||||
def to_bytes(self) -> bytes:
|
def to_bytes(self) -> bytes:
|
||||||
"""
|
"""
|
||||||
@ -581,7 +616,7 @@ class XmlModel(BaseModel):
|
|||||||
parent.remove(node)
|
parent.remove(node)
|
||||||
|
|
||||||
@classmethod
|
@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.
|
Instantiate model from a XML root.
|
||||||
"""
|
"""
|
||||||
@ -661,7 +696,7 @@ class XmlModel(BaseModel):
|
|||||||
for node in nodes:
|
for node in nodes:
|
||||||
if field.is_model_list:
|
if field.is_model_list:
|
||||||
klass = field.inner_type
|
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
|
# clear node as it was checked in from_xml() already
|
||||||
node.text = None
|
node.text = None
|
||||||
@ -691,7 +726,7 @@ class XmlModel(BaseModel):
|
|||||||
if node is None:
|
if node is None:
|
||||||
continue
|
continue
|
||||||
klass = field.origin_type
|
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
|
# clear node as it was checked in from_xml() already
|
||||||
node.text = None
|
node.text = None
|
||||||
@ -707,16 +742,16 @@ class XmlModel(BaseModel):
|
|||||||
continue
|
continue
|
||||||
value = cls.value_from_string(field, node.text)
|
value = cls.value_from_string(field, node.text)
|
||||||
node.text = None
|
node.text = None
|
||||||
|
cls._remove_processed_node(root, node)
|
||||||
if value is None:
|
if value is None:
|
||||||
if field.is_optional:
|
if field.is_optional:
|
||||||
continue
|
continue
|
||||||
value = ""
|
value = ""
|
||||||
kwargs[field_name] = value
|
kwargs[field_name] = value
|
||||||
cls._remove_processed_node(root, node)
|
|
||||||
|
|
||||||
cls._remove_processed_node(None, root)
|
cls._remove_processed_node(None, root)
|
||||||
|
|
||||||
obj = cls(**kwargs)
|
obj = cls(**kwargs, _apiurl=apiurl)
|
||||||
obj.__dict__["_root"] = orig_root
|
obj.__dict__["_root"] = orig_root
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@ -760,7 +795,7 @@ class XmlModel(BaseModel):
|
|||||||
while True:
|
while True:
|
||||||
run_editor(f.name)
|
run_editor(f.name)
|
||||||
try:
|
try:
|
||||||
edited_obj = self.__class__.from_file(f.name)
|
edited_obj = self.__class__.from_file(f.name, apiurl=self._apiurl)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
edited_data = f.read()
|
edited_data = f.read()
|
||||||
break
|
break
|
||||||
|
@ -291,6 +291,49 @@ class Test(unittest.TestCase):
|
|||||||
self.assertEqual(c.field, "new-text")
|
self.assertEqual(c.field, "new-text")
|
||||||
self.assertEqual(c.field2, "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):
|
def test_get_callback(self):
|
||||||
class Model(BaseModel):
|
class Model(BaseModel):
|
||||||
quiet: bool = Field(
|
quiet: bool = Field(
|
||||||
@ -352,6 +395,57 @@ class Test(unittest.TestCase):
|
|||||||
self.assertIsInstance(m.field[0], BaseModel)
|
self.assertIsInstance(m.field[0], BaseModel)
|
||||||
self.assertEqual(m.field[0].text, "value")
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import io
|
||||||
import textwrap
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@ -149,6 +150,41 @@ class TestXmlModel(unittest.TestCase):
|
|||||||
m = ParentModel.from_string(expected)
|
m = ParentModel.from_string(expected)
|
||||||
self.assertEqual(m.to_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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user