1 from copy import copy, deepcopy
2 from pyperry.errors import ArgumentError, RecordNotFound
3
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 """
12 self.obj = obj
13 self.func = func
14
16 return self.obj.merge(self.func(*args, **kwargs))
17
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
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
155
156
157
161
162
165
166
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
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
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
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
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
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
317 """does the dirty work for includes value"""
318 includes = {}
319
320 if not value:
321 pass
322 elif hasattr(value, 'iteritems'):
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__'):
327 for v in value:
328 v = self._get_includes_value(v)
329 includes = self._deep_merge(includes, v)
330 else:
331 includes.update({value: {}})
332
333 return includes
334
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
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
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
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
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
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
425 cloned = deepcopy(self)
426 cloned.reset()
427 return cloned
428
430 self._records = None
431 self._query = None
432
434
435 return("<Relation for %s Query: %s>" %
436 (self.klass.__name__, str(self.params)) )
437
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