portal: added wip for PI validation page
[myslice.git] / manifold / core / filter.py
1 from types import StringTypes
2 try:
3     set
4 except NameError:
5     from sets import Set
6     set = Set
7
8 import time
9 import datetime # Jordan
10 #from manifold.util.parameter import Parameter, Mixed, python_type
11 from manifold.util.predicate import Predicate, eq
12 from itertools                  import ifilter
13
14 class Filter(set):
15     """
16     A filter is a set of predicates
17     """
18
19     #def __init__(self, s=()):
20     #    super(Filter, self).__init__(s)
21
22     @staticmethod
23     def from_list(l):
24         f = Filter()
25         try:
26             for element in l:
27                 f.add(Predicate(*element))
28         except Exception, e:
29             print "Error in setting Filter from list", e
30             return None
31         return f
32         
33     @staticmethod
34     def from_dict(d):
35         f = Filter()
36         for key, value in d.items():
37             if key[0] in Predicate.operators.keys():
38                 f.add(Predicate(key[1:], key[0], value))
39             else:
40                 f.add(Predicate(key, '=', value))
41         return f
42
43     def to_list(self):
44         ret = []
45         for predicate in self:
46             ret.append(predicate.to_list())
47         return ret
48         
49
50     @staticmethod
51     def from_clause(clause):
52         """
53         NOTE: We can only handle simple clauses formed of AND fields.
54         """
55         raise Exception, "Not implemented"
56
57     def filter_by(self, predicate):
58         self.add(predicate)
59         return self
60
61     def __str__(self):
62         return ' AND '.join([str(pred) for pred in self])
63
64     def __repr__(self):
65         return '<Filter: %s>' % ' AND '.join([str(pred) for pred in self])
66
67     def __key(self):
68         return tuple([hash(pred) for pred in self])
69
70     def __hash__(self):
71         return hash(self.__key())
72
73     def __additem__(self, value):
74         if value.__class__ != Predicate:
75             raise TypeError("Element of class Predicate expected, received %s" % value.__class__.__name__)
76         set.__additem__(self, value)
77
78     def keys(self):
79         return set([x.key for x in self])
80
81     # XXX THESE FUNCTIONS SHOULD ACCEPT MULTIPLE FIELD NAMES
82
83     def has(self, key):
84         for x in self:
85             if x.key == key:
86                 return True
87         return False
88
89     def has_op(self, key, op):
90         for x in self:
91             if x.key == key and x.op == op:
92                 return True
93         return False
94
95     def has_eq(self, key):
96         return self.has_op(key, eq)
97
98     def get(self, key):
99         ret = []
100         for x in self:
101             if x.key == key:
102                 ret.append(x)
103         return ret
104
105     def delete(self, key):
106         to_del = []
107         for x in self:
108             if x.key == key:
109                 to_del.append(x)
110         for x in to_del:
111             self.remove(x)
112             
113         #self = filter(lambda x: x.key != key, self)
114
115     def get_op(self, key, op):
116         if isinstance(op, (list, tuple, set)):
117             for x in self:
118                 if x.key == key and x.op in op:
119                     return x.value
120         else:
121             for x in self:
122                 if x.key == key and x.op == op:
123                     return x.value
124         return None
125
126     def get_eq(self, key):
127         return self.get_op(key, eq)
128
129     def set_op(self, key, op, value):
130         for x in self:
131             if x.key == key and x.op == op:
132                 x.value = value
133                 return
134         raise KeyError, key
135
136     def set_eq(self, key, value):
137         return self.set_op(key, eq, value)
138
139     def get_predicates(self, key):
140         # XXX Would deserve returning a filter (cf usage in SFA gateway)
141         ret = []
142         for x in self:
143             if x.key == key:
144                 ret.append(x)
145         return ret
146
147 #    def filter(self, dic):
148 #        # We go through every filter sequentially
149 #        for predicate in self:
150 #            print "predicate", predicate
151 #            dic = predicate.filter(dic)
152 #        return dic
153
154     def match(self, dic, ignore_missing=True):
155         for predicate in self:
156             if not predicate.match(dic, ignore_missing):
157                 return False
158         return True
159
160     def filter(self, l):
161         output = []
162         for x in l:
163             if self.match(x):
164                 output.append(x)
165         return output
166
167     def get_field_names(self):
168         field_names = set()
169         for predicate in self:
170             field_names |= predicate.get_field_names()
171         return field_names
172
173 #class OldFilter(Parameter, dict):
174 #    """
175 #    A type of parameter that represents a filter on one or more
176 #    columns of a database table.
177 #    Special features provide support for negation, upper and lower bounds, 
178 #    as well as sorting and clipping.
179 #
180 #
181 #    fields should be a dictionary of field names and types.
182 #    As of PLCAPI-4.3-26, we provide support for filtering on
183 #    sequence types as well, with the special '&' and '|' modifiers.
184 #    example : fields = {'node_id': Parameter(int, "Node identifier"),
185 #                        'hostname': Parameter(int, "Fully qualified hostname", max = 255),
186 #                        ...}
187 #
188 #
189 #    filter should be a dictionary of field names and values
190 #    representing  the criteria for filtering. 
191 #    example : filter = { 'hostname' : '*.edu' , site_id : [34,54] }
192 #    Whether the filter represents an intersection (AND) or a union (OR) 
193 #    of these criteria is determined by the join_with argument 
194 #    provided to the sql method below
195 #
196 #    Special features:
197 #
198 #    * a field starting with '&' or '|' should refer to a sequence type
199 #      the semantic is then that the object value (expected to be a list)
200 #      should contain all (&) or any (|) value specified in the corresponding
201 #      filter value. See other examples below.
202 #    example : filter = { '|role_ids' : [ 20, 40 ] }
203 #    example : filter = { '|roles' : ['tech', 'pi'] }
204 #    example : filter = { '&roles' : ['admin', 'tech'] }
205 #    example : filter = { '&roles' : 'tech' }
206 #
207 #    * a field starting with the ~ character means negation.
208 #    example :  filter = { '~peer_id' : None }
209 #
210 #    * a field starting with < [  ] or > means lower than or greater than
211 #      < > uses strict comparison
212 #      [ ] is for using <= or >= instead
213 #    example :  filter = { ']event_id' : 2305 }
214 #    example :  filter = { '>time' : 1178531418 }
215 #      in this example the integer value denotes a unix timestamp
216 #
217 #    * if a value is a sequence type, then it should represent 
218 #      a list of possible values for that field
219 #    example : filter = { 'node_id' : [12,34,56] }
220 #
221 #    * a (string) value containing either a * or a % character is
222 #      treated as a (sql) pattern; * are replaced with % that is the
223 #      SQL wildcard character.
224 #    example :  filter = { 'hostname' : '*.jp' } 
225 #
226 #    * the filter's keys starting with '-' are special and relate to sorting and clipping
227 #    * '-SORT' : a field name, or an ordered list of field names that are used for sorting
228 #      these fields may start with + (default) or - for denoting increasing or decreasing order
229 #    example : filter = { '-SORT' : [ '+node_id', '-hostname' ] }
230 #    * '-OFFSET' : the number of first rows to be ommitted
231 #    * '-LIMIT' : the amount of rows to be returned 
232 #    example : filter = { '-OFFSET' : 100, '-LIMIT':25}
233 #
234 #    Here are a few realistic examples
235 #
236 #    GetNodes ( { 'node_type' : 'regular' , 'hostname' : '*.edu' , '-SORT' : 'hostname' , '-OFFSET' : 30 , '-LIMIT' : 25 } )
237 #      would return regular (usual) nodes matching '*.edu' in alphabetical order from 31th to 55th
238 #
239 #    GetPersons ( { '|role_ids' : [ 20 , 40] } )
240 #      would return all persons that have either pi (20) or tech (40) roles
241 #
242 #    GetPersons ( { '&role_ids' : 10 } )
243 #    GetPersons ( { '&role_ids' : 10 } )
244 #    GetPersons ( { '|role_ids' : [ 10 ] } )
245 #    GetPersons ( { '|role_ids' : [ 10 ] } )
246 #      all 4 forms are equivalent and would return all admin users in the system
247 #    """
248 #
249 #    def __init__(self, fields = {}, filter = {}, doc = "Attribute filter"):
250 #        # Store the filter in our dict instance
251 #        dict.__init__(self, filter)
252 #
253 #        # Declare ourselves as a type of parameter that can take
254 #        # either a value or a list of values for each of the specified
255 #        # fields.
256 #        self.fields = dict ( [ ( field, Mixed (expected, [expected])) 
257 #                                 for (field,expected) in fields.iteritems() ] )
258 #
259 #        # Null filter means no filter
260 #        Parameter.__init__(self, self.fields, doc = doc, nullok = True)
261 #
262 #    def sql(self, api, join_with = "AND"):
263 #        """
264 #        Returns a SQL conditional that represents this filter.
265 #        """
266 #
267 #        # So that we always return something
268 #        if join_with == "AND":
269 #            conditionals = ["True"]
270 #        elif join_with == "OR":
271 #            conditionals = ["False"]
272 #        else:
273 #            assert join_with in ("AND", "OR")
274 #
275 #        # init 
276 #        sorts = []
277 #        clips = []
278 #
279 #        for field, value in self.iteritems():
280 #            # handle negation, numeric comparisons
281 #            # simple, 1-depth only mechanism
282 #
283 #            modifiers={'~' : False, 
284 #                       '<' : False, '>' : False,
285 #                       '[' : False, ']' : False,
286 #                       '-' : False,
287 #                       '&' : False, '|' : False,
288 #                       '{': False ,
289 #                       }
290 #            def check_modifiers(field):
291 #                if field[0] in modifiers.keys():
292 #                    modifiers[field[0]] = True
293 #                    field = field[1:]
294 #                    return check_modifiers(field)
295 #                return field
296 #            field = check_modifiers(field)
297 #
298 #            # filter on fields
299 #            if not modifiers['-']:
300 #                if field not in self.fields:
301 #                    raise PLCInvalidArgument, "Invalid filter field '%s'" % field
302 #
303 #                # handling array fileds always as compound values
304 #                if modifiers['&'] or modifiers['|']:
305 #                    if not isinstance(value, (list, tuple, set)):
306 #                        value = [value,]
307 #
308 #                if isinstance(value, (list, tuple, set)):
309 #                    # handling filters like '~slice_id':[]
310 #                    # this should return true, as it's the opposite of 'slice_id':[] which is false
311 #                    # prior to this fix, 'slice_id':[] would have returned ``slice_id IN (NULL) '' which is unknown 
312 #                    # so it worked by coincidence, but the negation '~slice_ids':[] would return false too
313 #                    if not value:
314 #                        if modifiers['&'] or modifiers['|']:
315 #                            operator = "="
316 #                            value = "'{}'"
317 #                        else:
318 #                            field=""
319 #                            operator=""
320 #                            value = "FALSE"
321 #                    else:
322 #                        value = map(str, map(api.db.quote, value))
323 #                        if modifiers['&']:
324 #                            operator = "@>"
325 #                            value = "ARRAY[%s]" % ", ".join(value)
326 #                        elif modifiers['|']:
327 #                            operator = "&&"
328 #                            value = "ARRAY[%s]" % ", ".join(value)
329 #                        else:
330 #                            operator = "IN"
331 #                            value = "(%s)" % ", ".join(value)
332 #                else:
333 #                    if value is None:
334 #                        operator = "IS"
335 #                        value = "NULL"
336 #                    elif isinstance(value, StringTypes) and \
337 #                            (value.find("*") > -1 or value.find("%") > -1):
338 #                        operator = "LIKE"
339 #                        # insert *** in pattern instead of either * or %
340 #                        # we dont use % as requests are likely to %-expansion later on
341 #                        # actual replacement to % done in PostgreSQL.py
342 #                        value = value.replace ('*','***')
343 #                        value = value.replace ('%','***')
344 #                        value = str(api.db.quote(value))
345 #                    else:
346 #                        operator = "="
347 #                        if modifiers['<']:
348 #                            operator='<'
349 #                        if modifiers['>']:
350 #                            operator='>'
351 #                        if modifiers['[']:
352 #                            operator='<='
353 #                        if modifiers[']']:
354 #                            operator='>='
355 #                        #else:
356 #                        #    value = str(api.db.quote(value))
357 #                        # jordan
358 #                        if isinstance(value, StringTypes) and value[-2:] != "()": # XXX
359 #                            value = str(api.db.quote(value))
360 #                        if isinstance(value, datetime.datetime):
361 #                            value = str(api.db.quote(str(value)))
362
363 #                #if prefix: 
364 #                #    field = "%s.%s" % (prefix,field)
365 #                if field:
366 #                    clause = "\"%s\" %s %s" % (field, operator, value)
367 #                else:
368 #                    clause = "%s %s %s" % (field, operator, value)
369 #
370 #                if modifiers['~']:
371 #                    clause = " ( NOT %s ) " % (clause)
372 #
373 #                conditionals.append(clause)
374 #            # sorting and clipping
375 #            else:
376 #                if field not in ('SORT','OFFSET','LIMIT'):
377 #                    raise PLCInvalidArgument, "Invalid filter, unknown sort and clip field %r"%field
378 #                # sorting
379 #                if field == 'SORT':
380 #                    if not isinstance(value,(list,tuple,set)):
381 #                        value=[value]
382 #                    for field in value:
383 #                        order = 'ASC'
384 #                        if field[0] == '+':
385 #                            field = field[1:]
386 #                        elif field[0] == '-':
387 #                            field = field[1:]
388 #                            order = 'DESC'
389 #                        if field not in self.fields:
390 #                            raise PLCInvalidArgument, "Invalid field %r in SORT filter"%field
391 #                        sorts.append("%s %s"%(field,order))
392 #                # clipping
393 #                elif field == 'OFFSET':
394 #                    clips.append("OFFSET %d"%value)
395 #                # clipping continued
396 #                elif field == 'LIMIT' :
397 #                    clips.append("LIMIT %d"%value)
398 #
399 #        where_part = (" %s " % join_with).join(conditionals)
400 #        clip_part = ""
401 #        if sorts:
402 #            clip_part += " ORDER BY " + ",".join(sorts)
403 #        if clips:
404 #            clip_part += " " + " ".join(clips)
405 ##       print 'where_part=',where_part,'clip_part',clip_part
406 #        return (where_part,clip_part)
407 #