Skip to content

Commit db758bf

Browse files
committed
Merge branch 'dev'
2 parents 77cdc4e + 025ff65 commit db758bf

File tree

14 files changed

+201
-73
lines changed

14 files changed

+201
-73
lines changed

CHANGELOG.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
1.3.5
2+
=====
3+
4+
* restoreIndex and restoreIndexes in collection will restore previously deleted indexes
5+
* added max_conflict_retries to handle arango's 1200
6+
* added single session so AikidoSessio.Holders can share a single request session
7+
* added task deletion to tests reset
8+
* added drop() to tasks to remove all tasks in one command
9+
* better documentation of connection class
10+
* False is not considered a Null value while validating
11+
* Removed redundant document creation functions
12+
* More explicit validation error with field name
13+
114
1.3.4
215
=====
316
* Bugfix: Query iterator now returns all elements instead of a premature empty list

DESCRIPTION.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Python Object Wrapper for ArangoDB_ with built-in validation
2-
===========================================================
2+
=============================================================
33

44
pyArango aims to be an easy to use driver for ArangoDB with built in validation. Collections are treated as types that apply to the documents within. You can be 100% permissive or enforce schemas and validate fields on set, on save or on both.
55

@@ -15,4 +15,4 @@ pyArango is developed by `Tariq Daouda`_, the full source code is available from
1515
For the latest news about pyArango, you can follow me on twitter `@tariqdaouda`_.
1616
If you have any issues with it, please file a github issue.
1717

18-
.. _@tariqdaouda: https://www.twitter.com/tariqdaouda
18+
.. _@tariqdaouda: https://www.twitter.com/tariqdaouda

pyArango/collection.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -315,25 +315,11 @@ def delete(self):
315315
raise DeletionError(data["errorMessage"], data)
316316

317317
def createDocument(self, initDict = None):
318-
"""create and returns a document populated with the defaults or with the values in initDict"""
319-
if initDict is not None:
320-
return self.createDocument_(initDict)
321-
else:
322-
return self.createDocument_(self.defaultDocument)
323-
# if self._validation["on_load"]:
324-
# self._validation["on_load"] = False
325-
# self._validation["on_load"] = True
326-
# return self.createDocument_(self.defaultDocument)
327-
328-
def createDocument_(self, initDict = None):
329318
"""create and returns a completely empty document or one populated with initDict"""
330319
res = dict(self.defaultDocument)
331-
if initDict is None:
332-
initV = {}
333-
else:
334-
initV = initDict
335-
res.update(initV)
336-
320+
if initDict is not None:
321+
res.update(initDict)
322+
337323
return self.documentClass(self, res)
338324

339325
def _writeBatch(self):
@@ -614,6 +600,34 @@ def ensureFulltextIndex(self, fields, minLength = None, name = None):
614600
self.indexes_by_name[name] = ind
615601
return ind
616602

603+
def ensureIndex(self, index_type, fields, name=None, **index_args):
604+
"""Creates an index of any type."""
605+
data = {
606+
"type" : index_type,
607+
"fields" : fields,
608+
}
609+
data.update(index_args)
610+
611+
if name:
612+
data["name"] = name
613+
614+
ind = Index(self, creationData = data)
615+
self.indexes[index_type][ind.infos["id"]] = ind
616+
if name:
617+
self.indexes_by_name[name] = ind
618+
return ind
619+
620+
def restoreIndexes(self, indexes_dct=None):
621+
"""restores all previously removed indexes"""
622+
if indexes_dct is None:
623+
indexes_dct = self.indexes
624+
625+
for typ in indexes_dct.keys():
626+
if typ != "primary":
627+
for name, idx in indexes_dct[typ].items():
628+
infos = dict(idx.infos)
629+
del infos["fields"]
630+
self.ensureIndex(typ, idx.infos["fields"], **infos)
617631

