1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """DB-API 2.0 compliant module |
|
19 |
|
20 Take a look at http://www.python.org/peps/pep-0249.html |
|
21 |
|
22 (most parts of this document are reported here in docstrings) |
|
23 """ |
|
24 |
|
25 __docformat__ = "restructuredtext en" |
|
26 |
|
27 from threading import currentThread |
|
28 from logging import getLogger |
|
29 from time import time, clock |
|
30 from itertools import count |
|
31 from warnings import warn |
|
32 from os.path import join |
|
33 from uuid import uuid4 |
|
34 from urlparse import urlparse |
|
35 |
|
36 from logilab.common.logging_ext import set_log_methods |
|
37 from logilab.common.decorators import monkeypatch, cachedproperty |
|
38 from logilab.common.deprecation import deprecated |
|
39 |
|
40 from cubicweb import (ETYPE_NAME_MAP, AuthenticationError, ProgrammingError, |
|
41 cwvreg, cwconfig) |
|
42 from cubicweb.repoapi import get_repository |
|
43 from cubicweb.req import RequestSessionBase |
|
44 |
|
45 |
|
46 _MARKER = object() |
|
47 |
|
48 def _fake_property_value(self, name): |
|
49 try: |
|
50 return super(DBAPIRequest, self).property_value(name) |
|
51 except KeyError: |
|
52 return '' |
|
53 |
|
54 def fake(*args, **kwargs): |
|
55 return None |
|
56 |
|
57 def multiple_connections_fix(): |
|
58 """some monkey patching necessary when an application has to deal with |
|
59 several connections to different repositories. It tries to hide buggy class |
|
60 attributes since classes are not designed to be shared among multiple |
|
61 registries. |
|
62 """ |
|
63 defaultcls = cwvreg.CWRegistryStore.REGISTRY_FACTORY[None] |
|
64 |
|
65 etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes'] |
|
66 orig_etype_class = etypescls.orig_etype_class = etypescls.etype_class |
|
67 @monkeypatch(defaultcls) |
|
68 def etype_class(self, etype): |
|
69 """return an entity class for the given entity type. |
|
70 Try to find out a specific class for this kind of entity or |
|
71 default to a dump of the class registered for 'Any' |
|
72 """ |
|
73 usercls = orig_etype_class(self, etype) |
|
74 if etype == 'Any': |
|
75 return usercls |
|
76 usercls.e_schema = self.schema.eschema(etype) |
|
77 return usercls |
|
78 |
|
79 def multiple_connections_unfix(): |
|
80 etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes'] |
|
81 etypescls.etype_class = etypescls.orig_etype_class |
|
82 |
|
83 |
|
84 class ConnectionProperties(object): |
|
85 def __init__(self, cnxtype=None, close=True, log=False): |
|
86 if cnxtype is not None: |
|
87 warn('[3.16] cnxtype argument is deprecated', DeprecationWarning, |
|
88 stacklevel=2) |
|
89 self.cnxtype = cnxtype |
|
90 self.log_queries = log |
|
91 self.close_on_del = close |
|
92 |
|
93 |
|
94 @deprecated('[3.19] the dbapi is deprecated. Have a look at the new repoapi.') |
|
95 def _repo_connect(repo, login, **kwargs): |
|
96 """Constructor to create a new connection to the given CubicWeb repository. |
|
97 |
|
98 Returns a Connection instance. |
|
99 |
|
100 Raises AuthenticationError if authentication failed |
|
101 """ |
|
102 cnxid = repo.connect(unicode(login), **kwargs) |
|
103 cnx = Connection(repo, cnxid, kwargs.get('cnxprops')) |
|
104 if cnx.is_repo_in_memory: |
|
105 cnx.vreg = repo.vreg |
|
106 return cnx |
|
107 |
|
108 def connect(database, login=None, |
|
109 cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs): |
|
110 """Constructor for creating a connection to the CubicWeb repository. |
|
111 Returns a :class:`Connection` object. |
|
112 |
|
113 Typical usage:: |
|
114 |
|
115 cnx = connect('myinstance', login='me', password='toto') |
|
116 |
|
117 `database` may be: |
|
118 |
|
119 * a simple instance id for in-memory connection |
|
120 |
|
121 * a uri like scheme://host:port/instanceid where scheme must be |
|
122 'inmemory' |
|
123 |
|
124 Other arguments: |
|
125 |
|
126 :login: |
|
127 the user login to use to authenticate. |
|
128 |
|
129 :cnxprops: |
|
130 a :class:`ConnectionProperties` instance, allowing to specify |
|
131 the connection method (eg in memory). |
|
132 |
|
133 :setvreg: |
|
134 flag telling if a registry should be initialized for the connection. |
|
135 Don't change this unless you know what you're doing. |
|
136 |
|
137 :mulcnx: |
|
138 Will disappear at some point. Try to deal with connections to differents |
|
139 instances in the same process unless specified otherwise by setting this |
|
140 flag to False. Don't change this unless you know what you're doing. |
|
141 |
|
142 :initlog: |
|
143 flag telling if logging should be initialized. You usually don't want |
|
144 logging initialization when establishing the connection from a process |
|
145 where it's already initialized. |
|
146 |
|
147 :kwargs: |
|
148 there goes authentication tokens. You usually have to specify a password |
|
149 for the given user, using a named 'password' argument. |
|
150 |
|
151 """ |
|
152 if not urlparse(database).scheme: |
|
153 warn('[3.16] give an qualified URI as database instead of using ' |
|
154 'host/cnxprops to specify the connection method', |
|
155 DeprecationWarning, stacklevel=2) |
|
156 puri = urlparse(database) |
|
157 method = puri.scheme.lower() |
|
158 assert method == 'inmemory' |
|
159 config = cwconfig.instance_configuration(puri.netloc) |
|
160 repo = get_repository(database, config=config) |
|
161 vreg = repo.vreg |
|
162 cnx = _repo_connect(repo, login, cnxprops=cnxprops, **kwargs) |
|
163 cnx.vreg = vreg |
|
164 return cnx |
|
165 |
|
166 def in_memory_repo(config): |
|
167 """Return and in_memory Repository object from a config (or vreg)""" |
|
168 if isinstance(config, cwvreg.CWRegistryStore): |
|
169 vreg = config |
|
170 config = None |
|
171 else: |
|
172 vreg = None |
|
173 # get local access to the repository |
|
174 return get_repository('inmemory://', config=config, vreg=vreg) |
|
175 |
|
176 def in_memory_repo_cnx(config, login, **kwargs): |
|
177 """useful method for testing and scripting to get a dbapi.Connection |
|
178 object connected to an in-memory repository instance |
|
179 """ |
|
180 # connection to the CubicWeb repository |
|
181 repo = in_memory_repo(config) |
|
182 return repo, _repo_connect(repo, login, **kwargs) |
|
183 |
|
184 # XXX web only method, move to webconfig? |
|
185 def anonymous_session(vreg): |
|
186 """return a new anonymous session |
|
187 |
|
188 raises an AuthenticationError if anonymous usage is not allowed |
|
189 """ |
|
190 anoninfo = vreg.config.anonymous_user() |
|
191 if anoninfo[0] is None: # no anonymous user |
|
192 raise AuthenticationError('anonymous access is not authorized') |
|
193 anon_login, anon_password = anoninfo |
|
194 # use vreg's repository cache |
|
195 repo = vreg.config.repository(vreg) |
|
196 anon_cnx = _repo_connect(repo, anon_login, password=anon_password) |
|
197 anon_cnx.vreg = vreg |
|
198 return DBAPISession(anon_cnx, anon_login) |
|
199 |
|
200 |
|
201 class _NeedAuthAccessMock(object): |
|
202 def __getattribute__(self, attr): |
|
203 raise AuthenticationError() |
|
204 def __nonzero__(self): |
|
205 return False |
|
206 |
|
207 class DBAPISession(object): |
|
208 def __init__(self, cnx, login=None): |
|
209 self.cnx = cnx |
|
210 self.data = {} |
|
211 self.login = login |
|
212 # dbapi session identifier is the same as the first connection |
|
213 # identifier, but may later differ in case of auto-reconnection as done |
|
214 # by the web authentication manager (in cw.web.views.authentication) |
|
215 if cnx is not None: |
|
216 self.sessionid = cnx.sessionid |
|
217 else: |
|
218 self.sessionid = uuid4().hex |
|
219 |
|
220 @property |
|
221 def anonymous_session(self): |
|
222 return not self.cnx or self.cnx.anonymous_connection |
|
223 |
|
224 def __repr__(self): |
|
225 return '<DBAPISession %r>' % self.sessionid |
|
226 |
|
227 |
|
228 class DBAPIRequest(RequestSessionBase): |
|
229 #: Request language identifier eg: 'en' |
|
230 lang = None |
|
231 |
|
232 def __init__(self, vreg, session=None): |
|
233 super(DBAPIRequest, self).__init__(vreg) |
|
234 #: 'language' => translation_function() mapping |
|
235 try: |
|
236 # no vreg or config which doesn't handle translations |
|
237 self.translations = vreg.config.translations |
|
238 except AttributeError: |
|
239 self.translations = {} |
|
240 #: cache entities built during the request |
|
241 self._eid_cache = {} |
|
242 if session is not None: |
|
243 self.set_session(session) |
|
244 else: |
|
245 # these args are initialized after a connection is |
|
246 # established |
|
247 self.session = DBAPISession(None) |
|
248 self.cnx = self.user = _NeedAuthAccessMock() |
|
249 self.set_default_language(vreg) |
|
250 |
|
251 def get_option_value(self, option, foreid=None): |
|
252 if foreid is not None: |
|
253 warn('[3.19] foreid argument is deprecated', DeprecationWarning, |
|
254 stacklevel=2) |
|
255 return self.cnx.get_option_value(option) |
|
256 |
|
257 def set_session(self, session): |
|
258 """method called by the session handler when the user is authenticated |
|
259 or an anonymous connection is open |
|
260 """ |
|
261 self.session = session |
|
262 if session.cnx: |
|
263 self.cnx = session.cnx |
|
264 self.execute = session.cnx.cursor(self).execute |
|
265 self.user = self.cnx.user(self) |
|
266 self.set_entity_cache(self.user) |
|
267 |
|
268 def execute(self, *args, **kwargs): # pylint: disable=E0202 |
|
269 """overriden when session is set. By default raise authentication error |
|
270 so authentication is requested. |
|
271 """ |
|
272 raise AuthenticationError() |
|
273 |
|
274 def set_default_language(self, vreg): |
|
275 try: |
|
276 lang = vreg.property_value('ui.language') |
|
277 except Exception: # property may not be registered |
|
278 lang = 'en' |
|
279 try: |
|
280 self.set_language(lang) |
|
281 except KeyError: |
|
282 # this occurs usually during test execution |
|
283 self._ = self.__ = unicode |
|
284 self.pgettext = lambda x, y: unicode(y) |
|
285 |
|
286 # server-side service call ################################################# |
|
287 |
|
288 def call_service(self, regid, **kwargs): |
|
289 return self.cnx.call_service(regid, **kwargs) |
|
290 |
|
291 # entities cache management ############################################### |
|
292 |
|
293 def entity_cache(self, eid): |
|
294 return self._eid_cache[eid] |
|
295 |
|
296 def set_entity_cache(self, entity): |
|
297 self._eid_cache[entity.eid] = entity |
|
298 |
|
299 def cached_entities(self): |
|
300 return self._eid_cache.values() |
|
301 |
|
302 def drop_entity_cache(self, eid=None): |
|
303 if eid is None: |
|
304 self._eid_cache = {} |
|
305 else: |
|
306 del self._eid_cache[eid] |
|
307 |
|
308 # low level session data management ####################################### |
|
309 |
|
310 @deprecated('[3.19] use session or transaction data') |
|
311 def get_shared_data(self, key, default=None, pop=False, txdata=False): |
|
312 """see :meth:`Connection.get_shared_data`""" |
|
313 return self.cnx.get_shared_data(key, default, pop, txdata) |
|
314 |
|
315 @deprecated('[3.19] use session or transaction data') |
|
316 def set_shared_data(self, key, value, txdata=False, querydata=None): |
|
317 """see :meth:`Connection.set_shared_data`""" |
|
318 if querydata is not None: |
|
319 txdata = querydata |
|
320 warn('[3.10] querydata argument has been renamed to txdata', |
|
321 DeprecationWarning, stacklevel=2) |
|
322 return self.cnx.set_shared_data(key, value, txdata) |
|
323 |
|
324 # server session compat layer ############################################# |
|
325 |
|
326 def entity_metas(self, eid): |
|
327 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
|
328 return self.cnx.entity_metas(eid) |
|
329 |
|
330 def source_defs(self): |
|
331 """return the definition of sources used by the repository.""" |
|
332 return self.cnx.source_defs() |
|
333 |
|
334 @deprecated('[3.19] use .entity_metas(eid) instead') |
|
335 def describe(self, eid, asdict=False): |
|
336 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
|
337 return self.cnx.describe(eid, asdict) |
|
338 |
|
339 # these are overridden by set_log_methods below |
|
340 # only defining here to prevent pylint from complaining |
|
341 info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None |
|
342 |
|
343 set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi')) |
|
344 |
|
345 |
|
346 |
|
347 # cursor / connection objects ################################################## |
|
348 |
|
349 class Cursor(object): |
|
350 """These objects represent a database cursor, which is used to manage the |
|
351 context of a fetch operation. Cursors created from the same connection are |
|
352 not isolated, i.e., any changes done to the database by a cursor are |
|
353 immediately visible by the other cursors. Cursors created from different |
|
354 connections are isolated. |
|
355 """ |
|
356 |
|
357 def __init__(self, connection, repo, req=None): |
|
358 """This read-only attribute return a reference to the Connection |
|
359 object on which the cursor was created. |
|
360 """ |
|
361 self.connection = connection |
|
362 """optionnal issuing request instance""" |
|
363 self.req = req |
|
364 self._repo = repo |
|
365 self._sessid = connection.sessionid |
|
366 |
|
367 def close(self): |
|
368 """no effect""" |
|
369 pass |
|
370 |
|
371 def _txid(self): |
|
372 return self.connection._txid(self) |
|
373 |
|
374 def execute(self, rql, args=None, build_descr=True): |
|
375 """execute a rql query, return resulting rows and their description in |
|
376 a :class:`~cubicweb.rset.ResultSet` object |
|
377 |
|
378 * `rql` should be a Unicode string or a plain ASCII string, containing |
|
379 the rql query |
|
380 |
|
381 * `args` the optional args dictionary associated to the query, with key |
|
382 matching named substitution in `rql` |
|
383 |
|
384 * `build_descr` is a boolean flag indicating if the description should |
|
385 be built on select queries (if false, the description will be en empty |
|
386 list) |
|
387 |
|
388 on INSERT queries, there will be one row for each inserted entity, |
|
389 containing its eid |
|
390 |
|
391 on SET queries, XXX describe |
|
392 |
|
393 DELETE queries returns no result. |
|
394 |
|
395 .. Note:: |
|
396 to maximize the rql parsing/analyzing cache performance, you should |
|
397 always use substitute arguments in queries, i.e. avoid query such as:: |
|
398 |
|
399 execute('Any X WHERE X eid 123') |
|
400 |
|
401 use:: |
|
402 |
|
403 execute('Any X WHERE X eid %(x)s', {'x': 123}) |
|
404 """ |
|
405 rset = self._repo.execute(self._sessid, rql, args, |
|
406 build_descr=build_descr, **self._txid()) |
|
407 rset.req = self.req |
|
408 return rset |
|
409 |
|
410 |
|
411 class LogCursor(Cursor): |
|
412 """override the standard cursor to log executed queries""" |
|
413 |
|
414 def execute(self, operation, parameters=None, build_descr=True): |
|
415 """override the standard cursor to log executed queries""" |
|
416 tstart, cstart = time(), clock() |
|
417 rset = Cursor.execute(self, operation, parameters, build_descr=build_descr) |
|
418 self.connection.executed_queries.append((operation, parameters, |
|
419 time() - tstart, clock() - cstart)) |
|
420 return rset |
|
421 |
|
422 def check_not_closed(func): |
|
423 def decorator(self, *args, **kwargs): |
|
424 if self._closed is not None: |
|
425 raise ProgrammingError('Closed connection %s' % self.sessionid) |
|
426 return func(self, *args, **kwargs) |
|
427 return decorator |
|
428 |
|
429 class Connection(object): |
|
430 """DB-API 2.0 compatible Connection object for CubicWeb |
|
431 """ |
|
432 # make exceptions available through the connection object |
|
433 ProgrammingError = ProgrammingError |
|
434 # attributes that may be overriden per connection instance |
|
435 cursor_class = Cursor |
|
436 vreg = None |
|
437 _closed = None |
|
438 |
|
439 def __init__(self, repo, cnxid, cnxprops=None): |
|
440 self._repo = repo |
|
441 self.sessionid = cnxid |
|
442 self._close_on_del = getattr(cnxprops, 'close_on_del', True) |
|
443 self._web_request = False |
|
444 if cnxprops and cnxprops.log_queries: |
|
445 self.executed_queries = [] |
|
446 self.cursor_class = LogCursor |
|
447 |
|
448 @property |
|
449 def is_repo_in_memory(self): |
|
450 """return True if this is a local, aka in-memory, connection to the |
|
451 repository |
|
452 """ |
|
453 try: |
|
454 from cubicweb.server.repository import Repository |
|
455 except ImportError: |
|
456 # code not available, no way |
|
457 return False |
|
458 return isinstance(self._repo, Repository) |
|
459 |
|
460 @property # could be a cached property but we want to prevent assigment to |
|
461 # catch potential programming error. |
|
462 def anonymous_connection(self): |
|
463 login = self._repo.user_info(self.sessionid)[1] |
|
464 anon_login = self.vreg.config.get('anonymous-user') |
|
465 return login == anon_login |
|
466 |
|
467 def __repr__(self): |
|
468 if self.anonymous_connection: |
|
469 return '<Connection %s (anonymous)>' % self.sessionid |
|
470 return '<Connection %s>' % self.sessionid |
|
471 |
|
472 def __enter__(self): |
|
473 return self.cursor() |
|
474 |
|
475 def __exit__(self, exc_type, exc_val, exc_tb): |
|
476 if exc_type is None: |
|
477 self.commit() |
|
478 else: |
|
479 self.rollback() |
|
480 return False #propagate the exception |
|
481 |
|
482 def __del__(self): |
|
483 """close the remote connection if necessary""" |
|
484 if self._closed is None and self._close_on_del: |
|
485 try: |
|
486 self.close() |
|
487 except Exception: |
|
488 pass |
|
489 |
|
490 # server-side service call ################################################# |
|
491 |
|
492 @check_not_closed |
|
493 def call_service(self, regid, **kwargs): |
|
494 return self._repo.call_service(self.sessionid, regid, **kwargs) |
|
495 |
|
496 # connection initialization methods ######################################## |
|
497 |
|
498 def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True): |
|
499 config = self.vreg.config |
|
500 if cubes is _MARKER: |
|
501 cubes = self._repo.get_cubes() |
|
502 elif cubes is None: |
|
503 cubes = () |
|
504 else: |
|
505 if not isinstance(cubes, (list, tuple)): |
|
506 cubes = (cubes,) |
|
507 if expand: |
|
508 cubes = config.expand_cubes(cubes) |
|
509 if subpath is None: |
|
510 subpath = esubpath = ('entities', 'views') |
|
511 else: |
|
512 esubpath = subpath |
|
513 if 'views' in subpath: |
|
514 esubpath = list(subpath) |
|
515 esubpath.remove('views') |
|
516 esubpath.append(join('web', 'views')) |
|
517 # first load available configs, necessary for proper persistent |
|
518 # properties initialization |
|
519 config.load_available_configs() |
|
520 # then init cubes |
|
521 config.init_cubes(cubes) |
|
522 # then load appobjects into the registry |
|
523 vpath = config.build_appobjects_path(reversed(config.cubes_path()), |
|
524 evobjpath=esubpath, |
|
525 tvobjpath=subpath) |
|
526 self.vreg.register_objects(vpath) |
|
527 |
|
528 def use_web_compatible_requests(self, baseurl, sitetitle=None): |
|
529 """monkey patch DBAPIRequest to fake a cw.web.request, so you should |
|
530 able to call html views using rset from a simple dbapi connection. |
|
531 |
|
532 You should call `load_appobjects` at some point to register those views. |
|
533 """ |
|
534 DBAPIRequest.property_value = _fake_property_value |
|
535 DBAPIRequest.next_tabindex = count().next |
|
536 DBAPIRequest.relative_path = fake |
|
537 DBAPIRequest.url = fake |
|
538 DBAPIRequest.get_page_data = fake |
|
539 DBAPIRequest.set_page_data = fake |
|
540 # XXX could ask the repo for it's base-url configuration |
|
541 self.vreg.config.set_option('base-url', baseurl) |
|
542 self.vreg.config.uiprops = {} |
|
543 self.vreg.config.datadir_url = baseurl + '/data' |
|
544 # XXX why is this needed? if really needed, could be fetched by a query |
|
545 if sitetitle is not None: |
|
546 self.vreg['propertydefs']['ui.site-title'] = {'default': sitetitle} |
|
547 self._web_request = True |
|
548 |
|
549 def request(self): |
|
550 if self._web_request: |
|
551 from cubicweb.web.request import DBAPICubicWebRequestBase |
|
552 req = DBAPICubicWebRequestBase(self.vreg, False) |
|
553 req.get_header = lambda x, default=None: default |
|
554 req.set_session = lambda session: DBAPIRequest.set_session( |
|
555 req, session) |
|
556 req.relative_path = lambda includeparams=True: '' |
|
557 else: |
|
558 req = DBAPIRequest(self.vreg) |
|
559 req.set_session(DBAPISession(self)) |
|
560 return req |
|
561 |
|
562 @check_not_closed |
|
563 def user(self, req=None, props=None): |
|
564 """return the User object associated to this connection""" |
|
565 # cnx validity is checked by the call to .user_info |
|
566 eid, login, groups, properties = self._repo.user_info(self.sessionid, |
|
567 props) |
|
568 if req is None: |
|
569 req = self.request() |
|
570 rset = req.eid_rset(eid, 'CWUser') |
|
571 if self.vreg is not None and 'etypes' in self.vreg: |
|
572 user = self.vreg['etypes'].etype_class('CWUser')( |
|
573 req, rset, row=0, groups=groups, properties=properties) |
|
574 else: |
|
575 from cubicweb.entity import Entity |
|
576 user = Entity(req, rset, row=0) |
|
577 user.cw_attr_cache['login'] = login # cache login |
|
578 return user |
|
579 |
|
580 @check_not_closed |
|
581 def check(self): |
|
582 """raise `BadConnectionId` if the connection is no more valid, else |
|
583 return its latest activity timestamp. |
|
584 """ |
|
585 return self._repo.check_session(self.sessionid) |
|
586 |
|
587 def _txid(self, cursor=None): # pylint: disable=E0202 |
|
588 # XXX could now handle various isolation level! |
|
589 # return a dict as bw compat trick |
|
590 return {'txid': currentThread().getName()} |
|
591 |
|
592 # session data methods ##################################################### |
|
593 |
|
594 @check_not_closed |
|
595 def get_shared_data(self, key, default=None, pop=False, txdata=False): |
|
596 """return value associated to key in the session's data dictionary or |
|
597 session's transaction's data if `txdata` is true. |
|
598 |
|
599 If pop is True, value will be removed from the dictionary. |
|
600 |
|
601 If key isn't defined in the dictionary, value specified by the |
|
602 `default` argument will be returned. |
|
603 """ |
|
604 return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata) |
|
605 |
|
606 @check_not_closed |
|
607 def set_shared_data(self, key, value, txdata=False): |
|
608 """set value associated to `key` in shared data |
|
609 |
|
610 if `txdata` is true, the value will be added to the repository |
|
611 session's query data which are cleared on commit/rollback of the current |
|
612 transaction. |
|
613 """ |
|
614 return self._repo.set_shared_data(self.sessionid, key, value, txdata) |
|
615 |
|
616 # meta-data accessors ###################################################### |
|
617 |
|
618 @check_not_closed |
|
619 def source_defs(self): |
|
620 """Return the definition of sources used by the repository.""" |
|
621 return self._repo.source_defs() |
|
622 |
|
623 @check_not_closed |
|
624 def get_schema(self): |
|
625 """Return the schema currently used by the repository.""" |
|
626 return self._repo.get_schema() |
|
627 |
|
628 @check_not_closed |
|
629 def get_option_value(self, option, foreid=None): |
|
630 """Return the value for `option` in the configuration. |
|
631 |
|
632 `foreid` argument is deprecated and now useless (as of 3.19). |
|
633 """ |
|
634 if foreid is not None: |
|
635 warn('[3.19] foreid argument is deprecated', DeprecationWarning, |
|
636 stacklevel=2) |
|
637 return self._repo.get_option_value(option) |
|
638 |
|
639 |
|
640 @check_not_closed |
|
641 def entity_metas(self, eid): |
|
642 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
|
643 try: |
|
644 return self._repo.entity_metas(self.sessionid, eid, **self._txid()) |
|
645 except AttributeError: |
|
646 # talking to pre 3.19 repository |
|
647 metas = self._repo.describe(self.sessionid, eid, **self._txid()) |
|
648 if len(metas) == 3: # even older backward compat |
|
649 metas = list(metas) |
|
650 metas.append(metas[1]) |
|
651 return dict(zip(('type', 'source', 'extid', 'asource'), metas)) |
|
652 |
|
653 |
|
654 @deprecated('[3.19] use .entity_metas(eid) instead') |
|
655 @check_not_closed |
|
656 def describe(self, eid, asdict=False): |
|
657 try: |
|
658 metas = self._repo.entity_metas(self.sessionid, eid, **self._txid()) |
|
659 except AttributeError: |
|
660 metas = self._repo.describe(self.sessionid, eid, **self._txid()) |
|
661 # talking to pre 3.19 repository |
|
662 if len(metas) == 3: # even older backward compat |
|
663 metas = list(metas) |
|
664 metas.append(metas[1]) |
|
665 if asdict: |
|
666 return dict(zip(('type', 'source', 'extid', 'asource'), metas)) |
|
667 return metas[:-1] |
|
668 if asdict: |
|
669 metas['asource'] = meta['source'] # XXX pre 3.19 client compat |
|
670 return metas |
|
671 return metas['type'], metas['source'], metas['extid'] |
|
672 |
|
673 |
|
674 # db-api like interface #################################################### |
|
675 |
|
676 @check_not_closed |
|
677 def commit(self): |
|
678 """Commit pending transaction for this connection to the repository. |
|
679 |
|
680 may raises `Unauthorized` or `ValidationError` if we attempted to do |
|
681 something we're not allowed to for security or integrity reason. |
|
682 |
|
683 If the transaction is undoable, a transaction id will be returned. |
|
684 """ |
|
685 return self._repo.commit(self.sessionid, **self._txid()) |
|
686 |
|
687 @check_not_closed |
|
688 def rollback(self): |
|
689 """This method is optional since not all databases provide transaction |
|
690 support. |
|
691 |
|
692 In case a database does provide transactions this method causes the the |
|
693 database to roll back to the start of any pending transaction. Closing |
|
694 a connection without committing the changes first will cause an implicit |
|
695 rollback to be performed. |
|
696 """ |
|
697 self._repo.rollback(self.sessionid, **self._txid()) |
|
698 |
|
699 @check_not_closed |
|
700 def cursor(self, req=None): |
|
701 """Return a new Cursor Object using the connection. |
|
702 """ |
|
703 if req is None: |
|
704 req = self.request() |
|
705 return self.cursor_class(self, self._repo, req=req) |
|
706 |
|
707 @check_not_closed |
|
708 def close(self): |
|
709 """Close the connection now (rather than whenever __del__ is called). |
|
710 |
|
711 The connection will be unusable from this point forward; an Error (or |
|
712 subclass) exception will be raised if any operation is attempted with |
|
713 the connection. The same applies to all cursor objects trying to use the |
|
714 connection. Note that closing a connection without committing the |
|
715 changes first will cause an implicit rollback to be performed. |
|
716 """ |
|
717 self._repo.close(self.sessionid, **self._txid()) |
|
718 del self._repo # necessary for proper garbage collection |
|
719 self._closed = 1 |
|
720 |
|
721 # undo support ############################################################ |
|
722 |
|
723 @check_not_closed |
|
724 def undoable_transactions(self, ueid=None, req=None, **actionfilters): |
|
725 """Return a list of undoable transaction objects by the connection's |
|
726 user, ordered by descendant transaction time. |
|
727 |
|
728 Managers may filter according to user (eid) who has done the transaction |
|
729 using the `ueid` argument. Others will only see their own transactions. |
|
730 |
|
731 Additional filtering capabilities is provided by using the following |
|
732 named arguments: |
|
733 |
|
734 * `etype` to get only transactions creating/updating/deleting entities |
|
735 of the given type |
|
736 |
|
737 * `eid` to get only transactions applied to entity of the given eid |
|
738 |
|
739 * `action` to get only transactions doing the given action (action in |
|
740 'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or |
|
741 'D'. |
|
742 |
|
743 * `public`: when additional filtering is provided, their are by default |
|
744 only searched in 'public' actions, unless a `public` argument is given |
|
745 and set to false. |
|
746 """ |
|
747 actionfilters.update(self._txid()) |
|
748 txinfos = self._repo.undoable_transactions(self.sessionid, ueid, |
|
749 **actionfilters) |
|
750 if req is None: |
|
751 req = self.request() |
|
752 for txinfo in txinfos: |
|
753 txinfo.req = req |
|
754 return txinfos |
|
755 |
|
756 @check_not_closed |
|
757 def transaction_info(self, txuuid, req=None): |
|
758 """Return transaction object for the given uid. |
|
759 |
|
760 raise `NoSuchTransaction` if not found or if session's user is not |
|
761 allowed (eg not in managers group and the transaction doesn't belong to |
|
762 him). |
|
763 """ |
|
764 txinfo = self._repo.transaction_info(self.sessionid, txuuid, |
|
765 **self._txid()) |
|
766 if req is None: |
|
767 req = self.request() |
|
768 txinfo.req = req |
|
769 return txinfo |
|
770 |
|
771 @check_not_closed |
|
772 def transaction_actions(self, txuuid, public=True): |
|
773 """Return an ordered list of action effectued during that transaction. |
|
774 |
|
775 If public is true, return only 'public' actions, eg not ones triggered |
|
776 under the cover by hooks, else return all actions. |
|
777 |
|
778 raise `NoSuchTransaction` if the transaction is not found or if |
|
779 session's user is not allowed (eg not in managers group and the |
|
780 transaction doesn't belong to him). |
|
781 """ |
|
782 return self._repo.transaction_actions(self.sessionid, txuuid, public, |
|
783 **self._txid()) |
|
784 |
|
785 @check_not_closed |
|
786 def undo_transaction(self, txuuid): |
|
787 """Undo the given transaction. Return potential restoration errors. |
|
788 |
|
789 raise `NoSuchTransaction` if not found or if session's user is not |
|
790 allowed (eg not in managers group and the transaction doesn't belong to |
|
791 him). |
|
792 """ |
|
793 return self._repo.undo_transaction(self.sessionid, txuuid, |
|
794 **self._txid()) |
|
795 |
|
796 in_memory_cnx = deprecated('[3.16] use _repo_connect instead)')(_repo_connect) |
|