Coverage for auttcomp/extensions.py: 95%

190 statements  

« 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 

9 

10T = TypeVar('T') 

11T2 = TypeVar('T2') 

12K = TypeVar('K') 

13 

14def id_param(x:Any) -> Any: 

15 return x 

16 

17KeyValuePair = namedtuple("KeyValuePair", ["key", "value"]) 

18 

19class Api(Composable[P, R]): 

20 

21 @staticmethod 

22 @Composable 

23 def id(data: T) -> Callable[[], T]: 

24 '''create an identity function for the given data''' 

25 

26 @Composable 

27 def partial_id() -> T: 

28 return data 

29 

30 return partial_id 

31 

32 @staticmethod 

33 @Composable 

34 def at(func: Callable[[T], R]) -> Callable[[T], R]: 

35 '''property selector''' 

36 

37 @Composable 

38 def partial_at(obj: T) -> R: 

39 return func(obj) 

40 

41 return partial_at 

42 

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 ''' 

49 

50 @Composable 

51 def partial_map(data: Iterable[T]) -> Iterable[R]: 

52 return map(func, data) 

53 

54 return partial_map 

55 

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''' 

60 

61 @Composable 

62 def partial_foreach(data: Iterable[T]) -> None: 

63 for x in data: 

64 func(x) 

65 

66 return partial_foreach 

67 

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 ''' 

74 

75 @Composable 

76 def partial_filter(data: Iterable[T]) -> Iterable[T]: 

77 return filter(func, data) 

78 

79 return partial_filter 

80 

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 ''' 

87 

88 @Composable 

89 def partial_reduce(data: Iterable[T]) -> R: 

90 return functools.reduce(func, data) 

91 

92 return partial_reduce 

93 

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 ''' 

100 

101 @Composable 

102 def partial_reduce(data: Iterable[T]) -> R: 

103 return functools.reduce(func, data, initial) 

104 

105 return partial_reduce 

106 

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) 

112 

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 ''' 

119 

120 return list(functools.reduce(lambda a, b: a + [b] if b not in a else a, data, [])) 

121 

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)) 

127 

128 @staticmethod 

129 @Composable 

130 def flatmap(func: Callable[[T], R] = id_param) -> Callable[[Iterable[Iterable[T]]], Iterable[R]]: 

131 '''iterable implementation of flatmap''' 

132 

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 

138 

139 return partial_flatmap 

140 

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) 

146 

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''' 

151 

152 @Composable 

153 def partial_any(data: Iterable[T]) -> bool: 

154 return any(map(func, data)) 

155 

156 return partial_any 

157 

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''' 

162 

163 @Composable 

164 def partial_all(data: Iterable[T]) -> bool: 

165 return all(map(func, data)) 

166 

167 return partial_all 

168 

169 @staticmethod 

170 @Composable 

171 def reverse(data: Iterable[T]) -> Iterable[T]: 

172 '''python's reverse''' 

173 return reversed(ObjUtil.exec_generator(data)) 

174 

175 @staticmethod 

176 @Composable 

177 def sort(data: Iterable[T]) -> Iterable[T]: 

178 '''python's sort''' 

179 return sorted(ObjUtil.exec_generator(data)) 

180 

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''' 

185 

186 @Composable 

187 def partial_sort_by(data: Iterable[T]) -> Iterable[T]: 

188 return sorted(ObjUtil.exec_generator(data), key=func) 

189 

190 return partial_sort_by 

191 

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''' 

196 

197 @Composable 

198 def partial_sort_by_desc(data: Iterable[T]) -> Iterable[T]: 

199 return sorted(ObjUtil.exec_generator(data), key=func, reverse=True) 

200 

201 return partial_sort_by_desc 

202 

203 @staticmethod 

204 @Composable 

205 def take(count: int) -> Callable[[Iterable[T]], Iterable[T]]: 

206 '''basically yielded list[0:count]''' 

207 

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 

216 

217 return partial_take 

218 

219 @staticmethod 

220 @Composable 

221 def skip(count: int) -> Callable[[Iterable[T]], Iterable[T]]: 

222 '''basically yielded list[count:]''' 

223 

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 

231 

232 return partial_skip 

233 

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 ''' 

242 

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)) 

247 

248 return partial_group 

249 

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''' 

260 

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) 

265 

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))) 

273 

274 return partial_join 

275 

276 @staticmethod 

277 @Composable 

278 def zip(data:Iterable[T]) -> Callable[[Iterable[T2]], Iterable[Tuple[T2, T]]]: 

279 '''curried version of itertools.zip_longest''' 

280 

281 @Composable 

282 def partial_zip(data2: Iterable[T2]) -> Iterable[Tuple[T2, T]]: 

283 return itertools.zip_longest(data2, data) 

284 

285 return partial_zip 

286 

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''' 

291 

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) 

299 

300 return partial_flatnest