204 # connection to the CubicWeb repository |
206 # connection to the CubicWeb repository |
205 cnxprops = ConnectionProperties('inmemory') |
207 cnxprops = ConnectionProperties('inmemory') |
206 cnx = repo_connect(repo, login, cnxprops=cnxprops, **kwargs) |
208 cnx = repo_connect(repo, login, cnxprops=cnxprops, **kwargs) |
207 return repo, cnx |
209 return repo, cnx |
208 |
210 |
|
211 class _NeedAuthAccessMock(object): |
|
212 def __getattribute__(self, attr): |
|
213 raise AuthenticationError() |
|
214 def __nonzero__(self): |
|
215 return False |
|
216 |
|
217 class DBAPISession(object): |
|
218 def __init__(self, cnx, login=None, authinfo=None): |
|
219 self.cnx = cnx |
|
220 self.data = {} |
|
221 self.login = login |
|
222 self.authinfo = authinfo |
|
223 # dbapi session identifier is the same as the first connection |
|
224 # identifier, but may later differ in case of auto-reconnection as done |
|
225 # by the web authentication manager (in cw.web.views.authentication) |
|
226 if cnx is not None: |
|
227 self.sessionid = cnx.sessionid |
|
228 else: |
|
229 self.sessionid = None |
|
230 |
|
231 @property |
|
232 def anonymous_session(self): |
|
233 return not self.cnx or self.cnx.anonymous_connection |
|
234 |
209 |
235 |
210 class DBAPIRequest(RequestSessionBase): |
236 class DBAPIRequest(RequestSessionBase): |
211 |
237 |
212 def __init__(self, vreg, cnx=None): |
238 def __init__(self, vreg, session=None): |
213 super(DBAPIRequest, self).__init__(vreg) |
239 super(DBAPIRequest, self).__init__(vreg) |
214 try: |
240 try: |
215 # no vreg or config which doesn't handle translations |
241 # no vreg or config which doesn't handle translations |
216 self.translations = vreg.config.translations |
242 self.translations = vreg.config.translations |
217 except AttributeError: |
243 except AttributeError: |
218 self.translations = {} |
244 self.translations = {} |
219 self.set_default_language(vreg) |
245 self.set_default_language(vreg) |
220 # cache entities built during the request |
246 # cache entities built during the request |
221 self._eid_cache = {} |
247 self._eid_cache = {} |
222 # these args are initialized after a connection is |
248 if session is not None: |
223 # established |
249 self.set_session(session) |
224 self.cnx = None # connection associated to the request |
250 else: |
225 self._user = None # request's user, set at authentication |
251 # these args are initialized after a connection is |
226 if cnx is not None: |
252 # established |
227 self.set_connection(cnx) |
253 self.session = None |
|
254 self.cnx = self.user = _NeedAuthAccessMock() |
228 |
255 |
229 def base_url(self): |
256 def base_url(self): |
230 return self.vreg.config['base-url'] |
257 return self.vreg.config['base-url'] |
231 |
258 |
232 def from_controller(self): |
259 def from_controller(self): |
233 return 'view' |
260 return 'view' |
234 |
261 |
235 def set_connection(self, cnx, user=None): |
262 def set_session(self, session, user=None): |
236 """method called by the session handler when the user is authenticated |
263 """method called by the session handler when the user is authenticated |
237 or an anonymous connection is open |
264 or an anonymous connection is open |
238 """ |
265 """ |
239 self.cnx = cnx |
266 self.session = session |
240 self.cursor = cnx.cursor(self) |
267 if session.cnx: |
241 self.set_user(user) |
268 self.cnx = session.cnx |
|
269 self.execute = session.cnx.cursor(self).execute |
|
270 if user is None: |
|
271 user = self.cnx.user(self, {'lang': self.lang}) |
|
272 if user is not None: |
|
273 self.user = user |
|
274 self.set_entity_cache(user) |
|
275 |
|
276 def execute(self, *args, **kwargs): |
|
277 """overriden when session is set. By default raise authentication error |
|
278 so authentication is requested. |
|
279 """ |
|
280 raise AuthenticationError() |
242 |
281 |
243 def set_default_language(self, vreg): |
282 def set_default_language(self, vreg): |
244 try: |
283 try: |
245 self.lang = vreg.property_value('ui.language') |
284 self.lang = vreg.property_value('ui.language') |
246 except: # property may not be registered |
285 except: # property may not be registered |
281 else: |
312 else: |
282 del self._eid_cache[eid] |
313 del self._eid_cache[eid] |
283 |
314 |
284 # low level session data management ####################################### |
315 # low level session data management ####################################### |
285 |
316 |
286 def session_data(self): |
|
287 """return a dictionnary containing session data""" |
|
288 return self.cnx.session_data() |
|
289 |
|
290 def get_session_data(self, key, default=None, pop=False): |
|
291 """return value associated to `key` in session data""" |
|
292 if self.cnx is None: |
|
293 return default # before the connection has been established |
|
294 return self.cnx.get_session_data(key, default, pop) |
|
295 |
|
296 def set_session_data(self, key, value): |
|
297 """set value associated to `key` in session data""" |
|
298 return self.cnx.set_session_data(key, value) |
|
299 |
|
300 def del_session_data(self, key): |
|
301 """remove value associated to `key` in session data""" |
|
302 return self.cnx.del_session_data(key) |
|
303 |
|
304 def get_shared_data(self, key, default=None, pop=False): |
317 def get_shared_data(self, key, default=None, pop=False): |
305 """return value associated to `key` in shared data""" |
318 """return value associated to `key` in shared data""" |
306 return self.cnx.get_shared_data(key, default, pop) |
319 return self.cnx.get_shared_data(key, default, pop) |
307 |
320 |
308 def set_shared_data(self, key, value, querydata=False): |
321 def set_shared_data(self, key, value, querydata=False): |
315 """ |
328 """ |
316 return self.cnx.set_shared_data(key, value, querydata) |
329 return self.cnx.set_shared_data(key, value, querydata) |
317 |
330 |
318 # server session compat layer ############################################# |
331 # server session compat layer ############################################# |
319 |
332 |
|
333 def describe(self, eid): |
|
334 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
|
335 return self.cnx.describe(eid) |
|
336 |
|
337 def source_defs(self): |
|
338 """return the definition of sources used by the repository.""" |
|
339 return self.cnx.source_defs() |
|
340 |
320 def hijack_user(self, user): |
341 def hijack_user(self, user): |
321 """return a fake request/session using specified user""" |
342 """return a fake request/session using specified user""" |
322 req = DBAPIRequest(self.vreg) |
343 req = DBAPIRequest(self.vreg) |
323 req.set_connection(self.cnx, user) |
344 req.set_session(self.session, user) |
324 return req |
345 return req |
325 |
346 |
326 @property |
347 @deprecated('[3.8] use direct access to req.session.data dictionary') |
327 def user(self): |
348 def session_data(self): |
328 if self._user is None and self.cnx: |
349 """return a dictionnary containing session data""" |
329 self.set_user(self.cnx.user(self, {'lang': self.lang})) |
350 return self.session.data |
330 return self._user |
351 |
331 |
352 @deprecated('[3.8] use direct access to req.session.data dictionary') |
332 def set_user(self, user): |
353 def get_session_data(self, key, default=None, pop=False): |
333 self._user = user |
354 if pop: |
334 if user: |
355 return self.session.data.pop(key, default) |
335 self.set_entity_cache(user) |
356 return self.session.data.get(key, default) |
336 |
357 |
337 def execute(self, *args, **kwargs): |
358 @deprecated('[3.8] use direct access to req.session.data dictionary') |
338 """Session interface compatibility""" |
359 def set_session_data(self, key, value): |
339 return self.cursor.execute(*args, **kwargs) |
360 self.session.data[key] = value |
|
361 |
|
362 @deprecated('[3.8] use direct access to req.session.data dictionary') |
|
363 def del_session_data(self, key): |
|
364 self.session.data.pop(key, None) |
|
365 |
340 |
366 |
341 set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi')) |
367 set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi')) |
342 |
368 |
343 |
369 |
344 # exceptions ################################################################## |
370 # exceptions ################################################################## |
349 disconnect occurs, the data source name is not found, a transaction could |
375 disconnect occurs, the data source name is not found, a transaction could |
350 not be processed, a memory allocation error occurred during processing, |
376 not be processed, a memory allocation error occurred during processing, |
351 etc. |
377 etc. |
352 """ |
378 """ |
353 |
379 |
354 # module level objects ######################################################## |
380 |
355 |
381 # cursor / connection objects ################################################## |
356 |
382 |
357 apilevel = '2.0' |
383 class Cursor(object): |
358 |
384 """These objects represent a database cursor, which is used to manage the |
359 """Integer constant stating the level of thread safety the interface supports. |
385 context of a fetch operation. Cursors created from the same connection are |
360 Possible values are: |
386 not isolated, i.e., any changes done to the database by a cursor are |
361 |
387 immediately visible by the other cursors. Cursors created from different |
362 0 Threads may not share the module. |
388 connections are isolated. |
363 1 Threads may share the module, but not connections. |
389 """ |
364 2 Threads may share the module and connections. |
390 |
365 3 Threads may share the module, connections and |
391 def __init__(self, connection, repo, req=None): |
366 cursors. |
392 """This read-only attribute return a reference to the Connection |
367 |
393 object on which the cursor was created. |
368 Sharing in the above context means that two threads may use a resource without |
394 """ |
369 wrapping it using a mutex semaphore to implement resource locking. Note that |
395 self.connection = connection |
370 you cannot always make external resources thread safe by managing access using |
396 """optionnal issuing request instance""" |
371 a mutex: the resource may rely on global variables or other external sources |
397 self.req = req |
372 that are beyond your control. |
398 self._repo = repo |
373 """ |
399 self._sessid = connection.sessionid |
374 threadsafety = 1 |
400 |
375 |
401 def close(self): |
376 """String constant stating the type of parameter marker formatting expected by |
402 """no effect""" |
377 the interface. Possible values are : |
403 pass |
378 |
404 |
379 'qmark' Question mark style, |
405 def execute(self, rql, args=None, eid_key=None, build_descr=True): |
380 e.g. '...WHERE name=?' |
406 """execute a rql query, return resulting rows and their description in |
381 'numeric' Numeric, positional style, |
407 a :class:`~cubicweb.rset.ResultSet` object |
382 e.g. '...WHERE name=:1' |
408 |
383 'named' Named style, |
409 * `rql` should be an Unicode string or a plain ASCII string, containing |
384 e.g. '...WHERE name=:name' |
410 the rql query |
385 'format' ANSI C printf format codes, |
411 |
386 e.g. '...WHERE name=%s' |
412 * `args` the optional args dictionary associated to the query, with key |
387 'pyformat' Python extended format codes, |
413 matching named substitution in `rql` |
388 e.g. '...WHERE name=%(name)s' |
414 |
389 """ |
415 * `build_descr` is a boolean flag indicating if the description should |
390 paramstyle = 'pyformat' |
416 be built on select queries (if false, the description will be en empty |
391 |
417 list) |
392 |
418 |
393 # connection object ########################################################### |
419 on INSERT queries, there will be one row for each inserted entity, |
|
420 containing its eid |
|
421 |
|
422 on SET queries, XXX describe |
|
423 |
|
424 DELETE queries returns no result. |
|
425 |
|
426 .. Note:: |
|
427 to maximize the rql parsing/analyzing cache performance, you should |
|
428 always use substitute arguments in queries, i.e. avoid query such as:: |
|
429 |
|
430 execute('Any X WHERE X eid 123') |
|
431 |
|
432 use:: |
|
433 |
|
434 execute('Any X WHERE X eid %(x)s', {'x': 123}) |
|
435 """ |
|
436 if eid_key is not None: |
|
437 warn('[3.8] eid_key is deprecated, you can safely remove this argument', |
|
438 DeprecationWarning, stacklevel=2) |
|
439 # XXX use named argument for build_descr in case repo is < 3.8 |
|
440 rset = self._repo.execute(self._sessid, rql, args, build_descr=build_descr) |
|
441 rset.req = self.req |
|
442 return rset |
|
443 |
|
444 |
|
445 class LogCursor(Cursor): |
|
446 """override the standard cursor to log executed queries""" |
|
447 |
|
448 def execute(self, operation, parameters=None, eid_key=None, build_descr=True): |
|
449 """override the standard cursor to log executed queries""" |
|
450 if eid_key is not None: |
|
451 warn('[3.8] eid_key is deprecated, you can safely remove this argument', |
|
452 DeprecationWarning, stacklevel=2) |
|
453 tstart, cstart = time(), clock() |
|
454 rset = Cursor.execute(self, operation, parameters, build_descr=build_descr) |
|
455 self.connection.executed_queries.append((operation, parameters, |
|
456 time() - tstart, clock() - cstart)) |
|
457 return rset |
|
458 |
394 |
459 |
395 class Connection(object): |
460 class Connection(object): |
396 """DB-API 2.0 compatible Connection object for CubicWeb |
461 """DB-API 2.0 compatible Connection object for CubicWeb |
397 """ |
462 """ |
398 # make exceptions available through the connection object |
463 # make exceptions available through the connection object |
399 ProgrammingError = ProgrammingError |
464 ProgrammingError = ProgrammingError |
|
465 # attributes that may be overriden per connection instance |
|
466 anonymous_connection = False |
|
467 cursor_class = Cursor |
|
468 vreg = None |
|
469 _closed = None |
400 |
470 |
401 def __init__(self, repo, cnxid, cnxprops=None): |
471 def __init__(self, repo, cnxid, cnxprops=None): |
402 self._repo = repo |
472 self._repo = repo |
403 self.sessionid = cnxid |
473 self.sessionid = cnxid |
404 self._close_on_del = getattr(cnxprops, 'close_on_del', True) |
474 self._close_on_del = getattr(cnxprops, 'close_on_del', True) |
405 self._cnxtype = getattr(cnxprops, 'cnxtype', 'pyro') |
475 self._cnxtype = getattr(cnxprops, 'cnxtype', 'pyro') |
406 self._closed = None |
|
407 if cnxprops and cnxprops.log_queries: |
476 if cnxprops and cnxprops.log_queries: |
408 self.executed_queries = [] |
477 self.executed_queries = [] |
409 self.cursor_class = LogCursor |
478 self.cursor_class = LogCursor |
410 else: |
|
411 self.cursor_class = Cursor |
|
412 self.anonymous_connection = False |
|
413 self.vreg = None |
|
414 # session's data |
|
415 self.data = {} |
|
416 |
479 |
417 def __repr__(self): |
480 def __repr__(self): |
418 if self.anonymous_connection: |
481 if self.anonymous_connection: |
419 return '<Connection %s (anonymous)>' % self.sessionid |
482 return '<Connection %s (anonymous)>' % self.sessionid |
420 return '<Connection %s>' % self.sessionid |
483 return '<Connection %s>' % self.sessionid |
709 raise `NoSuchTransaction` if not found or if session's user is not |
752 raise `NoSuchTransaction` if not found or if session's user is not |
710 allowed (eg not in managers group and the transaction doesn't belong to |
753 allowed (eg not in managers group and the transaction doesn't belong to |
711 him). |
754 him). |
712 """ |
755 """ |
713 return self._repo.undo_transaction(self.sessionid, txuuid) |
756 return self._repo.undo_transaction(self.sessionid, txuuid) |
714 |
|
715 |
|
716 # cursor object ############################################################### |
|
717 |
|
718 class Cursor(object): |
|
719 """This represents a database cursor, which is used to manage the |
|
720 context of a fetch operation. Cursors created from the same connection are |
|
721 not isolated, i.e., any changes done to the database by a cursor are |
|
722 immediately visible by the other cursors. Cursors created from different |
|
723 connections can or can not be isolated, depending on how the transaction |
|
724 support is implemented (see also the connection's rollback() and commit() |
|
725 methods.) |
|
726 """ |
|
727 |
|
728 def __init__(self, connection, repo, req=None): |
|
729 # This read-only attribute returns a reference to the Connection |
|
730 # object on which the cursor was created. |
|
731 self.connection = connection |
|
732 # optionnal issuing request instance |
|
733 self.req = req |
|
734 |
|
735 # This read/write attribute specifies the number of rows to fetch at a |
|
736 # time with fetchmany(). It defaults to 1 meaning to fetch a single row |
|
737 # at a time. |
|
738 # Implementations must observe this value with respect to the fetchmany() |
|
739 # method, but are free to interact with the database a single row at a |
|
740 # time. It may also be used in the implementation of executemany(). |
|
741 self.arraysize = 1 |
|
742 |
|
743 self._repo = repo |
|
744 self._sessid = connection.sessionid |
|
745 self._res = None |
|
746 self._closed = None |
|
747 self._index = 0 |
|
748 |
|
749 def close(self): |
|
750 """Close the cursor now (rather than whenever __del__ is called). The |
|
751 cursor will be unusable from this point forward; an Error (or subclass) |
|
752 exception will be raised if any operation is attempted with the cursor. |
|
753 """ |
|
754 self._closed = True |
|
755 |
|
756 |
|
757 def execute(self, operation, parameters=None, eid_key=None, build_descr=True): |
|
758 """Prepare and execute a database operation (query or command). |
|
759 Parameters may be provided as sequence or mapping and will be bound to |
|
760 variables in the operation. Variables are specified in a |
|
761 database-specific notation (see the module's paramstyle attribute for |
|
762 details). |
|
763 |
|
764 A reference to the operation will be retained by the cursor. If the |
|
765 same operation object is passed in again, then the cursor can optimize |
|
766 its behavior. This is most effective for algorithms where the same |
|
767 operation is used, but different parameters are bound to it (many |
|
768 times). |
|
769 |
|
770 For maximum efficiency when reusing an operation, it is best to use the |
|
771 setinputsizes() method to specify the parameter types and sizes ahead |
|
772 of time. It is legal for a parameter to not match the predefined |
|
773 information; the implementation should compensate, possibly with a loss |
|
774 of efficiency. |
|
775 |
|
776 The parameters may also be specified as list of tuples to e.g. insert |
|
777 multiple rows in a single operation, but this kind of usage is |
|
778 depreciated: executemany() should be used instead. |
|
779 |
|
780 Return values are not defined by the DB-API, but this here it returns a |
|
781 ResultSet object. |
|
782 """ |
|
783 self._res = rset = self._repo.execute(self._sessid, operation, |
|
784 parameters, eid_key, build_descr) |
|
785 rset.req = self.req |
|
786 self._index = 0 |
|
787 return rset |
|
788 |
|
789 |
|
790 def executemany(self, operation, seq_of_parameters): |
|
791 """Prepare a database operation (query or command) and then execute it |
|
792 against all parameter sequences or mappings found in the sequence |
|
793 seq_of_parameters. |
|
794 |
|
795 Modules are free to implement this method using multiple calls to the |
|
796 execute() method or by using array operations to have the database |
|
797 process the sequence as a whole in one call. |
|
798 |
|
799 Use of this method for an operation which produces one or more result |
|
800 sets constitutes undefined behavior, and the implementation is |
|
801 permitted (but not required) to raise an exception when it detects that |
|
802 a result set has been created by an invocation of the operation. |
|
803 |
|
804 The same comments as for execute() also apply accordingly to this |
|
805 method. |
|
806 |
|
807 Return values are not defined. |
|
808 """ |
|
809 for parameters in seq_of_parameters: |
|
810 self.execute(operation, parameters) |
|
811 if self._res.rows is not None: |
|
812 self._res = None |
|
813 raise ProgrammingError('Operation returned a result set') |
|
814 |
|
815 |
|
816 def fetchone(self): |
|
817 """Fetch the next row of a query result set, returning a single |
|
818 sequence, or None when no more data is available. |
|
819 |
|
820 An Error (or subclass) exception is raised if the previous call to |
|
821 execute*() did not produce any result set or no call was issued yet. |
|
822 """ |
|
823 if self._res is None: |
|
824 raise ProgrammingError('No result set') |
|
825 row = self._res.rows[self._index] |
|
826 self._index += 1 |
|
827 return row |
|
828 |
|
829 |
|
830 def fetchmany(self, size=None): |
|
831 """Fetch the next set of rows of a query result, returning a sequence |
|
832 of sequences (e.g. a list of tuples). An empty sequence is returned |
|
833 when no more rows are available. |
|
834 |
|
835 The number of rows to fetch per call is specified by the parameter. If |
|
836 it is not given, the cursor's arraysize determines the number of rows |
|
837 to be fetched. The method should try to fetch as many rows as indicated |
|
838 by the size parameter. If this is not possible due to the specified |
|
839 number of rows not being available, fewer rows may be returned. |
|
840 |
|
841 An Error (or subclass) exception is raised if the previous call to |
|
842 execute*() did not produce any result set or no call was issued yet. |
|
843 |
|
844 Note there are performance considerations involved with the size |
|
845 parameter. For optimal performance, it is usually best to use the |
|
846 arraysize attribute. If the size parameter is used, then it is best |
|
847 for it to retain the same value from one fetchmany() call to the next. |
|
848 """ |
|
849 if self._res is None: |
|
850 raise ProgrammingError('No result set') |
|
851 if size is None: |
|
852 size = self.arraysize |
|
853 rows = self._res.rows[self._index:self._index + size] |
|
854 self._index += size |
|
855 return rows |
|
856 |
|
857 |
|
858 def fetchall(self): |
|
859 """Fetch all (remaining) rows of a query result, returning them as a |
|
860 sequence of sequences (e.g. a list of tuples). Note that the cursor's |
|
861 arraysize attribute can affect the performance of this operation. |
|
862 |
|
863 An Error (or subclass) exception is raised if the previous call to |
|
864 execute*() did not produce any result set or no call was issued yet. |
|
865 """ |
|
866 if self._res is None: |
|
867 raise ProgrammingError('No result set') |
|
868 if not self._res.rows: |
|
869 return [] |
|
870 rows = self._res.rows[self._index:] |
|
871 self._index = len(self._res) |
|
872 return rows |
|
873 |
|
874 |
|
875 def setinputsizes(self, sizes): |
|
876 """This can be used before a call to execute*() to predefine memory |
|
877 areas for the operation's parameters. |
|
878 |
|
879 sizes is specified as a sequence -- one item for each input parameter. |
|
880 The item should be a Type Object that corresponds to the input that |
|
881 will be used, or it should be an integer specifying the maximum length |
|
882 of a string parameter. If the item is None, then no predefined memory |
|
883 area will be reserved for that column (this is useful to avoid |
|
884 predefined areas for large inputs). |
|
885 |
|
886 This method would be used before the execute*() method is invoked. |
|
887 |
|
888 Implementations are free to have this method do nothing and users are |
|
889 free to not use it. |
|
890 """ |
|
891 pass |
|
892 |
|
893 |
|
894 def setoutputsize(self, size, column=None): |
|
895 """Set a column buffer size for fetches of large columns (e.g. LONGs, |
|
896 BLOBs, etc.). The column is specified as an index into the result |
|
897 sequence. Not specifying the column will set the default size for all |
|
898 large columns in the cursor. |
|
899 |
|
900 This method would be used before the execute*() method is invoked. |
|
901 |
|
902 Implementations are free to have this method do nothing and users are |
|
903 free to not use it. |
|
904 """ |
|
905 pass |
|
906 |
|
907 |
|
908 class LogCursor(Cursor): |
|
909 """override the standard cursor to log executed queries""" |
|
910 |
|
911 def execute(self, operation, parameters=None, eid_key=None, build_descr=True): |
|
912 """override the standard cursor to log executed queries""" |
|
913 tstart, cstart = time(), clock() |
|
914 rset = Cursor.execute(self, operation, parameters, eid_key, build_descr) |
|
915 self.connection.executed_queries.append((operation, parameters, |
|
916 time() - tstart, clock() - cstart)) |
|
917 return rset |
|