dukit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dukit/qlang/engine.py ADDED
@@ -0,0 +1,447 @@
1
+
2
+ import pandas as pd
3
+ import numpy as np
4
+ import typing
5
+ import re
6
+
7
+ from ..utils import (
8
+ log,
9
+ _build_log_context,
10
+ )
11
+ from ..typing import (
12
+ Box,
13
+ str_,
14
+ DTYPES_ALLOWED,
15
+ )
16
+
17
+
18
+ class Query(Box):
19
+
20
+ def __init__(
21
+ self,
22
+ df: pd.DataFrame,
23
+ code: str,
24
+ symbols_allowed: list['Symbol'],
25
+ symbol_start: 'Symbol',
26
+ symbol_stop: 'Symbol',
27
+ verbosity: int = 3,
28
+ ):
29
+
30
+ super().__init__()
31
+ df = df.copy().convert_dtypes()
32
+ df.columns = df.columns.to_series().convert_dtypes()
33
+ df.index = df.index.to_series().convert_dtypes()
34
+ cols = df.columns
35
+ rows = df.index
36
+ symbol_start.id = 0
37
+
38
+ #main attributes
39
+ self.df = df
40
+ self.code = code
41
+ self.verbosity = verbosity
42
+
43
+ #used for parsing
44
+ self.symbols_allowed = symbols_allowed
45
+ self.symbol_start = symbol_start
46
+ self.symbol_stop = symbol_stop
47
+ self.match_str = ''
48
+ self.tokens = []
49
+ self.ops = []
50
+ self.op = Symbol()
51
+
52
+ #masks for current selection state
53
+ self.mask_cols = pd.Series([True for col in cols])
54
+ self.mask_rows = pd.Series([True for idx in rows])
55
+ self.mask_cols.index = cols
56
+ self.mask_rows.index = rows
57
+ self.mask_vals = pd.DataFrame(
58
+ np.ones(self.df.shape, dtype=bool),
59
+ columns=cols,
60
+ index=rows,
61
+ )
62
+ self.masks_saved = {}
63
+
64
+ #optional style information
65
+ self.style_cols: None | pd.Series[str] = None
66
+ self.style_rows: None | pd.Series[str] = None
67
+ self.style_vals: None | pd.DataFrame = None
68
+
69
+ #styled/unstyled results
70
+ self.result = pd.DataFrame()
71
+ self.styled = self.result.style
72
+
73
+
74
+ def __str__(self) -> str:
75
+ txt_tokens = [token.name for token in self.tokens]
76
+ txt_ops = [op.operator for op in self.ops]
77
+ txt_tokens = str_(txt_tokens)
78
+ txt_ops = str_(txt_ops)
79
+ txt = (
80
+ '----------------Query object [q]----------------\n'
81
+ f'>>> q.code\n{self.code!r}\n\n\n'
82
+ f'>>> q.tokens\n{txt_tokens}\n\n\n'
83
+ f'>>> q.ops\n{txt_ops}\n\n\n'
84
+ f'>>> q.mask_cols\n{self.mask_cols}\n\n\n'
85
+ f'>>> q.mask_rows\n{self.mask_rows}\n\n\n'
86
+ f'>>> q.mask_vals\n{self.mask_vals}\n\n\n'
87
+ f'>>> q.masks_saved\n{self.masks_saved}\n\n\n'
88
+ f'>>> q.style_cols\n{self.style_cols}\n\n\n'
89
+ f'>>> q.style_rows\n{self.style_rows}\n\n\n'
90
+ f'>>> q.style_vals\n{self.style_vals}\n\n\n'
91
+ f'>>> q.df\n{self.df}\n\n\n'
92
+ '----------------Query object end----------------\n'
93
+ )
94
+ return txt
95
+
96
+
97
+ def __repr__(self) -> str:
98
+ txt_tokens = [token.name for token in self.tokens]
99
+ txt_ops = [op.operator for op in self.ops]
100
+ txt_tokens = str_(txt_tokens)
101
+ txt_ops = str_(txt_ops)
102
+ txt = (
103
+ '--------Query object [q]--------\n'
104
+ f'>>> q.code\n{self.code!r}\n'
105
+ f'>>> q.tokens\n{txt_tokens}\n'
106
+ f'>>> q.ops\n{txt_ops}\n'
107
+ '>>> q.df\n'
108
+ '>>> q.scan() #scan code into tokens\n'
109
+ '>>> q.parse() #parse tokens into ops\n'
110
+ '>>> q.run() #run ops on the df\n'
111
+ '>>> q.result\n'
112
+ '>>> q.styled\n'
113
+ '--------Query object end--------\n'
114
+ )
115
+ return txt
116
+
117
+
118
+ def scan(
119
+ self,
120
+ verbosity: int | None = None,
121
+ ) -> 'Query':
122
+ return scan(self, verbosity)
123
+
124
+
125
+ def parse(
126
+ self,
127
+ verbosity: int | None = None,
128
+ ) -> 'Query':
129
+ return parse(self, verbosity)
130
+
131
+
132
+ def run(
133
+ self,
134
+ verbosity: int | None = None,
135
+ ) -> 'Query':
136
+ return run(self, verbosity)
137
+
138
+
139
+ def show(self) -> pd.DataFrame | pd.io.formats.style.Styler:
140
+ any_style = (
141
+ self.style_cols is not None
142
+ or self.style_rows is not None
143
+ or self.style_vals is not None
144
+ )
145
+ if any_style:
146
+ return self.styled
147
+ else:
148
+ return self.result
149
+
150
+
151
+
152
+ class Symbol(Box):
153
+
154
+ #symbol attributes
155
+ id = 0
156
+ name = ''
157
+ category = ''
158
+ regex = ()
159
+
160
+ #op validation attributes
161
+ connectors_allowed: dict[str, str] = {}
162
+ scopes_allowed: dict[str, str] = {}
163
+ flags_allowed: dict[str, str] = {}
164
+ args_allowed: dict[str, str] = {}
165
+ args_min = 0
166
+ args_max = 0
167
+
168
+
169
+ def __init__(self, **kwargs):
170
+
171
+ #needed by all symbols
172
+ self.line = ''
173
+ self.linenum = 0
174
+ self.str_matched = ''
175
+ self.literal = ''
176
+
177
+ #only needed by op symbols
178
+ self.id = -1
179
+ self.connector: str = ''
180
+ self.scope: str = ''
181
+ self.operator: str = ''
182
+ self.flags: dict[str, str] = {}
183
+ self.args: list[str] = []
184
+ self.list_started = False
185
+ self.list_stopped = False
186
+
187
+ super().__init__(**kwargs)
188
+
189
+
190
+ def __str__(self):
191
+ if self.operator:
192
+ txt = (
193
+ f'--------Operation {self.id}--------\n'
194
+ f'connector: {self.connector!r}\n'
195
+ f'scope: {self.scope!r}\n'
196
+ f'operator: {self.operator!r}\n'
197
+ f'flags: {str_(self.flags)}\n'
198
+ f'args: {str_(self.args)}\n'
199
+ f'connectors_allowed: {str_(self.connectors_allowed)}\n'
200
+ f'scopes_allowed: {str_(self.scopes_allowed)}\n'
201
+ f'flags_allowed: {str_(self.flags_allowed)}\n'
202
+ f'args_allowed: {str_(self.args_allowed)}\n'
203
+ f'args_min: {self.args_min}\n'
204
+ f'args_max: {self.args_max}\n'
205
+ )
206
+ else:
207
+ txt = (
208
+ f'----Token {self.id}----\n'
209
+ f'name: {self.name!r}\n'
210
+ f'category: {self.category!r}\n'
211
+ f'regex: {self.regex!r}\n'
212
+ f'linenum: {self.linenum}\n'
213
+ f'str_matched: {self.str_matched!r}\n'
214
+ f'literal: {self.literal!r}\n'
215
+ )
216
+ return txt
217
+
218
+
219
+ def __repr__(self):
220
+ if self.operator:
221
+ txt = (
222
+ f'----Operation {self.id}----\n'
223
+ f'connector: {self.connector!r}\n'
224
+ f'scope: {self.scope!r}\n'
225
+ f'operator: {self.operator!r}\n'
226
+ f'flags: {str_(self.flags)}\n'
227
+ f'args: {str_(self.args)}\n'
228
+ )
229
+ else:
230
+ txt = f'<{self.name!r} {self.str_matched!r}>'
231
+ return txt
232
+
233
+
234
+ def build(self, str_matched: str) -> 'Symbol':
235
+ token = self.new()
236
+ token.str_matched = str_matched
237
+ return token
238
+
239
+
240
+ def parse(self, q: Query) -> Query:
241
+ return q
242
+
243
+
244
+ def run(self, q: Query) -> Query:
245
+ """carries behaviour unique to each op symbol"""
246
+ raise NotImplementedError()
247
+
248
+
249
+ def getter(
250
+ self,
251
+ series: pd.Series,
252
+ mask: pd.Series[bool],
253
+ arg: typing.Any,
254
+ q: Query,
255
+ ) -> pd.Series[bool]:
256
+ """only used by some op symbols"""
257
+ raise NotImplementedError()
258
+
259
+
260
+ def setter(
261
+ self,
262
+ series: pd.Series,
263
+ mask: pd.Series[bool],
264
+ args: list[typing.Any],
265
+ q: Query,
266
+ ) -> pd.Series:
267
+ """only used by some op symbols"""
268
+ raise NotImplementedError()
269
+
270
+
271
+ def styler(self) -> str:
272
+ """only used by some op symbols"""
273
+ raise NotImplementedError()
274
+
275
+
276
+
277
+ def scan(
278
+ q: Query,
279
+ verbosity: int | None = None,
280
+ ) -> Query:
281
+
282
+ code = q.code
283
+ line = ''
284
+ linenum = 1
285
+ if verbosity is None:
286
+ verbosity = q.verbosity
287
+
288
+ q.tokens.append(q.symbol_start)
289
+
290
+ while len(code) > 0:
291
+ for symbol in q.symbols_allowed:
292
+ token = _match_symbol(symbol, code)
293
+ if token is not None:
294
+ if token.name == 'Newline':
295
+ line = ''
296
+ linenum += 1
297
+ else:
298
+ line += token.str_matched
299
+ token.id = len(q.tokens)
300
+ token.line = line
301
+ token.linenum = linenum
302
+ q.tokens.append(token)
303
+ code = code[len(token.str_matched):]
304
+ break
305
+ else:
306
+ char = code[0]
307
+ line += char
308
+ msg = f'ERROR: Unrecognized character "{char}"'
309
+ context = _build_log_context(
310
+ 'scan',
311
+ line=f'"{line}"',
312
+ linenum=linenum,
313
+ verbosity=verbosity,
314
+ )
315
+ log(msg, context, verbosity)
316
+ code = code[1:]
317
+
318
+ q.symbol_stop.id = len(q.tokens)
319
+ q.tokens.append(q.symbol_stop)
320
+
321
+ msg = f'Debug: scanned code into {len(q.tokens)} tokens.'
322
+ context = _build_log_context(
323
+ 'dk.qlang.engine.scan',
324
+ code=q.code,
325
+ lines=linenum,
326
+ tokens=[token.name for token in q.tokens],
327
+ verbosity=verbosity,
328
+ )
329
+ log(msg, context, verbosity)
330
+ return q
331
+
332
+
333
+ def _match_symbol(
334
+ symbol,
335
+ code: str,
336
+ ) -> 'None | Symbol':
337
+ for regex in symbol.regex:
338
+ matched = re.match(regex, code)
339
+ if matched:
340
+ return symbol.build(matched.group())
341
+ return None
342
+
343
+
344
+
345
+ def parse(
346
+ q: Query,
347
+ verbosity: int | None = None,
348
+ ) -> Query:
349
+
350
+ if verbosity is None:
351
+ verbosity = q.verbosity
352
+
353
+ for token in q.tokens:
354
+ q = token.parse(q)
355
+
356
+ msg = f'Debug: parsed tokens into {len(q.ops)} ops.'
357
+ context = _build_log_context(
358
+ 'dk.qlang.engine.parse',
359
+ ops=[op.operator for op in q.ops],
360
+ verbosity=verbosity,
361
+ )
362
+ log(msg, context, verbosity)
363
+ return q
364
+
365
+
366
+
367
+ def run(
368
+ q: Query,
369
+ verbosity: int | None = None,
370
+ ) -> Query:
371
+
372
+ if verbosity is None:
373
+ verbosity = q.verbosity
374
+
375
+ for op in q.ops:
376
+ q = op.run(q)
377
+ _validate_dtypes(op, q, verbosity)
378
+
379
+ q.result = q.df.loc[q.mask_rows, q.mask_cols]
380
+ q.styled = _apply_styles(
381
+ q.result,
382
+ q,
383
+ verbosity,
384
+ )
385
+
386
+ msg = f'Debug: ran {len(q.ops)} ops.'
387
+ context = _build_log_context('dk.qlang.engine.run')
388
+ log(msg, context, verbosity)
389
+ return q
390
+
391
+
392
+ def _validate_dtypes(
393
+ op: Symbol,
394
+ q: Query,
395
+ verbosity: int,
396
+ ) -> None:
397
+
398
+ dtypes_current = set(q.df.dtypes.astype(str).unique())
399
+ if not dtypes_current.issubset(DTYPES_ALLOWED):
400
+ msg = 'WARNING: op resulted in invalid dtypes.'
401
+ context = _build_log_context(
402
+ 'dk.qlang.engine._validate_dtypes',
403
+ op=op,
404
+ dtypes_current=dtypes_current,
405
+ dtypes_allowed=DTYPES_ALLOWED,
406
+ verbosity=verbosity,
407
+ )
408
+ log(msg, context, verbosity)
409
+
410
+ return None
411
+
412
+
413
+
414
+ def _apply_styles(
415
+ result: pd.DataFrame,
416
+ q: 'Query',
417
+ verbosity: int = 3,
418
+ ) -> pd.io.formats.style.Styler:
419
+
420
+ styled = result.style
421
+
422
+ if q.style_cols is not None:
423
+ style_cols = q.style_cols[q.mask_cols].to_list()
424
+ styled = styled.apply_index(
425
+ lambda x: style_cols,
426
+ axis=1,
427
+ )
428
+
429
+ if q.style_rows is not None:
430
+ style_rows = q.style_rows[q.mask_rows].to_list()
431
+ styled = styled.apply_index(
432
+ lambda x: style_rows,
433
+ axis=0,
434
+ )
435
+
436
+ if q.style_vals is not None:
437
+ style_vals = (
438
+ q.style_vals
439
+ [q.mask_vals]
440
+ .loc[q.mask_rows, q.mask_cols]
441
+ )
442
+ styled = styled.apply(
443
+ lambda x: style_vals,
444
+ axis=None,
445
+ )
446
+
447
+ return styled