Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
2.1.0
=====
* Added getitem for documents at the database level
* Added fill_default() on documents to replace None values by schema defaults
* fill_default() is automatically called on save

2.0.2
=====
* Fixed contains functions
Expand Down
211 changes: 116 additions & 95 deletions pyArango/collection.py

Large diffs are not rendered by default.

23 changes: 17 additions & 6 deletions pyArango/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ class AikidoSession:
"""

class Holder(object):
def __init__(self, fct, auth, max_conflict_retries=5, verify=True):
def __init__(self, fct, auth, max_conflict_retries=5, verify=True, timeout=30):
self.fct = fct
self.auth = auth
self.max_conflict_retries = max_conflict_retries
if not isinstance(verify, bool) and not isinstance(verify, CA_Certificate) and not not isinstance(verify, str) :
raise ValueError("'verify' argument can only be of type: bool, CA_Certificate or str ")
self.verify = verify
self.timeout = timeout

def __call__(self, *args, **kwargs):
if self.auth:
Expand All @@ -50,10 +51,12 @@ def __call__(self, *args, **kwargs):
else :
kwargs["verify"] = self.verify

kwargs["timeout"] = self.timeout

try:
do_retry = True
retry = 0
while do_retry and retry < self.max_conflict_retries :
while do_retry and retry < self.max_conflict_retries:
ret = self.fct(*args, **kwargs)
do_retry = ret.status_code == 1200
try :
Expand Down Expand Up @@ -84,7 +87,8 @@ def __init__(
max_retries=5,
single_session=True,
log_requests=False,
pool_maxsize=10
pool_maxsize=10,
timeout=30,
):
if username:
self.auth = (username, password)
Expand All @@ -95,6 +99,7 @@ def __init__(
self.max_retries = max_retries
self.log_requests = log_requests
self.max_conflict_retries = max_conflict_retries
self.timeout = timeout

self.session = None
if single_session:
Expand Down Expand Up @@ -133,12 +138,13 @@ def __getattr__(self, request_function_name):

auth = object.__getattribute__(self, "auth")
verify = object.__getattribute__(self, "verify")
timeout = object.__getattribute__(self, "timeout")
if self.log_requests:
log = object.__getattribute__(self, "log")
log["nb_request"] += 1
log["requests"][request_function.__name__] += 1

return AikidoSession.Holder(request_function, auth, max_conflict_retries=self.max_conflict_retries, verify=verify)
return AikidoSession.Holder(request_function, auth, max_conflict_retries=self.max_conflict_retries, verify=verify, timeout=timeout)

def disconnect(self):
pass
Expand Down Expand Up @@ -180,6 +186,8 @@ class Connection(object):
max number of requests for a conflict error (1200 arangodb error). Does not work with gevents (grequests),
pool_maxsize: int
max number of open connections. (Not intended for grequest)
timeout: int
number of seconds to wait on a hanging connection before giving up
"""

LOAD_BLANCING_METHODS = {'round-robin', 'random'}
Expand All @@ -199,7 +207,8 @@ def __init__(
use_lock_for_reseting_jwt=True,
max_retries=5,
max_conflict_retries=5,
pool_maxsize=10
pool_maxsize=10,
timeout=30
):

if loadBalancing not in Connection.LOAD_BLANCING_METHODS:
Expand All @@ -215,6 +224,7 @@ def __init__(
self.max_retries = max_retries
self.max_conflict_retries = max_conflict_retries
self.action = ConnectionAction(self)
self.timeout = timeout

self.databases = {}
self.verbose = verbose
Expand Down Expand Up @@ -295,7 +305,8 @@ def create_aikido_session(
max_conflict_retries=self.max_conflict_retries,
max_retries=self.max_retries,
log_requests=False,
pool_maxsize=self.pool_maxsize
pool_maxsize=self.pool_maxsize,
timeout=self.timeout
)

def create_grequest_session(
Expand Down
18 changes: 11 additions & 7 deletions pyArango/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,16 +526,20 @@ def __contains__(self, name_or_id):
else:
return self.hasCollection(name_or_id) or self.hasGraph(name_or_id)

def __getitem__(self, collectionName):
"""use database[collectionName] to get a collection from the database"""
def __getitem__(self, col_or_doc_id):
"""use database[col_or_doc_id] to get a collection from the database"""
try:
return self.collections[collectionName]
except KeyError:
self.reload()
col_name, doc_key = col_or_doc_id.split('/')
return self.collections[col_name][doc_key]
except ValueError:
try:
return self.collections[collectionName]
return self.collections[col_or_doc_id]
except KeyError:
raise KeyError("Can't find any collection named : %s" % collectionName)
self.reload()
try:
return self.collections[col_or_doc_id]
except KeyError:
raise KeyError("Can't find any collection named : %s" % col_or_doc_id)

class DBHandle(Database):
"As the loading of a Database also triggers the loading of collections and graphs within. Only handles are loaded first. The full database are loaded on demand in a fully transparent manner."
Expand Down
21 changes: 15 additions & 6 deletions pyArango/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,7 @@ def validate(self):
return True

def set(self, dct):
"""Set the store using a dictionary"""
# if not self.mustValidate:
# self.store = dct
# self.patchStore = dct
# return

"""Set the values to a dict. Any missing value will be filled by it's default"""
for field, value in dct.items():
if field not in self.collection.arangoPrivates:
if isinstance(value, dict):
Expand All @@ -126,6 +121,14 @@ def set(self, dct):
else:
self[field] = value

def fill_default(self):
"""replace all None values with defaults"""
for field, value in self.validators.items():
if isinstance(value, dict):
self[field].fill_default()
elif self[field] is None:
self[field] = value.default

def __dir__(self):
return dir(self.getStore())

Expand Down Expand Up @@ -234,6 +237,10 @@ def to_default(self):
"""reset the document to the default values"""
self.reset(self.collection, self.collection.getDefaultDocument())

def fill_default(self):
"""reset the document to the default values"""
self._store.fill_default()

def validate(self):
"""validate the document"""
self._store.validate()
Expand Down Expand Up @@ -264,7 +271,9 @@ def save(self, waitForSync = False, **docArgs):
If you want to only update the modified fields use the .patch() function.
Use docArgs to put things such as 'waitForSync = True' (for a full list cf ArangoDB's doc).
It will only trigger a saving of the document if it has been modified since the last save. If you want to force the saving you can use forceSave()"""
self._store.fill_default()
payload = self._store.getStore()
# print(payload)
self._save(payload, waitForSync = False, **docArgs)

def _save(self, payload, waitForSync = False, **docArgs):
Expand Down
80 changes: 80 additions & 0 deletions pyArango/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest, copy
import os
from unittest.mock import MagicMock, patch

from pyArango.connection import *
from pyArango.database import *
Expand Down Expand Up @@ -122,7 +123,78 @@ class theCol(Collection):
doc.to_default()
self.assertEqual(doc["address"]["street"], "Paper street")
self.assertEqual(doc["name"], "Tyler Durden")

# @unittest.skip("stand by")
def test_fill_default(self):
class theCol(Collection):
_fields = {
"name": Field( default="Paper"),
"dct1":{
"num": Field(default=13),
"dct2":{
"str": Field(default='string'),
}
}
}

_validation = {
"on_save" : True,
"on_set" : True,
"allow_foreign_fields" : False
}

col = self.db.createCollection("theCol")
doc = col.createDocument()
doc['name'] = 'Orson'
doc['dct1']['num'] = None
doc['dct1']['dct2']['str'] = None

doc.fill_default()
self.assertEqual(doc['name'], 'Orson')
self.assertEqual(doc['dct1']['num'], 13)
self.assertEqual(doc['dct1']['dct2']['str'], 'string')

# @unittest.skip("stand by")
def test_fill_default_on_save(self):
class theCol(Collection):
_fields = {
"name": Field( default="Paper"),
"dct1":{
"num": Field(default=13),
"dct2":{
"str": Field(default='string'),
}
}
}

_validation = {
"on_save" : True,
"on_set" : True,
"allow_foreign_fields" : False
}

col = self.db.createCollection("theCol")
doc = col.createDocument()
doc['name'] = 'Orson'
doc['dct1']['num'] = None
doc['dct1']['dct2']['str'] = None

store = doc.getStore()
doc.save()

self.assertEqual(store['name'], 'Orson')
self.assertEqual(store['dct1']['num'], None)
self.assertEqual(store['dct1']['dct2']['str'], None)

self.assertEqual(doc['name'], 'Orson')
self.assertEqual(doc['dct1']['num'], 13)
self.assertEqual(doc['dct1']['dct2']['str'], 'string')

doc2 = col[doc['_key']]
self.assertEqual(doc2['name'], 'Orson')
self.assertEqual(doc2['dct1']['num'], 13)
self.assertEqual(doc2['dct1']['dct2']['str'], 'string')

# @unittest.skip("stand by")
def test_bulk_operations(self):
(collection, docs) = self.createManyUsersBulk(55, 17)
Expand Down Expand Up @@ -1072,7 +1144,15 @@ def test_tasks(self):
db_tasks.delete(task_id)
self.assertListEqual(db_tasks(), [])

# @unittest.skip("stand by")
def test_timeout_parameter(self):
# Create a Connection object with the desired timeout
timeout = 120
connection = Connection(arangoURL=ARANGODB_URL, username=ARANGODB_ROOT_USERNAME, password=ARANGODB_ROOT_PASSWORD, timeout=timeout)

# Verify that the Connection session was created with the correct timeout
assert connection.session.timeout == timeout

if __name__ == "__main__":
# Change default username/password in bash like this:
# export ARANGODB_ROOT_USERNAME=myUserName
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
setup(
name='pyArango',

version='2.0.2',
version='2.1.0',

description='An easy to use python driver for ArangoDB with built-in validation',
long_description=long_description,
Expand Down