Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
PyI40AAS
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Iterations
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Model registry
Operate
Environments
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Lehrstuhl für Informations- und Automatisierungssysteme
PyI40AAS
Commits
59ed8cf5
Commit
59ed8cf5
authored
4 years ago
by
Sebastian Heppner
Browse files
Options
Downloads
Patches
Plain Diff
backend.couchdb: Integrate CouchDBObjectStore functionality into backend.couchdb
parent
96000f0d
Branches
Branches containing commit
Tags
Tags containing commit
1 merge request
!50
Feature/backend couchdb
Pipeline
#349528
failed
4 years ago
Stage: test
Changes
2
Pipelines
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
aas/backend/couchdb.py
+115
-80
115 additions, 80 deletions
aas/backend/couchdb.py
test/backend/test_couchdb.py
+125
-69
125 additions, 69 deletions
test/backend/test_couchdb.py
with
240 additions
and
149 deletions
aas/backend/couchdb.py
+
115
−
80
View file @
59ed8cf5
...
@@ -11,7 +11,7 @@
...
@@ -11,7 +11,7 @@
"""
"""
Todo: Add module docstring
Todo: Add module docstring
"""
"""
from
typing
import
List
,
Dict
,
Any
,
Optional
,
Iterator
,
Iterable
from
typing
import
List
,
Dict
,
Any
,
Optional
,
Iterator
,
Iterable
,
Union
import
re
import
re
import
urllib.parse
import
urllib.parse
import
urllib.request
import
urllib.request
...
@@ -55,6 +55,7 @@ class CouchDBBackend(backends.Backend):
...
@@ -55,6 +55,7 @@ class CouchDBBackend(backends.Backend):
raise
raise
updated_store_object
=
data
[
'
data
'
]
updated_store_object
=
data
[
'
data
'
]
set_couchdb_revision
(
url
,
data
[
"
_rev
"
])
store_object
.
update_from
(
updated_store_object
)
store_object
.
update_from
(
updated_store_object
)
@classmethod
@classmethod
...
@@ -67,30 +68,19 @@ class CouchDBBackend(backends.Backend):
...
@@ -67,30 +68,19 @@ class CouchDBBackend(backends.Backend):
"
in the CouchDB
"
)
"
in the CouchDB
"
)
url
=
CouchDBBackend
.
_parse_source
(
store_object
.
source
)
url
=
CouchDBBackend
.
_parse_source
(
store_object
.
source
)
# We need to get the revision of the object, if it already exists, otherwise we cannot write to the Couchdb
# We need to get the revision of the object, if it already exists, otherwise we cannot write to the Couchdb
request
=
urllib
.
request
.
Request
(
if
get_couchdb_revision
(
url
)
is
None
:
url
,
raise
CouchDBConflictError
(
"
No revision found for the given object. Try calling `update` on it.
"
)
headers
=
{
'
Content-type
'
:
'
application/json
'
},
method
=
'
GET
'
)
couchdb_revision
:
Optional
[
str
]
=
None
try
:
revision_data
=
CouchDBBackend
.
do_request
(
request
)
couchdb_revision
=
revision_data
[
'
_rev
'
]
except
CouchDBServerError
as
e
:
if
not
e
.
code
==
404
:
raise
data_to_commit
:
Dict
[
str
,
Any
]
=
{
'
data
'
:
store_object
}
data
=
json
.
dumps
({
'
data
'
:
store_object
,
"
_rev
"
:
get_couchdb_revision
(
url
)},
if
couchdb_revision
:
cls
=
json_serialization
.
AASToJsonEncoder
)
data_to_commit
[
'
_rev
'
]
=
couchdb_revision
data
=
json
.
dumps
(
data_to_commit
,
cls
=
json_serialization
.
AASToJsonEncoder
)
request
=
urllib
.
request
.
Request
(
request
=
urllib
.
request
.
Request
(
url
,
url
,
headers
=
{
'
Content-type
'
:
'
application/json
'
},
headers
=
{
'
Content-type
'
:
'
application/json
'
},
method
=
'
PUT
'
,
method
=
'
PUT
'
,
data
=
data
.
encode
())
data
=
data
.
encode
())
try
:
try
:
CouchDBBackend
.
do_request
(
request
)
response
=
CouchDBBackend
.
do_request
(
request
)
set_couchdb_revision
(
url
,
response
[
"
rev
"
])
except
CouchDBServerError
as
e
:
except
CouchDBServerError
as
e
:
if
e
.
code
==
409
:
if
e
.
code
==
409
:
raise
CouchDBConflictError
(
"
Could not commit changes to id {} due to a concurrent modification in the
"
raise
CouchDBConflictError
(
"
Could not commit changes to id {} due to a concurrent modification in the
"
...
@@ -100,46 +90,6 @@ class CouchDBBackend(backends.Backend):
...
@@ -100,46 +90,6 @@ class CouchDBBackend(backends.Backend):
.
format
(
store_object
.
identification
,
url
))
from
e
.
format
(
store_object
.
identification
,
url
))
from
e
raise
raise
@classmethod
def
delete_object
(
cls
,
obj
:
"
Referable
"
):
# type: ignore
"""
Deletes the given object from the couchdb
:param obj: Object to delete
"""
if
not
isinstance
(
obj
,
model
.
Identifiable
):
raise
CouchDBSourceError
(
"
The given store_object is not Identifiable, therefore cannot be found
"
"
in the CouchDB
"
)
url
=
CouchDBBackend
.
_parse_source
(
obj
.
source
)
# We need to get the revision of the object, if it already exists, otherwise we cannot write to the Couchdb
req
=
urllib
.
request
.
Request
(
url
,
headers
=
{
'
Content-type
'
:
'
application/json
'
},
method
=
'
GET
'
)
try
:
data
=
CouchDBBackend
.
do_request
(
req
)
couchdb_revision
:
str
=
data
[
'
_rev
'
]
except
CouchDBServerError
as
e
:
if
e
.
code
==
404
:
raise
KeyError
(
"
Object with id {} was not found in the CouchDB at {}
"
.
format
(
obj
.
identification
,
url
))
from
e
raise
req
=
urllib
.
request
.
Request
(
url
+
"
?rev=
"
+
couchdb_revision
,
headers
=
{
'
Content-type
'
:
'
application/json
'
},
method
=
'
DELETE
'
)
try
:
CouchDBBackend
.
do_request
(
req
)
except
CouchDBServerError
as
e
:
if
e
.
code
==
409
:
raise
CouchDBConflictError
(
"
Could not delete object with to id {} due to a concurrent modification in
"
"
the database.
"
.
format
(
obj
.
identification
))
from
e
elif
e
.
code
==
404
:
raise
KeyError
(
"
Object with id {} was not found in the CouchDB at {}
"
.
format
(
obj
.
identification
,
url
))
from
e
raise
@classmethod
@classmethod
def
_parse_source
(
cls
,
source
:
str
)
->
str
:
def
_parse_source
(
cls
,
source
:
str
)
->
str
:
"""
"""
...
@@ -149,16 +99,16 @@ class CouchDBBackend(backends.Backend):
...
@@ -149,16 +99,16 @@ class CouchDBBackend(backends.Backend):
:return: URL to the document
:return: URL to the document
:raises CouchDBBackendSourceError, if the source has the wrong format
:raises CouchDBBackendSourceError, if the source has the wrong format
"""
"""
couchdb_s
=
re
.
match
(
"
couchdbs:
"
,
source
)
# Note: Works, since match only checks the beginning of the string
couchdb_s
=
re
.
match
(
"
couchdbs:
//
"
,
source
)
# Note: Works, since match only checks the beginning of the string
if
couchdb_s
:
if
couchdb_s
:
url
=
source
.
replace
(
"
couchdbs:
"
,
"
https://
"
,
1
)
url
=
source
.
replace
(
"
couchdbs:
//
"
,
"
https://
"
,
1
)
else
:
else
:
couchdb_wo_s
=
re
.
match
(
"
couchdb:
"
,
source
)
couchdb_wo_s
=
re
.
match
(
"
couchdb:
//
"
,
source
)
if
couchdb_wo_s
:
if
couchdb_wo_s
:
url
=
source
.
replace
(
"
couchdb:
"
,
"
http://
"
,
1
)
url
=
source
.
replace
(
"
couchdb:
//
"
,
"
http://
"
,
1
)
else
:
else
:
raise
CouchDBSourceError
(
"
Source has wrong format.
"
raise
CouchDBSourceError
(
"
Source has wrong format.
"
"
Expected to start with {couchdb, couchdbs}, got {
"
+
source
+
"
}
"
)
"
Expected to start with {couchdb
://
, couchdbs
://
}, got {
"
+
source
+
"
}
"
)
return
url
return
url
@classmethod
@classmethod
...
@@ -209,15 +159,15 @@ class CouchDBBackend(backends.Backend):
...
@@ -209,15 +159,15 @@ class CouchDBBackend(backends.Backend):
# Global registry for credentials for CouchDB Servers
# Global registry for credentials for CouchDB Servers
_credentials_store
:
urllib
.
request
.
HTTPPasswordMgrWithPriorAuth
=
urllib
.
request
.
HTTPPasswordMgrWithPriorAuth
()
_credentials_store
:
urllib
.
request
.
HTTPPasswordMgrWithPriorAuth
=
urllib
.
request
.
HTTPPasswordMgrWithPriorAuth
()
# todo: Why does this work and not HTTPPasswordMgrWithBasicAuth?
# Note: The HTTPPasswordMgr is not thread safe during writing, should be thread safe for reading only.
# https://stackoverflow.com/questions/29708708/http-basic-authentication-not-working-in-python-3-4
def
register_credentials
(
url
:
str
,
username
:
str
,
password
:
str
):
def
register_credentials
(
url
:
str
,
username
:
str
,
password
:
str
):
"""
"""
Register the credentials of a CouchDB server to the global credentials store
Register the credentials of a CouchDB server to the global credentials store
Todo: make thread safe
Warning: Do not use this function, while other threads may be accessing the credentials via the CouchDBObjectStore
or update or commit functions of model.base.Referable objects!
:param url: Toplevel URL
:param url: Toplevel URL
:param username: Username to that CouchDB instance
:param username: Username to that CouchDB instance
...
@@ -226,6 +176,39 @@ def register_credentials(url: str, username: str, password: str):
...
@@ -226,6 +176,39 @@ def register_credentials(url: str, username: str, password: str):
_credentials_store
.
add_password
(
None
,
url
,
username
,
password
,
is_authenticated
=
True
)
_credentials_store
.
add_password
(
None
,
url
,
username
,
password
,
is_authenticated
=
True
)
# Global registry for CouchDB Revisions
_revision_store
:
Dict
[
str
,
str
]
=
{}
def
set_couchdb_revision
(
url
:
str
,
revision
:
str
):
"""
Set the CouchDB revision of the given document in the revision store
:param url: URL to the CouchDB document
:param revision: CouchDB revision
"""
_revision_store
[
url
]
=
revision
def
get_couchdb_revision
(
url
:
str
)
->
Optional
[
str
]:
"""
Get the CouchDB revision from the revision store for the given URL to a CouchDB Document
:param url: URL to the CouchDB document
:return: CouchDB-revision, if there is one, otherwise returns None
"""
return
_revision_store
.
get
(
url
)
def
delete_couchdb_revision
(
url
:
str
):
"""
Delete the CouchDB revision from the revision store for the given URL to a CouchDB Document
:param url: URL to the CouchDB document
"""
del
_revision_store
[
url
]
class
CouchDBObjectStore
(
model
.
AbstractObjectStore
):
class
CouchDBObjectStore
(
model
.
AbstractObjectStore
):
"""
"""
An ObjectStore implementation for Identifiable PyI40AAS objects backed by a CouchDB database server.
An ObjectStore implementation for Identifiable PyI40AAS objects backed by a CouchDB database server.
...
@@ -253,9 +236,6 @@ class CouchDBObjectStore(model.AbstractObjectStore):
...
@@ -253,9 +236,6 @@ class CouchDBObjectStore(model.AbstractObjectStore):
"""
"""
self
.
url
:
str
=
url
self
.
url
:
str
=
url
self
.
database_name
:
str
=
database
self
.
database_name
:
str
=
database
self
.
ssl
:
bool
=
False
if
re
.
match
(
"
https:
"
,
self
.
url
):
self
.
ssl
=
True
def
check_database
(
self
,
create
=
False
):
def
check_database
(
self
,
create
=
False
):
"""
"""
...
@@ -287,10 +267,13 @@ class CouchDBObjectStore(model.AbstractObjectStore):
...
@@ -287,10 +267,13 @@ class CouchDBObjectStore(model.AbstractObjectStore):
method
=
'
PUT
'
)
method
=
'
PUT
'
)
CouchDBBackend
.
do_request
(
request
)
CouchDBBackend
.
do_request
(
request
)
def
get_identifiable
(
self
,
identifier
:
model
.
Identifier
)
->
model
.
Identifiable
:
def
get_identifiable
(
self
,
identifier
:
Union
[
str
,
model
.
Identifier
]
)
->
model
.
Identifiable
:
"""
"""
Retrieve an AAS object from the CouchDB by its Identifier
Retrieve an AAS object from the CouchDB by its Identifier
If the identifier is a string, it is assumed that the string is a correct couchdb-ID-string (according to the
internal conversion rules, see CouchDBObjectStore._transform_id() )
:raises KeyError: If no such object is stored in the database
:raises KeyError: If no such object is stored in the database
:raises CouchDBError: If error occur during the request to the CouchDB server (see `_do_request()` for details)
:raises CouchDBError: If error occur during the request to the CouchDB server (see `_do_request()` for details)
"""
"""
...
@@ -313,8 +296,9 @@ class CouchDBObjectStore(model.AbstractObjectStore):
...
@@ -313,8 +296,9 @@ class CouchDBObjectStore(model.AbstractObjectStore):
if
not
isinstance
(
obj
,
model
.
Identifiable
):
if
not
isinstance
(
obj
,
model
.
Identifiable
):
raise
CouchDBResponseError
(
"
The CouchDB document with id {} does not contain an identifiable AAS object.
"
raise
CouchDBResponseError
(
"
The CouchDB document with id {} does not contain an identifiable AAS object.
"
.
format
(
identifier
))
.
format
(
identifier
))
obj
.
_store
=
self
self
.
generate_source
(
obj
)
# Generate the source parameter of this object
obj
.
couchdb_revision
=
data
[
'
_rev
'
]
set_couchdb_revision
(
"
{}/{}/{}
"
.
format
(
self
.
url
,
self
.
database_name
,
urllib
.
parse
.
quote
(
identifier
,
safe
=
''
)),
data
[
"
_rev
"
])
return
obj
return
obj
def
add
(
self
,
x
:
model
.
Identifiable
)
->
None
:
def
add
(
self
,
x
:
model
.
Identifiable
)
->
None
:
...
@@ -335,12 +319,67 @@ class CouchDBObjectStore(model.AbstractObjectStore):
...
@@ -335,12 +319,67 @@ class CouchDBObjectStore(model.AbstractObjectStore):
method
=
'
PUT
'
,
method
=
'
PUT
'
,
data
=
data
.
encode
())
data
=
data
.
encode
())
try
:
try
:
CouchDBBackend
.
do_request
(
request
)
response
=
CouchDBBackend
.
do_request
(
request
)
set_couchdb_revision
(
"
{}/{}/{}
"
.
format
(
self
.
url
,
self
.
database_name
,
self
.
_transform_id
(
x
.
identification
)),
response
[
"
rev
"
])
except
CouchDBServerError
as
e
:
except
CouchDBServerError
as
e
:
if
e
.
code
==
409
:
if
e
.
code
==
409
:
raise
KeyError
(
"
Identifiable with id {} already exists in CouchDB database
"
.
format
(
x
.
identification
))
\
raise
KeyError
(
"
Identifiable with id {} already exists in CouchDB database
"
.
format
(
x
.
identification
))
\
from
e
from
e
raise
raise
self
.
generate_source
(
x
)
# Set the source of the object
def
discard
(
self
,
x
:
model
.
Identifiable
,
safe_delete
=
False
)
->
None
:
"""
Delete an Identifiable AAS object from the CouchDB database
:param x: The object to be deleted
:param safe_delete: If True, only delete the object if it has not been modified in the database in comparison to
the provided revision. This uses the CouchDB revision token and thus only works with
CouchDBIdentifiable objects retrieved from this database.
:raises KeyError: If the object does not exist in the database
:raises CouchDBConflictError: If safe_delete is true and the object has been modified or deleted in the database
:raises CouchDBError: If error occur during the request to the CouchDB server (see `_do_request()` for details)
"""
logger
.
debug
(
"
Deleting object %s from CouchDB database ...
"
,
repr
(
x
))
# If x is not a CouchDBIdentifiable, retrieve x from the database to get the current couchdb_revision
rev
=
get_couchdb_revision
(
"
{}/{}/{}
"
.
format
(
self
.
url
,
self
.
database_name
,
self
.
_transform_id
(
x
.
identification
)))
if
rev
is
not
None
:
logger
.
debug
(
"
using the object
'
s stored revision token %s for deletion.
"
%
rev
)
if
rev
is
None
:
if
safe_delete
:
raise
CouchDBConflictError
(
"
No CouchDBRevision found for the object
"
)
else
:
try
:
logger
.
debug
(
"
fetching the current object revision for deletion ...
"
)
self
.
get_identifiable
(
x
.
identification
)
except
KeyError
as
e
:
raise
KeyError
(
"
No AAS object with id {} exists in CouchDB database
"
.
format
(
x
.
identification
))
from
e
rev
=
get_couchdb_revision
(
"
{}/{}/{}
"
.
format
(
self
.
url
,
self
.
database_name
,
self
.
_transform_id
(
x
.
identification
)))
logger
.
debug
(
"
using the current object revision %s for deletion.
"
%
rev
)
request
=
urllib
.
request
.
Request
(
"
{}/{}/{}?rev={}
"
.
format
(
self
.
url
,
self
.
database_name
,
self
.
_transform_id
(
x
.
identification
),
rev
),
headers
=
{
'
Content-type
'
:
'
application/json
'
},
method
=
'
DELETE
'
)
try
:
CouchDBBackend
.
do_request
(
request
)
except
CouchDBServerError
as
e
:
if
e
.
code
==
404
:
raise
KeyError
(
"
No AAS object with id {} exists in CouchDB database
"
.
format
(
x
.
identification
))
from
e
elif
e
.
code
==
409
:
raise
CouchDBConflictError
(
"
Object with id {} has been modified in the database since
"
"
the version requested to be deleted.
"
.
format
(
x
.
identification
))
from
e
raise
delete_couchdb_revision
(
"
{}/{}/{}
"
.
format
(
self
.
url
,
self
.
database_name
,
self
.
_transform_id
(
x
.
identification
)))
def
__contains__
(
self
,
x
:
object
)
->
bool
:
def
__contains__
(
self
,
x
:
object
)
->
bool
:
"""
"""
...
@@ -357,7 +396,7 @@ class CouchDBObjectStore(model.AbstractObjectStore):
...
@@ -357,7 +396,7 @@ class CouchDBObjectStore(model.AbstractObjectStore):
identifier
=
x
.
identification
identifier
=
x
.
identification
else
:
else
:
return
False
return
False
logger
.
debug
(
"
Checking exist
a
nce of object with id %s in database ...
"
,
repr
(
x
))
logger
.
debug
(
"
Checking exist
e
nce of object with id %s in database ...
"
,
repr
(
x
))
request
=
urllib
.
request
.
Request
(
request
=
urllib
.
request
.
Request
(
"
{}/{}/{}
"
.
format
(
self
.
url
,
self
.
database_name
,
self
.
_transform_id
(
identifier
)),
"
{}/{}/{}
"
.
format
(
self
.
url
,
self
.
database_name
,
self
.
_transform_id
(
identifier
)),
headers
=
{
'
Accept
'
:
'
application/json
'
},
headers
=
{
'
Accept
'
:
'
application/json
'
},
...
@@ -430,12 +469,8 @@ class CouchDBObjectStore(model.AbstractObjectStore):
...
@@ -430,12 +469,8 @@ class CouchDBObjectStore(model.AbstractObjectStore):
:param identifiable: Identifiable object
:param identifiable: Identifiable object
"""
"""
source
:
str
=
self
.
url
source
:
str
=
self
.
url
.
replace
(
"
https://
"
,
"
couchdbs://
"
).
replace
(
"
http://
"
,
"
couchdb://
"
)
if
self
.
ssl
:
source
+=
"
/
"
+
self
.
database_name
+
"
/
"
+
self
.
_transform_id
(
identifiable
.
identification
)
source
.
replace
(
"
https://
"
,
"
couchdbs:
"
)
else
:
source
.
replace
(
"
http://
"
,
"
couchdb
"
)
source
+=
self
.
database_name
+
"
/
"
+
self
.
_transform_id
(
identifiable
.
identification
)
identifiable
.
source
=
source
identifiable
.
source
=
source
...
...
This diff is collapsed.
Click to expand it.
test/backend/test_couchdb.py
+
125
−
69
View file @
59ed8cf5
...
@@ -10,13 +10,13 @@
...
@@ -10,13 +10,13 @@
# specific language governing permissions and limitations under the License.
# specific language governing permissions and limitations under the License.
import
base64
import
base64
import
configparser
import
configparser
import
copy
import
os
import
os
import
unittest
import
unittest
import
urllib.request
import
urllib.request
import
urllib.error
import
urllib.error
from
aas.backend
import
backends
,
couchdb
from
aas.backend
import
backends
,
couchdb
from
aas
import
model
from
aas.examples.data.example_aas
import
*
from
aas.examples.data.example_aas
import
*
...
@@ -24,7 +24,7 @@ TEST_CONFIG = configparser.ConfigParser()
...
@@ -24,7 +24,7 @@ TEST_CONFIG = configparser.ConfigParser()
TEST_CONFIG
.
read
((
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
"
..
"
,
"
test_config.default.ini
"
),
TEST_CONFIG
.
read
((
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
"
..
"
,
"
test_config.default.ini
"
),
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
"
..
"
,
"
test_config.ini
"
)))
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
"
..
"
,
"
test_config.ini
"
)))
source_core
:
str
=
"
couchdb:
"
+
TEST_CONFIG
[
"
couchdb
"
][
"
url
"
].
lstrip
(
"
http://
"
)
+
"
/
"
+
\
source_core
:
str
=
"
couchdb:
//
"
+
TEST_CONFIG
[
"
couchdb
"
][
"
url
"
].
lstrip
(
"
http://
"
)
+
"
/
"
+
\
TEST_CONFIG
[
"
couchdb
"
][
"
database
"
]
+
"
/
"
TEST_CONFIG
[
"
couchdb
"
][
"
database
"
]
+
"
/
"
...
@@ -53,13 +53,13 @@ class CouchDBBackendOfflineMethodsTest(unittest.TestCase):
...
@@ -53,13 +53,13 @@ class CouchDBBackendOfflineMethodsTest(unittest.TestCase):
password
=
"
test_password
"
)
password
=
"
test_password
"
)
url
=
couchdb
.
CouchDBBackend
.
_parse_source
(
url
=
couchdb
.
CouchDBBackend
.
_parse_source
(
"
couchdbs:couchdb.plt.rwth-aachen.de:5984/path_to_db/path_to_doc
"
"
couchdbs:
//
couchdb.plt.rwth-aachen.de:5984/path_to_db/path_to_doc
"
)
)
expected_url
=
"
https://couchdb.plt.rwth-aachen.de:5984/path_to_db/path_to_doc
"
expected_url
=
"
https://couchdb.plt.rwth-aachen.de:5984/path_to_db/path_to_doc
"
self
.
assertEqual
(
expected_url
,
url
)
self
.
assertEqual
(
expected_url
,
url
)
url
=
couchdb
.
CouchDBBackend
.
_parse_source
(
url
=
couchdb
.
CouchDBBackend
.
_parse_source
(
"
couchdb:couchdb.plt.rwth-aachen.de:5984/path_to_db/path_to_doc
"
"
couchdb:
//
couchdb.plt.rwth-aachen.de:5984/path_to_db/path_to_doc
"
)
)
expected_url
=
"
http://couchdb.plt.rwth-aachen.de:5984/path_to_db/path_to_doc
"
expected_url
=
"
http://couchdb.plt.rwth-aachen.de:5984/path_to_db/path_to_doc
"
self
.
assertEqual
(
expected_url
,
url
)
self
.
assertEqual
(
expected_url
,
url
)
...
@@ -76,74 +76,130 @@ class CouchDBBackendOfflineMethodsTest(unittest.TestCase):
...
@@ -76,74 +76,130 @@ class CouchDBBackendOfflineMethodsTest(unittest.TestCase):
TEST_CONFIG
[
'
couchdb
'
][
'
database
'
],
TEST_CONFIG
[
'
couchdb
'
][
'
database
'
],
COUCHDB_ERROR
))
COUCHDB_ERROR
))
class
CouchDBBackendTest
(
unittest
.
TestCase
):
class
CouchDBBackendTest
(
unittest
.
TestCase
):
def
test_authorization
(
self
):
def
setUp
(
self
)
->
None
:
couchdb
.
register_credentials
(
TEST_CONFIG
[
"
couchdb
"
][
"
url
"
].
lstrip
(
"
http://
"
),
self
.
object_store
=
couchdb
.
CouchDBObjectStore
(
TEST_CONFIG
[
'
couchdb
'
][
'
url
'
],
TEST_CONFIG
[
'
couchdb
'
][
'
database
'
])
couchdb
.
register_credentials
(
TEST_CONFIG
[
"
couchdb
"
][
"
url
"
],
TEST_CONFIG
[
"
couchdb
"
][
"
user
"
],
TEST_CONFIG
[
"
couchdb
"
][
"
user
"
],
TEST_CONFIG
[
"
couchdb
"
][
"
password
"
])
TEST_CONFIG
[
"
couchdb
"
][
"
password
"
])
req
=
urllib
.
request
.
Request
(
"
{}/{}
"
.
format
(
TEST_CONFIG
[
"
couchdb
"
][
"
url
"
],
TEST_CONFIG
[
"
couchdb
"
][
"
database
"
]),
backends
.
register_backend
(
"
couchdb
"
,
couchdb
.
CouchDBBackend
)
headers
=
{
'
Content-type
'
:
'
application/json
'
})
self
.
object_store
.
check_database
()
couchdb
.
CouchDBBackend
.
do_request
(
req
)
def
test_commit_object
(
self
):
def
tearDown
(
self
)
->
None
:
test_object
=
create_example_submodel
()
self
.
object_store
.
clear
()
test_object
.
source
=
source_core
+
"
example_submodel
"
couchdb
.
CouchDBBackend
.
commit_object
(
test_object
,
test_object
,
[])
# Cleanup CouchDB
couchdb
.
CouchDBBackend
.
delete_object
(
test_object
)
def
test_commit_nested_object
(
self
):
def
test_object_store_add
(
self
):
backends
.
register_backend
(
"
couchdb
"
,
couchdb
.
CouchDBBackend
)
test_submodel
=
create_example_submodel
()
test_submodel
.
source
=
source_core
+
"
another_example_submodel
"
test_property
=
test_submodel
.
get_referable
(
"
ExampleSubmodelCollectionOrdered
"
).
get_referable
(
"
ExampleProperty
"
)
self
.
assertIsInstance
(
test_property
,
model
.
Property
)
test_property
.
commit
()
# Cleanup CouchDB
couchdb
.
CouchDBBackend
.
delete_object
(
test_submodel
)
def
test_update_object
(
self
):
test_object
=
create_example_submodel
()
test_object
=
create_example_submodel
()
test_object
.
source
=
source_core
+
"
example_submodel
"
self
.
object_store
.
add
(
test_object
)
couchdb
.
CouchDBBackend
.
commit_object
(
test_object
,
test_object
,
[])
self
.
assertEqual
(
test_object
.
source
,
source_core
+
"
IRI-https%3A%2F%2Facplt.org%2FTest_Submodel
"
)
couchdb
.
CouchDBBackend
.
update_object
(
test_object
,
test_object
,
[])
# Cleanup CouchDB
def
test_example_submodel_storing
(
self
)
->
None
:
couchdb
.
CouchDBBackend
.
delete_object
(
test_object
)
example_submodel
=
create_example_submodel
()
def
test_update_nested_object
(
self
):
# Add exmaple submodel
test_submodel
=
create_example_submodel
()
self
.
object_store
.
add
(
example_submodel
)
test_submodel
.
source
=
source_core
+
"
another_example_submodel
"
self
.
assertEqual
(
1
,
len
(
self
.
object_store
))
test_submodel
.
commit
()
self
.
assertIn
(
example_submodel
,
self
.
object_store
)
test_property
=
test_submodel
.
get_referable
(
"
ExampleSubmodelCollectionOrdered
"
).
get_referable
(
"
ExampleProperty
"
)
self
.
assertIsInstance
(
test_property
,
model
.
Property
)
# Restore example submodel and check data
test_property
.
value
=
"
A new value
"
submodel_restored
=
self
.
object_store
.
get_identifiable
(
test_property
.
update
()
model
.
Identifier
(
id_
=
'
https://acplt.org/Test_Submodel
'
,
id_type
=
model
.
IdentifierType
.
IRI
))
self
.
assertEqual
(
test_property
.
value
,
'
exampleValue
'
)
assert
(
isinstance
(
submodel_restored
,
model
.
Submodel
))
# Cleanup CouchDB
checker
=
AASDataChecker
(
raise_immediately
=
True
)
couchdb
.
CouchDBBackend
.
delete_object
(
test_submodel
)
check_example_submodel
(
checker
,
submodel_restored
)
def
test_commit_overwrite
(
self
):
# Delete example submodel
test_submodel
=
create_example_submodel
()
self
.
object_store
.
discard
(
submodel_restored
)
test_submodel
.
source
=
source_core
+
"
another_example_submodel
"
self
.
assertNotIn
(
example_submodel
,
self
.
object_store
)
test_submodel
.
commit
()
def
test_iterating
(
self
)
->
None
:
test_property
=
test_submodel
.
get_referable
(
"
ExampleSubmodelCollectionOrdered
"
).
get_referable
(
"
ExampleProperty
"
)
example_data
=
create_full_example
()
self
.
assertIsInstance
(
test_property
,
model
.
Property
)
test_property
.
value
=
"
A new value
"
# Add all objects
test_property
.
commit
()
for
item
in
example_data
:
self
.
object_store
.
add
(
item
)
test_property
.
value
=
"
Something else
"
test_property
.
update
()
self
.
assertEqual
(
6
,
len
(
self
.
object_store
))
self
.
assertEqual
(
test_property
.
value
,
"
A new value
"
)
# Cleanup Couchdb
# Iterate objects, add them to a DictObjectStore and check them
couchdb
.
CouchDBBackend
.
delete_object
(
test_submodel
)
retrieved_data_store
:
model
.
provider
.
DictObjectStore
[
model
.
Identifiable
]
=
model
.
provider
.
DictObjectStore
()
for
item
in
self
.
object_store
:
def
test_delete
(
self
):
retrieved_data_store
.
add
(
item
)
test_submodel
=
create_example_submodel
()
checker
=
AASDataChecker
(
raise_immediately
=
True
)
test_submodel
.
source
=
source_core
+
"
another_example_submodel
"
check_full_example
(
checker
,
retrieved_data_store
)
test_submodel
.
commit
()
couchdb
.
CouchDBBackend
.
delete_object
(
test_submodel
)
def
test_key_errors
(
self
)
->
None
:
# Double adding an object should raise a KeyError
example_submodel
=
create_example_submodel
()
self
.
object_store
.
add
(
example_submodel
)
with
self
.
assertRaises
(
KeyError
)
as
cm
:
self
.
object_store
.
add
(
example_submodel
)
self
.
assertEqual
(
"'
Identifiable with id Identifier(IRI=https://acplt.org/Test_Submodel) already exists in
"
"
CouchDB database
'"
,
str
(
cm
.
exception
))
# Querying a deleted object should raise a KeyError
retrieved_submodel
=
self
.
object_store
.
get_identifiable
(
model
.
Identifier
(
'
https://acplt.org/Test_Submodel
'
,
model
.
IdentifierType
.
IRI
))
self
.
object_store
.
discard
(
example_submodel
)
with
self
.
assertRaises
(
KeyError
)
as
cm
:
with
self
.
assertRaises
(
KeyError
)
as
cm
:
test_submodel
.
update
()
self
.
object_store
.
get_identifiable
(
model
.
Identifier
(
'
https://acplt.org/Test_Submodel
'
,
model
.
IdentifierType
.
IRI
))
self
.
assertEqual
(
self
.
assertEqual
(
"'
No Identifiable with id IRI-https://acplt.org/Test_Submodel found in CouchDB database
'"
,
"'
No Identifiable found in CouchDB at http://localhost:5984/aas_test/another_example_submodel
'"
,
str
(
cm
.
exception
))
str
(
cm
.
exception
))
# Double deleting should also raise a KeyError
with
self
.
assertRaises
(
KeyError
)
as
cm
:
self
.
object_store
.
discard
(
retrieved_submodel
)
self
.
assertEqual
(
"'
No AAS object with id Identifier(IRI=https://acplt.org/Test_Submodel) exists in
"
"
CouchDB database
'"
,
str
(
cm
.
exception
))
def
test_conflict_errors
(
self
):
# Preperation: add object and retrieve it from the database
example_submodel
=
create_example_submodel
()
self
.
object_store
.
add
(
example_submodel
)
retrieved_submodel
=
self
.
object_store
.
get_identifiable
(
model
.
Identifier
(
'
https://acplt.org/Test_Submodel
'
,
model
.
IdentifierType
.
IRI
))
# Simulate a concurrent modification
remote_modified_submodel
=
copy
.
copy
(
retrieved_submodel
)
remote_modified_submodel
.
id_short
=
"
newIdShort
"
remote_modified_submodel
.
commit
()
# Todo: With the current architecture, it is not possible to change something in the CouchDB without also
# changing the revision
"""
# Committing changes to the retrieved object should now raise a conflict error
retrieved_submodel.id_short =
"
myOtherNewIdShort
"
with self.assertRaises(couchdb.CouchDBConflictError) as cm:
retrieved_submodel.commit()
self.assertEqual(
"
Could not commit changes to id Identifier(IRI=https://acplt.org/Test_Submodel) due to a
"
"
concurrent modification in the database.
"
, str(cm.exception))
# Deleting the submodel with safe_delete should also raise a conflict error. Deletion without safe_delete should
# work
with self.assertRaises(couchdb.CouchDBConflictError) as cm:
self.object_store.discard(retrieved_submodel, True)
self.assertEqual(
"
Object with id Identifier(IRI=https://acplt.org/Test_Submodel) has been modified in the
"
"
database since the version requested to be deleted.
"
, str(cm.exception))
self.object_store.discard(retrieved_submodel, False)
self.assertEqual(0, len(self.object_store))
# Committing after deletion should also raise a conflict error
with self.assertRaises(couchdb.CouchDBConflictError) as cm:
retrieved_submodel.commit()
self.assertEqual(
"
Could not commit changes to id Identifier(IRI=https://acplt.org/Test_Submodel) due to a
"
"
concurrent modification in the database.
"
, str(cm.exception))
"""
def
test_editing
(
self
):
test_object
=
create_example_submodel
()
self
.
object_store
.
add
(
test_object
)
# Test if update restores changes
test_object
.
id_short
=
"
SomeNewIdShort
"
test_object
.
update
()
self
.
assertEqual
(
"
TestSubmodel
"
,
test_object
.
id_short
)
# Test if commit uploads changes
test_object
.
id_short
=
"
SomeNewIdShort
"
test_object
.
commit
()
new_test_object
=
self
.
object_store
.
get_identifiable
(
test_object
.
identification
)
self
.
assertEqual
(
"
SomeNewIdShort
"
,
new_test_object
.
id_short
)
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment