leasefilter: use f-strings instead of format()
[plcapi.git] / PLC / LeaseFilter.py
1 #
2 # Thierry Parmentelat -- INRIA
3 #
4 # Utilities for filtering on leases
5
6 # w0622: we keep on using 'filter' as a variable name
7 # pylint: disable=c0111, c0103, w0622
8
9 import time
10
11 from PLC.Faults import *                         # pylint: disable=w0401, w0614
12 from PLC.Filter import Filter
13 from PLC.Parameter import Parameter, Mixed
14 from PLC.Timestamp import Timestamp
15
16 # supersede the generic Filter class to support time intersection
17
18
19 class LeaseFilter(Filter):
20
21     # general notes on input parameters
22     # int_timestamp: number of seconds since the epoch
23     # str_timestamp: see Timestamp.sql_validate
24     # timeslot: a list [from, until], each being either int_timestamp or
25     # str_timestamp
26
27     local_fields = {
28         'alive': Mixed(
29             Parameter(int, "int_timestamp: leases alive at that time"),
30             Parameter(str, "str_timestamp: leases alive at that time"),
31             Parameter(list, "timeslot: the leases alive during this timeslot"),
32             ),
33         'clip': Mixed(
34             Parameter(int, "int_timestamp: leases alive after that time"),
35             Parameter(str, "str_timestamp: leases alive after that time"),
36             Parameter(list, "timeslot: the leases alive during this timeslot"),
37             ),
38         ########## macros
39         # {'day' : 0} : all leases from today and on
40         # {'day' : 1} : all leases today (localtime at the myplc)
41         # {'day' : 2} : all leases today and tomorrow (localtime at the myplc)
42         # etc..
43         'day': Parameter(
44             int,
45             "clip on a number of days from today and on;"
46             " 0 means no limit in the future"),
47     }
48
49     def __init__(self, fields=None, filter=None,
50                  doc="Lease filter -- adds the 'alive' and 'clip'"
51                  "capabilities for filtering on leases"):
52         if fields is None:
53             fields = {}
54         if filter is None:
55             filter = {}
56         Filter.__init__(self, fields, filter, doc)
57         self.fields.update(LeaseFilter.local_fields)
58
59     # canonical type
60     @staticmethod
61     def quote(timestamp):
62         return Timestamp.cast_long(timestamp)
63
64     # basic SQL utilities
65     @staticmethod
66     def sql_time_intersect(f1, u1, f2, u2):
67         # either f2 is in [f1,u1], or u2 is in [f1,u1], or f2<=f1<=u1<=u2
68         return (
69             f"(({f1} <= {f2}) AND ({f2} <= {u1})) "
70             f"OR (({f1} <= {u2}) AND ({u2} <= {u1})) "
71             f"OR (({f2}<={f1}) AND ({u1}<={u2}))")
72
73     @staticmethod
74     def time_in_range(timestamp, f1, u1):
75         return Timestamp.cast_long(f1) <= Timestamp.cast_long(timestamp) \
76             and Timestamp.cast_long(timestamp) <= Timestamp.cast_long(u1)
77
78     @staticmethod
79     def sql_time_in_range(timestamp, f1, u1):
80         # is timestamp in [f1, u1]
81         return f"(({f1} <= {timestamp}) AND ({timestamp} <= {u1}))"
82
83     @staticmethod
84     def sql_timeslot_after(f1, u1, mark):
85         # is the lease alive after mark, i.e. u1 >= mark
86         return f"({u1} >= {mark})"
87
88     # hooks for the local fields
89     def sql_alive(self, alive):
90         if isinstance(alive, (int, str)):
91             # the lease is alive at that time if from <= alive <= until
92             alive = LeaseFilter.quote(alive)
93             return LeaseFilter.sql_time_in_range(alive, 't_from', 't_until')
94         elif isinstance(alive, (tuple, list)):
95             (f, u) = alive
96             f = LeaseFilter.quote(f)
97             u = LeaseFilter.quote(u)
98             return LeaseFilter.sql_time_intersect(f, u, 't_from', 't_until')
99         else:
100             raise PLCInvalidArgument(f"LeaseFilter: alive field {alive}")
101
102     def sql_clip(self, clip):
103         if isinstance(clip, int) or isinstance(clip, str):
104             start = LeaseFilter.quote(clip)
105             return LeaseFilter.sql_timeslot_after('t_from', 't_until', start)
106         elif isinstance(clip, (tuple, list)):
107             (f, u) = clip
108             f = LeaseFilter.quote(f)
109             u = LeaseFilter.quote(u)
110             return LeaseFilter.sql_time_intersect(f, u, 't_from', 't_until')
111         else:
112             raise PLCInvalidArgument(f"LeaseFilter: clip field {clip}")
113
114     # the whole key to implementing day is to compute today's beginning
115     def today_start(self):
116         # a struct_time
117         st = time.localtime()
118         seconds_today = st.tm_hour * 3600 + st.tm_min * 60 + st.tm_sec
119         return int(time.time()) - seconds_today
120
121     # supersede the generic Filter 'sql' method
122     def sql(self, api, join_with="AND"):
123         # implement 'day' as a clip
124         if 'day' in self:
125             if 'clip' in self:
126                 raise PLCInvalidArgument("LeaseFilter cannot have both 'clip' and 'day'")
127             today = self.today_start()
128             nb_days = self['day']
129             if nb_days == 0:
130                 self['clip'] = today
131             else:
132                 self['clip'] = (today, today + nb_days * 24 * 3600)
133             del self['day']
134
135         # preserve locally what belongs to us, hide it from the superclass
136         # self.local is a dict    local_key : user_value
137         # self.negation is a dict  local_key : string
138         self.local = {}
139         self.negation = {}
140         for (k, v) in list(LeaseFilter.local_fields.items()):
141             if k in self:
142                 self.local[k] = self[k]
143                 del self[k]
144                 self.negation[k] = ""
145             elif ('~' + k) in self:
146                 self.local[k] = self['~' + k]
147                 del self['~' + k]
148                 self.negation[k] = "NOT "
149         # run the generic filtering code
150         (where_part, clip_part) = Filter.sql(self, api, join_with)
151         for (k, v) in list(self.local.items()):
152             try:
153                 # locate hook function associated with key
154                 method = LeaseFilter.__dict__['sql_' + k]
155                 where_part += (
156                     f" {self.join_with} {self.negation[k]}"
157                     f"({method(self, self.local[k])})"
158                 )
159             except Exception as e:
160                 raise PLCInvalidArgument(
161                     f"LeaseFilter: something wrong with filter "
162                     f"key {k}, value was {v} -- {e}")
163         return (where_part, clip_part)
164
165 # xxx not sure where this belongs yet
166 # given a set of nodes, and a timeslot,
167 # returns the available leases that have at least a given duration
168
169
170 def free_leases(api, node_ids, t_from, t_until, min_duration):
171
172     # get the leases for these nodes and timeslot
173     filter = {'node_id': node_ids,
174               'clip': (t_from, t_until),
175               # sort by node, and inside one node, chronologically
176               '-SORT': ('node_id', 't_from'),
177               }
178     leases = Leases(api, filter)
179
180     result = []
181
182     # sort node_ids
183     node_ids.sort()
184
185     # scan nodes from the input
186     input_node_id = 0
187     # scan nodes from the leases
188     lease_node_id = 0
189
190     return '?? what now ??'
191
192
193 def node_free_leases(node_id, node_leases, t_from, t_until):
194
195     # no lease yet : return one solid lease
196     if not node_leases:
197         return [{'node_id': node_id,
198                  't_from': t_from,
199                  't_until': t_until}]
200
201     result = []
202     current_time = t_from
203     is_on = LeaseFilter.time_in_range(
204         node_leases[0]['t_from'], t_from, t_until)
205
206     while True:
207         # print 'DBG','current_time',current_time,'is_on',is_on,'result',result
208         # lease is active
209         if is_on:
210             current_time = node_leases[0]['t_until']
211             is_on = False
212             del node_leases[0]
213             if not node_leases:
214                 return result
215         # free, has no remaining lease
216         elif not node_leases:
217             result.append(
218                 {'node_id': node_id,
219                  't_from': current_time, 't_until': t_until})
220             return result
221         # free and has remaining leases
222         else:
223             next_time = node_leases[0]['t_from']
224             result.append(
225                 {'node_id': node_id,
226                  't_from': current_time, 't_until': next_time})
227             current_time = next_time
228             is_on = True