618632
def validatePrivate(self, field, value):
619633
"""validate a private field value"""
@@ -757,8 +771,10 @@ def bulkSave(self, docs, onDuplicate="error", **params):
757771
if (r.status_code == 201) and "error" not in data:
758772
return True
759773
else:
760-
if data["errors"] > 0:
774+
if "errors" in data and data["errors"] > 0:
761775
raise UpdateError("%d documents could not be created" % data["errors"], data)
776+
elif data["error"]:
777+
raise UpdateError("Documents could not be created", data)
762778

763779
return data["updated"] + data["created"]
764780

@@ -909,15 +925,9 @@ def validateField(cls, fieldName, value):
909925
raise e
910926
return valValue
911927

912-
def createEdge(self):
928+
def createEdge(self, initValues = None):
913929
"""Create an edge populated with defaults"""
914-
return self.createDocument()
915-
916-
def createEdge_(self, initValues = None):
917-
"""Create an edge populated with initValues"""
918-
if not initValues:
919-
initValues = {}
920-
return self.createDocument_(initValues)
930+
return self.createDocument(initValues)
921931

922932
def getInEdges(self, vertex, rawResults = False):
923933
"""An alias for getEdges() that returns only the in Edges"""

pyArango/connection.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ class AikidoSession(object):
3333
"""
3434

3535
class Holder(object):
36-
def __init__(self, fct, auth, verify=True):
36+
def __init__(self, fct, auth, max_conflict_retries=5, verify=True):
3737
self.fct = fct
3838
self.auth = auth
39+
self.max_conflict_retries = max_conflict_retries
3940
if not isinstance(verify, bool) and not isinstance(verify, CA_Certificate) and not not isinstance(verify, str) :
4041
raise ValueError("'verify' argument can only be of type: bool, CA_Certificate or str ")
4142
self.verify = verify
@@ -49,7 +50,12 @@ def __call__(self, *args, **kwargs):
4950
kwargs["verify"] = self.verify
5051

5152
try:
52-
ret = self.fct(*args, **kwargs)
53+
status_code = 1200
54+
retry = 0
55+
while status_code == 1200 and retry < self.max_conflict_retries :
56+
ret = self.fct(*args, **kwargs)
57+
status_code = ret.status_code
58+
retry += 1
5359
except:
5460
print ("===\nUnable to establish connection, perhaps arango is not running.\n===")
5561
raise
@@ -62,7 +68,7 @@ def __call__(self, *args, **kwargs):
6268
ret.json = JsonHook(ret)
6369
return ret
6470

65-
def __init__(self, username, password, verify=True, max_retries=5, log_requests=False):
71+
def __init__(self, username, password, verify=True, max_conflict_retries=5, max_retries=5, single_session=True, log_requests=False):
6672
if username:
6773
self.auth = (username, password)
6874
else:
@@ -71,19 +77,33 @@ def __init__(self, username, password, verify=True, max_retries=5, log_requests=
7177
self.verify = verify
7278
self.max_retries = max_retries
7379
self.log_requests = log_requests
80+
self.max_conflict_retries = max_conflict_retries
81+
82+
self.session = None
83+
if single_session:
84+
self.session = self._make_session()
7485

7586
if log_requests:
7687
self.log = {}
7788
self.log["nb_request"] = 0
7889
self.log["requests"] = {}
7990

91+
def _make_session(self):
92+
session = requests.Session()
93+
http = requests.adapters.HTTPAdapter(max_retries=self.max_retries)
94+
https = requests.adapters.HTTPAdapter(max_retries=self.max_retries)
95+
session.mount('http://', http)
96+
session.mount('https://', https)
97+
98+
return session
99+
80100
def __getattr__(self, request_function_name):
101+
if self.session is not None:
102+
session = self.session
103+
else :
104+
session = self._make_session()
105+
81106
try:
82-
session = requests.Session()
83-
http = requests.adapters.HTTPAdapter(max_retries=self.max_retries)
84-
https = requests.adapters.HTTPAdapter(max_retries=self.max_retries)
85-
session.mount('http://', http)
86-
session.mount('https://', https)
87107
request_function = getattr(session, request_function_name)
88108
except AttributeError:
89109
raise AttributeError("Attribute '%s' not found (no Aikido move available)" % request_function_name)
@@ -95,15 +115,46 @@ def __getattr__(self, request_function_name):
95115
log["nb_request"] += 1
96116
log["requests"][request_function.__name__] += 1
97117

98-
return AikidoSession.Holder(request_function, auth, verify)
118+
return AikidoSession.Holder(request_function, auth, max_conflict_retries=self.max_conflict_retries, verify=verify)
99119

100120
def disconnect(self):
101121
pass
102122

103123
class Connection(object):
104124
"""This is the entry point in pyArango and directly handles databases.
105125
@param arangoURL: can be either a string url or a list of string urls to different coordinators
106-
@param use_grequests: allows for running concurent requets."""
126+
@param use_grequests: allows for running concurent requets.
127+
128+
Parameters
129+
----------
130+
arangoURL: list or str
131+
list of urls or url for connecting to the db
132+
133+
username: str
134+
for credentials
135+
password: str
136+
for credentials
137+
verify: bool
138+
check the validity of the CA certificate
139+
verbose: bool
140+
flag for addictional prints during run
141+
statsdClient: instance
142+
statsd instance
143+
reportFileName: str
144+
where to save statsd report
145+
loadBalancing: str
146+
type of load balancing between collections
147+
use_grequests: bool
148+
parallelise requests using gevents. Use with care as gevents monkey patches python, this could have unintended concequences on other packages
149+
use_jwt_authentication: bool
150+
use JWT authentication
151+
use_lock_for_reseting_jwt: bool
152+
use lock for reseting gevents authentication
153+
max_retries: int
154+
max number of retries for a request
155+
max_conflict_retries: int
156+
max number of requests for a conflict error (1200 arangodb error). Does not work with gevents (grequests),
157+
"""
107158

108159
LOAD_BLANCING_METHODS = {'round-robin', 'random'}
109160

@@ -120,6 +171,7 @@ def __init__(self,
120171
use_jwt_authentication=False,
121172
use_lock_for_reseting_jwt=True,
122173
max_retries=5,
174+
max_conflict_retries=5
123175
):
124176

125177
if loadBalancing not in Connection.LOAD_BLANCING_METHODS:
@@ -132,6 +184,7 @@ def __init__(self,
132184
self.use_jwt_authentication = use_jwt_authentication
133185
self.use_lock_for_reseting_jwt = use_lock_for_reseting_jwt
134186
self.max_retries = max_retries
187+
self.max_conflict_retries = max_conflict_retries
135188
self.action = ConnectionAction(self)
136189

137190
self.databases = {}
@@ -211,7 +264,16 @@ def resetSession(self, username=None, password=None, verify=True):
211264
verify
212265
)
213266
else:
214-
self.session = AikidoSession(username, password, verify, self.max_retries)
267+
# self.session = AikidoSession(username, password, verify, self.max_retries)
268+
self.session = AikidoSession(
269+
username=username,
270+
password=password,
271+
verify=verify,
272+
single_session=True,
273+
max_conflict_retries=self.max_conflict_retries,
274+
max_retries=self.max_retries,
275+
log_requests=False
276+
)
215277

216278
def reload(self):
217279
"""Reloads the database list.

pyArango/database.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ def createCollection(self, className = 'Collection', **colProperties):
128128
colProperties["type"] = CONST.COLLECTION_DOCUMENT_TYPE
129129

130130
payload = json.dumps(colProperties, default=str)
131-
r = self.connection.session.post(self.getCollectionsURL(), data = payload)
132-
data = r.json()
131+
req = self.connection.session.post(self.getCollectionsURL(), data = payload)
132+
data = req.json()
133133

134-
if r.status_code == 200 and not data["error"]:
134+
if req.status_code == 200 and not data["error"]:
135135
col = colClass(self, data)
136136
self.collections[col.name] = col
137137
return self.collections[col.name]
@@ -210,6 +210,10 @@ def hasGraph(self, name):
210210
"""returns true if the databse has a graph by the name of 'name'"""
211211
return name in self.graphs
212212

213+
def __contains__(self, name):
214+
"""if name in database"""
215+
return self.hasCollection(name) or self.hasGraph(name)
216+
213217
def dropAllCollections(self):
214218
"""drops all public collections (graphs included) from the database"""
215219
for graph_name in self.graphs:
@@ -335,15 +339,15 @@ def fetch_list(
335339
batch_index = 0
336340
result = []
337341
while True:
338-
if len(query.response['result']) is 0:
342+
if len(query.response['result']) == 0:
339343
break
340344
result.extend(query.response['result'])
341345
batch_index += 1
342346
query.nextBatch()
343347
except StopIteration:
344348
if log is not None:
345349
log(result)
346-
if len(result) is not 0:
350+
if len(result) != 0:
347351
return result
348352
except:
349353
raise
@@ -401,7 +405,7 @@ def fetch_list_as_batches(
401405
)
402406
batch_index = 0
403407
while True:
404-
if len(query.response['result']) is 0:
408+
if len(query.response['result']) == 0:
405409
break
406410
if log is not None:
407411
log(
@@ -456,7 +460,7 @@ def no_fetch_run(
456460
).response
457461
if log is not None:
458462
log(response["result"])
459-
if len(response["result"]) is 0:
463+
if len(response["result"]) == 0:
460464
return
461465
raise AQLFetchError("No results should be returned for the query.")
462466

pyArango/doc/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Welcome to pyArango's documentation!
99
.. image:: https://travis-ci.org/tariqdaouda/pyArango.svg?branch=1.2.2
1010
:target: https://travis-ci.org/tariqdaouda/pyArango
1111
.. image:: https://img.shields.io/badge/python-2.7%2C%203.5-blue.svg
12-
.. image:: https://img.shields.io/badge/arangodb-3.0-blue.svg
12+
.. image:: https://img.shields.io/badge/arangodb-3.6-blue.svg
1313

1414
pyArango is a python driver for the NoSQL amazing database ArangoDB_ first written by `Tariq Daouda`_. As of January 2019 pyArango was handed over to the ArangoDB-Community that now ensures the developement and maintenance. It has a very light interface and built in validation. pyArango is distributed under the ApacheV2 Licence and the full source code can be found on github_.
1515

pyArango/document.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ def validateField(self, field):
7070
return self[field].validate()
7171

7272
if field in self.patchStore:
73-
return self.validators[field].validate(self.patchStore[field])
73+
try:
74+
return self.validators[field].validate(self.patchStore[field])
75+
except ValidationError as e:
76+
raise ValidationError( "'%s' -> %s" % ( field, str(e)) )
7477
else:
7578
try:
7679
return self.validators[field].validate(self.store[field])

pyArango/graph.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def createEdge(self, collectionName, _fromId, _toId, edgeAttributes, waitForSync
171171
raise CreationError("Unable to create edge, %s" % r.json()["errorMessage"], data)
172172

173173
def link(self, definition, doc1, doc2, edgeAttributes, waitForSync = False):
174-
"A shorthand for createEdge that takes two documents as input"
174+
"""A shorthand for createEdge that takes two documents as input"""
175175
if type(doc1) is DOC.Document:
176176
if not doc1._id:
177177
doc1.save()
@@ -189,7 +189,7 @@ def link(self, definition, doc1, doc2, edgeAttributes, waitForSync = False):
189189
return self.createEdge(definition, doc1_id, doc2_id, edgeAttributes, waitForSync)
190190

191191
def unlink(self, definition, doc1, doc2):
192-
"deletes all links between doc1 and doc2"
192+
"""deletes all links between doc1 and doc2"""
193193
links = self.database[definition].fetchByExample( {"_from": doc1._id,"_to" : doc2._id}, batchSize = 100)
194194
for l in links:
195195
self.deleteEdge(l)

0 commit comments

Comments
 (0)