Coverage for auttcomp/extensions.py: 95%
190 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-24 12:00 -0600
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-24 12:00 -0600
1from collections import namedtuple
2from .utility import ObjUtil
3from .shape_eval import eval_shape
4from .composable import Composable, P, R
5from typing import Callable, Any, Tuple, Iterable, TypeVar
6from typing import Callable
7import functools
8import itertools
10T = TypeVar('T')
11T2 = TypeVar('T2')
12K = TypeVar('K')
14def id_param(x:Any) -> Any:
15 return x
17KeyValuePair = namedtuple("KeyValuePair", ["key", "value"])
19class Api(Composable[P, R]):
21 @staticmethod
22 @Composable
23 def id(data: T) -> Callable[[], T]:
24 '''create an identity function for the given data'''
26 @Composable
27 def partial_id() -> T:
28 return data
30 return partial_id
32 @staticmethod
33 @Composable
34 def at(func: Callable[[T], R]) -> Callable[[T], R]:
35 '''property selector'''
37 @Composable
38 def partial_at(obj: T) -> R:
39 return func(obj)
41 return partial_at
43 @staticmethod
44 @Composable
45 def map(func: Callable[[T], R]) -> Callable[[Iterable[T]], Iterable[R]]:
46 '''curried version of python's map:
47 map(func, *iterables) --> map object\n\nMake an iterator that computes the function using arguments from\neach of the iterables. Stops when the shortest iterable is exhausted.
48 '''
50 @Composable
51 def partial_map(data: Iterable[T]) -> Iterable[R]:
52 return map(func, data)
54 return partial_map
56 @staticmethod
57 @Composable
58 def foreach(func: Callable[[T], R]) -> Callable[[Iterable[T]], None]:
59 '''exec the func for each element in the iterable'''
61 @Composable
62 def partial_foreach(data: Iterable[T]) -> None:
63 for x in data:
64 func(x)
66 return partial_foreach
68 @staticmethod
69 @Composable
70 def filter(func: Callable[[T], R] = id_param) -> Callable[[Iterable[T]], Iterable[T]]:
71 '''curried version of python's filter
72 filter(function or None, iterable) --> filter object\n\nReturn an iterator yielding those items of iterable for which function(item)\nis true. If function is None, return the items that are true.
73 '''
75 @Composable
76 def partial_filter(data: Iterable[T]) -> Iterable[T]:
77 return filter(func, data)
79 return partial_filter
81 @staticmethod
82 @Composable
83 def reduce(func: Callable[[T, T], R]) -> Callable[[Iterable[T]], R]:
84 '''curried version of functools's reduce (to use initial value, use reduce2)
85 reduce(function, iterable) -> value\n\nApply a function of two arguments cumulatively to the items of an iterable, from left to right.\n\nThis effectively reduces the iterable to a single value. If initial is present,\nit is placed before the items of the iterable in the calculation, and serves as\na default when the iterable is empty.\n\nFor example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])\ncalculates ((((1 + 2) + 3) + 4) + 5).
86 '''
88 @Composable
89 def partial_reduce(data: Iterable[T]) -> R:
90 return functools.reduce(func, data)
92 return partial_reduce
94 @staticmethod
95 @Composable
96 def reduce2(func: Callable[[T, T], R], initial: T) -> Callable[[Iterable[T]], R]:
97 '''curried version of functools's reduce
98 reduce(function, iterable, initial) -> value\n\nApply a function of two arguments cumulatively to the items of an iterable, from left to right.\n\nThis effectively reduces the iterable to a single value. If initial is present,\nit is placed before the items of the iterable in the calculation, and serves as\na default when the iterable is empty.\n\nFor example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])\ncalculates ((((1 + 2) + 3) + 4) + 5).
99 '''
101 @Composable
102 def partial_reduce(data: Iterable[T]) -> R:
103 return functools.reduce(func, data, initial)
105 return partial_reduce
107 @staticmethod
108 @Composable
109 def list(data: Iterable[T]) -> list[T]:
110 '''Built-in mutable sequence.\n\nIf no argument is given, the constructor creates a new empty list.\nThe argument must be an iterable if specified.'''
111 return list(data)
113 @staticmethod
114 @Composable
115 def distinct(data: Iterable[T]) -> Iterable[T]:
116 '''a general purpose distinct implementation where performance is not required
117 if your data is compatible, you may be able to use distinctSet
118 '''
120 return list(functools.reduce(lambda a, b: a + [b] if b not in a else a, data, []))
122 @staticmethod
123 @Composable
124 def distinct_set(data: Iterable[T]) -> Iterable[T]:
125 '''implementation of distinct using python's set, but limited to qualifying primitive types'''
126 return list(set(data))
128 @staticmethod
129 @Composable
130 def flatmap(func: Callable[[T], R] = id_param) -> Callable[[Iterable[Iterable[T]]], Iterable[R]]:
131 '''iterable implementation of flatmap'''
133 @Composable
134 def partial_flatmap(data: Iterable[Iterable[T]]) -> Iterable[R]:
135 for ys in map(func, data):
136 for y in ys:
137 yield y
139 return partial_flatmap
141 @staticmethod
142 @Composable
143 def shape(data: Any) -> Any:
144 '''evaluates the shape of data, returns a shape object'''
145 return eval_shape(data)
147 @staticmethod
148 @Composable
149 def any(func: Callable[[T], R] = id_param) -> Callable[[Iterable[T]], bool]:
150 '''curried version of python's any function. Returns True if any element satisfies the condition'''
152 @Composable
153 def partial_any(data: Iterable[T]) -> bool:
154 return any(map(func, data))
156 return partial_any
158 @staticmethod
159 @Composable
160 def all(func: Callable[[T], R] = id_param) -> Callable[[Iterable[T]], bool]:
161 '''curried version of python's any function. Returns True if all elements satisfy the condition'''
163 @Composable
164 def partial_all(data: Iterable[T]) -> bool:
165 return all(map(func, data))
167 return partial_all
169 @staticmethod
170 @Composable
171 def reverse(data: Iterable[T]) -> Iterable[T]:
172 '''python's reverse'''
173 return reversed(ObjUtil.exec_generator(data))
175 @staticmethod
176 @Composable
177 def sort(data: Iterable[T]) -> Iterable[T]:
178 '''python's sort'''
179 return sorted(ObjUtil.exec_generator(data))
181 @staticmethod
182 @Composable
183 def sort_by(func: Callable[[T], R]) -> Callable[[Iterable[T]], Iterable[T]]:
184 '''curried version of python's sort with key selector'''
186 @Composable
187 def partial_sort_by(data: Iterable[T]) -> Iterable[T]:
188 return sorted(ObjUtil.exec_generator(data), key=func)
190 return partial_sort_by
192 @staticmethod
193 @Composable
194 def sort_by_desc(func: Callable[[T], R]) -> Callable[[Iterable[T]], Iterable[T]]:
195 '''curried version of python's sort w/ key selector followed by reverse'''
197 @Composable
198 def partial_sort_by_desc(data: Iterable[T]) -> Iterable[T]:
199 return sorted(ObjUtil.exec_generator(data), key=func, reverse=True)
201 return partial_sort_by_desc
203 @staticmethod
204 @Composable
205 def take(count: int) -> Callable[[Iterable[T]], Iterable[T]]:
206 '''basically yielded list[0:count]'''
208 @Composable
209 def partial_take(data: Iterable[T]) -> Iterable[T]:
210 iter_count = 0
211 for x in data:
212 iter_count += 1
213 if iter_count > count:
214 break
215 yield x
217 return partial_take
219 @staticmethod
220 @Composable
221 def skip(count: int) -> Callable[[Iterable[T]], Iterable[T]]:
222 '''basically yielded list[count:]'''
224 @Composable
225 def partial_skip(data: Iterable[T]) -> Iterable[R]:
226 iter_count = 0
227 for x in data:
228 iter_count += 1
229 if iter_count > count:
230 yield x
232 return partial_skip
234 @staticmethod
235 @Composable
236 def group(func: Callable[[T], K] = id_param) -> Callable[[Iterable[T]], Iterable[KeyValuePair[K, Iterable[T]]]]:
237 '''curried version of itertools.groupby
238 sort by key is used before grouping to achieve singular grouping
239 f.groupby(lambda x.property)
240 this implementation runs the iterable for the grouping, but yields the key/value pair as a new iterable
241 '''
243 @Composable
244 def partial_group(data: Iterable[T]) -> Iterable[KeyValuePair[R, Iterable[T]]]:
245 for key, value in itertools.groupby(sorted(ObjUtil.exec_generator(data), key=func), key=func):
246 yield KeyValuePair(key, ObjUtil.exec_generator(value))
248 return partial_group
250 @staticmethod
251 @Composable
252 def join(
253 left_data: Iterable[T],
254 left_key_func: Callable[[T], K],
255 right_key_func: Callable[[T], K],
256 left_value_selector: Callable[[T], Any] = id_param,
257 right_value_selector: Callable[[T], Any] = id_param
258 ) -> Callable[[T2], Iterable[Tuple[K, Tuple[T, T2]]]]:
259 '''(inner join) combine two groups by key'''
261 @Composable
262 def partial_join(right_data: Iterable[T2]) -> Iterable[Tuple[K, Tuple[T, T2]]]:
263 left_group = Api.group(left_key_func)(left_data)
264 right_group = Api.group(right_key_func)(right_data)
266 tracker = {}
267 for lg in left_group:
268 tracker[lg.key] = lg.value
269 for rg in right_group:
270 lv = tracker.get(rg.key)
271 if lv is not None:
272 yield rg.key, (list(map(left_value_selector, lv)), list(map(right_value_selector, rg.value)))
274 return partial_join
276 @staticmethod
277 @Composable
278 def zip(data:Iterable[T]) -> Callable[[Iterable[T2]], Iterable[Tuple[T2, T]]]:
279 '''curried version of itertools.zip_longest'''
281 @Composable
282 def partial_zip(data2: Iterable[T2]) -> Iterable[Tuple[T2, T]]:
283 return itertools.zip_longest(data2, data)
285 return partial_zip
287 @staticmethod
288 @Composable
289 def flatnest(path_selector:Callable[[Any], Any], data_selector:Callable[[Any], Any]) -> Callable[[Any], Iterable[Any]]:
290 '''yield properties of a recursive structure by data_selector, following the path_selector'''
292 @Composable
293 def partial_flatnest(model:Any) -> Iterable[Any]:
294 if model is not None:
295 yield data_selector(model)
296 next = path_selector(model)
297 if next is not None:
298 yield from partial_flatnest(next)
300 return partial_flatnest