PageRenderTime 85ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/bulbs/model.py

https://github.com/HonzaKral/bulbs
Python | 481 lines | 388 code | 26 blank | 67 comment | 13 complexity | d3a6b7dd8e86ea3deec40275de19022f MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright 2011 James Thornton (http://jamesthornton.com)
  4. # BSD License (see LICENSE for details)
  5. #
  6. """
  7. Base classes for modeling domain objects that wrap vertices and edges.
  8. """
  9. import config
  10. from rest import Resource
  11. from index import IndexProxy
  12. from element import Vertex, VertexProxy, Edge, EdgeProxy
  13. from typesystem import TypeSystem, ClassProperty
  14. class Model(TypeSystem):
  15. """
  16. Abstract base class for Node and Relationship.
  17. It's a sublcass of TypeSystem, which provides a mechanism for type-checking
  18. database properties before they're saved in the database.
  19. To create a type-checked database property, use the Property class and
  20. datatype classes, which are located in the property module. Example::
  21. name = Property(String, nullable=False)
  22. age = Property(Integer)
  23. """
  24. # You can override this default resource.
  25. resource = Resource(config.DATABASE_URL)
  26. @property
  27. def eid(self):
  28. """Return the element ID. Override this to change it from eid."""
  29. return self._id
  30. #@property
  31. #def element_type(self):
  32. # """Return the element type."""
  33. # return self._data.get(config.TYPE_VAR,None)
  34. #return self._data[TYPE_VAR]
  35. @classmethod
  36. def get(self,_id):
  37. """
  38. Returns the element for the specified ID.
  39. ::param _id: The element's ID.
  40. """
  41. return self._element_proxy.get(_id)
  42. @classmethod
  43. def get_all(self):
  44. """Returns all the elements for the model type."""
  45. index_name = self._element_proxy._path()
  46. target = "%s/indices/%s" % (self.resource.db_name,index_name)
  47. params = dict(key="element_type",value=self.element_type)
  48. resp = self.resource.get(target,params)
  49. for result in resp.results:
  50. yield self(self.resource,result)
  51. @classmethod
  52. def remove(self,_id,params):
  53. """
  54. Removes a property from the element for the specified ID.
  55. ::param _id: The element's ID.
  56. ::param params: The element's property to remove.
  57. """
  58. return self._element_proxy.remove(_id,params)
  59. @classmethod
  60. def create_index(self,index_keys=None,index_type="automatic"):
  61. """
  62. Creates an index for the model.
  63. ::param index_keys: The specific keys to index. If set to None,
  64. any key can be indexed. Defaults to None.
  65. ::param index_type: The type of index to create. Either manual
  66. or automatic. Defaults to automatic. See
  67. Rexster docs for definitions.
  68. """
  69. index_name = self._get_index_name()
  70. index_class = self._get_index_class()
  71. index_attributes = (index_name,index_class,index_type,index_keys)
  72. return self.index_proxy.create(*index_attributes)
  73. @classmethod
  74. def delete_index(self):
  75. """Deletes the model's index."""
  76. index_name = self._get_element_key()
  77. return self.index_proxy.delete(index_name)
  78. @classmethod
  79. def _get_element_proxy(self):
  80. """
  81. Returns the element's proxy class. The Node and Relationship classes
  82. override this.
  83. """
  84. raise NotImplementedError
  85. @classmethod
  86. def _get_index_proxy(self):
  87. """Returns the index's proxy class."""
  88. return IndexProxy(self.resource,self)
  89. @classmethod
  90. def _get_index_name(self):
  91. """Returns the model's index name."""
  92. return self._get_element_key()
  93. @classmethod
  94. def _get_index(self):
  95. """Returns the model's index."""
  96. index_name = self._get_index_name()
  97. return self.index_proxy.get(index_name)
  98. index = ClassProperty(_get_index)
  99. index_proxy = ClassProperty(_get_index_proxy)
  100. #def element_type_sanity_check(self,results):
  101. # element_type = self.get_element_type(results)
  102. # if element_type is not None:
  103. # if element_type != getattr(self,TYPE_VAR):
  104. # raise("DB element type (%) doesn't match class element type (%s)" % \
  105. # (element_type, getattr(self,TYPE_VAR)))
  106. #def get_arg(self,name,default=None):
  107. # return self._kwds.get(name,default)
  108. def _create(self,*args,**kwds):
  109. """
  110. Create a new element in the database.
  111. ::param *args: Optional dict of name/value pairs of database properties.
  112. ::param **kwds: name/value pairs of database properties to store.
  113. """
  114. self._set_keyword_attributes(kwds)
  115. self._validate_property_data()
  116. data = self._get_property_data()
  117. args = list(args)
  118. args.append(data)
  119. self.before_created()
  120. # Using super here b/c Vertex and Edge have different create methods
  121. #resp = super(self.__class__,self).create(*args,raw=True)
  122. # got a "mismatched input, expecting double star" in Jython so
  123. # passing raw as a "double star"
  124. kwds = dict(raw=True)
  125. resp = self._element_proxy.create(*args,**kwds)
  126. self._initialize_element(self.resource,resp.results)
  127. #self.set_element_data(resp.results)
  128. self.after_created()
  129. def _read(self,results):
  130. """
  131. Read an element's data that was retrieved from the DB and set its model
  132. values.
  133. ::param results: A list containing the results returned by Rexster.
  134. """
  135. self.before_read()
  136. self._initialize_element(self.resource,results)
  137. #self.set_element_data(results)
  138. self._set_property_data(results)
  139. self.after_read()
  140. def _update(self,eid,kwds):
  141. """
  142. Updates the element in the database.
  143. ::param eid: The ID of the element to update.
  144. ::param **kwds: name/value pairs of database properties to store.
  145. """
  146. self.eid = eid
  147. self._set_keyword_attributes(kwds)
  148. self.save()
  149. def save(self):
  150. """
  151. Saves/updates the element's data in the database.
  152. """
  153. self._validate_property_data()
  154. data = self._get_property_data()
  155. self.before_updated()
  156. #resp = super(self.__class__,self).update(self.eid,data,raw=True)
  157. resp = self._element_proxy.update(self.eid,data,raw=True)
  158. self._initialize_element(self.resource,resp.results)
  159. #self.set_element_data(resp.results)
  160. self.after_updated()
  161. def delete(self):
  162. """Deletes an element from the database."""
  163. # Should we make this a classmethod instead?
  164. # Should we provide an option to set a deleted flag or just override this?
  165. self.before_deleted()
  166. #resp = super(self.__class__,self).delete(self)
  167. resp = self._element_proxy.delete(self)
  168. self.after_deleted()
  169. return resp
  170. def initialize(self,args,kwds):
  171. """
  172. Initialize the model.
  173. If results is passed in, that means data was retrieved from the DB
  174. via a get request or gremlin query so we just set the property values
  175. based on what is stored in the DB.
  176. If eid was passed in, that means we're updating the element so we
  177. set the model's attributes based on the keyword variables passed in,
  178. and we save in the DB any attributes specified as DB Properties.
  179. If neither results nor eid were passed in, that means we're creating a
  180. new element so we set the model's attributes based on the keyword
  181. variables that were passed in, and we save in the DB any attributes
  182. specified as DB Properties.
  183. Also, we set self.kwds so the vars are available to the before/after wrappers.
  184. ::param *args: Optional dict of name/value pairs of database properties.
  185. ::param **kwds: name/value pairs of database properties to store.
  186. """
  187. # We're doing a little arg dance instead of keyword mangling.
  188. # This avoids problems if a user wants to use the word "results" as a
  189. # Property name (actually, no it doesn't, b/c you're using results
  190. # elsewhere. Ugh -- I hate keyword mangling.
  191. _id = None
  192. results = None
  193. args = list(args)
  194. # save kwds so it's available to before/after wrappers
  195. self._kwds = kwds
  196. # in case someone wants to pass in a dict of data instead of by keywords
  197. _data = kwds.pop("_data",{})
  198. # in case someone passed in a dict of data plus some data by keywords
  199. _data.update(**kwds)
  200. if args and isinstance(args[0],Resource):
  201. self.resource = args.pop(0)
  202. if args and isinstance(args[0],dict):
  203. results = args.pop(0)
  204. if args and isinstance(args[0],int):
  205. _id = args.pop(0)
  206. #print "RESULTS:", results
  207. self.before_initialized()
  208. if results is not None:
  209. # must have been a get or gremlin request
  210. Model._read(self,results)
  211. elif _id is not None:
  212. # calling Model explicitly b/c Vertex/Edge have an update method too
  213. Model._update(self,_id,_data)
  214. elif args or kwds:
  215. # calling Model explicitly b/c Vertex/Edge have a create method too
  216. Model._create(self,*args,**_data)
  217. else:
  218. # create an empty Node (can't have an empty Relationship b/c must have label)
  219. Model._create(self,{})
  220. self.after_initialized()
  221. def before_initialized(self):
  222. """Virtual method run before the model is initialized."""
  223. pass
  224. def after_initialized(self):
  225. """Virtual method run after the model is initialized."""
  226. pass
  227. def before_created(self):
  228. """Virtual method run before an element is created in the DB."""
  229. pass
  230. def after_created(self):
  231. """Virtual method run after an element is created in the DB."""
  232. pass
  233. def before_read(self):
  234. """Virtual method run before element data is read from the DB."""
  235. pass
  236. def after_read(self):
  237. """Virtual method run after element data is read from the DB."""
  238. pass
  239. def before_updated(self):
  240. """Virtual method run before an element is updated in the DB."""
  241. pass
  242. def after_updated(self):
  243. """Virtual method run after an element is updated in the DB."""
  244. pass
  245. def before_deleted(self):
  246. """Virtual method run before an element is deleted from the DB."""
  247. pass
  248. def after_deleted(self):
  249. """Virtual method run after an element is deleted from the DB."""
  250. pass
  251. class Node(Vertex,Model):
  252. """
  253. An abstract base class used to create classes that model domain objects. It is
  254. not meant to be used directly
  255. To use this, create a subclass specific to the type of data you are
  256. storing.
  257. Example model declaration::
  258. from bulbs.model import Node
  259. from bulbs.property import Property, String, Integer
  260. class Person(Node):
  261. element_type = "person"
  262. name = Property(String, nullable=False)
  263. age = Property(Integer)
  264. def after_created():
  265. # include code to create relationships and to index the node
  266. pass
  267. Example usage::
  268. # Create a node in the DB:
  269. >>> james = Person(name="James Thornton")
  270. >>> james.eid
  271. 3
  272. >>> james.name
  273. 'James Thornton'
  274. # Get a node from the DB:
  275. >>> james = Person.get(3)
  276. # Update the node in the DB:
  277. >>> james.age = 34
  278. >>> james.save()
  279. """
  280. # IMPORTANT:
  281. # Can't do the metamagic unless object has been created so we can't use
  282. # __new__ because all the initialzed vars would be on the class and not the
  283. # object. Don't worry about setting the object attributes for the kwds data
  284. # because the initialize sets them and the grandancestor Element provides
  285. # DB data to you by overriding __getattr__.
  286. #element_type = "node"
  287. def __init__(self,*args,**kwds):
  288. """
  289. Initialize the node.
  290. ::param *args: Optional dict of name/value pairs of database properties.
  291. ::param **kwds: name/value pairs of database properties to store.
  292. """
  293. # pass results as the first (and only) arg to init values from gets
  294. # pass eid as the first arg if you are updating the element
  295. self.initialize(args,kwds)
  296. @classmethod
  297. def _get_element_key(self):
  298. """Returns the element's key that's used for stuff like the index name."""
  299. element_key = getattr(self,config.TYPE_VAR)
  300. return element_key
  301. @classmethod
  302. def _get_index_class(self):
  303. """Returns the element's base class."""
  304. return "vertex"
  305. @classmethod
  306. def _get_element_proxy(self):
  307. """Returns the element's proxy."""
  308. return VertexProxy(self.resource,self)
  309. _element_proxy = ClassProperty(_get_element_proxy)
  310. class Relationship(Edge,Model):
  311. """
  312. An abstract base class used to create classes that model domain objects. It is
  313. not meant to be used directly
  314. To use this, create a subclass specific to the type of data you are
  315. storing.
  316. Example usage for an edge between a blog entry node and its creating user::
  317. class CreatedBy(Relationship):
  318. label = "created_by"
  319. timestamp = Property(Float, default="current_timestamp", nullable=False)
  320. @property
  321. def entry(self):
  322. return Entry.get(self.outV)
  323. @property
  324. def user(self):
  325. return User.get(self.inV)
  326. def current_timestamp(self):
  327. return time.time()
  328. >>> entry = Entry(text="example blog entry")
  329. >>> james = Person(name="James")
  330. >>> CreatedBy(entry,james)
  331. # Or if you just want to create a basic relationship between two nodes, do::
  332. >>> Relationship.create(entry,"created_by",james)
  333. """
  334. def __init__(self,*args,**kwds):
  335. args = list(args)
  336. if args and isinstance(args[1],Vertex):
  337. # 2nd arg is a Vertex so they're in form of CreatedBy(entry,james)
  338. # TODO: clean up this arg thing -- this is a quick hack to fix a bug
  339. outV = args.pop(0)
  340. inV = args.pop(0)
  341. args = (outV,self.label,inV)
  342. self.initialize(args,kwds)
  343. @classmethod
  344. def create(self,outV,label,inV,**kwds):
  345. """
  346. Create a generic relationship between two nodes.
  347. ::param outV: the outgoing vertex.
  348. ::param label: the edge's label.
  349. ::param inV: the incoming vertex.
  350. ::param **kwds: Optional keyword arguments. Name/value pairs of properties to store.
  351. """
  352. return Relationship(outV,label,inV,**kwds)
  353. @classmethod
  354. def _get_element_key(self):
  355. """Returns the element's key that's used for stuff like the index name."""
  356. return self.label
  357. @classmethod
  358. def _get_index_class(self):
  359. """Returns the element's base class."""
  360. return "edge"
  361. @classmethod
  362. def _get_element_proxy(self):
  363. """Returns the element's proxy."""
  364. return EdgeProxy(self.resource,self)
  365. _element_proxy = ClassProperty(_get_element_proxy)