Skip to content
Snippets Groups Projects
Commit 287a9a04 authored by Quazgar's avatar Quazgar
Browse files

Merge branch 'f-version-history' into 'dev'

F version history

See merge request caosdb/caosdb-pylib!38
parents 1ce56524 7b3cd8f6
No related branches found
No related tags found
No related merge requests found
......@@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ###
* Versioning support (experimental).
* Versioning support (experimental). The version db.Version class can
represents particular entity versions and also the complete history of an
entity.
### Changed ###
......
......@@ -42,11 +42,19 @@ class Version():
id : str, optional
See attribute `id`. Default: None
date : str, optional
See attribute `data`. Default: None
See attribute `date`. Default: None
username : str, optional
See attribute `username`. Default: None
realm : str, optional
See attribute `realm`. Default: None
predecessors : list of Version, optional
See attribute `predecessors`. Default: empty list.
successors : list of Version, optional
See attribute `successors`. Default: empty list.
is_head : bool
See attribute `is_head`. Default: False
is_complete_history : bool
See attribute `is_complete_history`. Default: False
Attributes
----------
......@@ -55,6 +63,10 @@ class Version():
date : str
UTC Timestamp of the version, i.e. the date and time when the entity of
this version has been inserted or modified.
username : str
The username of the user who inserted or updated this version.
realm : str
The realm of the user who inserted or updated this version.
predecessors : list of Version
Predecessors are the older entity versions which have been modified
into this version. Usually, there is only one predecessor. However,
......@@ -66,14 +78,64 @@ class Version():
is only one successor. However, this API allows that a single entity
may co-exist in several versions (e.g. several proposals for the next
entity status). That would result in more than one successor.
is_head : bool or string
If true, this indicates that this version is the HEAD if true.
Otherwise it is not known whether this is the head or not. Any string
matching "true" (case-insensitively) is regarded as True.
Nota bene: This property should typically be set if the server response
indicated that this is the head version.
is_complete_history : bool or string
If true, this indicates that this version contains the full version
history. That means, that the predecessors and successors have their
respective predecessors and successors attached as well and the tree is
completely available. Any string matching "true" (case-insensitively)
is regarded as True.
Nota bene: This property should typically be set if the server response
indicated that the full version history is included in its response.
"""
# pylint: disable=redefined-builtin
def __init__(self, id=None, date=None, predecessors=None, successors=None):
def __init__(self, id=None, date=None, username=None, realm=None,
predecessors=None, successors=None, is_head=False,
is_complete_history=False):
"""Typically the `predecessors` or `successors` should not "link back" to an existing Version
object."""
self.id = id
self.date = date
self.username = username
self.realm = realm
self.predecessors = predecessors if predecessors is not None else []
self.successors = successors if successors is not None else []
self.is_head = str(is_head).lower() == "true"
self.is_complete_history = str(is_complete_history).lower() == "true"
def get_history(self):
""" Returns a flat list of Version instances representing the history
of the entity.
The list items are ordered by the relation between the versions,
starting with the oldest version.
The items in the list have no predecessors or successors attached.
Note: This method only returns reliable results if
`self.is_complete_history is True` and it will not retrieve the full
version history if it is not present.
Returns
-------
list of Version
"""
versions = []
for p in self.predecessors:
# assuming that predecessors don't have any successors
versions = p.get_history()
versions.append(Version(id=self.id, date=self.date,
username=self.username, realm=self.realm))
for s in self.successors:
# assuming that successors don't have any predecessors
versions.extend(s.get_history())
return versions
def to_xml(self, tag="Version"):
"""Serialize this version to xml.
......@@ -99,9 +161,15 @@ class Version():
xml.set("id", self.id)
if self.date is not None:
xml.set("date", self.date)
if self.username is not None:
xml.set("username", self.username)
if self.realm is not None:
xml.set("realm", self.realm)
if self.predecessors is not None:
for p in self.predecessors:
xml.append(p.to_xml(tag="Predecessor"))
if self.is_head is True:
xml.set("head", "true")
if self.successors is not None:
for s in self.successors:
xml.append(s.to_xml(tag="Successor"))
......@@ -122,8 +190,9 @@ class Version():
Parameters
----------
xml : etree.Element
A 'Version' xml element, with 'id' and 'date' attributes and
'Predecessor' and 'Successor' child elements.
A 'Version' xml element, with 'id', possibly 'date', `username`,
`realm`, and `head` attributes as well as 'Predecessor' and
'Successor' child elements.
Returns
-------
......@@ -133,6 +202,9 @@ class Version():
predecessors = [Version.from_xml(p) for p in xml if p.tag.lower() == "predecessor"]
successors = [Version.from_xml(s) for s in xml if s.tag.lower() == "successor"]
return Version(id=xml.get("id"), date=xml.get("date"),
is_head=xml.get("head"),
is_complete_history=xml.get("completeHistory"),
username=xml.get("username"), realm=xml.get("realm"),
predecessors=predecessors, successors=successors)
def __hash__(self):
......
......@@ -27,16 +27,21 @@ from caosdb import Record
from caosdb.common.utils import xml2str
from caosdb.common.versioning import Version
from .test_property import testrecord
from lxml import etree
def test_constructor():
v = Version(id="1234abcd", date="2020-01-01T20:15:00.000UTC",
username="testuser", realm="CaosDB", is_head=True,
predecessors=[Version(id="2345abdc",
date="2020-01-01T20:00:00.000UTC")],
successors=[Version(id="3465abdc",
date="2020-01-01T20:30:00.000UTC")])
assert v.id == "1234abcd"
assert v.date == "2020-01-01T20:15:00.000UTC"
assert v.username == "testuser"
assert v.realm == "CaosDB"
assert v.is_head is True
assert isinstance(v.predecessors, list)
assert isinstance(v.predecessors[0], Version)
assert isinstance(v.successors, list)
......@@ -48,21 +53,27 @@ def test_constructor():
def test_to_xml():
v = test_constructor()
xmlstr = xml2str(v.to_xml())
assert xmlstr == ('<Version id="{i}" date="{d}">\n'
assert xmlstr == ('<Version id="{i}" date="{d}" username="{u}" realm="{r}"'
' head="{h}">\n'
' <Predecessor id="{pi}" date="{pd}"/>\n'
' <Successor id="{si}" date="{sd}"/>\n'
'</Version>\n').format(i=v.id, d=v.date,
u=v.username, r=v.realm,
h=str(v.is_head).lower(),
pi=v.predecessors[0].id,
pd=v.predecessors[0].date,
si=v.successors[0].id,
sd=v.successors[0].date)
xmlstr2 = xml2str(v.to_xml(tag="OtherVersionTag"))
assert xmlstr2 == ('<OtherVersionTag id="{i}" date="{d}">\n'
assert xmlstr2 == ('<OtherVersionTag id="{i}" date="{d}" username="{u}" '
'realm="{r}" head="{h}">\n'
' <Predecessor id="{pi}" date="{pd}"/>\n'
' <Successor id="{si}" date="{sd}"/>\n'
'</OtherVersionTag>\n'
).format(i=v.id, d=v.date, pi=v.predecessors[0].id,
).format(i=v.id, d=v.date, u=v.username, r=v.realm,
h=str(v.is_head).lower(),
pi=v.predecessors[0].id,
pd=v.predecessors[0].date,
si=v.successors[0].id, sd=v.successors[0].date)
......@@ -142,3 +153,30 @@ def test_version_serialization():
# <Record><Version id="test-version" date="asdfsadf"/></Record>
assert "test-version" == r.to_xml().xpath("/Record/Version/@id")[0]
assert "asdfsadf" == r.to_xml().xpath("/Record/Version/@date")[0]
def test_get_history():
xml_str = """
<Version id="vid6" username="user1" realm="Realm1" date="date6" completeHistory="true">
<Predecessor id="vid5" username="user1" realm="Realm1" date="date5">
<Predecessor id="vid4" username="user1" realm="Realm1" date="date4">
<Predecessor id="vid3" username="user1" realm="Realm1" date="date3">
<Predecessor id="vid2" username="user1" realm="Realm1" date="date2">
<Predecessor id="vid1" username="user1" realm="Realm1" date="date1" />
</Predecessor>
</Predecessor>
</Predecessor>
</Predecessor>
<Successor id="vid7" username="user1" realm="Realm1" date="date7">
<Successor id="vid8" username="user1" realm="Realm1" date="date8">
<Successor id="vid9" username="user1" realm="Realm1" date="date9">
<Successor id="vid10" username="user1" realm="Realm1" date="date10" />
</Successor>
</Successor>
</Successor>
</Version>"""
version = Version.from_xml(etree.fromstring(xml_str))
assert version.is_complete_history is True
assert version.get_history() == [Version(id=f"vid{i+1}", date=f"date{i+1}",
username="user1", realm="Realm1")
for i in range(10)]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment