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/__init__.py +108 -0
- dukit/_test_utils.py +374 -0
- dukit/diffing.py +1461 -0
- dukit/excel.py +134 -0
- dukit/interfaces.py +160 -0
- dukit/os.py +280 -0
- dukit/pandas.py +563 -0
- dukit/py.typed +0 -0
- dukit/qlang/__init__.py +61 -0
- dukit/qlang/engine.py +447 -0
- dukit/qlang/symbols.py +6867 -0
- dukit/typing.py +683 -0
- dukit/utils.py +288 -0
- dukit-0.1.0.dist-info/METADATA +113 -0
- dukit-0.1.0.dist-info/RECORD +16 -0
- dukit-0.1.0.dist-info/WHEEL +4 -0
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
|