Package pyperry :: Module relation
[frames] | no frames]

Source Code for Module pyperry.relation

  1  from copy import copy, deepcopy 
  2  from pyperry.errors import ArgumentError, RecordNotFound 
  3   
4 -class DelayedMerge(object):
5 """ 6 This little class takes a Relation object, and a function that returns a 7 Relation object when initialized. When it is called it passes the params 8 on to the function and executes it merging the result on to the original 9 Relation object. This enables chaining scope methods. 10 """
11 - def __init__(self, obj, func):
12 self.obj = obj 13 self.func = func
14
15 - def __call__(self, *args, **kwargs):
16 return self.obj.merge(self.func(*args, **kwargs))
17
18 -class Relation(object):
19 """ 20 Relations 21 ========= 22 23 The C{Relation} class represents an abstract query for a data store. It 24 provides a set of query methods and a set of finder methods that allow you 25 to build and execute a query respectively. While the method names and 26 terminology used in this class are representative of SQL queries, the 27 resulting query may be used for non-SQL data stores given that an 28 appropriate adapter has been written for that data store. 29 30 Method delgation 31 ---------------- 32 33 L{pyperry.Base} delegates any calls to query methods or finder methods to a 34 pre-initialized C{Relation} class. That means that if you have a Person 35 model, instead of having to write C{Relation(Person).order('last_name')} 36 to create a relation, you can simply write C{Person.order('last_name')}. 37 38 Method chaining 39 --------------- 40 41 All query methods can be chained. That means that every query method 42 returns a new C{Relation} instance that is a copy of the old relation 43 relation merged with the result of calling the current query method. This 44 saves a lot of typing when writing longer queries like 45 C{Person.order('last_name').limit(10).offset(100).where(age=24).all()} 46 Once you call one of the finder methods, the query gets executed and the 47 result of that query is returned, which breaks the method chain. 48 49 Query methods 50 ------------- 51 52 There are two kinds of query methods: singular and plural. Singular query 53 methods only store one value in the underlying relation, so succesive calls 54 to a singular query method overwrite the old value with the new value. Plural 55 query methods may have multiple values, so successive calls to a plural 56 query method append or merge the old value with the new value so that all 57 values are present in the resulting query. Please note that not all query 58 methods will apply to all data stores. 59 60 Singular query methods 61 ~~~~~~~~~~~~~~~~~~~~~~ 62 63 - B{limit:} (int) limit the number of records returned by the data 64 store to the value specified 65 - B{offset:} (int) exclude the first N records from the result where 66 N is the value passed to offset 67 - B{from:} (string) specify the source of the records within the data 68 store, such as a table name in a SQL database 69 - B{from_:} alias of C{from} 70 71 Plural query methods 72 ~~~~~~~~~~~~~~~~~~~~ 73 74 - B{select:} (string) only include the given attributes in the 75 resulting records 76 - B{where:} (string, dict) specify conditions that the records must 77 meet to be included in the result 78 - B{order:} (string) order the records by the given values 79 - B{joins:} (string) a full SQL join clause 80 - B{includes:} (string, dict) L{eager load <PreloadAssociations>} any 81 associations matching the given values. Include values may be nested 82 in a dict. 83 - B{conditions:} alias of C{where} 84 - B{group:} (string) group the records by the given values 85 - B{having:} (string) specify conditions that apply only to the group 86 values 87 - B{modifiers:} (dict) include any additional information in the 88 relation. Modfiers allow you to include data in your queries that may 89 be useful to a L{processor or 90 middleware<pyperry.adapter.abstract_adapter>} you write. The 91 modifiers value is not included in the dictionary returned by the 92 L{query} method, so the modifiers will not be passed on to the data 93 store. 94 95 Finder methods 96 ============== 97 98 - B{L{first}:} return all records represented by the current relation 99 - B{L{all}:} return only the first record represented by the currennt 100 relation 101 102 Finder options 103 ============== 104 105 The finder methods will also accept a dictionary or keyword arguments 106 that specify a query without you having to actually call the query methods. 107 The keys should be named the same as the corresponding query methods and 108 the values should be the same values you would normally pass to those query 109 methods. For example, the following to queries are equivalent:: 110 111 Person.order('last_name').limit(10).all() 112 Person.all({'order': 'last_name', 'limit': 10}) 113 114 Some other methods also accept finder options as a dictionary or keyword 115 arguments such as the C{scope} method and association methods on 116 L{pyperry.Base}. 117 118 """ 119 120 singular_query_methods = ['limit', 'offset', 'from', 'sql'] 121 plural_query_methods = ['select', 'group', 'order', 'joins', 'includes', 122 'where', 'having'] 123 aliases = { 'from_': 'from', 'conditions': 'where' } 124
125 - def __init__(self, klass):
126 """Set klass this relation object is mapped to""" 127 self.klass = klass 128 self.params = {} 129 self._query = None 130 self._records = None 131 132 for method in self.singular_query_methods: 133 self.params[method] = None 134 for method in self.plural_query_methods: 135 self.params[method] = [] 136 self.params['modifiers'] = []
137 138 # Dynamically create the query methods as they are needed
139 - def __getattr__(self, key):
140 """Delegate missing attributes to appropriate places""" 141 if key in self.singular_query_methods: 142 self.create_singular_method(key) 143 return getattr(self, key) 144 elif key in self.plural_query_methods: 145 self.create_plural_method(key) 146 return getattr(self, key) 147 elif key in self.aliases.keys(): 148 return getattr(self, self.aliases[key]) 149 # TODO: Investigate why the BALLS I can't just say self.klass here 150 # without infinite recursion... 151 elif key in self.__dict__['klass'].scopes.keys(): 152 return DelayedMerge(self, getattr(self.__dict__['klass'], key)) 153 else: 154 raise AttributeError
155 156 # Allows use of iterator like structures on the Relation model. 157 # (like for loops, array comprehensions, etc.)
158 - def __iter__(self):
159 for item in self.fetch_records(): 160 yield(item)
161 162 # Delgates array indexing and splicing to records list
163 - def __getitem__(self, index):
164 return self.fetch_records().__getitem__(index)
165 166 # Delegates len() to the records list
167 - def __len__(self):
168 return len(self.fetch_records())
169 170
171 - def first(self, options={}, **kwargs):
172 """Apply a limit scope of 1 and return the resulting singular value""" 173 options.update({ 'limit': 1 }) 174 records = self.all(options, **kwargs) 175 if len(records) < 1: 176 raise RecordNotFound('could not find a record for %s' % 177 self.klass.__name__) 178 return records[0]
179
180 - def all(self, options={}, **kwargs):
181 """ 182 Apply any finder options passed and execute the query returning the 183 list of records 184 """ 185 return self.apply_finder_options(options, **kwargs).fetch_records()
186
187 - def find(self, pks_or_mode, options={}, **kwargs):
188 """ 189 Returns a record or list of records matching the primary key or array 190 of primary keys given as its first argument. If the first argument is 191 one of 'first' or 'all', the C{first} or C{all} finder methods 192 respectively are called with the given finder options from the second 193 argument. 194 """ 195 if pks_or_mode == 'all': 196 return self.all(options, **kwargs) 197 elif pks_or_mode == 'first': 198 return self.first(options, **kwargs) 199 elif isinstance(pks_or_mode, list): 200 result = self.where({self.klass.primary_key(): pks_or_mode}) 201 if len(result) < len(pks_or_mode): 202 raise RecordNotFound( 203 self._record_not_found_message(pks_or_mode, result)) 204 return result 205 elif isinstance(pks_or_mode, str) or isinstance(pks_or_mode, int): 206 return self.where({self.klass.primary_key(): pks_or_mode}).first() 207 else: 208 raise ArgumentError('unkown arguments for find method')
209
210 - def _record_not_found_message(self, pk_array, results):
211 err = "Couldn't find %s records for all primary key values in %s. " 212 err += "(expected %d records but only found %d)" 213 n = len(pk_array) 214 m = len(results) 215 return err % (self.klass.__name__, str(pk_array), n, m)
216
217 - def apply_finder_options(self, options={}, **kwargs):
218 """Apply given dictionary as finder options returning a new relation""" 219 self = self.clone() 220 options = copy(options) 221 options.update(kwargs) 222 223 valid_methods = ( 224 self.singular_query_methods + 225 self.plural_query_methods + 226 self.aliases.keys() + 227 ['modifiers']) 228 229 for method in set(valid_methods) & set(options.keys()): 230 if self.aliases.get(method): 231 value = options[method] 232 method = self.aliases[method] 233 else: 234 value = options[method] 235 236 if value: 237 self = getattr(self, method)(value) 238 239 return self
240 241
242 - def merge(self, relation):
243 """Merge given relation onto self returning a new relation""" 244 self = self.clone() 245 246 query_methods = (self.singular_query_methods + 247 self.plural_query_methods + 248 ['modifiers']) 249 250 for method in query_methods: 251 value = relation.params[method] 252 if value and isinstance(value, list): 253 self = getattr(self, method)(*value) 254 elif value: 255 self = getattr(self, method)(value) 256 257 return self
258
259 - def query(self):
260 """ 261 Return the query dictionary. This is used to form the dictionary of 262 values used in the fetch_records call. 263 """ 264 if self._query: return self._query 265 266 self._query = {} 267 query_methods = [method for method in 268 (self.plural_query_methods + self.singular_query_methods) 269 if method is not 'includes'] 270 271 for method in query_methods: 272 value = self.params[method] 273 if value: 274 self._query[method] = self._eval_lambdas(value) 275 276 value = self.includes_value() 277 if value: 278 self._query['includes'] = value 279 280 return self._query
281
282 - def fetch_records(self):
283 """Perform the query and return the resulting list (aliased as list)""" 284 if self._records is None: 285 self._records = self.klass.fetch_records(self) 286 return self._records
287 list = fetch_records 288
289 - def includes_value(self):
290 """ 291 Combines arguments passed to includes into a single dict to support 292 nested includes. 293 294 For example, the following query:: 295 296 r = relation.includes('foo') 297 r = r.includes({'bar': 'baz'}) 298 r = r.includes('boo', {'bar': 'biz'}) 299 300 will result in an includes dict like this:: 301 302 { 303 'foo': {}, 304 'bar': {'biz': {}, 'baz': {}}, 305 'boo': {} 306 } 307 308 """ 309 values = self.params['includes'] 310 if not values: return 311 312 values = [(v() if callable(v) else v) for v in values] 313 nested_includes = self._get_includes_value(values) 314 return nested_includes
315
316 - def _get_includes_value(self, value):
317 """does the dirty work for includes value""" 318 includes = {} 319 320 if not value: # leaf node 321 pass 322 elif hasattr(value, 'iteritems'): # dict 323 for k, v in value.iteritems(): 324 v = {k: self._get_includes_value(v)} 325 includes = self._deep_merge(includes, v) 326 elif hasattr(value, '__iter__'): # list, but not string 327 for v in value: 328 v = self._get_includes_value(v) 329 includes = self._deep_merge(includes, v) 330 else: # string 331 includes.update({value: {}}) 332 333 return includes
334
335 - def _deep_merge(self, a, b):
336 """ 337 Recursively merges dict b into dict a, such that if a[x] is a dict and 338 b[x] is a dict, b[x] is merged into a[x] instead of b[x] overwriting 339 a[x]. 340 341 """ 342 a = copy(a) 343 for k, v in b.iteritems(): 344 if k in a and hasattr(v, 'iteritems'): 345 a[k] = self._deep_merge(a[k], v) 346 else: 347 a[k] = v 348 return a
349
350 - def modifiers(self, value):
351 """ 352 A pseudo query method used to store additional data on a relation. 353 354 modifiers expects its value to be either a dict or a lambda that 355 returns a dict. Successive calls to modifiers will 'merge' the values 356 it receives into a single dict. The modifiers method behaves almost 357 identical to a plural query method. You can even use the modifiers 358 method in your scopes. The only difference is that the modifiers value 359 is not included in the dict returned by the query() method. The purpose 360 of having a modifiers query method is to include additional data in the 361 query that may be of interest to middlewares or adapters but is not 362 inherent to the query itself. 363 364 """ 365 rel = self.clone() 366 if value is None: 367 rel.params['modifiers'] = [] 368 else: 369 rel.params['modifiers'].append(value) 370 return rel
371
372 - def modifiers_value(self):
373 """ 374 Returns the combined dict of all values passed to the modifers method. 375 """ 376 if hasattr(self, '_modifiers'): 377 return self._modifiers 378 379 self._modifiers = {} 380 for value in self.params['modifiers']: 381 if callable(value): 382 value = value() 383 try: 384 self._modifiers.update(value) 385 except: 386 raise TypeError( 387 'modifier values must evaluate to dict-like objects') 388 389 return self._modifiers
390
391 - def create_singular_method(self, key):
392 """ 393 Create a default singular method with the given key. For special 394 functionality you can create a explicit method that will shadow this 395 implementation. These methods will be created dynamically at runtime. 396 """ 397 def method(self, value): 398 self = self.clone() 399 self.params[key] = value 400 return self
401 402 method.__name__ = key 403 setattr(self.__class__, key, method)
404
405 - def create_plural_method(self, key):
406 """ 407 Create a default plural method with the given key. For special 408 functionality you can create a explicit method that will shadow this 409 implementation. These methods will be created dynamically at runtime. 410 """ 411 def method(self, *value, **kwargs): 412 self = self.clone() 413 # If they are passing in a list rather than a tuple 414 if len(value) == 1 and isinstance(value[0], list): 415 value = value[0] 416 self.params[key] += list(value) 417 if len(kwargs) > 0: 418 self.params[key].append(kwargs) 419 return self
420 421 method.__name__ = key 422 setattr(self.__class__, key, method) 423
424 - def clone(self):
425 cloned = deepcopy(self) 426 cloned.reset() 427 return cloned
428
429 - def reset(self):
430 self._records = None 431 self._query = None
432
433 - def __repr__(self):
434 # return repr(self.fetch_records()) 435 return("<Relation for %s Query: %s>" % 436 (self.klass.__name__, str(self.params)) )
437
438 - def _eval_lambdas(self, value):
439 if type(value).__name__ == 'list': 440 return [ self._eval_lambdas(item) for item in value ] 441 elif type(value).__name__ == 'function': 442 return value() 443 else: 444 return value
445