Mercurial > p > roundup > code
comparison roundup/hyperdb.py @ 1356:83f33642d220 maint-0.5
[[Metadata associated with this commit was garbled during conversion from CVS
to Subversion.]]
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Thu, 09 Jan 2003 22:59:22 +0000 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 1242:3d0158c8c32b | 1356:83f33642d220 |
|---|---|
| 1 # | |
| 2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/) | |
| 3 # This module is free software, and you may redistribute it and/or modify | |
| 4 # under the same terms as Python, so long as this copyright message and | |
| 5 # disclaimer are retained in their original form. | |
| 6 # | |
| 7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR | |
| 8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING | |
| 9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE | |
| 10 # POSSIBILITY OF SUCH DAMAGE. | |
| 11 # | |
| 12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, | |
| 13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | |
| 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" | |
| 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, | |
| 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | |
| 17 # | |
| 18 # $Id: hyperdb.py,v 1.84 2002-10-07 00:52:51 richard Exp $ | |
| 19 | |
| 20 """ | |
| 21 Hyperdatabase implementation, especially field types. | |
| 22 """ | |
| 23 | |
| 24 # standard python modules | |
| 25 import sys, os, time, re | |
| 26 | |
| 27 # roundup modules | |
| 28 import date, password | |
| 29 | |
| 30 # configure up the DEBUG and TRACE captures | |
| 31 class Sink: | |
| 32 def write(self, content): | |
| 33 pass | |
| 34 DEBUG = os.environ.get('HYPERDBDEBUG', '') | |
| 35 if DEBUG and __debug__: | |
| 36 if DEBUG == 'stdout': | |
| 37 DEBUG = sys.stdout | |
| 38 else: | |
| 39 DEBUG = open(DEBUG, 'a') | |
| 40 else: | |
| 41 DEBUG = Sink() | |
| 42 TRACE = os.environ.get('HYPERDBTRACE', '') | |
| 43 if TRACE and __debug__: | |
| 44 if TRACE == 'stdout': | |
| 45 TRACE = sys.stdout | |
| 46 else: | |
| 47 TRACE = open(TRACE, 'w') | |
| 48 else: | |
| 49 TRACE = Sink() | |
| 50 def traceMark(): | |
| 51 print >>TRACE, '**MARK', time.ctime() | |
| 52 del Sink | |
| 53 | |
| 54 # | |
| 55 # Types | |
| 56 # | |
| 57 class String: | |
| 58 """An object designating a String property.""" | |
| 59 def __init__(self, indexme='no'): | |
| 60 self.indexme = indexme == 'yes' | |
| 61 def __repr__(self): | |
| 62 ' more useful for dumps ' | |
| 63 return '<%s>'%self.__class__ | |
| 64 | |
| 65 class Password: | |
| 66 """An object designating a Password property.""" | |
| 67 def __repr__(self): | |
| 68 ' more useful for dumps ' | |
| 69 return '<%s>'%self.__class__ | |
| 70 | |
| 71 class Date: | |
| 72 """An object designating a Date property.""" | |
| 73 def __repr__(self): | |
| 74 ' more useful for dumps ' | |
| 75 return '<%s>'%self.__class__ | |
| 76 | |
| 77 class Interval: | |
| 78 """An object designating an Interval property.""" | |
| 79 def __repr__(self): | |
| 80 ' more useful for dumps ' | |
| 81 return '<%s>'%self.__class__ | |
| 82 | |
| 83 class Link: | |
| 84 """An object designating a Link property that links to a | |
| 85 node in a specified class.""" | |
| 86 def __init__(self, classname, do_journal='yes'): | |
| 87 ''' Default is to not journal link and unlink events | |
| 88 ''' | |
| 89 self.classname = classname | |
| 90 self.do_journal = do_journal == 'yes' | |
| 91 def __repr__(self): | |
| 92 ' more useful for dumps ' | |
| 93 return '<%s to "%s">'%(self.__class__, self.classname) | |
| 94 | |
| 95 class Multilink: | |
| 96 """An object designating a Multilink property that links | |
| 97 to nodes in a specified class. | |
| 98 | |
| 99 "classname" indicates the class to link to | |
| 100 | |
| 101 "do_journal" indicates whether the linked-to nodes should have | |
| 102 'link' and 'unlink' events placed in their journal | |
| 103 """ | |
| 104 def __init__(self, classname, do_journal='yes'): | |
| 105 ''' Default is to not journal link and unlink events | |
| 106 ''' | |
| 107 self.classname = classname | |
| 108 self.do_journal = do_journal == 'yes' | |
| 109 def __repr__(self): | |
| 110 ' more useful for dumps ' | |
| 111 return '<%s to "%s">'%(self.__class__, self.classname) | |
| 112 | |
| 113 class Boolean: | |
| 114 """An object designating a boolean property""" | |
| 115 def __repr__(self): | |
| 116 'more useful for dumps' | |
| 117 return '<%s>' % self.__class__ | |
| 118 | |
| 119 class Number: | |
| 120 """An object designating a numeric property""" | |
| 121 def __repr__(self): | |
| 122 'more useful for dumps' | |
| 123 return '<%s>' % self.__class__ | |
| 124 # | |
| 125 # Support for splitting designators | |
| 126 # | |
| 127 class DesignatorError(ValueError): | |
| 128 pass | |
| 129 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')): | |
| 130 ''' Take a foo123 and return ('foo', 123) | |
| 131 ''' | |
| 132 m = dre.match(designator) | |
| 133 if m is None: | |
| 134 raise DesignatorError, '"%s" not a node designator'%designator | |
| 135 return m.group(1), m.group(2) | |
| 136 | |
| 137 # | |
| 138 # the base Database class | |
| 139 # | |
| 140 class DatabaseError(ValueError): | |
| 141 '''Error to be raised when there is some problem in the database code | |
| 142 ''' | |
| 143 pass | |
| 144 class Database: | |
| 145 '''A database for storing records containing flexible data types. | |
| 146 | |
| 147 This class defines a hyperdatabase storage layer, which the Classes use to | |
| 148 store their data. | |
| 149 | |
| 150 | |
| 151 Transactions | |
| 152 ------------ | |
| 153 The Database should support transactions through the commit() and | |
| 154 rollback() methods. All other Database methods should be transaction-aware, | |
| 155 using data from the current transaction before looking up the database. | |
| 156 | |
| 157 An implementation must provide an override for the get() method so that the | |
| 158 in-database value is returned in preference to the in-transaction value. | |
| 159 This is necessary to determine if any values have changed during a | |
| 160 transaction. | |
| 161 | |
| 162 | |
| 163 Implementation | |
| 164 -------------- | |
| 165 | |
| 166 All methods except __repr__ and getnode must be implemented by a | |
| 167 concrete backend Class. | |
| 168 | |
| 169 ''' | |
| 170 | |
| 171 # flag to set on retired entries | |
| 172 RETIRED_FLAG = '__hyperdb_retired' | |
| 173 | |
| 174 def __init__(self, config, journaltag=None): | |
| 175 """Open a hyperdatabase given a specifier to some storage. | |
| 176 | |
| 177 The 'storagelocator' is obtained from config.DATABASE. | |
| 178 The meaning of 'storagelocator' depends on the particular | |
| 179 implementation of the hyperdatabase. It could be a file name, | |
| 180 a directory path, a socket descriptor for a connection to a | |
| 181 database over the network, etc. | |
| 182 | |
| 183 The 'journaltag' is a token that will be attached to the journal | |
| 184 entries for any edits done on the database. If 'journaltag' is | |
| 185 None, the database is opened in read-only mode: the Class.create(), | |
| 186 Class.set(), and Class.retire() methods are disabled. | |
| 187 """ | |
| 188 raise NotImplementedError | |
| 189 | |
| 190 def post_init(self): | |
| 191 """Called once the schema initialisation has finished.""" | |
| 192 raise NotImplementedError | |
| 193 | |
| 194 def __getattr__(self, classname): | |
| 195 """A convenient way of calling self.getclass(classname).""" | |
| 196 raise NotImplementedError | |
| 197 | |
| 198 def addclass(self, cl): | |
| 199 '''Add a Class to the hyperdatabase. | |
| 200 ''' | |
| 201 raise NotImplementedError | |
| 202 | |
| 203 def getclasses(self): | |
| 204 """Return a list of the names of all existing classes.""" | |
| 205 raise NotImplementedError | |
| 206 | |
| 207 def getclass(self, classname): | |
| 208 """Get the Class object representing a particular class. | |
| 209 | |
| 210 If 'classname' is not a valid class name, a KeyError is raised. | |
| 211 """ | |
| 212 raise NotImplementedError | |
| 213 | |
| 214 def clear(self): | |
| 215 '''Delete all database contents. | |
| 216 ''' | |
| 217 raise NotImplementedError | |
| 218 | |
| 219 def getclassdb(self, classname, mode='r'): | |
| 220 '''Obtain a connection to the class db that will be used for | |
| 221 multiple actions. | |
| 222 ''' | |
| 223 raise NotImplementedError | |
| 224 | |
| 225 def addnode(self, classname, nodeid, node): | |
| 226 '''Add the specified node to its class's db. | |
| 227 ''' | |
| 228 raise NotImplementedError | |
| 229 | |
| 230 def serialise(self, classname, node): | |
| 231 '''Copy the node contents, converting non-marshallable data into | |
| 232 marshallable data. | |
| 233 ''' | |
| 234 return node | |
| 235 | |
| 236 def setnode(self, classname, nodeid, node): | |
| 237 '''Change the specified node. | |
| 238 ''' | |
| 239 raise NotImplementedError | |
| 240 | |
| 241 def unserialise(self, classname, node): | |
| 242 '''Decode the marshalled node data | |
| 243 ''' | |
| 244 return node | |
| 245 | |
| 246 def getnode(self, classname, nodeid, db=None, cache=1): | |
| 247 '''Get a node from the database. | |
| 248 ''' | |
| 249 raise NotImplementedError | |
| 250 | |
| 251 def hasnode(self, classname, nodeid, db=None): | |
| 252 '''Determine if the database has a given node. | |
| 253 ''' | |
| 254 raise NotImplementedError | |
| 255 | |
| 256 def countnodes(self, classname, db=None): | |
| 257 '''Count the number of nodes that exist for a particular Class. | |
| 258 ''' | |
| 259 raise NotImplementedError | |
| 260 | |
| 261 def getnodeids(self, classname, db=None): | |
| 262 '''Retrieve all the ids of the nodes for a particular Class. | |
| 263 ''' | |
| 264 raise NotImplementedError | |
| 265 | |
| 266 def storefile(self, classname, nodeid, property, content): | |
| 267 '''Store the content of the file in the database. | |
| 268 | |
| 269 The property may be None, in which case the filename does not | |
| 270 indicate which property is being saved. | |
| 271 ''' | |
| 272 raise NotImplementedError | |
| 273 | |
| 274 def getfile(self, classname, nodeid, property): | |
| 275 '''Store the content of the file in the database. | |
| 276 ''' | |
| 277 raise NotImplementedError | |
| 278 | |
| 279 def addjournal(self, classname, nodeid, action, params): | |
| 280 ''' Journal the Action | |
| 281 'action' may be: | |
| 282 | |
| 283 'create' or 'set' -- 'params' is a dictionary of property values | |
| 284 'link' or 'unlink' -- 'params' is (classname, nodeid, propname) | |
| 285 'retire' -- 'params' is None | |
| 286 ''' | |
| 287 raise NotImplementedError | |
| 288 | |
| 289 def getjournal(self, classname, nodeid): | |
| 290 ''' get the journal for id | |
| 291 ''' | |
| 292 raise NotImplementedError | |
| 293 | |
| 294 def pack(self, pack_before): | |
| 295 ''' pack the database | |
| 296 ''' | |
| 297 raise NotImplementedError | |
| 298 | |
| 299 def commit(self): | |
| 300 ''' Commit the current transactions. | |
| 301 | |
| 302 Save all data changed since the database was opened or since the | |
| 303 last commit() or rollback(). | |
| 304 ''' | |
| 305 raise NotImplementedError | |
| 306 | |
| 307 def rollback(self): | |
| 308 ''' Reverse all actions from the current transaction. | |
| 309 | |
| 310 Undo all the changes made since the database was opened or the last | |
| 311 commit() or rollback() was performed. | |
| 312 ''' | |
| 313 raise NotImplementedError | |
| 314 | |
| 315 # | |
| 316 # The base Class class | |
| 317 # | |
| 318 class Class: | |
| 319 """ The handle to a particular class of nodes in a hyperdatabase. | |
| 320 | |
| 321 All methods except __repr__ and getnode must be implemented by a | |
| 322 concrete backend Class. | |
| 323 """ | |
| 324 | |
| 325 def __init__(self, db, classname, **properties): | |
| 326 """Create a new class with a given name and property specification. | |
| 327 | |
| 328 'classname' must not collide with the name of an existing class, | |
| 329 or a ValueError is raised. The keyword arguments in 'properties' | |
| 330 must map names to property objects, or a TypeError is raised. | |
| 331 """ | |
| 332 raise NotImplementedError | |
| 333 | |
| 334 def __repr__(self): | |
| 335 '''Slightly more useful representation | |
| 336 ''' | |
| 337 return '<hyperdb.Class "%s">'%self.classname | |
| 338 | |
| 339 # Editing nodes: | |
| 340 | |
| 341 def create(self, **propvalues): | |
| 342 """Create a new node of this class and return its id. | |
| 343 | |
| 344 The keyword arguments in 'propvalues' map property names to values. | |
| 345 | |
| 346 The values of arguments must be acceptable for the types of their | |
| 347 corresponding properties or a TypeError is raised. | |
| 348 | |
| 349 If this class has a key property, it must be present and its value | |
| 350 must not collide with other key strings or a ValueError is raised. | |
| 351 | |
| 352 Any other properties on this class that are missing from the | |
| 353 'propvalues' dictionary are set to None. | |
| 354 | |
| 355 If an id in a link or multilink property does not refer to a valid | |
| 356 node, an IndexError is raised. | |
| 357 """ | |
| 358 raise NotImplementedError | |
| 359 | |
| 360 _marker = [] | |
| 361 def get(self, nodeid, propname, default=_marker, cache=1): | |
| 362 """Get the value of a property on an existing node of this class. | |
| 363 | |
| 364 'nodeid' must be the id of an existing node of this class or an | |
| 365 IndexError is raised. 'propname' must be the name of a property | |
| 366 of this class or a KeyError is raised. | |
| 367 | |
| 368 'cache' indicates whether the transaction cache should be queried | |
| 369 for the node. If the node has been modified and you need to | |
| 370 determine what its values prior to modification are, you need to | |
| 371 set cache=0. | |
| 372 """ | |
| 373 raise NotImplementedError | |
| 374 | |
| 375 def getnode(self, nodeid, cache=1): | |
| 376 ''' Return a convenience wrapper for the node. | |
| 377 | |
| 378 'nodeid' must be the id of an existing node of this class or an | |
| 379 IndexError is raised. | |
| 380 | |
| 381 'cache' indicates whether the transaction cache should be queried | |
| 382 for the node. If the node has been modified and you need to | |
| 383 determine what its values prior to modification are, you need to | |
| 384 set cache=0. | |
| 385 ''' | |
| 386 return Node(self, nodeid, cache=cache) | |
| 387 | |
| 388 def set(self, nodeid, **propvalues): | |
| 389 """Modify a property on an existing node of this class. | |
| 390 | |
| 391 'nodeid' must be the id of an existing node of this class or an | |
| 392 IndexError is raised. | |
| 393 | |
| 394 Each key in 'propvalues' must be the name of a property of this | |
| 395 class or a KeyError is raised. | |
| 396 | |
| 397 All values in 'propvalues' must be acceptable types for their | |
| 398 corresponding properties or a TypeError is raised. | |
| 399 | |
| 400 If the value of the key property is set, it must not collide with | |
| 401 other key strings or a ValueError is raised. | |
| 402 | |
| 403 If the value of a Link or Multilink property contains an invalid | |
| 404 node id, a ValueError is raised. | |
| 405 """ | |
| 406 raise NotImplementedError | |
| 407 | |
| 408 def retire(self, nodeid): | |
| 409 """Retire a node. | |
| 410 | |
| 411 The properties on the node remain available from the get() method, | |
| 412 and the node's id is never reused. | |
| 413 | |
| 414 Retired nodes are not returned by the find(), list(), or lookup() | |
| 415 methods, and other nodes may reuse the values of their key properties. | |
| 416 """ | |
| 417 raise NotImplementedError | |
| 418 | |
| 419 def is_retired(self, nodeid): | |
| 420 '''Return true if the node is rerired | |
| 421 ''' | |
| 422 raise NotImplementedError | |
| 423 | |
| 424 def destroy(self, nodeid): | |
| 425 """Destroy a node. | |
| 426 | |
| 427 WARNING: this method should never be used except in extremely rare | |
| 428 situations where there could never be links to the node being | |
| 429 deleted | |
| 430 WARNING: use retire() instead | |
| 431 WARNING: the properties of this node will not be available ever again | |
| 432 WARNING: really, use retire() instead | |
| 433 | |
| 434 Well, I think that's enough warnings. This method exists mostly to | |
| 435 support the session storage of the cgi interface. | |
| 436 | |
| 437 The node is completely removed from the hyperdb, including all journal | |
| 438 entries. It will no longer be available, and will generally break code | |
| 439 if there are any references to the node. | |
| 440 """ | |
| 441 | |
| 442 def history(self, nodeid): | |
| 443 """Retrieve the journal of edits on a particular node. | |
| 444 | |
| 445 'nodeid' must be the id of an existing node of this class or an | |
| 446 IndexError is raised. | |
| 447 | |
| 448 The returned list contains tuples of the form | |
| 449 | |
| 450 (date, tag, action, params) | |
| 451 | |
| 452 'date' is a Timestamp object specifying the time of the change and | |
| 453 'tag' is the journaltag specified when the database was opened. | |
| 454 """ | |
| 455 raise NotImplementedError | |
| 456 | |
| 457 # Locating nodes: | |
| 458 def hasnode(self, nodeid): | |
| 459 '''Determine if the given nodeid actually exists | |
| 460 ''' | |
| 461 raise NotImplementedError | |
| 462 | |
| 463 def setkey(self, propname): | |
| 464 """Select a String property of this class to be the key property. | |
| 465 | |
| 466 'propname' must be the name of a String property of this class or | |
| 467 None, or a TypeError is raised. The values of the key property on | |
| 468 all existing nodes must be unique or a ValueError is raised. | |
| 469 """ | |
| 470 raise NotImplementedError | |
| 471 | |
| 472 def getkey(self): | |
| 473 """Return the name of the key property for this class or None.""" | |
| 474 raise NotImplementedError | |
| 475 | |
| 476 def labelprop(self, default_to_id=0): | |
| 477 ''' Return the property name for a label for the given node. | |
| 478 | |
| 479 This method attempts to generate a consistent label for the node. | |
| 480 It tries the following in order: | |
| 481 1. key property | |
| 482 2. "name" property | |
| 483 3. "title" property | |
| 484 4. first property from the sorted property name list | |
| 485 ''' | |
| 486 raise NotImplementedError | |
| 487 | |
| 488 def lookup(self, keyvalue): | |
| 489 """Locate a particular node by its key property and return its id. | |
| 490 | |
| 491 If this class has no key property, a TypeError is raised. If the | |
| 492 'keyvalue' matches one of the values for the key property among | |
| 493 the nodes in this class, the matching node's id is returned; | |
| 494 otherwise a KeyError is raised. | |
| 495 """ | |
| 496 raise NotImplementedError | |
| 497 | |
| 498 def find(self, **propspec): | |
| 499 """Get the ids of nodes in this class which link to the given nodes. | |
| 500 | |
| 501 'propspec' consists of keyword args propname={nodeid:1,} | |
| 502 'propname' must be the name of a property in this class, or a | |
| 503 KeyError is raised. That property must be a Link or Multilink | |
| 504 property, or a TypeError is raised. | |
| 505 | |
| 506 Any node in this class whose 'propname' property links to any of the | |
| 507 nodeids will be returned. Used by the full text indexing, which knows | |
| 508 that "foo" occurs in msg1, msg3 and file7, so we have hits on these | |
| 509 issues: | |
| 510 | |
| 511 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) | |
| 512 """ | |
| 513 raise NotImplementedError | |
| 514 | |
| 515 def filter(self, search_matches, filterspec, sort=(None,None), | |
| 516 group=(None,None)): | |
| 517 ''' Return a list of the ids of the active nodes in this class that | |
| 518 match the 'filter' spec, sorted by the group spec and then the | |
| 519 sort spec. | |
| 520 | |
| 521 "filterspec" is {propname: value(s)} | |
| 522 "sort" and "group" are (dir, prop) where dir is '+', '-' or None | |
| 523 and prop is a prop name or None | |
| 524 "search_matches" is {nodeid: marker} | |
| 525 | |
| 526 The filter must match all properties specificed - but if the | |
| 527 property value to match is a list, any one of the values in the | |
| 528 list may match for that property to match. | |
| 529 ''' | |
| 530 raise NotImplementedError | |
| 531 | |
| 532 def count(self): | |
| 533 """Get the number of nodes in this class. | |
| 534 | |
| 535 If the returned integer is 'numnodes', the ids of all the nodes | |
| 536 in this class run from 1 to numnodes, and numnodes+1 will be the | |
| 537 id of the next node to be created in this class. | |
| 538 """ | |
| 539 raise NotImplementedError | |
| 540 | |
| 541 # Manipulating properties: | |
| 542 def getprops(self, protected=1): | |
| 543 """Return a dictionary mapping property names to property objects. | |
| 544 If the "protected" flag is true, we include protected properties - | |
| 545 those which may not be modified. | |
| 546 """ | |
| 547 raise NotImplementedError | |
| 548 | |
| 549 def addprop(self, **properties): | |
| 550 """Add properties to this class. | |
| 551 | |
| 552 The keyword arguments in 'properties' must map names to property | |
| 553 objects, or a TypeError is raised. None of the keys in 'properties' | |
| 554 may collide with the names of existing properties, or a ValueError | |
| 555 is raised before any properties have been added. | |
| 556 """ | |
| 557 raise NotImplementedError | |
| 558 | |
| 559 def index(self, nodeid): | |
| 560 '''Add (or refresh) the node to search indexes | |
| 561 ''' | |
| 562 raise NotImplementedError | |
| 563 | |
| 564 class Node: | |
| 565 ''' A convenience wrapper for the given node | |
| 566 ''' | |
| 567 def __init__(self, cl, nodeid, cache=1): | |
| 568 self.__dict__['cl'] = cl | |
| 569 self.__dict__['nodeid'] = nodeid | |
| 570 self.__dict__['cache'] = cache | |
| 571 def keys(self, protected=1): | |
| 572 return self.cl.getprops(protected=protected).keys() | |
| 573 def values(self, protected=1): | |
| 574 l = [] | |
| 575 for name in self.cl.getprops(protected=protected).keys(): | |
| 576 l.append(self.cl.get(self.nodeid, name, cache=self.cache)) | |
| 577 return l | |
| 578 def items(self, protected=1): | |
| 579 l = [] | |
| 580 for name in self.cl.getprops(protected=protected).keys(): | |
| 581 l.append((name, self.cl.get(self.nodeid, name, cache=self.cache))) | |
| 582 return l | |
| 583 def has_key(self, name): | |
| 584 return self.cl.getprops().has_key(name) | |
| 585 def __getattr__(self, name): | |
| 586 if self.__dict__.has_key(name): | |
| 587 return self.__dict__[name] | |
| 588 try: | |
| 589 return self.cl.get(self.nodeid, name, cache=self.cache) | |
| 590 except KeyError, value: | |
| 591 # we trap this but re-raise it as AttributeError - all other | |
| 592 # exceptions should pass through untrapped | |
| 593 pass | |
| 594 # nope, no such attribute | |
| 595 raise AttributeError, str(value) | |
| 596 def __getitem__(self, name): | |
| 597 return self.cl.get(self.nodeid, name, cache=self.cache) | |
| 598 def __setattr__(self, name, value): | |
| 599 try: | |
| 600 return self.cl.set(self.nodeid, **{name: value}) | |
| 601 except KeyError, value: | |
| 602 raise AttributeError, str(value) | |
| 603 def __setitem__(self, name, value): | |
| 604 self.cl.set(self.nodeid, **{name: value}) | |
| 605 def history(self): | |
| 606 return self.cl.history(self.nodeid) | |
| 607 def retire(self): | |
| 608 return self.cl.retire(self.nodeid) | |
| 609 | |
| 610 | |
| 611 def Choice(name, db, *options): | |
| 612 '''Quick helper to create a simple class with choices | |
| 613 ''' | |
| 614 cl = Class(db, name, name=String(), order=String()) | |
| 615 for i in range(len(options)): | |
| 616 cl.create(name=options[i], order=i) | |
| 617 return hyperdb.Link(name) | |
| 618 | |
| 619 # vim: set filetype=python ts=4 sw=4 et si |
