OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2006-2008 Mitch Garnaat http://garnaat.org/ |
| 2 # |
| 3 # Permission is hereby granted, free of charge, to any person obtaining a |
| 4 # copy of this software and associated documentation files (the |
| 5 # "Software"), to deal in the Software without restriction, including |
| 6 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 7 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 8 # persons to whom the Software is furnished to do so, subject to the fol- |
| 9 # lowing conditions: |
| 10 # |
| 11 # The above copyright notice and this permission notice shall be included |
| 12 # in all copies or substantial portions of the Software. |
| 13 # |
| 14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 16 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
| 17 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 18 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 19 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 20 # IN THE SOFTWARE. |
| 21 import boto |
| 22 from boto.utils import find_class, Password |
| 23 from boto.sdb.db.key import Key |
| 24 from boto.sdb.db.model import Model |
| 25 from datetime import datetime |
| 26 from xml.dom.minidom import getDOMImplementation, parse, parseString, Node |
| 27 |
| 28 ISO8601 = '%Y-%m-%dT%H:%M:%SZ' |
| 29 |
| 30 class XMLConverter: |
| 31 """ |
| 32 Responsible for converting base Python types to format compatible with under
lying |
| 33 database. For SimpleDB, that means everything needs to be converted to a st
ring |
| 34 when stored in SimpleDB and from a string when retrieved. |
| 35 |
| 36 To convert a value, pass it to the encode or decode method. The encode meth
od |
| 37 will take a Python native value and convert to DB format. The decode method
will |
| 38 take a DB format value and convert it to Python native format. To find the
appropriate |
| 39 method to call, the generic encode/decode methods will look for the type-spe
cific |
| 40 method by searching for a method called "encode_<type name>" or "decode_<typ
e name>". |
| 41 """ |
| 42 def __init__(self, manager): |
| 43 self.manager = manager |
| 44 self.type_map = { bool : (self.encode_bool, self.decode_bool), |
| 45 int : (self.encode_int, self.decode_int), |
| 46 long : (self.encode_long, self.decode_long), |
| 47 Model : (self.encode_reference, self.decode_reference)
, |
| 48 Key : (self.encode_reference, self.decode_reference), |
| 49 Password : (self.encode_password, self.decode_password
), |
| 50 datetime : (self.encode_datetime, self.decode_datetime
)} |
| 51 |
| 52 def get_text_value(self, parent_node): |
| 53 value = '' |
| 54 for node in parent_node.childNodes: |
| 55 if node.nodeType == node.TEXT_NODE: |
| 56 value += node.data |
| 57 return value |
| 58 |
| 59 def encode(self, item_type, value): |
| 60 if item_type in self.type_map: |
| 61 encode = self.type_map[item_type][0] |
| 62 return encode(value) |
| 63 return value |
| 64 |
| 65 def decode(self, item_type, value): |
| 66 if item_type in self.type_map: |
| 67 decode = self.type_map[item_type][1] |
| 68 return decode(value) |
| 69 else: |
| 70 value = self.get_text_value(value) |
| 71 return value |
| 72 |
| 73 def encode_prop(self, prop, value): |
| 74 if isinstance(value, list): |
| 75 if hasattr(prop, 'item_type'): |
| 76 new_value = [] |
| 77 for v in value: |
| 78 item_type = getattr(prop, "item_type") |
| 79 if Model in item_type.mro(): |
| 80 item_type = Model |
| 81 new_value.append(self.encode(item_type, v)) |
| 82 return new_value |
| 83 else: |
| 84 return value |
| 85 else: |
| 86 return self.encode(prop.data_type, value) |
| 87 |
| 88 def decode_prop(self, prop, value): |
| 89 if prop.data_type == list: |
| 90 if hasattr(prop, 'item_type'): |
| 91 item_type = getattr(prop, "item_type") |
| 92 if Model in item_type.mro(): |
| 93 item_type = Model |
| 94 values = [] |
| 95 for item_node in value.getElementsByTagName('item'): |
| 96 value = self.decode(item_type, item_node) |
| 97 values.append(value) |
| 98 return values |
| 99 else: |
| 100 return self.get_text_value(value) |
| 101 else: |
| 102 return self.decode(prop.data_type, value) |
| 103 |
| 104 def encode_int(self, value): |
| 105 value = int(value) |
| 106 return '%d' % value |
| 107 |
| 108 def decode_int(self, value): |
| 109 value = self.get_text_value(value) |
| 110 if value: |
| 111 value = int(value) |
| 112 else: |
| 113 value = None |
| 114 return value |
| 115 |
| 116 def encode_long(self, value): |
| 117 value = long(value) |
| 118 return '%d' % value |
| 119 |
| 120 def decode_long(self, value): |
| 121 value = self.get_text_value(value) |
| 122 return long(value) |
| 123 |
| 124 def encode_bool(self, value): |
| 125 if value == True: |
| 126 return 'true' |
| 127 else: |
| 128 return 'false' |
| 129 |
| 130 def decode_bool(self, value): |
| 131 value = self.get_text_value(value) |
| 132 if value.lower() == 'true': |
| 133 return True |
| 134 else: |
| 135 return False |
| 136 |
| 137 def encode_datetime(self, value): |
| 138 return value.strftime(ISO8601) |
| 139 |
| 140 def decode_datetime(self, value): |
| 141 value = self.get_text_value(value) |
| 142 try: |
| 143 return datetime.strptime(value, ISO8601) |
| 144 except: |
| 145 return None |
| 146 |
| 147 def encode_reference(self, value): |
| 148 if isinstance(value, str) or isinstance(value, unicode): |
| 149 return value |
| 150 if value == None: |
| 151 return '' |
| 152 else: |
| 153 val_node = self.manager.doc.createElement("object") |
| 154 val_node.setAttribute('id', value.id) |
| 155 val_node.setAttribute('class', '%s.%s' % (value.__class__.__module__
, value.__class__.__name__)) |
| 156 return val_node |
| 157 |
| 158 def decode_reference(self, value): |
| 159 if not value: |
| 160 return None |
| 161 try: |
| 162 value = value.childNodes[0] |
| 163 class_name = value.getAttribute("class") |
| 164 id = value.getAttribute("id") |
| 165 cls = find_class(class_name) |
| 166 return cls.get_by_ids(id) |
| 167 except: |
| 168 return None |
| 169 |
| 170 def encode_password(self, value): |
| 171 if value and len(value) > 0: |
| 172 return str(value) |
| 173 else: |
| 174 return None |
| 175 |
| 176 def decode_password(self, value): |
| 177 value = self.get_text_value(value) |
| 178 return Password(value) |
| 179 |
| 180 |
| 181 class XMLManager(object): |
| 182 |
| 183 def __init__(self, cls, db_name, db_user, db_passwd, |
| 184 db_host, db_port, db_table, ddl_dir, enable_ssl): |
| 185 self.cls = cls |
| 186 if not db_name: |
| 187 db_name = cls.__name__.lower() |
| 188 self.db_name = db_name |
| 189 self.db_user = db_user |
| 190 self.db_passwd = db_passwd |
| 191 self.db_host = db_host |
| 192 self.db_port = db_port |
| 193 self.db_table = db_table |
| 194 self.ddl_dir = ddl_dir |
| 195 self.s3 = None |
| 196 self.converter = XMLConverter(self) |
| 197 self.impl = getDOMImplementation() |
| 198 self.doc = self.impl.createDocument(None, 'objects', None) |
| 199 |
| 200 self.connection = None |
| 201 self.enable_ssl = enable_ssl |
| 202 self.auth_header = None |
| 203 if self.db_user: |
| 204 import base64 |
| 205 base64string = base64.encodestring('%s:%s' % (self.db_user, self.db_
passwd))[:-1] |
| 206 authheader = "Basic %s" % base64string |
| 207 self.auth_header = authheader |
| 208 |
| 209 def _connect(self): |
| 210 if self.db_host: |
| 211 if self.enable_ssl: |
| 212 from httplib import HTTPSConnection as Connection |
| 213 else: |
| 214 from httplib import HTTPConnection as Connection |
| 215 |
| 216 self.connection = Connection(self.db_host, self.db_port) |
| 217 |
| 218 def _make_request(self, method, url, post_data=None, body=None): |
| 219 """ |
| 220 Make a request on this connection |
| 221 """ |
| 222 if not self.connection: |
| 223 self._connect() |
| 224 try: |
| 225 self.connection.close() |
| 226 except: |
| 227 pass |
| 228 self.connection.connect() |
| 229 headers = {} |
| 230 if self.auth_header: |
| 231 headers["Authorization"] = self.auth_header |
| 232 self.connection.request(method, url, body, headers) |
| 233 resp = self.connection.getresponse() |
| 234 return resp |
| 235 |
| 236 def new_doc(self): |
| 237 return self.impl.createDocument(None, 'objects', None) |
| 238 |
| 239 def _object_lister(self, cls, doc): |
| 240 for obj_node in doc.getElementsByTagName('object'): |
| 241 if not cls: |
| 242 class_name = obj_node.getAttribute('class') |
| 243 cls = find_class(class_name) |
| 244 id = obj_node.getAttribute('id') |
| 245 obj = cls(id) |
| 246 for prop_node in obj_node.getElementsByTagName('property'): |
| 247 prop_name = prop_node.getAttribute('name') |
| 248 prop = obj.find_property(prop_name) |
| 249 if prop: |
| 250 if hasattr(prop, 'item_type'): |
| 251 value = self.get_list(prop_node, prop.item_type) |
| 252 else: |
| 253 value = self.decode_value(prop, prop_node) |
| 254 value = prop.make_value_from_datastore(value) |
| 255 setattr(obj, prop.name, value) |
| 256 yield obj |
| 257 |
| 258 def reset(self): |
| 259 self._connect() |
| 260 |
| 261 def get_doc(self): |
| 262 return self.doc |
| 263 |
| 264 def encode_value(self, prop, value): |
| 265 return self.converter.encode_prop(prop, value) |
| 266 |
| 267 def decode_value(self, prop, value): |
| 268 return self.converter.decode_prop(prop, value) |
| 269 |
| 270 def get_s3_connection(self): |
| 271 if not self.s3: |
| 272 self.s3 = boto.connect_s3(self.aws_access_key_id, self.aws_secret_ac
cess_key) |
| 273 return self.s3 |
| 274 |
| 275 def get_list(self, prop_node, item_type): |
| 276 values = [] |
| 277 try: |
| 278 items_node = prop_node.getElementsByTagName('items')[0] |
| 279 except: |
| 280 return [] |
| 281 for item_node in items_node.getElementsByTagName('item'): |
| 282 value = self.converter.decode(item_type, item_node) |
| 283 values.append(value) |
| 284 return values |
| 285 |
| 286 def get_object_from_doc(self, cls, id, doc): |
| 287 obj_node = doc.getElementsByTagName('object')[0] |
| 288 if not cls: |
| 289 class_name = obj_node.getAttribute('class') |
| 290 cls = find_class(class_name) |
| 291 if not id: |
| 292 id = obj_node.getAttribute('id') |
| 293 obj = cls(id) |
| 294 for prop_node in obj_node.getElementsByTagName('property'): |
| 295 prop_name = prop_node.getAttribute('name') |
| 296 prop = obj.find_property(prop_name) |
| 297 value = self.decode_value(prop, prop_node) |
| 298 value = prop.make_value_from_datastore(value) |
| 299 if value != None: |
| 300 try: |
| 301 setattr(obj, prop.name, value) |
| 302 except: |
| 303 pass |
| 304 return obj |
| 305 |
| 306 def get_props_from_doc(self, cls, id, doc): |
| 307 """ |
| 308 Pull out the properties from this document |
| 309 Returns the class, the properties in a hash, and the id if provided as a
tuple |
| 310 :return: (cls, props, id) |
| 311 """ |
| 312 obj_node = doc.getElementsByTagName('object')[0] |
| 313 if not cls: |
| 314 class_name = obj_node.getAttribute('class') |
| 315 cls = find_class(class_name) |
| 316 if not id: |
| 317 id = obj_node.getAttribute('id') |
| 318 props = {} |
| 319 for prop_node in obj_node.getElementsByTagName('property'): |
| 320 prop_name = prop_node.getAttribute('name') |
| 321 prop = cls.find_property(prop_name) |
| 322 value = self.decode_value(prop, prop_node) |
| 323 value = prop.make_value_from_datastore(value) |
| 324 if value != None: |
| 325 props[prop.name] = value |
| 326 return (cls, props, id) |
| 327 |
| 328 |
| 329 def get_object(self, cls, id): |
| 330 if not self.connection: |
| 331 self._connect() |
| 332 |
| 333 if not self.connection: |
| 334 raise NotImplementedError("Can't query without a database connection
") |
| 335 url = "/%s/%s" % (self.db_name, id) |
| 336 resp = self._make_request('GET', url) |
| 337 if resp.status == 200: |
| 338 doc = parse(resp) |
| 339 else: |
| 340 raise Exception("Error: %s" % resp.status) |
| 341 return self.get_object_from_doc(cls, id, doc) |
| 342 |
| 343 def query(self, cls, filters, limit=None, order_by=None): |
| 344 if not self.connection: |
| 345 self._connect() |
| 346 |
| 347 if not self.connection: |
| 348 raise NotImplementedError("Can't query without a database connection
") |
| 349 |
| 350 from urllib import urlencode |
| 351 |
| 352 query = str(self._build_query(cls, filters, limit, order_by)) |
| 353 if query: |
| 354 url = "/%s?%s" % (self.db_name, urlencode({"query": query})) |
| 355 else: |
| 356 url = "/%s" % self.db_name |
| 357 resp = self._make_request('GET', url) |
| 358 if resp.status == 200: |
| 359 doc = parse(resp) |
| 360 else: |
| 361 raise Exception("Error: %s" % resp.status) |
| 362 return self._object_lister(cls, doc) |
| 363 |
| 364 def _build_query(self, cls, filters, limit, order_by): |
| 365 import types |
| 366 if len(filters) > 4: |
| 367 raise Exception('Too many filters, max is 4') |
| 368 parts = [] |
| 369 properties = cls.properties(hidden=False) |
| 370 for filter, value in filters: |
| 371 name, op = filter.strip().split() |
| 372 found = False |
| 373 for property in properties: |
| 374 if property.name == name: |
| 375 found = True |
| 376 if types.TypeType(value) == types.ListType: |
| 377 filter_parts = [] |
| 378 for val in value: |
| 379 val = self.encode_value(property, val) |
| 380 filter_parts.append("'%s' %s '%s'" % (name, op, val)
) |
| 381 parts.append("[%s]" % " OR ".join(filter_parts)) |
| 382 else: |
| 383 value = self.encode_value(property, value) |
| 384 parts.append("['%s' %s '%s']" % (name, op, value)) |
| 385 if not found: |
| 386 raise Exception('%s is not a valid field' % name) |
| 387 if order_by: |
| 388 if order_by.startswith("-"): |
| 389 key = order_by[1:] |
| 390 type = "desc" |
| 391 else: |
| 392 key = order_by |
| 393 type = "asc" |
| 394 parts.append("['%s' starts-with ''] sort '%s' %s" % (key, key, type)
) |
| 395 return ' intersection '.join(parts) |
| 396 |
| 397 def query_gql(self, query_string, *args, **kwds): |
| 398 raise NotImplementedError("GQL queries not supported in XML") |
| 399 |
| 400 def save_list(self, doc, items, prop_node): |
| 401 items_node = doc.createElement('items') |
| 402 prop_node.appendChild(items_node) |
| 403 for item in items: |
| 404 item_node = doc.createElement('item') |
| 405 items_node.appendChild(item_node) |
| 406 if isinstance(item, Node): |
| 407 item_node.appendChild(item) |
| 408 else: |
| 409 text_node = doc.createTextNode(item) |
| 410 item_node.appendChild(text_node) |
| 411 |
| 412 def save_object(self, obj, expected_value=None): |
| 413 """ |
| 414 Marshal the object and do a PUT |
| 415 """ |
| 416 doc = self.marshal_object(obj) |
| 417 if obj.id: |
| 418 url = "/%s/%s" % (self.db_name, obj.id) |
| 419 else: |
| 420 url = "/%s" % (self.db_name) |
| 421 resp = self._make_request("PUT", url, body=doc.toxml()) |
| 422 new_obj = self.get_object_from_doc(obj.__class__, None, parse(resp)) |
| 423 obj.id = new_obj.id |
| 424 for prop in obj.properties(): |
| 425 try: |
| 426 propname = prop.name |
| 427 except AttributeError: |
| 428 propname = None |
| 429 if propname: |
| 430 value = getattr(new_obj, prop.name) |
| 431 if value: |
| 432 setattr(obj, prop.name, value) |
| 433 return obj |
| 434 |
| 435 |
| 436 def marshal_object(self, obj, doc=None): |
| 437 if not doc: |
| 438 doc = self.new_doc() |
| 439 if not doc: |
| 440 doc = self.doc |
| 441 obj_node = doc.createElement('object') |
| 442 |
| 443 if obj.id: |
| 444 obj_node.setAttribute('id', obj.id) |
| 445 |
| 446 obj_node.setAttribute('class', '%s.%s' % (obj.__class__.__module__, |
| 447 obj.__class__.__name__)) |
| 448 root = doc.documentElement |
| 449 root.appendChild(obj_node) |
| 450 for property in obj.properties(hidden=False): |
| 451 prop_node = doc.createElement('property') |
| 452 prop_node.setAttribute('name', property.name) |
| 453 prop_node.setAttribute('type', property.type_name) |
| 454 value = property.get_value_for_datastore(obj) |
| 455 if value is not None: |
| 456 value = self.encode_value(property, value) |
| 457 if isinstance(value, list): |
| 458 self.save_list(doc, value, prop_node) |
| 459 elif isinstance(value, Node): |
| 460 prop_node.appendChild(value) |
| 461 else: |
| 462 text_node = doc.createTextNode(unicode(value).encode("ascii"
, "ignore")) |
| 463 prop_node.appendChild(text_node) |
| 464 obj_node.appendChild(prop_node) |
| 465 |
| 466 return doc |
| 467 |
| 468 def unmarshal_object(self, fp, cls=None, id=None): |
| 469 if isinstance(fp, str) or isinstance(fp, unicode): |
| 470 doc = parseString(fp) |
| 471 else: |
| 472 doc = parse(fp) |
| 473 return self.get_object_from_doc(cls, id, doc) |
| 474 |
| 475 def unmarshal_props(self, fp, cls=None, id=None): |
| 476 """ |
| 477 Same as unmarshalling an object, except it returns |
| 478 from "get_props_from_doc" |
| 479 """ |
| 480 if isinstance(fp, str) or isinstance(fp, unicode): |
| 481 doc = parseString(fp) |
| 482 else: |
| 483 doc = parse(fp) |
| 484 return self.get_props_from_doc(cls, id, doc) |
| 485 |
| 486 def delete_object(self, obj): |
| 487 url = "/%s/%s" % (self.db_name, obj.id) |
| 488 return self._make_request("DELETE", url) |
| 489 |
| 490 def set_key_value(self, obj, name, value): |
| 491 self.domain.put_attributes(obj.id, {name : value}, replace=True) |
| 492 |
| 493 def delete_key_value(self, obj, name): |
| 494 self.domain.delete_attributes(obj.id, name) |
| 495 |
| 496 def get_key_value(self, obj, name): |
| 497 a = self.domain.get_attributes(obj.id, name) |
| 498 if name in a: |
| 499 return a[name] |
| 500 else: |
| 501 return None |
| 502 |
| 503 def get_raw_item(self, obj): |
| 504 return self.domain.get_item(obj.id) |
| 505 |
| 506 def set_property(self, prop, obj, name, value): |
| 507 pass |
| 508 |
| 509 def get_property(self, prop, obj, name): |
| 510 pass |
| 511 |
| 512 def load_object(self, obj): |
| 513 if not obj._loaded: |
| 514 obj = obj.get_by_id(obj.id) |
| 515 obj._loaded = True |
| 516 return obj |
| 517 |
OLD | NEW |